/* The stylesheet. Five rules the whole UI depends on, the chrome base.html needs, and then
   one block per screen as that screen arrives (Tasks D5.2, D5.4, D5.6); nothing here is
   speculative layout for a screen that does not exist yet.

   No build step, no bundler, no npm. The client owns this file at handover and has to be
   able to open it. */

/* Every `N.N:1` written below is WCAG 2.1 contrast against the stated background, and
   tests/web/test_accessibility.py recomputes each one from these hex values and fails if the
   two disagree by more than 0.05. That is not decoration either: four of these numbers were
   written by hand and all four were wrong -- --ink was labelled 16.1 and measures 17.4, --link
   7.6 and measures 8.0, --focus 5.3 and measures 6.3, --ink-quiet 8.6 and measures 8.9. Each
   error happened to be in the safe direction. A number nobody computes is a number that is
   right by luck. */

:root {
  --ink: #1a1a1a;          /* 17.4:1 on --paper */
  --ink-quiet: #4a4a4a;    /*  8.9:1 on --paper */
  --paper: #ffffff;
  --line: #c9c9c9;         /* a border, never text: no contrast claim belongs here */
  --link: #0b4f9e;         /*  8.0:1 on --paper, and 4.5:1 is the bar for 16px text */
  --focus: #b3300a;        /*  6.3:1 on --paper; a different hue from --link so the ring
                              is visible on a focused link, not just on a focused button */
  --alert: #8a1c1c;        /*  9.3:1 on --paper */
}

*, *::before, *::after { box-sizing: border-box; }

/* `hidden` is an attribute the browser honours with `[hidden] { display: none }` in its OWN
   stylesheet, which any author rule outranks. `.notice { display: block }` below did exactly
   that, and the result was measured on this branch: the status screen's "Something has
   changed since this page loaded" paragraph ships with `hidden` set, tests/web/test_status.py
   asserts the attribute is in the markup, and the paragraph rendered anyway -- 1385x24px, on
   screen, on every load, telling the owner something had changed before anything had. With
   JavaScript off there is nothing that could ever take it away again.

   So this rule is `!important`, and it is the one place in this file that needs to be: it has
   to beat every class rule written after it, including the ones nobody has written yet. An
   element the markup says is hidden is hidden. */
[hidden] { display: none !important; }

body {
  margin: 0;
  font: 16px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;
  color: var(--ink);
  background: var(--paper);
}

/* ---------------------------------------------------------------- 1. focus is visible
   Never `outline: none`. The owner may be on a keyboard, and a focus ring that is removed
   for looking untidy is the single most common way a page becomes unusable without a
   mouse. `:focus-visible` shows it for keyboard and programmatic focus and keeps it off a
   mouse click, so there is no reason left to remove it. */

:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
  border-radius: 2px;
}

/* `#main` is focused programmatically by the skip link, and browsers disagree about
   whether that counts as `:focus-visible`. Matched on plain `:focus` on purpose: someone
   who just jumped past the navigation has to be able to see where they landed. */
#main:focus { outline: 3px solid var(--focus); outline-offset: 4px; }

/* ---------------------------------------------------------------- 2. the skip link
   Off-screen until focused, then the first thing on the page. Positioned rather than
   `display: none`, which would take it out of the tab order and defeat the point. */

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 10;
  padding: 0.5rem 1rem;
  background: var(--paper);
  color: var(--link);
  border: 2px solid var(--focus);
}

.skip-link:focus { left: 0; }

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%);
  overflow: hidden;
  white-space: nowrap;
}

/* ---------------------------------------------------------------- 3. hit targets
   44x44 CSS px, not 24. WCAG 2.2 SC 2.5.8 asks for 24 and that is the floor this file used
   to hold; the owner may be doing this one-handed on a phone at an auction, which is what
   SC 2.5.5 and every phone platform's own guidance are written for. 44 is the number both
   of those give, so it is the number here.

   A bare `min-height` on `a` is inert -- an inline element ignores it -- so anything meant
   to be pressed is laid out as an inline-flex box and gets both dimensions. A link inside a
   sentence stays inline and is exempt under the spec's own inline exception; that is the one
   case the rules below deliberately leave out.

   Physical `min-height`/`min-width` rather than the logical pair used elsewhere in this
   file. The page is `lang="en"` and horizontal-tb, so the two are identical here, and these
   are the property names the measurement in tests/web/test_accessibility.py reads back. */

button, select, summary, .topbar nav a, a.action {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  min-width: 44px;
  font: inherit;
}

input, textarea { min-height: 44px; font: inherit; }

/* Their intrinsic size is about 13px, which is under a third of the minimum, and the
   settings screen (Task D5.6) is a column of them. Sized rather than merely floored, so the
   box that is drawn is the box that is pressed. */
input[type="checkbox"], input[type="radio"] {
  min-height: 44px;
  min-width: 44px;
  width: 44px;
  height: 44px;
}

/* ---------------------------------------------------------------- 4. reduced motion
   Whole-page, deliberately: this UI polls and re-renders, and someone who has asked their
   operating system for less movement should not have to trust every future screen to
   remember. Near-zero rather than `none`, so a transitionend / animationend listener still
   fires and nothing waits forever on an event that was cancelled. */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

/* ---------------------------------------------------------------- 5. switched off, not
   switched invisible
   A control the browser draws as disabled is drawn in the browser's own grey, and Chrome's
   is #b4b4b4 on #fafafa -- measured at 1.99:1, against the 4.5:1 this text needs. WCAG lets
   an inactive control off that hook; the owner reading it does not. The one place this
   happens is the reorder pair on the settings screen, where "Move Walk-around up" is off
   because walk-around is already first, and a label nobody can read cannot say so.

   `:disabled` and not `button:disabled`: `_declarations_for` in tests/web/test_app_boot.py
   finds the single rule whose selector carries the word `button`, and a second one makes the
   44px target guard read a rule it was not written for.

   The dashed border is the second signal and the ordinal in the `<ol>` beside it is the
   first -- "1." is why the up button is off, and that is on the page whatever the colours
   are doing. */

:disabled {
  color: var(--ink-quiet);      /* 7.6:1 on the #ededed fill below */
  background: #ededed;
  border: 1px dashed var(--line);
  cursor: not-allowed;
}

/* ---------------------------------------------------------------- chrome */

.topbar {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem 1.5rem;
  padding: 0.75rem 1.25rem;
  border-block-end: 1px solid var(--line);
}

.wordmark { margin: 0; font-weight: 700; }

.topbar nav ul {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 1.25rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* One link colour for the whole app. Without this the nav read as `--link` and every link in
   a screen's own content read as the browser's `#0000EE`, which is two different blues on one
   page -- both of them pass contrast, so nothing computed could see it. The screens that set
   their own colour (`.skip-link`, the current nav item) still win on specificity. */
a { color: var(--link); }

.topbar nav a { gap: 0.4rem; }

/* The current screen is marked twice: `aria-current` says it to a screen reader, and this
   says it to everyone else. Weight and an underline, not colour alone. */
.topbar nav a[aria-current="page"] {
  color: var(--ink);
  font-weight: 700;
  text-decoration-thickness: 2px;
  text-underline-offset: 4px;
}

.badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-inline-size: 1.5rem;
  padding: 0 0.4rem;
  border-radius: 999px;
  background: var(--alert);
  color: var(--paper);   /* 9.3:1 on --alert */
  font-size: 0.875rem;
  font-weight: 700;
}

main { padding: 1.25rem; }

/* The one table in the app is the in-progress list on the status screen, and a table with no
   cell padding puts "Working out clip types" hard against "4 minutes" -- two columns that
   read as one sentence. Nothing computed catches that: the contrast passes and the box does
   not overflow. `text-align: start` rather than `left` because `<th>` centres by default. */
table { border-collapse: collapse; margin-block-end: 1rem; }
th, td { text-align: start; padding: 0.4rem 1.75rem 0.4rem 0; vertical-align: baseline; }

/* ---------------------------------------------------------------- screen 1: review

   Nothing below is the only carrier of anything. Every state this screen shows -- we
   weren't sure, no pictures were saved, this machine has left the queue, this name can't be
   opened -- is a sentence in the markup first; the rules here give those sentences somewhere
   to sit and never replace them. A rule removed from this file loses the layout and keeps
   every meaning, which is the test. */

.lede { max-inline-size: 65ch; color: var(--ink); }
.footnote, .meta { color: var(--ink-quiet); font-size: 0.9375rem; }

.queue { margin: 0; padding: 0; list-style: none; max-inline-size: 75ch; }

.queue__row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.25rem 0.75rem;
  padding: 0.75rem 0;
  border-block-end: 1px solid var(--line);
}

.queue__row a.action { font-weight: 700; }
.queue .reason { flex: 1 1 20ch; }

/* One machine -------------------------------------------------------------------------- */

.clip {
  max-inline-size: 75ch;
  margin: 0 0 1.5rem;
  padding: 0.75rem 1rem 1rem;
  border: 1px solid var(--line);
}

.clip legend { font-weight: 700; padding-inline: 0.25rem; }

/* `auto-fit` with a minimum, so three frames sit side by side on a laptop and stack on a
   phone without a media query. `aspect-ratio` and the intrinsic width/height on the <img>
   both hold the space before the JPEG arrives -- a thumbnail that pops in and pushes the
   select down is a click on the wrong control. */
.thumbs {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
  gap: 0.5rem;
  margin-block-end: 0.75rem;
}

.thumbs img {
  inline-size: 100%;
  block-size: auto;
  aspect-ratio: 280 / 158;
  object-fit: cover;
  background: #f2f2f2;
  border: 1px solid var(--line);
}

.no-thumbs, .guess { margin-block: 0 0.75rem; }
.no-thumbs { color: var(--ink-quiet); }

.clip label { display: block; margin-block-end: 0.25rem; font-weight: 700; }
.clip select { inline-size: min(100%, 28ch); padding: 0.4rem; }

/* The alert colour is the second signal, never the first: the sentence inside is
   "We weren't sure about this one." and it says so with the border switched off. */
.guess strong:first-child { color: var(--alert); }

.errors {
  max-inline-size: 75ch;
  margin-block-end: 1.5rem;
  padding: 0.75rem 1rem;
  border: 2px solid var(--alert);
}

/* What a screen says after it redirected to itself: "Settings saved", "Undone" (base.html).
   The rule is the second signal and the position is the third; the first is the sentence,
   which says what happened on its own and reads the same with this rule deleted. Ordinary
   --ink text on the fill below, so it is 16.1:1 whatever the role turns out to be. */
.banner {
  max-inline-size: 75ch;
  margin-block: 0 1.5rem;
  padding: 0.5rem 0.75rem;
  border-inline-start: 4px solid var(--link);
  background: #f4f6f9;
}

.errors h2 { margin-block-start: 0; font-size: 1.125rem; }

/* The same message the summary links to, printed against the field it is about. The weight
   and the colour are the second and third signals; the first is that it is a sentence
   sitting under the label, and it reads identically with both switched off. */
.field-error {
  margin-block: 0.25rem 0;
  color: var(--alert);
  font-weight: 700;
}

.notice, .warn {
  display: block;
  color: var(--alert);
  font-weight: 700;
}

/* `.save`, and NOT `button[type="submit"]`. The skeleton's hit-target rule is the one
   selector in this file that carries `button`, and `test_the_stylesheet_carries_the_four_
   rules_the_skeleton_promises` reads it by finding the single rule whose selector contains
   that word -- a second one makes the lookup ambiguous and the 24px guard unreadable. A
   class keeps that guard pointed at the rule it was written for, and this button still
   inherits the 24px minimum from it. */
.save {
  padding: 0.6rem 1.25rem;
  background: var(--link);
  color: var(--paper);   /* 8.0:1 on --link */
  border: 1px solid var(--link);
  font-weight: 700;
}

a.secondary { margin-inline-start: 1rem; color: var(--link); }

.empty { color: var(--ink-quiet); max-inline-size: 65ch; }

/* Video settings (Task D5.6) ------------------------------------------------------------
   A long form for one non-technical owner, so: one column, every control under its own
   label, and the help text under the control it is about rather than beside it. No selector
   here carries the word `button` -- `_declarations_for(css, "button")` in
   tests/web/test_app_boot.py finds the ONE rule whose selector contains it, and a second
   would make the 24px target guard read a rule it was not written for. */

.group {
  max-inline-size: 75ch;
  margin: 0 0 1.75rem;
  padding: 0.75rem 1rem 1rem;
  border: 1px solid var(--line);
}

.group > legend { font-weight: 700; font-size: 1.125rem; padding-inline: 0.25rem; }

/* The legend carries an `<h2>` (Task D5.8) so the six groups are reachable from a screen
   reader's heading list and the outline runs h1 -> h2 -> h3 with nothing skipped. It has to
   look exactly like the legend did before, so the heading gives up its own size and margin. */
.group > legend h2 { margin: 0; font: inherit; }

/* Four small upload forms live outside the long settings form, so replacing one shared file
   never posts forty unrelated controls. Three states, and the word carries each of them
   before the colour does: Installed, Missing, and Off for a whoosh the owner emptied on
   purpose. Off is quiet rather than red -- nothing is wrong, and the alert colour on a
   deliberate choice is the screen telling somebody to fix what they chose. */
.asset-manager > h2 { margin-block: 0 0.25rem; font-size: 1.125rem; }

.asset-upload {
  margin-block: 1rem 0;
  padding-block: 0 1.25rem;
  border-block-end: 1px solid var(--line);
}

.asset-upload:last-child { padding-block-end: 0; border-block-end: 0; }

.asset-upload__heading {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.4rem 0.75rem;
}

.asset-upload__heading h3 { margin: 0; font-size: 1rem; }

.asset-state {
  display: inline-flex;
  padding: 0.1rem 0.45rem;
  border: 1px solid var(--ink);
  border-radius: 2px;
  font-size: 0.875rem;
  font-weight: 700;
}

.asset-state--missing { color: var(--alert); border-color: var(--alert); }

.asset-state--off { color: var(--ink-quiet); border-color: var(--ink-quiet); } /* 8.9:1 on --paper */

.asset-filename {
  margin-block: 0.35rem 0.6rem;
  color: var(--ink-quiet);
  overflow-wrap: anywhere;
}

.asset-filename code { color: var(--ink); }

.asset-upload input[type="file"] {
  display: block;
  max-inline-size: 100%;
  margin-block-end: 0.25rem;
}

.asset-upload .save { margin-block-start: 0.75rem; }

/* Each control sits in its own row so the label, the field and the help text form one
   block: a `for`/`id` pair is what binds them for a screen reader, and this is what binds
   them for everybody else. */
.group .row { margin-block: 1rem 1.25rem; }
.group .row h3 { margin-block: 0 0.5rem; font-size: 1rem; }
.group label { display: block; margin-block-end: 0.25rem; font-weight: 700; }
.group input[type="text"], .group input[type="number"], .group select {
  display: block;
  padding: 0.4rem;
  border: 1px solid var(--line);
}
.group input[type="number"] { inline-size: 12ch; }
.group input[type="text"], .group select { inline-size: min(100%, 44ch); }

/* A checkbox and its label read as one line; the help text stays under both. `--ink-quiet`
   is 8.9:1 on --paper, so the help is quieter without being harder to read.

   The row is a flex line rather than a run of inline boxes. A checkbox is an inline replaced
   element and sits on the text baseline, so at 44px it hangs a whole line above the words it
   belongs to -- measured at 1440px, the box ran 428px to 472px while its label ran 456px to
   475px, which reads as a checkbox floating over the wrong sentence. `align-items: center`
   puts the two on one line whatever the box's height turns out to be, and the help paragraph
   takes a full basis so it still drops underneath both. */
.group .row.check {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  column-gap: 0.5rem;
}
.group .check label { display: inline; }
.group .check .help { flex-basis: 100%; }
.help { color: var(--ink-quiet); max-inline-size: 70ch; margin-block: 0.25rem 0; }

.reorder { margin: 0.5rem 0 1rem; padding-inline-start: 1.5rem; }
.reorder li { margin-block-end: 0.5rem; }
.reorder li span { display: inline-block; min-inline-size: 14ch; font-weight: 700; }

.rotation { margin: 0 0 1.25rem; padding-inline-start: 1.25rem; color: var(--ink-quiet); }
.rotation li { margin-block-end: 0.25rem; max-inline-size: 70ch; }

/* Read-only facts. A `<dl>` rather than a disabled input: there is no control here, so
   there is nothing for a label to be `for`, and `<dt>` already names the value.

   `minmax(0, 1fr)` and not a bare `1fr`, which means `minmax(auto, 1fr)`: the auto minimum
   grows the track to its content's min-content width, so a long value pushes the grid past
   its container instead of wrapping inside it. Nothing here is long enough to do that today,
   which is exactly why it would arrive as a surprise. */
.facts {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 0.25rem 1rem;
  margin-block: 0.5rem;
}
.facts dt { font-weight: 700; }
.facts dd { margin: 0; }

/* The settings-file error message, printed exactly as pipeline.config wrote it -- it is
   multi-line and carries a path, and re-wrapping a path is how a path stops being copyable. */
.problem {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  margin-block: 0.75rem;
  font: inherit;
}

/* Undo (Task D5.7), set well below Save rather than beside it. The two are usually minutes
   apart -- save, look at the result, change your mind -- and side by side is how a mis-click
   becomes the thing you were trying to undo. Deliberately unstyled otherwise: it inherits the
   24px target minimum from the skeleton rule, and a second emphatic button next to the save
   would read as a second way of doing the same thing. */
.undo { margin-block-start: 2.5rem; }
.undo .help { max-inline-size: 60ch; margin-block-start: 0.5rem; }
