/* vidtheque dashboard — the projection room at its minimalist end.
   The normative version of everything below is DESIGN.md at the repo root;
   docs/design/dashboard.md wins on function, data, clamps and copy.

   Two rules carry this surface:

   * **mono means the machine said it, sans means a human wrote it.** Titles,
     channel names and sentences are Archivo. Every timecode, duration, id,
     model key, error code, byte count, state word and clock is JetBrains Mono
     and tabular. Micro-labels are mono too: a column name is the machine's own
     vocabulary.
   * **every state is a word as well as a colour.** The five tones reinforce;
     they never carry the message.

   What this surface spends, out of the whole system: the ground scale, the
   hairlines, the two faces, zero radius, gold kept scarce (the active nav item,
   the focus ring, the sorted column, a timecode worth clicking, the hovered
   row's edge) and lime kept for the one thing it means — what the machine read
   off the screen, which on this surface is the OCR overlay and its lines and
   nowhere else. What it does not spend: display type, imagery as decoration,
   shadows, spectacle, motion that is not a machine working.

   The first six custom properties are asserted equal to public/static/style.css
   (test_dashboard.py::test_the_dashboard_palette_matches_the_demos) — the thing
   that stops the two surfaces drifting into two visual worlds. They are pinned
   aliases and carry no value of their own.

   Fonts are vendored, latin subset, relative `src` — static/fonts/PROVENANCE.md
   has the sizes, the licences and the two things checked inside the files. */

/* ------------------------------------------------------------------ fonts */

/* Relative, not `{{ root }}`-absolute and never PUBLIC_URL: this stylesheet is
   loaded from an SSH tunnel on a port nobody predicted. The copies here are
   byte-identical to public/static/fonts/, which is the document of record —
   `public/static/*` is only routed under VIDTHEQUE_PUBLIC_READONLY=1, so a
   dashboard font served from there 404s in a private deployment. */

@font-face {
  font-family: "Archivo VF";
  font-style: normal;
  font-display: block;
  font-weight: 100 900;
  src: url("fonts/archivo-latin-wght-normal.woff2") format("woff2-variations");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "JetBrains Mono VF";
  font-style: normal;
  font-display: block;
  font-weight: 100 800;
  src: url("fonts/jetbrains-mono-latin-wght-normal.woff2") format("woff2-variations");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ------------------------------------------------------------- 1. the world */

/* One scheme. `color-scheme: dark` is declared, there is no light palette, no
   `prefers-color-scheme` block and no toggle — a projection room does not have
   a day mode (DESIGN.md).

   The six pinned role properties come first, together, so they read as the
   contract they are. The palette test resolves each one through its aliases and
   compares the two surfaces as mappings, so they are held to the same *colour*
   without either file being forced into the other's spelling or ordering.
   Adding a seventh here means adding it to public/static/style.css in the same
   commit — which is exactly the friction it exists to create. */

:root {
  color-scheme: dark;

  --bg: var(--pitch);
  --fg: #f3f0ea;
  --muted: var(--fg2);
  --line: var(--seam);
  --accent: var(--gold);
  --raised: var(--plate);

  /* ---- the ground. A depth scale, not a set of options: a thing sits one
     plate above what contains it, and no further (The Plate Rule). */
  --pitch: #040405;      /* the page */
  --void: #08080a;       /* the rail, the one step off the page that is a band */
  --console: #0a0a0e;    /* a surface the machine writes into — a field, a log */
  --plate: #0e0e12;      /* a real box's ground */
  --plate2: #141419;     /* the head or foot band inside a box */
  --plate3: #1a1a20;     /* the topmost plate; inert fills. Nothing sits above */
  --seam: #242429;       /* the hairline that separates without announcing */
  --seam2: #33333b;      /* the edge of a real box, one step louder */
  --black: #000;         /* the ground *behind a frame*. Image boxes only */
  --edge-in: rgba(255, 255, 255, .06);
  --scrim: rgb(0 0 0 / 0.66);

  /* ---- the ink. `--fg3` is 3.60:1 on the page: over the 3:1 non-text floor,
     under the 4.5:1 text floor, so it is atmosphere and a mark — never the only
     carrier of a fact. On this surface label ink is `--fg2`. */
  --fg2: #9d968c;
  --fg3: #6b665e;

  /* ---- gold, the one accent: *this is the moment you are pointing at*. */
  --gold: #e7b455;
  --gold-hi: #f6cd78;
  --gold-ink: #120c02;
  --gold-6: rgba(231, 180, 85, .06);
  --gold-12: rgba(231, 180, 85, .12);
  --gold-44: rgba(231, 180, 85, .44);

  /* ---- lime, the seen channel. The Lime Rule: it marks evidence the machine
     read off the screen and nothing else. On this surface that is the OCR
     detection boxes, the on-screen-text lines they point at, and the legend
     that names them. There is no lime on any other dashboard page. */
  --seen: #c9f938;
  --seen-24: rgba(201, 249, 56, .24);
  --seen-48: rgba(201, 249, 56, .48);

  /* ---- the state tones. One hex each; the ground and the border are mixed
     from the ink, so a tone is one decision and not three. This is the surface
     the five tones exist for. */
  --tone-ok: #71d083;
  --tone-ok-bg: color-mix(in srgb, var(--tone-ok) 10%, var(--plate));
  --tone-ok-line: color-mix(in srgb, var(--tone-ok) 26%, var(--plate));
  --tone-warn: #ffa057;
  --tone-warn-bg: color-mix(in srgb, var(--tone-warn) 10%, var(--plate));
  --tone-warn-line: color-mix(in srgb, var(--tone-warn) 26%, var(--plate));
  --tone-bad: #ff9592;
  --tone-bad-bg: color-mix(in srgb, var(--tone-bad) 10%, var(--plate));
  --tone-bad-line: color-mix(in srgb, var(--tone-bad) 26%, var(--plate));
  --tone-work: #70b8ff;
  --tone-work-bg: color-mix(in srgb, var(--tone-work) 10%, var(--plate));
  --tone-work-line: color-mix(in srgb, var(--tone-work) 26%, var(--plate));
  /* A waiting state is the absence of a colour, not a fifth one. */
  --tone-wait: var(--fg2);
  --tone-wait-bg: color-mix(in srgb, var(--fg2) 10%, var(--plate));
  --tone-wait-line: color-mix(in srgb, var(--fg2) 26%, var(--plate));
  --tone-neutral: var(--tone-wait);
  --tone-neutral-bg: var(--tone-wait-bg);
  --tone-neutral-line: var(--tone-wait-line);

  /* The remaining two role aliases. `--panel` is the band inside a box (a table
     head); `--rule` is the edge of a real box; `--mark` is a non-text mark. */
  --panel: var(--plate2);
  --rule: var(--seam2);
  --mark: var(--fg3);

  /* --------------------------------------------------------------- type */

  --font-sans: "Archivo VF", system-ui, -apple-system, sans-serif;
  --font-mono: "JetBrains Mono VF", ui-monospace, SFMono-Regular, monospace;

  /* The weight law: five sans rungs and no sixth. A value within ±20 of a rung
     is an optical tweak on a variable face and needs no amendment. */
  --w-display: 210;
  --w-body: 340;
  --w-strong: 520;
  --w-action: 600;
  --w-label: 600;
  --w-tag: 700;

  /* The ladder, and only the ladder. Sans: `headline` is used at its floor on
     this surface — a corpus string at 3.5vw is four lines of video title and
     the metric here is information per screen (docs/design/dashboard.md §5.6).
     Everything under it drops immediately to cell / label / machine. */
  --t-headline: 1.9rem;   /* 30.4px — the page title */
  --t-question: 1.5rem;   /* 24px — the page title below --bp-hand */
  --t-cell: 0.875rem;     /* 14px — cells, rows, sentences, the body default */
  --t-machine: 0.8125rem; /* 13px — ids, model keys, error codes, clocks */
  --t-query: 0.84375rem;  /* 13.5px — what a human types into a field */
  --t-log: 0.78125rem;    /* 12.5px — an error string, an event message */
  --t-machine-sm: 0.6875rem; /* 11px — a confidence, a scale tick */
  --t-label: 0.625rem;    /* 10px — a column name, a state word, a nav group */
  --t-label-sm: 0.59375rem; /* 9.5px — a ledger column name */
  --t-figure: clamp(1.6rem, 2.5vw, 2.5rem); /* a ledger number */

  --lh-tight: 1.1;
  --lh-cell: 1.4;
  --lh-body: 1.55;
  --lh-log: 1.85;

  /* ---------------------------------------------------------- the lockup */

  /* The Font-Logo Rule. The logo is the word and nothing else: `vidtheque` in
     lower case, in the text face, with the full stop in gold — the receipt's
     full stop, because the product's whole argument is that an answer ends in a
     citation. v5's numbers are the contract. */
  --brand-size: 16.5px;
  --brand-weight: 500;
  --brand-track: -0.035em;

  /* ------------------------------------------------------------- metrics */

  /* 4px base, no exceptions. A value off this grid is a new primitive and
     DESIGN.md has to say so. */
  --s1: 0.25rem;
  --s2: 0.5rem;
  --s3: 0.75rem;
  --s4: 1rem;
  --s5: 1.25rem;
  --s6: 1.5rem;
  --s8: 2rem;
  --s10: 2.5rem;
  --s12: 3rem;

  /* The app shell. The content column is fluid — an Operate surface reads left
     to right across whatever width the operator gave it — with an ultra-wide
     backstop, because past 1760px a table row stops being scannable. */
  --rail: 15rem;     /* 240px — the nav rail; 34px items, same as a table row */
  --wide: 110rem;    /* 1760px — where the content column stops growing */
  --prose: 70ch;     /* running text */
  --note: 74ch;      /* a machine note, a sentence about a state */
  --row: 2.125rem;   /* 34px — the table row, the nav item, every control */
  /* 20px — every marker that can share a state cell: a pill, an error code, a
     `statepair` key, the deferral countdown. One height, so a row of them is a
     strip rather than a stack of boxes at three sizes. */
  --chip-h: 1.25rem;
  /* 144px — every picker in a filter band, whatever it is currently showing.
     A select sized by its own content re-flows the controls beside it the
     moment the reader changes its value, and a band whose geometry moves when
     you use it is the layout shift Tom reported (round 4, item 7b). Wide
     enough for the longest value any of them holds — `indexed_at`,
     `transcript`, `all states` — plus the disclosure mark's own gutter. */
  --pick-w: 9rem;
  /* The two scrollboxes, both bounded in *lines* rather than in pixels, so the
     box is as tall as the thing it holds and not as tall as a guess. Ten OCR
     lines at `cell`; eighteen transcript cues at the same. */
  --ocrbox-h: 14rem;
  --cuebox-h: 34rem;
  /* The width the column name takes when a table row becomes a stacked block
     below 52rem: the label's basis and the indent a wrapped value needs to line
     up under the other values are the same number. */
  --stacked-label: 5.5rem;

  /* Colour only, and only at --t-fast: hover may move a ground, an ink, a
     border or an image filter. It may never move geometry. */
  --t-fast: 0.16s;
  --t-state: 0.25s;
}

/* ---------------------------------------------------------------- 2. base */

/* Shape: there is no radius. A frame has square corners, and every box on this
   surface is a frame, a plate or a rule. */
* { box-sizing: border-box; border-radius: 0; }
[hidden] { display: none !important; }
html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font-sans);
  font-size: var(--t-cell);
  font-weight: var(--w-body);
  line-height: var(--lh-body);
  letter-spacing: -0.004em;
}

a { color: inherit; }
a:hover { color: var(--accent); }

h1, h2, h3 { font-weight: var(--w-strong); letter-spacing: -0.02em; }

::selection { background: var(--accent); color: var(--gold-ink); }

:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.skip {
  position: absolute;
  left: var(--s2); top: -3rem;
  z-index: 5;
  padding: var(--s2) var(--s3);
  background: var(--plate);
  border: 1px solid var(--seam2);
}
/* No slide. A skip link appearing is not a machine working, and decorative
   motion is banned outright (DESIGN.md, the Motion Law). */
.skip:focus { top: var(--s2); }

.sr-only {
  position: absolute;
  /* Screen-reader-only text has no visual size, so it must not inherit one. */
  font-size: var(--t-cell);
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ------------------------------------------------- the two-channel rule */

/* Everything the machine produced, in one selector list: a count, a clock, a
   duration, an id, a model key, an error string, a byte figure, a state word.
   Tabular by construction and ligature-free, because in `error_code` and
   `t=1234` the literal characters are the information — JetBrains Mono ships
   `calt` and would otherwise draw `->` as an arrow. */
.mono,
code,
time,
.num,
.at,
.figure > dd,
.minirow-figure,
.chip-n,
.cov,
.conf,
.countdown,
.meter-label,
.digest-count,
.errtext,
.eventtext,
.ocrtext,
.badge,
.deployment,
.timeline-scale,
.scrubspan,
.scrubmeta,
.shot-caption {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-variant-ligatures: none;
}

/* Mono runs optically larger than the sans at the same size; an inline machine
   string inside a sentence is set down one notch so the line does not step up
   around it. */
code { font-size: var(--t-machine); overflow-wrap: anywhere; }

/* Every machine string long enough to be a paragraph breaks anywhere, wherever
   it is printed: a yt-dlp traceback is one token as often as it is a sentence,
   and the rule belongs to the string rather than to the table one copy of it
   happens to sit in. */
.errtext, .eventtext { overflow-wrap: anywhere; }

.muted { color: var(--muted); }
.sep { padding: 0 var(--s1); color: var(--mark); }
/* The suffix on a figure. The floor is the label size, so a `vid` next to a
   count is never smaller than the column label above it. `--fg2` and not
   `--fg3`, because a unit is half of what the number means. Gold is reserved
   for the *ledger's* units below — that is the system's `ledger-figure`
   component and it is the page's signature object; a `vid` in a channel row is
   not a moment anybody is pointing at. */
.unit {
  font-size: max(var(--t-label), 0.62em);
  font-weight: var(--w-body);
  color: var(--muted);
  letter-spacing: 0;
}
.num { text-align: right; }

/* A label: 10px mono, tracked, uppercase, in `--fg2` — the machine's own
   vocabulary for a column name, a group, a field. One idiom, used above every
   ledger figure, over every table column and in front of every stacked cell. */
.label,
.panel-title,
.figure > dt,
.field label,
.field-range > legend,
.checks > legend,
.grid thead th {
  font-family: var(--font-mono);
  font-size: var(--t-label);
  font-weight: var(--w-label);
  line-height: var(--lh-cell);
  text-transform: uppercase;
  letter-spacing: 0.19em;
  font-variant-ligatures: none;
  color: var(--muted);
}

/* ------------------------------------------------------------- 3. the shell */

/* A persistent rail on the left and a fluid column that takes everything else.
   The rail is `position: sticky` rather than `fixed` because sticky keeps it in
   flow — the grid track reserves its width, nothing needs a matching margin,
   and the column scrolls under it for free. */
.shell {
  display: grid;
  grid-template-columns: var(--rail) minmax(0, 1fr);
  min-height: 100dvh;
}

.rail {
  position: sticky;
  top: 0;
  align-self: start;
  display: flex;
  flex-direction: column;
  /* Exactly one viewport tall, so it sticks and its hairline runs the full
     height of the window however long the page under it gets. */
  height: 100dvh;
  overflow-y: auto;
  padding-bottom: calc(var(--s4) + env(safe-area-inset-bottom));
  background: var(--void);
  border-right: 1px solid var(--seam);
}

/* `minmax(0, 1fr)` above and `min-width: 0` here are what let a too-wide table
   scroll inside its own wrapper instead of pushing the grid track wider. */
.col {
  display: flex;
  flex-direction: column;
  min-width: 0;
  padding: 0 var(--s6) calc(var(--s10) + env(safe-area-inset-bottom));
}

.col > main { flex: 1 1 auto; }
.col > main, .col > footer { width: 100%; max-width: var(--wide); }

/* -------------------------------------------------------------- the lockup */

.rail-brand {
  padding: var(--s5) var(--s4) var(--s4);
  border-bottom: 1px solid var(--seam);
}

/* `display: flex; align-items: baseline; gap: 12px` — the gap is for whatever
   sits *beside* the word, never for a drawing. There is no drawn mark in the
   lockup: the identity is carried by the word (The Font-Logo Rule), and the
   film-frame favicon is a tab icon, not a brandmark. */
.mark {
  display: flex;
  align-items: baseline;
  gap: var(--s3);
  text-decoration: none;
  color: var(--fg);
}
.mark b {
  font-family: var(--font-sans);
  font-size: var(--brand-size);
  font-weight: var(--brand-weight);
  line-height: var(--lh-tight);
  letter-spacing: var(--brand-track);
}
/* The period is the receipt's full stop. It inherits everything else. */
.mark i { font-style: normal; color: var(--accent); }
/* The one place hover does not go gold: a wordmark that changes colour under
   the pointer reads as a control. It underlines instead. */
.mark:hover { color: var(--fg); }
.mark:hover b { text-decoration: underline; text-underline-offset: 3px; }

/* ---------------------------------------------------------------- the nav */

.nav { padding: var(--s2) 0; }

.nav-group { margin: var(--s3) 0 var(--s1); padding: 0 var(--s4); color: var(--fg3); }
.nav-group + .navlist { margin-top: 0; }

.navlist { margin: 0; padding: 0; list-style: none; }

/* Full-bleed to the rail's edges, so the active item's gold edge lands on the
   boundary and the item reads as a notch into the content ground rather than as
   a pill floating on the rail. Height is `--row`: a nav item and a table row
   are the same 34px, which is what stops the rail feeling like a different
   product from the grid beside it. */
.navlist a {
  display: block;
  padding: var(--s2) var(--s4);
  font-size: var(--t-cell);
  line-height: var(--lh-cell);
  color: var(--muted);
  text-decoration: none;
  transition: color var(--t-fast), background-color var(--t-fast);
}
.navlist a:hover { color: var(--fg); background: var(--plate); }

/* You-are-here: the item takes the *page* ground, so the rail opens onto the
   content at exactly that row, with the one signal as its edge. */
.navlist a[aria-current="page"] {
  color: var(--accent);
  background: var(--pitch);
  box-shadow: inset 2px 0 0 var(--accent);
}

/* What this deployment is allowed to do is true of every page, so it belongs to
   the chassis and not to a page header. */
.rail-foot {
  margin-top: auto;
  padding: var(--s4) var(--s4) 0;
  border-top: 1px solid var(--seam);
}

.deployment {
  display: flex;
  flex-direction: column;
  margin: 0;
  color: var(--muted);
  font-size: var(--t-machine-sm);
  line-height: var(--lh-cell);
}

.deployment-why {
  margin: var(--s2) 0 0;
  color: var(--muted);
  font-size: var(--t-cell);
  line-height: var(--lh-cell);
}

.signout { margin: var(--s3) 0 0; }
/* Full-bleed in the rail, like a nav item: it is the one control in the chassis
   and it should read as part of the column, not as a widget dropped into it. */
.signout button { width: 100%; }

/* -------------------------------------------------------------- page heads */

.crumbs { margin: var(--s5) 0 0; color: var(--muted); font-size: var(--t-machine-sm); }
.crumbs a { text-decoration: none; }

/* The content column's own top edge — full width, hairline under, the title
   left and whatever states the page is in on the right. Not sticky: a title
   band pinned to the top of an operator surface spends the scarce dimension on
   something you read once. */
.pagehead {
  padding: var(--s6) 0 var(--s4);
  margin-bottom: var(--s6);
  border-bottom: 1px solid var(--seam);
}
.pagehead-line {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s2) var(--s6);
}
.pagehead h1 {
  margin: 0;
  font-size: var(--t-headline);
  font-weight: var(--w-display);
  line-height: var(--lh-tight);
  letter-spacing: -0.034em;
  text-wrap: balance;
  /* A page title is a corpus string — one of them is a 78-character identifier
     with no space in it. `anywhere` rather than `break-word` because only
     `anywhere` also reports a zero min-content width, which is what stops the
     flex header band being widened by the same string. */
  overflow-wrap: anywhere;
}
/* A machine string inside a page title — `Job job_deferred01`. At 0.72em the id
   carries the same visual weight as the sans beside it, which is the point: the
   id is the title and `Job` is the noun in front of it. */
.pagehead h1 code {
  font-size: 0.72em;
  font-weight: 400;
  color: var(--fg);
  letter-spacing: -0.01em;
}
/* Deliberately not a flex row: half of this line is sentence, half is machine
   string, and a flex container swallows the spaces between them. */
.pagehead-meta {
  margin: var(--s2) 0 0;
  max-width: var(--note);
  color: var(--muted);
  font-size: var(--t-machine-sm);
}
/* Inside the title line the meta is a state strip, not a paragraph. The *box*
   goes to the far end; the text inside it stays left-aligned, because the line
   wraps at more widths than a media query can name and a strip that has dropped
   under a left-aligned title still setting itself hard right reads as a stray. */
.pagehead-line > .pagehead-meta { margin: 0; max-width: 100%; }
/* One fact — a label and the machine string it names — is one unbreakable unit,
   so a strip that runs out of room wraps between facts. */
.fact { white-space: nowrap; }
.pagehead-meta .mono { color: var(--fg); }
.pagehead-meta a { color: var(--accent); }

/* ------------------------------------------------------------------ panels */

/* A panel is a label and a hairline, not a card. Nested boxes are how a
   dashboard stops being scannable, and no rule on this surface is thicker than
   1px except the one that means something: a panel whose halves disagree. */
.panel { margin-top: var(--s8); padding-top: var(--s3); border-top: 1px solid var(--seam); }
.panel-title { margin: 0 0 var(--s3); }
/* A panel heading is a label; a video title is not. The subject keeps its own
   case and its own register inside the heading. */
.panel-title .subject {
  font-family: var(--font-sans);
  text-transform: none;
  letter-spacing: -0.012em;
  font-size: var(--t-cell);
  font-weight: var(--w-strong);
  color: var(--fg);
}

/* A panel whose two halves stopped agreeing. The rule is the alarm; the words
   under it still say what happened, because colour is never the message. */
.panel.is-drift { border-top: 2px solid var(--tone-bad); }
.panel.is-drift > .panel-title { color: var(--tone-bad); }

/* Declared above, live below: the pair that makes the models panel a diff. */
.live-state {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  align-items: baseline;
  margin: var(--s3) 0 0;
}

.panel-note {
  margin: var(--s3) 0 0;
  max-width: var(--note);
  color: var(--muted);
  font-size: var(--t-cell);
}

/* ------------------------------------------------------------- the digest */

/* A bounded preview and an honest expander, for the blocks whose length is a
   property of the corpus rather than of the design. The contract is the whole
   primitive: the preview is a documented number of items; the expander names a
   real remainder counted from the same list; it expands in place, because on
   this surface the full text is the receipt; and it is `<details>`, so the
   collapsed state is readable markup and the control is the platform's.

   The marker is `+` and `−`, two characters: this surface has no icon
   language. */
.digest { margin: var(--s2) 0 0; }

.digest > summary {
  display: inline-block;
  list-style: none;
  padding: 1px 0;
  color: var(--muted);
  font-size: var(--t-machine-sm);
  border-bottom: 1px dashed var(--seam2);
  cursor: pointer;
  transition: color var(--t-fast), border-color var(--t-fast);
}
.digest > summary::-webkit-details-marker { display: none; }
.digest > summary::before { content: "+ "; }
.digest[open] > summary::before { content: "\2212 "; }
.digest > summary:hover { color: var(--fg); border-bottom-color: var(--gold-44); }

.digest-count { color: var(--fg); }

/* The empty state, and it has a voice: name the absence, name the cause, name
   the move. An operator who opens a panel with nothing in it is asking one
   question — is this broken, or is this correct? */
.empty { padding: var(--s1) 0 var(--s2); }

.empty-lead {
  margin: 0;
  max-width: var(--note);
  font-size: var(--t-cell);
  font-weight: var(--w-strong);
  color: var(--fg);
}

.empty-note {
  margin: 0;
  max-width: var(--note);
  color: var(--muted);
  font-size: var(--t-cell);
}
.empty-lead + .empty-note { margin-top: var(--s1); }

/* Zones. A full-bleed column needs a way to put two or three panels side by
   side; a three-line list stretched across 1600px is a sentence with 1400px of
   nothing after it. Every track is `minmax(0, …)` so a table inside one scrolls
   in its wrapper instead of widening the zone. */
.split { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 var(--s10); }
.split-main { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); }
.split > * { min-width: 0; }
.split > .panel:nth-child(2) { margin-top: var(--s8); }

/* ------------------------------------------------------------- the ledger */

/* The overview's signature, and the reason this page is not a grid of stat
   cards: one plate across the full measure, five columns divided by hairlines,
   the figures in mono. It reads as a line in a log because that is what it is —
   the corpus, counted, at this second. */
.ledger {
  position: relative;
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  margin: 0;
  background: var(--plate);
  border: 1px solid var(--seam2);
}

/* The corner tick: a 12×1px and a 1×12px gold rule meeting at the top-left
   corner. It is the mark of a plate on a light table, it costs no box, and it
   is the system's one ornament — once per page, on the page's signature
   object, and nowhere else on this surface. */
.ledger::before,
.ledger::after {
  content: "";
  position: absolute;
  top: -1px;
  left: -1px;
  background: var(--accent);
}
.ledger::before { width: 12px; height: 1px; }
.ledger::after { width: 1px; height: 12px; }

.ledger > .figure {
  padding: var(--s5);
  border-left: 1px solid var(--seam);
}
.ledger > .figure:first-child { border-left: 0; }

.figures {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr));
  gap: var(--s5) var(--s8);
  margin: 0;
}

.figure { min-width: 0; }
.ledger > .figure > dt { font-size: var(--t-label-sm); letter-spacing: 0.17em; }
.ledger > .figure > dd { font-size: var(--t-figure); }
.ledger .unit { color: var(--accent); }

/* A figure outside the ledger — "what was stored", the storage panel — takes
   the same rung at its floor. The ledger is the one band on the surface whose
   numbers are the point of the page; six counts under a panel label are a
   reading, not a headline, and matching them would flatten the two. */
.figure > dd {
  margin: var(--s2) 0 0;
  font-size: 1.6rem;
  font-weight: 300;
  line-height: 1;
  letter-spacing: -0.045em;
  color: var(--fg);
}

.figure > dd.figure-note {
  margin-top: var(--s2);
  font-family: var(--font-sans);
  font-size: var(--t-cell);
  font-weight: var(--w-body);
  line-height: var(--lh-cell);
  letter-spacing: -0.004em;
  color: var(--muted);
}
.figure-note a { color: var(--accent); }

/* -------------------------------------------------------------------- gaps */

/* Sentences with a number in them, not a chart. The number is the link, because
   the number is the thing you want to go and look at — and a zero is good news,
   so it does not wear the accent. */
.gaplist { margin: 0; padding: 0; list-style: none; font-size: var(--t-cell); }
.gaplist li {
  display: flex;
  gap: var(--s3);
  align-items: baseline;
  padding: var(--s2) 0;
  border-bottom: 1px solid var(--seam);
}
.gaplist li:last-child { border-bottom: 0; }
.gaplist a {
  flex: 0 0 3.5rem;
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--t-machine);
  text-align: right;
  color: var(--accent);
  text-decoration: none;
}
.gaplist a:hover { text-decoration: underline; }
.gaplist a.gap-none { color: var(--muted); }

/* -------------------------------------------------------------------- rows */

.rowlist { margin: 0; padding: 0; list-style: none; }

.row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
  align-items: flex-start;
  padding: var(--s2) 0;
  border-top: 1px solid var(--seam);
}
.rowlist > .row:first-child { border-top: 0; }

.row-shot { flex: 0 0 auto; line-height: 0; text-decoration: none; }
/* Basis `0`, not `auto`. Flex line-breaking uses an item's hypothetical main
   size, so with `auto` a long conference title exceeded the line on its own and
   pushed the thumbnail onto a line of its own with the clock stranded on a
   third. With basis 0 the body takes the space the fixed items leave. */
.row-body { flex: 1 1 0; min-width: 0; }
.row-title { font-weight: var(--w-strong); text-decoration: none; overflow-wrap: anywhere; }
.row-meta {
  display: block;
  margin: 2px 0 0;
  color: var(--muted);
  font-size: var(--t-machine-sm);
}

/* The far end of a row: one fact, right-aligned, so a wide column reads as a
   column of clocks rather than as a ragged list of sentences. */
.row-when {
  flex: 0 0 auto;
  margin: 0 0 0 auto;
  padding-left: var(--s3);
  color: var(--muted);
  font-size: var(--t-machine-sm);
  text-align: right;
  white-space: nowrap;
}

/* `wrap`, because one of these rows carries an error string: unwrapped, the
   degraded list squeezed a video id down to one character per line and took the
   page sideways doing it. The error is the wide thing, so the error is what
   moves to its own line. */
.minirow {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s3);
  padding: var(--s1) 0;
  border-top: 1px solid var(--seam);
  font-size: var(--t-cell);
}
.minirow > .errtext { flex: 1 1 18rem; min-width: 0; font-size: var(--t-log); }
.rowlist.tight > .minirow:first-child { border-top: 0; }
.minirow > :first-child { flex: 1 1 auto; min-width: 0; }
.minirow a { text-decoration: none; }
.minirow a:hover { text-decoration: underline; }
.minirow-figure { flex: 0 0 auto; font-size: var(--t-machine); }

/* A chapter row leads with its timestamp, so the title — not the clock — is the
   thing that takes the width. */
.minirow > .at:first-child { flex: 0 0 auto; }
.minirow > .at:first-child + span { flex: 1 1 auto; min-width: 0; }

/* Fixed box, mirrored in the img attributes: a row keeps its height whether the
   frame has loaded, failed, or never existed. Black is the ground behind a
   frame, so a still that has not decoded yet leaves black rather than a plate. */
.thumb {
  display: block;
  width: 96px;
  height: 54px;
  object-fit: cover;
  background: var(--black);
  box-shadow: inset 0 0 0 1px var(--edge-in);
}

.thumb-empty {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--muted);
  font-family: var(--font-mono);
  font-size: var(--t-label);
  letter-spacing: 0.19em;
  text-transform: uppercase;
}

/* ------------------------------------------------------------------- chips */

.chiplist { display: flex; flex-wrap: wrap; gap: var(--s1); margin: 0; padding: 0; list-style: none; }

/* A chip is a whole object — `series:a-conference-series-with-a-long-slug` is
   one filter, not a paragraph — and it is never truncated: the slug *is* the
   link, and a chip that says `series:a-conf…` is a filter you cannot read. The
   cell it widens is inside `.tablewrap`, which is what that wrapper is for. */
.chip {
  display: inline-flex;
  gap: var(--s2);
  align-items: baseline;
  padding: 2px var(--s2);
  font-size: var(--t-machine-sm);
  color: var(--muted);
  text-decoration: none;
  white-space: nowrap;
  border: 1px solid var(--seam);
  transition: color var(--t-fast), border-color var(--t-fast);
}
.chip:hover { color: var(--fg); border-color: var(--gold-44); }
.chip-n { font-size: var(--t-label); color: var(--fg); }
.chip-small { padding: 0 var(--s1); font-size: var(--t-label); }

.taglist { display: inline-flex; flex-wrap: wrap; gap: var(--s1); }

/* ------------------------------------------------------------------ states */

/* Every pill carries its own word, so the colour is reinforcement and never the
   message. Ink, ground and hairline all come from one tone, and the ground and
   the border are mixed from the ink. */
.pill {
  /* `inline-block` and a line-height, **not** `inline-flex`: a flex container
     makes an item of every text run inside it and drops the whitespace between
     them, which turned the countdown's own `held <span>1m 28s</span> more` into
     `held1m 28smore` (caught in review, 2026-08-10). The box carries the height
     and the leading centres the text. */
  display: inline-block;
  /* One height for every marker that sits in a state cell — the pills, the
     error code, the countdown. It was not one before: the countdown's 13px
     text inside 4px of inset made it 26px beside a 17px pill, and a row of
     three markers at two heights reads as two kinds of thing (Tom,
     2026-08-10). `--chip-h` is what makes them one strip. */
  height: var(--chip-h);
  line-height: calc(var(--chip-h) - 2px);
  padding: 0 var(--s2);
  font-family: var(--font-mono);
  font-size: var(--t-label-sm);
  font-weight: var(--w-label);
  letter-spacing: 0.17em;
  text-transform: uppercase;
  font-variant-ligatures: none;
  color: var(--tone, var(--tone-neutral));
  background: var(--tone-bg, var(--tone-neutral-bg));
  border: 1px solid var(--tone-line, var(--tone-neutral-line));
  vertical-align: -0.3em;
  white-space: nowrap;
}
.pill-small { font-size: var(--t-label-sm); }

/* A key and the state it names, as one object: the key on the band plate, the
   state's own pill flush against it, one hairline around the pair. Built for
   the video header, where a bare pill, a floating 10px `data_status` and a
   second bare pill were three objects with nothing saying which label owned
   which pill (Tom, 2026-08-10); reused on the overview, where one fact left
   over from a removed pair had no business still wearing a row label. */
.statepair {
  display: inline-block;
  vertical-align: -0.3em;
  white-space: nowrap;
  /* The two halves are inline-blocks on one line, so the line box between them
     would otherwise show as a gap in the middle of a joined object. */
  font-size: 0;
}
.statepair-key {
  display: inline-block;
  height: var(--chip-h);
  line-height: calc(var(--chip-h) - 2px);
  padding: 0 var(--s2);
  font-family: var(--font-mono);
  font-size: var(--t-label-sm);
  font-weight: var(--w-label);
  letter-spacing: 0.17em;
  text-transform: uppercase;
  font-variant-ligatures: none;
  color: var(--muted);
  background: var(--plate2);
  border: 1px solid var(--seam2);
  border-right: 0;
}
.statepair > .pill { vertical-align: top; }

.tone-ok      { --tone: var(--tone-ok);      --tone-bg: var(--tone-ok-bg);      --tone-line: var(--tone-ok-line); }
.tone-warn    { --tone: var(--tone-warn);    --tone-bg: var(--tone-warn-bg);    --tone-line: var(--tone-warn-line); }
.tone-bad     { --tone: var(--tone-bad);     --tone-bg: var(--tone-bad-bg);     --tone-line: var(--tone-bad-line); }
.tone-work    { --tone: var(--tone-work);    --tone-bg: var(--tone-work-bg);    --tone-line: var(--tone-work-line); }
.tone-wait    { --tone: var(--tone-wait);    --tone-bg: var(--tone-wait-bg);    --tone-line: var(--tone-wait-line); }
.tone-neutral { --tone: var(--tone-neutral); --tone-bg: var(--tone-neutral-bg); --tone-line: var(--tone-neutral-line); }

/* A tone on a run of text rather than on a pill: the word inside a sentence that
   the sentence is about. Only two exist, because only two are ever the subject
   of one — something is wrong, or something is waiting. */
span.tone-bad { color: var(--tone-bad); }
span.tone-warn { color: var(--tone-warn); }

/* Coverage: three letters, and the missing one is hollow *and* struck, so it
   reads as absent without colour. */
.coverage { display: inline-flex; gap: 2px; }

.cov {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.25rem;
  height: 1.25rem;
  font-size: var(--t-label);
  font-weight: var(--w-label);
  border: 1px solid var(--seam);
}
.cov-on { color: var(--tone-ok); background: var(--tone-ok-bg); border-color: var(--tone-ok-line); }
.cov-off { color: var(--muted); border-style: dashed; text-decoration: line-through; }

/* ----------------------------------------------------------------- filters */

/* The control strip: a plate on the page, with each field an inset well the
   machine writes into. Pitch → plate → console, one step at a time. */
.filters {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--s3) var(--s4);
  padding: var(--s3) var(--s4);
  background: var(--plate);
  border: 1px solid var(--seam2);
}

.field { display: flex; flex-direction: column; gap: var(--s1); }
/* One flexible field per band and everything else fixed (Tom, 2026-08-10,
   round 4, item 7b). A text input takes its width from the platform's `size`
   and a select takes its from its own longest option, so a band of intrinsic
   widths re-flows the moment the reader changes what any of them holds — the
   layout shift Tom saw on applying a filter. With one flexible field the
   band's geometry is a function of the viewport and of nothing else. */
.field-wide { flex: 1 1 16rem; min-width: 0; }
.field-text { flex: 0 0 13rem; min-width: 0; }
.field-text > input { width: 100%; }
.field-narrow { flex: 0 0 5rem; }
.field-narrow > input { width: 100%; }
/* A field that owns its own line rather than sharing a flex row. */
.field-block { display: flex; flex-direction: column; gap: var(--s1); }

/* A range is one question asked with two inputs, so it is a fieldset — and the
   browser's own box around it goes, like every other box here. The legend is
   dragged back onto the label ladder by hand: `<legend>` ships with padding of
   its own and does not inherit the `.field label` selector. */
.field-range { min-width: 0; margin: 0; padding: 0; border: 0; }
.field-range > legend { padding: 0; }
/* The two dates read as one control: an en dash between them, at the same 34px
   height as everything else in the band, wrapping as a unit. */
.field-range > .range {
  display: flex;
  align-items: center;
  gap: var(--s1);
  margin-top: var(--s1);
}
.range-sep { color: var(--mark); }
/* The buttons pack at the end of the controls, not at the far end of the band:
   a two-field filter bar on a 1900px screen must not put Apply 1400px from the
   select it applies to. */
.field-actions { flex-direction: row; gap: var(--s2); align-items: center; }

/* Every control in the bar is exactly `--row` tall — the same 34px as a table
   row and a nav item — so the band reads as one object instead of as four
   widgets the platform drew at four heights. The ground is `--console`: a field
   is a surface the machine writes into, and it sits one step *under* the plate
   around it rather than one step above. Every value in this bar is the
   machine's own vocabulary (`ready`, `E_RATE_LIMIT`, `2026-08-09`, a video id),
   so the controls are mono. */
.field input, .field select {
  height: var(--row);
  padding: 0 var(--s3);
  font-family: var(--font-mono);
  font-size: var(--t-query);
  font-variant-ligatures: none;
  line-height: var(--lh-cell);
  color: var(--fg);
  background: var(--console);
  border: 1px solid var(--seam2);
  transition: border-color var(--t-fast);
}
/* ---- the picker

   **Overturns the first pass** (Tom, 2026-08-10, round 4). That pass kept the
   platform's own disclosure arrow, on the argument that a drawn caret would be
   the first glyph of an icon language this system deliberately does not have.
   What it bought on the operator's actual machine was a rounded, shaded,
   OS-accented macOS control sitting in a band of square 34px hairline boxes —
   the one thing on the page that was not this system. `appearance: none`, and
   the band's own chrome.

   The mark is not an icon: it is a 6px square with two of its four hairlines
   drawn, turned 45°, in the same ink as a label. Every other rule in this band
   is a 1px hairline and so is this one. The *list* the control opens is still
   the platform's, and stays there — that is the part a surface with no
   JavaScript cannot draw, and a reimplementation would be a listbox widget
   with its own keyboard model to get wrong. */
.pick { position: relative; display: flex; min-width: 0; }
/* Only the select loses its platform drawing — never a checkbox, which
   `appearance: none` would erase, and never a date field, whose calendar
   button is the platform's own answer to a question this band does not ask. */
.pick > select {
  width: 100%;
  padding-right: var(--s6);
  appearance: none;
}
.pick::after {
  content: "";
  position: absolute;
  top: 50%;
  right: var(--s3);
  width: 6px;
  height: 6px;
  border-right: 1px solid var(--fg2);
  border-bottom: 1px solid var(--fg2);
  transform: translateY(-70%) rotate(45deg);
  transition: border-color var(--t-fast);
  /* A mark that follows a control must never be the thing the pointer is on. */
  pointer-events: none;
}
.pick:hover::after { border-color: var(--accent); }
/* Every picker in a band is the same width, whatever it is currently showing.
   A select sized by its own content changes width when the reader changes its
   value, and one control resizing re-flows every control beside it — which is
   the layout shift item 7b is about. This is what stops it, and it stops it in
   the one place both bands read from. */
.field-pick { flex: 0 0 var(--pick-w); }
.field input::placeholder { color: var(--fg3); opacity: 1; }
.field input:hover, .field select:hover { border-color: var(--gold-44); }
/* A date input carries the platform's calendar button inside its own box. Fixed
   width rather than `auto`: the two ends of a range must be the same width
   whether or not either has a value, or an empty "to" collapses and the dash
   stops reading as a range. */
.field-range input {
  width: 8.5rem;
  padding-right: var(--s1);
  font-variant-numeric: tabular-nums;
}

/* Controls are labels, not sentences: 10px tracked mono in a 34px box, which is
   the system's `button-ghost`. Nothing on this surface is a gold fill — gold
   here means the active nav item, the focus ring, the sorted column, a timecode
   worth clicking and the hovered row's edge, and a filled Apply button would
   make it mean "a button" as well. */
button, .ghostlink {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: var(--row);
  padding: 0 var(--s3);
  font-family: var(--font-mono);
  font-size: var(--t-label);
  font-weight: var(--w-label);
  letter-spacing: 0.19em;
  text-transform: uppercase;
  font-variant-ligatures: none;
  color: var(--fg);
  background: var(--plate3);
  border: 1px solid var(--seam2);
  text-decoration: none;
  cursor: pointer;
  transition: color var(--t-fast), border-color var(--t-fast), background-color var(--t-fast);
}
button:hover, .ghostlink:hover { color: var(--accent); border-color: var(--gold-44); }
/* The secondary of the pair: the same control with no ground under it. */
button.ghost, .ghostlink { color: var(--muted); background: transparent; }
button.ghost:hover, .ghostlink:hover { color: var(--accent); background: transparent; }
button[disabled] { color: var(--fg3); border-color: var(--seam); cursor: default; }
button[disabled]:hover { color: var(--fg3); border-color: var(--seam); }

/* ------------------------------------------------------------------ tables */

/* The strip that says what the table below is: how many rows arrived, whether
   there are more, and — on jobs — whether the page is still being patched. */
.tablecount {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0 var(--s4);
  margin: var(--s5) 0 var(--s1);
  color: var(--muted);
  font-size: var(--t-machine-sm);
}
/* The sentence is one flex item, not five: a flex container makes an item of
   every text run between elements, and `of ~<span>4</span>.` came out as
   `of ~ 4 .` with a gap either side of the number. */
.tablecount > * { min-width: 0; }
.tablecount .shown {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: var(--t-machine);
  color: var(--fg);
}

/* The `live` badge that used to sit here is gone (Tom, 2026-08-10). It said the
   *page* was refreshing, which on a dashboard served by the process that also
   holds the job runner is always true; liveness belongs to the jobs, and the
   rows report it themselves — see `.meter.is-working` below. */

/* `position: relative` is not decoration, it is the clip. `.sr-only` is
   `position: absolute` with no offsets, so its containing block is the nearest
   positioned ancestor — and inside a table there was none, which made it the
   page, and an out-of-flow box whose containing block is the page is not
   clipped by a scroll container it merely sits inside. The coverage column's
   invisible descriptions scrolled the whole videos page 185px sideways. */
.tablewrap { position: relative; overflow-x: auto; }

.grid {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--t-cell);
  line-height: var(--lh-cell);
}

.grid caption { text-align: left; }

.grid thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  padding: var(--s2);
  text-align: left;
  white-space: nowrap;
  background: var(--panel);
  /* An inset shadow, not a border: `border-collapse: collapse` drops a sticky
     cell's own border the moment it detaches, and a head with no underline
     floating over rows is worse than no sticky head at all. */
  box-shadow: inset 0 -1px 0 var(--seam2);
}
.grid thead th.num { text-align: right; }
/* The column the table is ordered by. No caret glyph: this surface has no icon
   language, and inventing one for a sort arrow is a much bigger new primitive
   than a colour. The head's own inset underline is redrawn in the signal and
   the label goes to the foreground. This *is* the moment you are pointing at,
   because you asked for it in the URL; `aria-sort` carries it for the screen
   reader. */
.grid thead th[aria-sort] {
  color: var(--fg);
  box-shadow: inset 0 -1px 0 var(--accent);
}
.grid thead th[aria-sort] a { color: var(--fg); }
.grid thead a { text-decoration: none; color: inherit; }
.grid thead a:hover { color: var(--fg); text-decoration: underline; }

.grid tbody th, .grid tbody td {
  height: var(--row);
  padding: var(--s2);
  text-align: left;
  vertical-align: top;
  border-bottom: 1px solid var(--seam);
  font-weight: var(--w-body);
}
.grid tbody td.num { text-align: right; font-size: var(--t-machine); }
.grid tbody th[scope="row"] { font-weight: var(--w-strong); }

/* The row you are pointing at: one plate up, and a 1px gold edge in the gutter.
   The edge, not the fill, is what makes it readable at a glance. */
.grid tbody tr:hover {
  background: var(--plate);
  box-shadow: inset 1px 0 0 var(--accent);
}

.videos .col-shot { width: 96px; padding-right: 0; }
.videos .col-title { min-width: 16rem; }
.videos .row-meta { font-size: var(--t-machine-sm); }
.videos time { white-space: nowrap; }

/* ------------------------------------------------------------------ stages */

.stages .col-model { max-width: 22rem; }
.stages tr.is-bad th[scope="row"] code { color: var(--tone-bad); }
/* An `absent` stage is a dimmed rule, not a missing row: a stage with no
   `video_stages` row never ran, which is a different fact from a stage that ran
   and produced nothing — so the row stays, at the full seven, and recedes. The
   pill inside it still carries the word. */
.stages tr.is-absent th[scope="row"] code,
.stages tr.is-absent td { color: var(--muted); }

/* One line, directly under the header, naming the stage that failed. Not a
   `.notice`: a notice's 2px rule is the alarm on this surface and the failed
   stage already has one in the table below. */
.stage-alarm {
  margin: var(--s4) 0 0;
  max-width: var(--note);
  font-size: var(--t-cell);
  line-height: var(--lh-body);
}
.stage-alarm a { color: var(--accent); }

.stage-error td { padding-top: 0; border-bottom: 1px solid var(--seam); }
.stage-error .label { color: var(--tone-bad); }
.stage-error .errtext { font-size: var(--t-log); }

.pager { display: flex; gap: var(--s2); margin-top: var(--s4); }

/* ---------------------------------------------------------------- timeline */

/* The spine, and the first thing after the header: one bar per shot across the
   video's duration, positioned as a percentage of the runtime. Real data, so it
   earns the pixels — this is the video's cut structure, not a sparkline
   standing in for content. It is not a `.panel`: its label sits under it with
   the scale, where a film strip's footage counter goes. */
.timeband { position: relative; margin: 0; --timeline-h: 3rem; }
/* No rule under it: the next `.panel` brings its own hairline 32px later, and
   two rules that close together read as one thick rule that means nothing. */
.timeband-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s1) var(--s6);
  margin: var(--s2) 0 0;
}
.timeband-foot .panel-note { margin: 0; }

/* The hatch is the one mark here whose meaning is not written beside it, so the
   band carries a two-item key. Both swatches are literally the bars' own fills
   at bar scale, which is the only way a key is honest. */
.timeband-legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s1) var(--s4);
  margin: 0;
  color: var(--muted);
  font-size: var(--t-cell);
  white-space: nowrap;
}
.swatch {
  display: inline-block;
  width: var(--s6);
  height: var(--s3);
  margin-right: var(--s2);
  vertical-align: -2px;
}
.swatch-kept { background: var(--fg2); }
.swatch-dedup {
  background:
    repeating-linear-gradient(135deg, var(--mark) 0 2px, transparent 2px 6px),
    var(--console);
}

.timeline {
  position: relative;
  height: var(--timeline-h, 3rem);
  margin: 0;
  padding: 0;
  list-style: none;
  background: var(--console);
  border: 1px solid var(--seam2);
  overflow: hidden;
}

/* A shot can be two seconds of a two-hour talk. `min-width` keeps it visible
   rather than sub-pixel: the bar's *position* is the fact, and a mark you
   cannot see is a mark that lies about the cut structure. */
.shotbar { position: absolute; top: 0; bottom: 0; min-width: 3px; }

/* The bars are neutral and the one you are pointing at is gold. A band of sixty
   gold bars would be sixty moments you are pointing at, which is none. */
.shotbar a {
  display: block;
  height: 100%;
  background: var(--fg2);
  border-right: 1px solid var(--console);
  transition: background-color var(--t-fast);
}
.shotbar a:hover, .shotbar a:focus-visible { background: var(--accent); }
.shotbar:focus-within { z-index: 1; }

/* The timeline and the strip are two views of one thing; pointing at either end
   lights the other, which is the only way that link is visible at all.

   **The mark goes on the frame box, never on the `<img>` inside it** (found in
   review, 2026-08-10, round 4). `.framebtn` is `overflow: hidden` and its image
   fills it exactly, so an `outline-offset: 2px` on the image was drawn entirely
   outside the image's box — that is, entirely inside the clip — and every gold
   mark in this panel was invisible on every screen. An element's own outline is
   not clipped by its own `overflow`, so it belongs on the button. Three marks
   were affected and all three are below: this one, `:target`, and the evidence
   selection. */
.shotbar.is-linked a { background: var(--accent); }
.framecard.is-linked .framebtn { outline: 2px solid var(--accent); outline-offset: 2px; }

/* A shot whose every frame was deduplicated: hatched, so it reads as "captured,
   then dropped" and not as an empty stretch of video. The bar paints its own
   ground and hatches at full strength on top of it — a 0.4 hatch of `--mark`
   could never reach the 3:1 floor a non-text mark has to clear. */
.shotbar.is-dedup a {
  background:
    repeating-linear-gradient(135deg, var(--mark) 0 2px, transparent 2px 6px),
    var(--console);
}
.shotbar.is-dedup a:hover, .shotbar.is-dedup a:focus-visible {
  background:
    repeating-linear-gradient(135deg, var(--accent) 0 2px, transparent 2px 6px),
    var(--console);
}

/* ---- the scrub preview

   Point anywhere along the band and the shot under the pointer shows its own
   first keyframe. It reports the *shot*, not the pointer: the span and the kept
   ratio are index facts and read the same whether a mouse or the arrow keys got
   here, where a cursor-derived clock would be a number the index never wrote.

   `top: 0` is the band's own top edge and the transform lifts the box above it,
   so the preview never covers the bars it is describing. `pointer-events: none`
   because a box that follows the pointer must never be the thing the pointer is
   over. */
.scrubpreview {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 2;
  /* 12rem — the served frame is 192px, so the picture lands very slightly
     downscaled, which is the right direction to be wrong in. Fixed, because a
     preview that resizes with its content jumps as you sweep. */
  width: 12rem;
  padding: var(--s1);
  background: var(--plate);
  border: 1px solid var(--seam2);
  transform: translate(-50%, calc(-100% - var(--s2)));
  pointer-events: none;
}
.scrubpreview[hidden] { display: none; }
/* Scrolled until the band is near the top of the viewport there is no room
   above it, and a preview clipped by the window edge is worse than one under
   the scale. `--timeline-h` is the band's own height, so the two cannot drift. */
.scrubpreview.is-below {
  top: var(--timeline-h);
  transform: translate(-50%, var(--s2));
}

/* The box owns the geometry, so a keyframe still loading or gone missing leaves
   the ground behind and nothing moves. */
.scrubshot {
  display: block;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: var(--black);
  box-shadow: inset 0 0 0 1px var(--edge-in);
  line-height: 0;
}
.scrubshot img { width: 100%; height: 100%; object-fit: cover; }

/* Both lines are machine strings — a span the index wrote and a count — so both
   are tabular by construction. A span whose digits shift width as you sweep the
   band is the one thing a scrub preview must not do. */
.scrubspan,
.scrubmeta {
  display: block;
  margin-top: var(--s1);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.scrubspan { font-size: var(--t-machine); color: var(--fg); }
.scrubmeta { font-size: var(--t-machine-sm); color: var(--muted); }

/* The footage counter. Five marks at the quarters, each a hairline tick and the
   clock it stands at, positioned at its true percentage rather than spaced
   evenly by flexbox — a scale whose labels are only approximately where they
   say they are is worse than no scale. */
.timeline-scale {
  position: relative;
  height: 1.25rem;
  margin: var(--s1) 0 0;
  color: var(--muted);
  font-size: var(--t-machine-sm);
}

.tick {
  position: absolute;
  top: 0;
  transform: translateX(-50%);
  padding-top: var(--s2);
  white-space: nowrap;
}
.tick::before {
  content: "";
  position: absolute;
  top: 0;
  left: 50%;
  width: 1px;
  height: var(--s1);
  background: var(--mark);
}
/* The ends label the ends: a centred `0:00` would hang half of itself off the
   left of the band, and its tick belongs on the band's own edge. */
.tick:first-child { transform: none; }
.tick:first-child::before { left: 0; }
.tick:last-child { transform: translateX(-100%); }
.tick:last-child::before { left: 100%; }

/* ------------------------------------------------------------------ frames */

/* One grid for every keyframe on the page (Tom, 2026-08-10, round 4, item 2).
   It replaces `.strip` (a 9rem contact sheet of every frame) and `.ocrgrid` (a
   20rem grid of the frames with text), which by then drew the same frame twice
   at two sizes. The cell is the size a detection box needs to be pointed at
   rather than the size a thumbnail wants to be scanned at — this grid is read,
   not swept — and `align-items: start` so a card holding thirty OCR lines
   grows without stretching the three empty ones in its row. */
.frames {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
  gap: var(--s6) var(--s4);
  align-items: start;
  margin: 0;
  padding: 0;
  list-style: none;
}

.framecard { min-width: 0; scroll-margin-top: var(--s8); }

/* The frame a shot bar sent you to keeps a mark on it. `:target` rather than a
   click handler: the timeline's bars are real links to `#frame-N`, so the
   selection survives the navigation, the back button and a reload, and it costs
   no JavaScript. */
.framecard:target .framebtn {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.framecard:target .at { color: var(--gold-hi); }

/* The box, not the image, owns the geometry: a keyframe file that has gone
   missing leaves black where it was, and nothing below it moves.
   `position: relative` arrived with the merge — the detection boxes are drawn
   inside this button now, where `.ocrstage` used to hold them. */
.framebtn {
  position: relative;
  display: block;
  width: 100%;
  /* `height: auto` is load-bearing and was missing (found 2026-08-10, second
     pass). The chassis gives every `button` the 34px control height, a
     *definite* height beats an aspect ratio, and every frame in the strip was
     collapsing to a 34px letterbox — invisible in the fixture only because its
     JPEGs do not decode. A frame is not a control and does not take the
     control height. */
  height: auto;
  aspect-ratio: 16 / 9;
  padding: 0;
  overflow: hidden;
  background: var(--black);
  border: 0;
  /* Nor the control's type: the box holds a picture, and the alt text of a
     frame that failed to decode is a sentence, not a label. */
  font: inherit;
  letter-spacing: normal;
  text-transform: none;
  color: var(--fg2);
  box-shadow: inset 0 0 0 1px var(--edge-in);
  line-height: 0;
  cursor: zoom-in;
}
.framebtn:hover { color: var(--fg2); }

.framebtn img { width: 100%; height: 100%; object-fit: cover; transition: filter var(--t-fast); }

/* A deduplicated frame is dimmed, not hidden. Pointing at it brings it back:
   a filter change is the one thing hover is allowed to move. */
.framecard.is-dup .framebtn img { filter: brightness(.42) saturate(.7); }
.framecard.is-dup:hover .framebtn img { filter: none; }

.framemeta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s1);
  margin: var(--s1) 0 0;
  font-size: var(--t-machine-sm);
}

/* The timecode worth clicking — one of the five things gold means here. */
.at {
  font-size: var(--t-machine);
  color: var(--accent);
  text-decoration: none;
}
.at:hover { text-decoration: underline; }

.dupnote {
  color: var(--muted);
  border-bottom: 1px dashed var(--seam2);
}

/* --------------------------------------------------------------------- ocr */

/* The Lime Rule, and the frames grid is the whole of it on this surface:
   `--seen` marks what the machine read off the screen — the detection box, the
   line it points at, and the legend that names them. It is not a second accent
   and it appears on no other dashboard page.

   `.ocrgrid`, `.ocrframe` and `.ocrstage` went with the merge (round 4, item
   2). There is no second grid, no second card and no second still: the boxes
   are drawn inside `.framebtn`, which was already a fixed-aspect clipping box
   on `--black` behind a 1px inset hairline — which is exactly what
   `.ocrstage` was. */

.ocrbox {
  position: absolute;
  border: 1px solid var(--seen-24);
  transition: border-color var(--t-fast), background-color var(--t-fast);
}
/* In the grid the boxes are a drawing, not a target: at 512px a detection box
   is a few millimetres of screen, and a pointer aimed at one would only be
   stealing the click that opens the frame. The full-size interaction is the
   overlay's. */
.framebtn .ocrbox { pointer-events: none; }

/* Every line the machine read, in one continuous list, in a box that scrolls
   (Tom, 2026-08-10). The `+ N more` digest that used to bound this is gone, and
   with it the split that capped the box↔line linkage at whatever a stylesheet
   could enumerate. `--ocrbox-h` is ten lines: past that a slide is a document
   and the reader wants to scroll it, under it the box shrinks to fit and the
   panel keeps its near-uniform grid row. */
.ocrlines {
  margin: var(--s2) 0 0;
  padding: 0;
  list-style: none;
  max-height: var(--ocrbox-h);
  overflow-y: auto;
  overscroll-behavior: contain;
  font-size: var(--t-cell);
  line-height: var(--lh-cell);
}

.ocrline {
  display: flex;
  gap: var(--s2);
  align-items: baseline;
  padding: 2px 0 2px var(--s2);
  scroll-margin: var(--s2);
  /* One step louder than the box at rest: a 1px rule down the left of a text
     line has a fraction of a detection box's perimeter to be seen in, and this
     rule is what says "the machine read this off the screen" for a reader who
     never points at anything. */
  border-left: 1px solid var(--seen-48);
  transition: border-color var(--t-fast), background-color var(--t-fast);
}

/* Point at a line, light its box; point at a box, light its line. The pairing
   is by index — `data-line` on both — which is what makes it hold for *every*
   line rather than for the first eight a stylesheet could name as `:has()`
   pairs. `dashboard.js` toggles `.is-lit`; `:hover` is here as well so a
   pointer over a line or a box is lit whether or not the script ran.

   What is lost with the script blocked is the *pointing*, and only that: every
   line, its text, its confidence and its box are server-rendered and complete
   either way. The receipt does not depend on JavaScript; being able to aim at
   one end of it does. */
.ocrbox.is-lit,
.ocrbox:hover {
  border-color: var(--seen);
  border-width: 1.5px;
  background: color-mix(in srgb, var(--seen) 12%, transparent);
}
.ocrline.is-lit,
.ocrline:hover {
  border-left-color: var(--seen);
  background: color-mix(in srgb, var(--seen) 6%, transparent);
}

/* Selected into evidence: a shot bar was clicked and this is the moment it
   pointed at. Gold, because that is exactly "the moment you are pointing at" —
   and an outline rather than a fill, like the hovered table row.

   On the *box*, not on the image inside it: the box is `overflow: hidden` with
   the image filling it exactly, so an offset outline on the image was clipped
   by the box it sat in and painted nothing at all. See the note beside
   `.framecard.is-linked` — it was the same bug in three places. One selector
   now, not two, because one card is the whole of a frame. */
.framecard.is-selected .framebtn {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.framecard.is-selected .at { color: var(--gold-hi); }

.ocrtext { flex: 1 1 auto; min-width: 0; font-size: var(--t-machine); overflow-wrap: anywhere; }

.conf { flex: 0 0 auto; color: var(--muted); font-size: var(--t-machine-sm); }

/* Confidence inside the OCR digest, de-emphasised but never dropped: it is part
   of the receipt, and it is the one number a reader needs when a word looks
   wrong. A fixed basis so thirty of them read as a column; `align-self: start`
   so it sits against the *first* line of a wrapped entry, which is the line it
   is about. */
.ocrline > .conf { flex: 0 0 2.25rem; align-self: start; padding-top: 3px; text-align: right; }

/* -------------------------------------------------------------- transcript */

/* How big this transcript is, printed above the box (Tom, 2026-08-10, round
   4). It used to be where you *are* in it — `cues 1–150 of 1,203` — which the
   scrollbar was already saying, moved under the reader on every appended batch,
   and answered nothing a reader wanted. Totals do: the count the "What was
   stored" band already read, plus words and characters summed server-side with
   the chunk marker's own definition, so a chunk's `n words` and the video's
   `words` are the same kind of number. */
.cuepos {
  margin: 0 0 var(--s2);
  color: var(--muted);
  font-size: var(--t-machine-sm);
}
.cuepos .mono { color: var(--fg); font-size: var(--t-machine); }

/* The scrollbox. Bounded, because a four-hour talk is three thousand cues and
   the panels under this one are not worth burying; scrolled rather than paged,
   because the next batch arrives without throwing the strip, the OCR panel and
   the reader's place away. `overscroll-behavior: contain` so reaching the end
   of the transcript does not carry on and scroll the page. */
.cuebox {
  max-height: var(--cuebox-h);
  overflow-y: auto;
  overscroll-behavior: contain;
  border-top: 1px solid var(--seam);
  border-bottom: 1px solid var(--seam);
}

/* Shown by the appender while a real request is in flight, and hidden the
   moment one lands: the Motion Law's one sanctioned spinner-shaped thing, and
   it is a word rather than a spinner. */
.cueload {
  margin: 0;
  padding: var(--s2) 0 var(--s2) var(--s3);
  color: var(--muted);
  font-family: var(--font-mono);
  font-size: var(--t-label);
  font-weight: var(--w-label);
  letter-spacing: 0.19em;
  text-transform: uppercase;
}

.cues { margin: 0; padding: 0; list-style: none; }

.cue {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  align-items: baseline;
  padding: var(--s1) 0 var(--s1) var(--s3);
  border-left: 1px solid transparent;
  font-size: var(--t-cell);
}

/* The rule down the left is the chunk — the span of cues that became one
   embedding. */
.cue.in-chunk, .chunkmark { border-left-color: var(--seam); }

/* A chunk marker is a machine string — a sequence number, two timecodes and a
   character count — so it is mono, and it is not uppercased: a sentence-length
   run of text never goes through the label idiom. */
.chunkmark {
  margin-top: var(--s2);
  padding: 2px 0 2px var(--s3);
  border-left: 1px solid var(--seam);
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-variant-ligatures: none;
  font-size: var(--t-machine);
  color: var(--muted);
}

.cuetext { flex: 1 1 20rem; min-width: 0; }
.cue .speaker { font-size: var(--t-machine-sm); color: var(--muted); }

/* Three sources, three kinds of evidence, said in a word — the demo's own
   vocabulary, so a badge means the same thing on both surfaces. Not lime: a
   caption track is not something the machine read off the screen. */
.badge {
  display: inline-block;
  padding: 0 var(--s1);
  font-size: var(--t-label);
  font-weight: var(--w-label);
  letter-spacing: 0.19em;
  text-transform: uppercase;
  color: var(--muted);
  border: 1px solid var(--seam);
  vertical-align: 1px;
  white-space: nowrap;
}
.badge-screen { text-transform: none; letter-spacing: 0; border-style: dashed; }

/* -------------------------------------------------------------------- jobs */

/* Wider than it was, because the cell holds a conference title now rather than
   a fourteen-character handle (Tom, 2026-08-10, round 4). The id moved down to
   the meta line with the kind and the priority, where the other machine strings
   on this row already were. */
.jobs .col-title { min-width: 18rem; }
.jobs time { white-space: nowrap; }
/* The rest of the batch, counted. The label idiom rather than the title's,
   because "+9 more" is a count and not a name. */
.row-more {
  margin-left: var(--s2);
  color: var(--muted);
  font-family: var(--font-mono);
  font-size: var(--t-machine-sm);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.jobitems .col-title { min-width: 14rem; }

/* Progress as a rule, not a widget: the same hairline the tables are made of,
   filled to the fraction the job already computed, in the work tone — because
   what it reports is a machine working. The number beside it is the accessible
   answer; the bar is aria-hidden and adds nothing but a glance.

   **How this feels alive, and why none of it is decoration** (Tom, 2026-08-10:
   "don't add useless animation but it gotta feel alive"). Three real signals
   and no fourth:

   1. the fill's width transitions over `--poll-ms` — the *server's own poll
      interval*, written onto the root by `jobs.js` — so a stage that moved
      between two measurements is drawn advancing across the gap instead of
      snapping. The duration is a data cadence, not a chosen one;
   2. `.is-working` is set only while `state == running`, i.e. the runner has
      claimed the job and a stage is executing. It draws a bright cap on the
      fill's leading edge: static geometry, no animation, and it is simply
      absent on a queued, deferred, finished or failed job;
   3. the wall clock beside the row ticks up once a second while the job is
      live — "a counter ticking up: counts are counts", which is the Motion
      Law's own entry, and it is the thing a reader actually watches.

   Nothing pulses, nothing sweeps, there is no indeterminate stripe, and a job
   that is not running draws a bar that does not move — which is the honest
   picture of a machine that is not working. */
.meter {
  position: relative;
  display: inline-block;
  width: 4.5rem;
  height: 4px;
  vertical-align: 1px;
  overflow: hidden;
  background: var(--plate3);
}
/* On the job's own page the meter is the page's one figure of how far along
   this is, so it gets `--s2` of height rather than the table row's hairline. */
.meter-wide { width: 12rem; height: var(--s2); }
.meter-fill {
  display: block;
  height: 100%;
  background: var(--tone-work);
  transition: width var(--poll-ms, var(--t-state)) linear;
}
/* The head of the bar, and only while there is a machine at it. `--fg` rather
   than a second tone: this is the *edge* of the work tone, not a new state, and
   the pill beside it already prints the word `running`. */
.meter.is-working .meter-fill { box-shadow: inset -2px 0 0 var(--fg); }
.meter-label { margin-left: var(--s1); font-size: var(--t-machine); color: var(--muted); }

/* ---- the hint

   A percentage nobody can decompose is a number a reader has to trust (Tom,
   2026-08-10, round 4: "the progress % is unexplained"). The breakdown it is
   made of is on the row already — done, failed, skipped, cancelled, still to
   run — and what is not is the rule that turns them into one number, which is
   two sentences and does not belong printed on sixty rows.

   So: a plate, one step above the page, revealed by pointing at the figure or
   tabbing to it. No shadow — DESIGN.md gives this surface none, and a plate
   with a hairline against `--pitch` is already a step off the ground. No
   transition either: a thing that is either shown or not shown is not the
   machine working, and the Motion Law has nothing to say about it. The
   anchor takes `tabindex` and `aria-describedby`, so a keyboard reaches the
   same sentence a pointer does and a screen reader is told the figure has
   one. */
.hinted { position: relative; display: inline-block; }
.hint {
  position: absolute;
  top: calc(100% + var(--s1));
  left: 0;
  z-index: 2;
  display: none;
  width: max-content;
  max-width: 24rem;
  padding: var(--s2) var(--s3);
  color: var(--fg2);
  background: var(--plate);
  border: 1px solid var(--seam2);
  font-family: var(--font-mono);
  font-size: var(--t-machine-sm);
  font-variant-numeric: tabular-nums;
  line-height: var(--lh-cell);
  white-space: normal;
}
/* The last row is the one whose hint would fall out of the bottom of the
   table's own wrapper — which is a scroll container, so "out of" means
   "clipped". It opens upward instead, over the rows above it, which are inside
   the wrapper by construction. */
.grid tbody tr:last-child .hint { top: auto; bottom: calc(100% + var(--s1)); }
.hinted:hover > .hint,
.hinted:focus-within > .hint { display: block; }
/* The figure is a control now in one respect only — it can be tabbed to — so
   it says so the way everything else on this surface does: on hover, in the
   accent, with no geometry moving. */
.hinted > [tabindex]:hover, .hinted > [tabindex]:focus-visible { color: var(--accent); }
/* Both lines carry a fact, so both are `--fg2`: `--fg3` is atmosphere and
   DESIGN.md fences it out of anything a reader has to be able to read. What
   separates them is the gap, not the ink. */
.hint-line { display: block; }
.hint-line + .hint-line { margin-top: var(--s1); }

.progressline {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2);
}
.progressline .meter-label, .progressline .stale-note { margin-left: 0; }

/* The countdown — the highest-value line on that page, because "deferred for
   another 240s" was true and invisible on every surface vidtheque had. It is
   the same `--chip-h` as the pills it stands beside (Tom, 2026-08-10: it was
   26px against their 17px and the cell read as two kinds of object). What still
   makes it outrank them is what should: it is the only dashed border on the
   row, it is the warn tone, and it is the one marker in the cell set in
   readable machine text rather than in the tracked label idiom. */
.countdown {
  display: inline-block;
  height: var(--chip-h);
  line-height: calc(var(--chip-h) - 2px);
  padding: 0 var(--s2);
  font-size: var(--t-machine-sm);
  color: var(--tone-warn);
  background: var(--tone-warn-bg);
  border: 1px dashed var(--tone-warn-line);
  vertical-align: -0.3em;
  white-space: nowrap;
}

.stale-note { margin-left: var(--s3); font-size: var(--t-machine-sm); color: var(--muted); }

/* The event log. Machine text end to end, and the deferral timeline reads as a
   column of clocks. */
.events { margin: 0; padding: 0; list-style: none; }
.event {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s1) var(--s2);
  align-items: baseline;
  padding: var(--s1) 0;
  border-bottom: 1px solid var(--seam);
  font-size: var(--t-log);
  line-height: var(--lh-log);
}
.event:last-child { border-bottom: 0; }
/* The clocks are a column, not the first word of a sentence. At 03:00 the
   question is "when did the deferrals start", and the answer is the shape of
   the left edge. */
.event .at { flex: 0 0 9rem; color: var(--muted); }
/* The level pill is a column too, for the same reason. A *minimum*, not a fixed
   basis: a fixed one stretched the pill itself and made the one badge on the
   page that is 30px of word inside a 72px box read as a different primitive. */
.event > .pill { flex: 0 0 auto; min-width: 3.5rem; text-align: center; }
.eventtext { flex: 1 1 22rem; min-width: 0; font-size: var(--t-log); }
/* An event that arrived while the page was open. A left rule rather than a
   flash: it survives being scrolled past, which an animation does not. */
.event.is-new { margin-left: calc(var(--s2) * -1); padding-left: var(--s2); border-left: 1px solid var(--accent); }

/* -------------------------------------------------------------- write side */

/* Every control below is a real `<form method="post">` — the pages keep working
   with JavaScript off, which is why there is no fetch() anywhere in this
   surface's script. Nothing here is a new visual idea: the buttons are the
   buttons, the fields are the fields, the panels are panels under a hairline. */

/* ---- the row action cell (videos table) */

/* Two controls, on one line, at the row's own height. The column is narrow on
   purpose: a table of sixty rows with a wide action column reads as a control
   panel, and the row is still the thing you came for. */
.col-actions { white-space: nowrap; }
.col-actions > * { vertical-align: middle; }
.rowform { display: inline; margin: 0; }
/* Both controls sit one step down from the 34px chassis height, because two of
   them inside a 34px row would touch its rules. */
.col-actions button, .col-actions .ghostlink {
  height: var(--s6);
  padding: 0 var(--s2);
  font-size: var(--t-label-sm);
  letter-spacing: 0.17em;
}
.col-actions .ghostlink { margin-left: var(--s1); }

/* ---- fields the read-only surface never needed */

/* The paste box is machine text and is set as machine text: JetBrains Mono was
   picked for one measurable property — it draws 0/O and 1/l/I apart — and a
   column of YouTube ids like `kCc8FmEb1nY` is precisely the string that
   property was bought for. */
.field textarea {
  /* Four table rows tall — the paste box is the page's subject and it should
     look like it holds a list, not like an input that grew. */
  min-height: calc(var(--row) * 4);
  padding: var(--s3);
  font-family: var(--font-mono);
  font-size: var(--t-machine);
  font-variant-numeric: tabular-nums;
  font-variant-ligatures: none;
  line-height: var(--lh-body);
  color: var(--fg);
  background: var(--console);
  border: 1px solid var(--seam2);
  resize: vertical;
}
.field textarea::placeholder { color: var(--fg3); opacity: 1; }
.field textarea:hover { border-color: var(--gold-44); }
.field :disabled { color: var(--fg3); cursor: default; }

/* The sentence under a control, in body voice. Not a `title` and not a
   placeholder: both of those are text you have to go and find, and this is the
   half of the contract the label has no room for. */
.field-help {
  margin: var(--s2) 0 0;
  max-width: var(--prose);
  color: var(--muted);
  font-size: var(--t-cell);
  line-height: var(--lh-body);
}

/* ---- checkbox rows */

/* Not a fieldset with a box: the group is a label and its rows, and the
   browser's own border is removed. */
.checks {
  margin: var(--s5) 0 0;
  padding: 0;
  border: 0;
}
.checks > legend { padding: 0; }

.check {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 0 var(--s2);
  align-items: baseline;
  padding: var(--s2) 0;
  border-bottom: 1px solid var(--seam);
  cursor: pointer;
}
.check:last-child { border-bottom: 0; }
.check input {
  grid-row: 1 / 3;
  align-self: center;
  width: var(--s4);
  height: var(--s4);
  accent-color: var(--accent);
}
.check-word { font-size: var(--t-cell); font-weight: var(--w-strong); }
.check-note { color: var(--muted); font-size: var(--t-cell); line-height: var(--lh-cell); }

/* ---- the index form */

/* One column, measured. A form is prose with controls in it — the reader goes
   down it once, top to bottom — so it takes `--prose` like every other sentence
   here, and not the full-bleed width the scanning grids take. */
.indexform { margin-top: var(--s3); max-width: var(--prose); }
/* One wrapping row of controls, the filter bar's metrics without the filter
   bar's ground: this is the page's subject, not a strip above a table. */
.formrow {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--s3) var(--s4);
  margin-top: var(--s5);
}
.indexform .field-actions { margin-top: var(--s5); }

/* ---- the receipt */

.receipt .joblist { margin: var(--s3) 0 0; padding: 0; list-style: none; }
.receipt .joblist > li {
  display: flex;
  flex-wrap: wrap;
  gap: 0 var(--s3);
  align-items: baseline;
  padding: var(--s2) 0;
  border-top: 1px solid var(--seam);
}
.receipt .empty-lead { margin-top: var(--s4); }

/* ---- sign-in */

/* The one page here with a single job. The panel keeps the full-width hairline
   every other panel has — a rule that stops two thirds of the way across reads
   as an unfinished page — and the *content* is capped, which is the rule
   everywhere else: prose is measured, grids are not. */
.signin-form {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--s3) var(--s4);
  margin-top: var(--s4);
  max-width: var(--prose);
}
/* A password field is a short string and should look like one. */
.signin-form .field-wide { flex: 0 1 20rem; }
.signin-error {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s1) var(--s2);
  align-items: baseline;
  margin: var(--s3) 0 0;
  font-size: var(--t-cell);
}

/* ---- the manage panel (video detail) */

.manage-action > .label { display: block; margin: 0 0 var(--s2); }
.manage-action > .rowform { display: block; margin-top: var(--s3); }
.tagform {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: var(--s3) var(--s4);
  margin-top: var(--s3);
}
.manage .split > .manage-action:nth-child(2) { margin-top: 0; }

/* ----------------------------------------------------------------- notices */

/* Not a card: a band under a rule thick enough to be the loudest thing on the
   page, in the tone of what went wrong. */
.notice {
  margin-top: var(--s6);
  padding: var(--s3) 0 var(--s4);
  border-top: 2px solid var(--tone-line, var(--seam2));
}
.notice-bad { --tone: var(--tone-bad); --tone-line: var(--tone-bad); }
.notice-title {
  margin: 0;
  font-size: var(--t-cell);
  font-weight: var(--w-strong);
  color: var(--tone, var(--fg));
}
/* Sans, not mono. This paragraph is a sentence a human wrote about what went
   wrong; the machine's contribution to it — the `error_code`, the tool name,
   the env var — arrives inside `<code>` or `.errtext` and is mono there. */
.notice-detail {
  margin: var(--s2) 0 0;
  max-width: var(--note);
  font-size: var(--t-cell);
  color: var(--muted);
  overflow-wrap: anywhere;
}
.notice-next { margin: var(--s2) 0 0; max-width: var(--note); font-size: var(--t-cell); }

/* ---------------------------------------------------------------- lightbox */

/* The one thing on this surface that has been lifted off the wall, so it is the
   one thing that casts a shadow. */
.shot {
  width: min(96vw, 72rem);
  max-width: min(96vw, 72rem);
  padding: 0;
  color: var(--fg);
  background: var(--plate);
  border: 1px solid var(--seam2);
  box-shadow: 0 40px 90px -24px rgba(0, 0, 0, .94);
}

.shot::backdrop { background: var(--scrim); }

.shot-inner { padding: var(--s3); }

/* The caption and the close control belong above the picture, not on top of it:
   the top-right corner of a frame is where a slide puts its title. */
.shot-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s3);
  margin-bottom: var(--s2);
}

/* The stage owns the geometry, so a box can never land outside the picture and
   the dialog does not resize under the pointer while the bytes arrive. */
.shot-stage {
  position: relative;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  line-height: 0;
  background: var(--black);
  box-shadow: inset 0 0 0 1px var(--edge-in);
}

.shot-img { display: block; width: 100%; height: 100%; object-fit: contain; }

.shot-boxes { position: absolute; inset: 0; }
.shot-boxes .ocrbox { border-color: var(--seen); border-width: 1.5px; }
.shot-boxes[hidden] { display: none; }
/* Here the boxes *are* targets — at 1152px a detection box is something a
   pointer can find, which is the whole reason the two-way interaction lives in
   the overlay rather than over a 512px thumbnail in the grid. */
.shot-boxes .ocrbox { pointer-events: auto; cursor: default; }

/* The frame's lines, beside the frame, at a size you can read a slide off. Two
   columns where there is room: a transcription of a dense slide is thirty short
   strings and one long column of them would push the picture off the screen.

   **A grid, not multicol** (Tom, 2026-08-10, review round 4). `columns: 2` on a
   box with a bounded height does not give two columns and a vertical scroll: it
   paginates the overflow *sideways*, so a 189-line slide measured 15,501px wide
   inside a 1,126px scrollport and the box a reader could see was one screenful
   of a horizontal filmstrip. Pointing at a detection box then slid the whole
   list under the pointer, which is the bug that got reported. A grid overflows
   down, which is the direction this box has always claimed to scroll. The cost
   is stated: the lines read left-to-right and then down rather than down the
   first column and then the second. `overflow: hidden auto` is belt and braces
   — nothing here can overflow horizontally (`.ocrtext` wraps anywhere), and if
   anything ever does it must not become a sideways scroll again. */
.shot-lines {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
  gap: 0 var(--s6);
  align-content: start;
  margin: var(--s3) 0 0;
  padding: 0;
  list-style: none;
  max-height: 11rem;
  overflow: hidden auto;
  overscroll-behavior: contain;
  font-size: var(--t-cell);
  line-height: var(--lh-cell);
}
.shot-lines > .ocrline { min-width: 0; }

.shot-foot {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--s2) var(--s4);
  margin-top: var(--s2);
}

.shot-caption { margin: 0; font-size: var(--t-machine); }
.shot-toggle {
  display: flex;
  gap: var(--s2);
  align-items: center;
  color: var(--muted);
  font-family: var(--font-mono);
  font-size: var(--t-label);
  font-weight: var(--w-label);
  letter-spacing: 0.19em;
  text-transform: uppercase;
}
.shot-toggle input { accent-color: var(--seen); }
.shot-link { color: var(--accent); font-size: var(--t-machine); text-decoration: none; }
.shot-link:hover { text-decoration: underline; }

.shot-close { flex: 0 0 auto; }

/* ------------------------------------------------------------------ footer */

footer {
  margin-top: var(--s12);
  padding-top: var(--s5);
  border-top: 1px solid var(--seam);
  font-size: var(--t-machine-sm);
  color: var(--muted);
}
footer p { margin: 0; max-width: var(--note); }

/* The signature: the same lockup as the rail, once more at the foot of the
   page, so the surface is signed rather than merely credited. It stays at rail
   scale — the system's large footer signature is a landing-page gesture, and
   this surface's whole brief is that it does not make gestures. The version
   sits beside it in mono, because a version is a machine string and a name is
   not: the two-channel rule, at 1:1 scale, on the brand. */
.footline {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 var(--s3);
  margin: 0 0 var(--s1);
}
.footline .sep { padding: 0; }
.fmark {
  font-family: var(--font-sans);
  font-size: var(--brand-size);
  font-weight: var(--brand-weight);
  line-height: var(--lh-tight);
  letter-spacing: var(--brand-track);
  color: var(--fg);
}
.fmark i { font-style: normal; color: var(--accent); }

/* -------------------------------------------------------------- 4. widths */

/* Two of the system's breakpoints and one of the dashboard's own, which is the
   allowance DESIGN.md gives a surface for a real structural change. In order:
   `--bp-stack` 1120px (the rail becomes a strip, the zones stack), the
   dashboard's 52rem (a table stops being a table), `--bp-hand` 780px (chrome
   tightens for a hand). */

@media (max-width: 70rem) {
  /* 1120px — `--bp-stack`. There is no room for 240px of navigation beside the
     content, so the rail stops being a column and becomes a strip across the
     top: the word, a wrapping row of links with the current one underlined, and
     the deployment state on its own line. No drawer, no scrim, no checkbox — a
     nav of three items that fits on one line has nothing to gain from being
     hidden behind a control, and the control is the part of that pattern that
     costs the keyboard and the screen reader. */
  /* `auto 1fr`, explicitly: with both rows implicit the browser hands each a
     share of the leftover viewport height, which opened a 107px hole between
     the strip and the page title on a short page. */
  .shell { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto 1fr; }

  .rail {
    position: static;
    height: auto;
    overflow: visible;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--s2) var(--s6);
    padding: var(--s5) var(--s4) var(--s3);
    border-right: 0;
    border-bottom: 1px solid var(--seam);
  }

  .rail-brand { padding: 0; border-bottom: 0; }
  .nav { padding: 0; }
  .nav-group { display: none; }

  .navlist { display: flex; flex-wrap: wrap; gap: 0 var(--s5); }
  .navlist a {
    padding: 0 0 var(--s1);
    border-bottom: 2px solid transparent;
  }
  .navlist a:hover { background: transparent; }
  .navlist a[aria-current="page"] {
    background: transparent;
    box-shadow: none;
    border-bottom-color: var(--accent);
  }

  .rail-foot { margin: 0 0 0 auto; padding: 0; border-top: 0; }
  .deployment { flex-direction: row; flex-wrap: wrap; gap: 0 var(--s3); }
  /* In the strip the foot is a row of facts, so its one control stops being a
     full-bleed rail item and goes back to being a button. */
  .signout { margin: var(--s2) 0 0; }
  .signout button { width: auto; }
  .deployment-why { margin-top: var(--s1); }

  .split { grid-template-columns: minmax(0, 1fr); gap: 0; }
  .manage .split > .manage-action:nth-child(2) { margin-top: var(--s6); }

  /* Five ledger columns stop fitting: two rows of the same band, not five
     stacked cards. */
  .ledger { grid-template-columns: repeat(3, 1fr); }
  .ledger > .figure:nth-child(3n + 1) { border-left: 0; }
  .ledger > .figure:nth-child(n + 4) { border-top: 1px solid var(--seam); }
}

@media (max-width: 52rem) {
  /* The dashboard's own breakpoint. Each row becomes its own stacked block with
     the column name in front of the value, which is the only shape that fits
     eight columns on a 375px screen without a horizontal scroll. */
  /* The head goes away entirely rather than into a 1px visually-hidden box:
     once `display: block` has taken the table apart there is no table left for
     a screen reader to associate a `<th scope="col">` with, and every cell in
     the stack prints its own column name. Sorting is not lost with it — `order`
     is a select in the filter bar above, at every width. */
  :is(.grid.videos, .grid.jobs) thead { display: none; }
  :is(.grid.videos, .grid.jobs),
  :is(.grid.videos, .grid.jobs) tbody,
  :is(.grid.videos, .grid.jobs) tr,
  :is(.grid.videos, .grid.jobs) th,
  :is(.grid.videos, .grid.jobs) td { display: block; }

  :is(.grid.videos, .grid.jobs) tr {
    padding: var(--s3) 0;
    border-bottom: 1px solid var(--seam);
  }
  :is(.grid.videos, .grid.jobs) tr:hover { background: none; box-shadow: none; }

  :is(.grid.videos, .grid.jobs) th, :is(.grid.videos, .grid.jobs) td {
    height: auto;
    padding: 1px 0;
    border-bottom: 0;
  }

  /* The value group wraps. Without this the jobs row put `queued`,
     `E_RATE_LIMIT` and the countdown on one unwrappable line beside a fixed
     label, and at 390 the countdown — the single line this page exists for —
     was pushed clean off the right edge of its own cell. */
  :is(.grid.videos, .grid.jobs) td[data-label]:not(.col-shot) {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s1) var(--s2);
    align-items: baseline;
  }
  /* A wrapped value starts a new flex line at the cell's left edge, so it is
     indented to the column the labels leave and the values stay a column. */
  :is(.grid.videos, .grid.jobs) td[data-label] > .countdown {
    flex: 0 0 auto;
    margin-left: calc(var(--stacked-label) + var(--s2));
  }
  :is(.grid.videos, .grid.jobs) td[data-label]:not(.col-shot)::before {
    content: attr(data-label);
    flex: 0 0 var(--stacked-label);
    font-family: var(--font-mono);
    font-size: var(--t-label);
    font-weight: var(--w-label);
    text-transform: uppercase;
    letter-spacing: 0.19em;
    color: var(--muted);
  }
  :is(.grid.videos, .grid.jobs) td.num { text-align: left; }
  .grid.videos .col-shot { width: auto; margin-bottom: var(--s1); }
  :is(.grid.videos, .grid.jobs) .col-title { min-width: 0; margin-bottom: var(--s1); }

  /* The other grids stay tables and scroll inside their own wrapper: a
     five-column provenance table squeezed into 375px breaks `text_embed` across
     three lines, which is worse than a swipe, and the items table carries a
     colspan error row that stacking would tear off its own item. The page body
     still never scrolls sideways; the wrapper does. */
  .grid:not(.videos, .jobs) { min-width: 34rem; }
}

@media (max-width: 48.75rem) {
  /* 780px — `--bp-hand`. */
  .col { padding: 0 var(--s4) calc(var(--s8) + env(safe-area-inset-bottom)); }

  .rail { gap: var(--s2) var(--s4); }
  .rail-foot { margin-left: 0; flex-basis: 100%; }
  .mark b, .fmark { font-size: 15px; }

  .pagehead { padding: var(--s5) 0 var(--s3); }
  .pagehead h1 { font-size: var(--t-question); font-weight: 250; letter-spacing: -0.028em; }
  .panel { margin-top: var(--s6); }

  /* Two columns, and the fifth figure takes the full width rather than sitting
     alone next to a hole. */
  .ledger { grid-template-columns: repeat(2, 1fr); }
  .ledger > .figure { padding: var(--s4); }
  .ledger > .figure:nth-child(odd) { border-left: 0; }
  .ledger > .figure:nth-child(n + 3) { border-top: 1px solid var(--seam); }
  .ledger > .figure:last-child:nth-child(odd) { grid-column: 1 / -1; }

  .figures { grid-template-columns: repeat(auto-fit, minmax(7.5rem, 1fr)); gap: var(--s4) var(--s5); }

  /* The row's far-end fact wraps to its own line and lines up under the title
     rather than squeezing it: at 375px a clock on the right turns a four-word
     title into five lines. */
  .row-when {
    flex: 1 1 100%;
    margin: 2px 0 0;
    padding-left: calc(96px + var(--s3));
    text-align: left;
  }

  .filters { padding: var(--s3); }
  .field-wide { flex-basis: 100%; }
  /* On a hand the fixed widths become fractions of the row rather than
     absolute ones: 13rem is wider than the viewport. Two per row for the text
     fields and the pickers, which is the same shape the ranges already take. */
  .field-text, .field-pick { flex: 1 1 9rem; }
  .field-actions { margin-left: 0; flex-basis: 100%; }
  .field-actions button { flex: 1 1 auto; }

  /* One column on a hand: a card whose still carries detection boxes and whose
     lines are a scrollbox does not halve. */
  .frames { grid-template-columns: 1fr; }

  .shot { width: 96vw; max-width: 96vw; }
}

/* --------------------------------------------------------------- 5. motion */

/* The Motion Law: movement only where the machine is working. The whole
   inventory of this surface is a hover changing a colour at `--t-fast` and a
   progress meter's width advancing as a real job advances — there is no pulse,
   no drift, no entrance, no spinner and no hover transform anywhere above.

   Freezing means painting the end state, never dropping the content. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
