/* ==========================================================================
   CineScope — themes

   Seven themes, one stylesheet. Nothing below this block names a colour: every
   rule reads a SEMANTIC token (--bg, --surface, --text, --accent, --danger …),
   and those tokens are derived once, here, from twenty-odd PALETTE SLOTS
   (--p-*). A theme is then just those slots filled in.

   That is what keeps a new theme to one short block instead of an audit of two
   thousand lines, and it is why the slots are named --p-* rather than after any
   one palette: "--ctp-teal: #88C0D0" under Nord would be a lie.

   The slots borrow Catppuccin's shape (crust..text for surfaces, then named
   hues) because every palette here has an equivalent for each, and having one
   fixed shape is what lets the derivations below be written once.

   Applied as <html data-theme="…"> by /theme.js, which is a blocking script in
   every page's <head> so the correct theme is on the element before first paint
   — a themed page that flashes the wrong colours first is worse than one theme.

   The one exception is the printed QR placard, which stays black on white
   whatever the theme, because it goes on paper. See "printed placards".
   ========================================================================== */

:root {
  /* Premium pass: the light a glass surface catches on its top edge. */
  --rim: inset 0 1px 0 color-mix(in srgb, white 7%, transparent);
  /* ---------------------------------------------------------------- shared */
  --radius-lg: 16px;
  --radius: 12px;
  --radius-sm: 9px;

  --font:
    ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, Roboto, Helvetica, Arial,
    sans-serif;
  --mono: ui-monospace, SFMono-Regular, 'SF Mono', 'JetBrains Mono', Menlo, Consolas, monospace;

  --ease: cubic-bezier(0.32, 0.72, 0, 1);

  /*
   * For things leaving, which is not the same curve as things arriving.
   *
   * `--ease` above is strongly front-loaded — it covers most of its distance in the first
   * third, which is exactly right for something appearing, because the eye reads the fast
   * start as responsiveness and the slow settle as weight. Run backwards it is wrong: a
   * pop-up closing on it was at 11% opacity two frames in and then spent a tenth of a
   * second being invisible. Leaving should start gently and accelerate away.
   */
  --ease-out: cubic-bezier(0.4, 0, 0.9, 1);

  /*
   * For something appearing from nothing, where the movement has to be *seen*.
   *
   * `--ease` is wrong for this and measurably so. It is front-loaded hard — a quarter of the
   * way through its duration it has covered four fifths of its distance — which is right for
   * an element already on screen that should feel responsive to a press, and wrong for one
   * arriving: the box was at 80% opacity on the first frame anybody could see it, so it read
   * as having simply been there. This spreads the distance across the duration instead, so
   * the middle of the animation is the middle of the movement.
   */
  --ease-arrive: cubic-bezier(0.22, 0.34, 0.28, 1);

  /*
   * The reticle's pupil. The one colour in this stylesheet that is not a theme
   * slot, because it is not decoration — it is the brand, and a mark that
   * changed colour with the theme would not be one. Every other part of the
   * wordmark takes currentColor and recolours freely around it.
   */
  --brand-pupil: #d9922b;
}

/* ----------------------------------------------------------------- semantic
   Written once in terms of the slots, so a theme sets colours and inherits every
   relationship between them. color-mix rather than more slots: the step from a
   surface to its border is a property of the design, not of the palette, and
   should not be rcircuitd six times.

   Also applied to [data-preview], the theme picker's swatches. A custom property
   is substituted where it is DECLARED, so --bg computed on :root keeps :root's
   crust no matter how a descendant redefines --p-crust — the derivations have to
   be re-declared on the card for its slots to reach them. */
:root,
[data-preview] {
  --bg: var(--p-crust);
  --bg-tint: radial-gradient(1200px 600px at 50% -10%, var(--p-tint) 0%, var(--p-crust) 60%);
  --surface: var(--p-mantle);
  --surface-2: var(--p-base);
  --surface-3: color-mix(in oklab, var(--p-base) 80%, var(--p-surface0));
  --border: color-mix(in oklab, var(--p-base) 58%, var(--p-surface0));
  --border-strong: var(--p-surface1);

  --text: var(--p-text);
  /* Slots of their own rather than subtext0 and overlay1 directly, so a theme can set how quiet
     its quiet text is without recolouring what else draws from those two. See Light. */
  --text-dim: var(--p-dim);
  --text-faint: var(--p-faint);

  --accent: var(--p-accent);
  --accent-hover: color-mix(in oklab, var(--p-accent) 80%, var(--p-text));
  /* Solid fills use a deepened accent: a palette's accent is usually tuned for
     text on the background, and glares when it becomes the background itself. */
  --accent-fill: var(--p-accent-fill);
  --accent-fill-hover: color-mix(in oklab, var(--p-accent-fill) 86%, var(--p-text));
  --accent-ink: var(--p-accent-ink); /* text on top of an accent fill */
  --accent-soft: color-mix(in oklab, var(--p-accent) 13%, var(--p-crust));
  --accent-line: linear-gradient(90deg, var(--p-accent-fill), var(--p-pink));
  --focus: var(--p-lavender);

  --ok: var(--p-green);
  --ok-soft: color-mix(in oklab, var(--p-green) 13%, var(--p-crust));
  --warn: var(--p-yellow);
  --warn-soft: color-mix(in oklab, var(--p-yellow) 13%, var(--p-crust));
  --danger: var(--p-red);
  --danger-soft: color-mix(in oklab, var(--p-red) 13%, var(--p-crust));
  --info: var(--p-blue);
  --info-soft: color-mix(in oklab, var(--p-blue) 13%, var(--p-crust));

  /* Depth comes from a hairline plus a soft drop, not a hard shadow. Both the
     hairline and the drop are slots because a light theme needs a white
     hairline and a far weaker drop than a dark one. */
  /*
   * THE GLASS, named once: the dock's material, promoted to the app's own. Two weights —
   * ordinary surfaces wear --glass, floating chrome (menus, dialogs, sticky bars) wears
   * --glass-strong with a backdrop blur where content genuinely passes beneath. Buttons and
   * cards take the mix WITHOUT blur on purpose: over the app's flat ground the translucency
   * alone gives the look, and a backdrop filter on every card in a long queue is a frame
   * budget spent on nothing a reader can see.
   */
  --glass: color-mix(in srgb, var(--surface-3) 55%, transparent);
  --glass-strong: color-mix(in srgb, var(--surface-3) 80%, transparent);
  --shadow: 0 1px 0 var(--p-hairline) inset, 0 10px 28px -12px var(--p-shadow);
  --shadow-lift: 0 1px 0 var(--p-hairline) inset, 0 18px 40px -16px var(--p-shadow-strong);
}


/* ------------------------------------------------------------------ Dark
   A neutral dark grey with a blue accent, for anyone who wants dark without a
   palette's opinions. Deliberately not a variation on Mocha — offering both
   only earns its place if they look meaningfully different.

   Also the bare :root, which makes it what the page renders with when no
   data-theme has been set at all — so the stylesheet's fallback and
   DEFAULT_THEME in src/db.js are the same look. The attribute selectors are
   more specific, so any theme that IS set still wins over this. */
:root,
:root[data-theme='dark'],
[data-preview='dark'] {
  color-scheme: dark;

  --p-crust: #0e0e11;
  --p-mantle: #16161a;
  --p-base: #1c1c21;
  --p-surface0: #2a2a31;
  --p-surface1: #3a3a44;
  --p-surface2: #4c4c58;
  --p-overlay0: #6a6a78;
  --p-overlay1: #8a8a99;
  --p-subtext0: #b4b4c0;
  --p-subtext1: #d0d0da;
  --p-text: #e8e8ef;
  --p-dim: #b4b4c0;
  --p-faint: #8a8a99;

  --p-pink: #e79ec8;
  --p-red: #f2777a;
  --p-peach: #f0a45d;
  --p-yellow: #e8d07a;
  --p-green: #8fd18a;
  --p-teal: #7fd0c4;
  --p-blue: #8ab4f8;
  --p-lavender: #a9b6f0;

  --p-accent: #8ab4f8;
  --p-accent-fill: #5b8dd9;
  --p-accent-ink: #0b1220;

  --p-tint: #17171d;
  --p-hairline: rgba(255, 255, 255, 0.05);
  --p-shadow: rgba(0, 0, 0, 0.7);
  --p-shadow-strong: rgba(0, 0, 0, 0.82);

  /* What the page fades into under the phone's dock — see the veil on body::after.
     Black on a dark theme and white on a light one: a shade on the first, a mist on
     the second, where black read as soot along the bottom of a daylight page. */
  --p-veil: #000000;
}

/* ------------------------------------------------------------------ Light
   A proven, contrast-checked light palette rather than one invented here, with
   one change of its own: the accent is the palette's BLUE, not its mauve, so
   Light is Dark in daylight rather than a second purple theme. Picking Light
   should change the time of day, not the accent colour.

   Two things invert. The page sits on Crust, the DARKEST of the three light
   surfaces, so cards on Mantle read as raised rather than sunken. And a solid
   accent fill needs white text on it rather than near-black. */
:root[data-theme='light'],
[data-preview='light'] {
  color-scheme: light;

  --p-crust: #dce0e8;
  --p-mantle: #e6e9ef;
  --p-base: #eff1f5;
  --p-surface0: #ccd0da;
  --p-surface1: #bcc0cc;
  --p-surface2: #acb0be;
  --p-overlay0: #9ca0b0;
  --p-overlay1: #8c8fa1;
  --p-subtext0: #6c6f85;
  --p-subtext1: #5c5f77;
  --p-text: #4c4f69;

  /* The two quieter text levels, darker than Latte's own subtext0 and overlay1. As shipped
     those read at 4.1:1 and 2.6:1 on a card, and 3.7:1 and 2.4:1 on the page itself: the
     hints under fields, the notes and the captions that carry most of this app's small text,
     and the operator found them hard to read. Now 5.1:1 and 4.6:1 on a card and 4.7:1 and
     4.2:1 on the page, with the three levels still apart (text itself is 6.6:1 on a card).
     The dim level is Latte's own subtext1, and faint is halfway between subtext0 and it. */
  --p-dim: #5c5f77;
  --p-faint: #64677e;

  --p-pink: #ea76cb;
  --p-red: #d20f39;
  --p-peach: #fe640b;
  --p-yellow: #df8e1d;
  --p-green: #40a02b;
  --p-teal: #179299;
  --p-blue: #1e66f5;
  --p-lavender: #7287fd;

  /* Deeper than the palette's own blue (#1e66f5, kept above for --info): that one
     only reaches 4.0:1 as text on a card, and --accent IS text here — links, the
     disclosure toggles, the "set to light right now" line. This clears 5:1, and
     white on it as a solid fill clears 6:1. */
  --p-accent: #1657d8;
  --p-accent-fill: #1657d8;
  --p-accent-ink: #ffffff;

  --p-tint: #eceef4;
  --p-hairline: rgba(255, 255, 255, 0.9);
  --p-shadow: rgba(76, 79, 105, 0.16);
  --p-shadow-strong: rgba(76, 79, 105, 0.22);
  --p-veil: #ffffff;
}

/* ------------------------------------------------------------- Your brand
   The one theme whose colours are not written here.

   A circuit picks ONE hex in Settings, and the blocking theme script writes four
   tokens onto the root from it — the fill as picked, an ink that can be read on
   that fill, a text-legible version of the same hue, and a tint (see brandTokens
   in js/brand-colour.js, which is also what the picker previews with).

   EVERYTHING ELSE IS BORROWED. The neutrals are Dark's or Light's, chosen beside
   the colour, because a picker over all eleven would let a cinema build a page
   nobody can read — and "match our brand" means the colour on the buttons, not a
   bespoke greyscale. The two blocks below are the borrowing, keyed on the base
   the script also writes; the accent slots then take the customer's tokens.

   The var() fallbacks are what a circuit that has picked NO colour gets: the
   ordinary Dark or Light accent, so selecting this theme before setting a colour
   is a no-op rather than a page of black buttons. */
:root[data-theme='custom'][data-brand-base='light'],
[data-preview='custom'][data-brand-base='light'] {
  color-scheme: light;

  --p-crust: #dce0e8;
  --p-mantle: #e6e9ef;
  --p-base: #eff1f5;
  --p-surface0: #e0e3ea;
  --p-surface1: #ccd0da;
  --p-surface2: #bcc0cc;
  --p-overlay0: #9ca0b0;
  --p-overlay1: #8c8fa1;
  --p-subtext0: #6c6f85;
  --p-subtext1: #5c5f77;
  --p-text: #4c4f69;
  --p-dim: #5c5f77;
  --p-faint: #64677e;

  --p-pink: #ea76cb;
  --p-red: #d20f39;
  --p-peach: #fe640b;
  --p-yellow: #df8e1d;
  --p-green: #40a02b;
  --p-teal: #179299;
  --p-blue: #1e66f5;
  --p-lavender: #7287fd;

  --p-accent: var(--brand-accent, #1e66f5);
  --p-accent-fill: var(--brand-accent-fill, #1e66f5);
  --p-accent-ink: var(--brand-accent-ink, #ffffff);

  --p-tint: var(--brand-tint, #e6e9ef);
  --p-hairline: rgba(255, 255, 255, 0.9);
  --p-shadow: rgba(76, 79, 105, 0.16);
  --p-shadow-strong: rgba(76, 79, 105, 0.22);
  --p-veil: #ffffff;
}

:root[data-theme='custom'],
[data-preview='custom'] {
  color-scheme: dark;

  --p-crust: #0e0e11;
  --p-mantle: #16161a;
  --p-base: #1c1c21;
  --p-surface0: #2a2a31;
  --p-surface1: #3a3a44;
  --p-surface2: #4c4c58;
  --p-overlay0: #6a6a78;
  --p-overlay1: #8a8a99;
  --p-subtext0: #b4b4c0;
  --p-subtext1: #d0d0da;
  --p-text: #e8e8ef;
  --p-dim: #b4b4c0;
  --p-faint: #8a8a99;

  --p-pink: #e79ec8;
  --p-red: #f2777a;
  --p-peach: #f0a45d;
  --p-yellow: #e8d07a;
  --p-green: #8fd18a;
  --p-teal: #7fd0c4;
  --p-blue: #8ab4f8;
  --p-lavender: #a9b6f0;

  --p-accent: var(--brand-accent, #8ab4f8);
  --p-accent-fill: var(--brand-accent-fill, #5b8dd9);
  --p-accent-ink: var(--brand-accent-ink, #0b1220);

  --p-tint: var(--brand-tint, #17171d);
  --p-hairline: rgba(255, 255, 255, 0.05);
  --p-shadow: rgba(0, 0, 0, 0.7);
  --p-shadow-strong: rgba(0, 0, 0, 0.82);
  --p-veil: #000000;
}

/* ----------------------------------------------------------------- System
   Not a palette: the device's own light/dark setting, resolved to Light or Dark
   by these two media queries.

   No JavaScript and no request — which means it also follows the device LIVE.
   Switching the OS to dark recolours an open page immediately, where a scripted
   version would need a reload or a matchMedia listener on every page.

   The slot values below are copies of the Light and Dark blocks above, because
   plain CSS has no way to say "be that block" and this project has no build step
   to generate it. test/theme.test.js asserts they stay identical, so the drift
   that would otherwise be invisible is a failing test instead.
   ------------------------------------------------------------------------- */
@media (prefers-color-scheme: light) {
  :root[data-theme='system'],
  [data-preview='system'] {
  color-scheme: light;

  --p-crust: #dce0e8;
  --p-mantle: #e6e9ef;
  --p-base: #eff1f5;
  --p-surface0: #ccd0da;
  --p-surface1: #bcc0cc;
  --p-surface2: #acb0be;
  --p-overlay0: #9ca0b0;
  --p-overlay1: #8c8fa1;
  --p-subtext0: #6c6f85;
  --p-subtext1: #5c5f77;
  --p-text: #4c4f69;
  --p-dim: #5c5f77;
  --p-faint: #64677e;

  --p-pink: #ea76cb;
  --p-red: #d20f39;
  --p-peach: #fe640b;
  --p-yellow: #df8e1d;
  --p-green: #40a02b;
  --p-teal: #179299;
  --p-blue: #1e66f5;
  --p-lavender: #7287fd;

  --p-accent: #1657d8;
  --p-accent-fill: #1657d8;
  --p-accent-ink: #ffffff;

  --p-tint: #eceef4;
  --p-hairline: rgba(255, 255, 255, 0.9);
  --p-shadow: rgba(76, 79, 105, 0.16);
  --p-shadow-strong: rgba(76, 79, 105, 0.22);
  --p-veil: #ffffff;
  }
}

@media (prefers-color-scheme: dark) {
  :root[data-theme='system'],
  [data-preview='system'] {
  color-scheme: dark;

  --p-crust: #0e0e11;
  --p-mantle: #16161a;
  --p-base: #1c1c21;
  --p-surface0: #2a2a31;
  --p-surface1: #3a3a44;
  --p-surface2: #4c4c58;
  --p-overlay0: #6a6a78;
  --p-overlay1: #8a8a99;
  --p-subtext0: #b4b4c0;
  --p-subtext1: #d0d0da;
  --p-text: #e8e8ef;
  --p-dim: #b4b4c0;
  --p-faint: #8a8a99;

  --p-pink: #e79ec8;
  --p-red: #f2777a;
  --p-peach: #f0a45d;
  --p-yellow: #e8d07a;
  --p-green: #8fd18a;
  --p-teal: #7fd0c4;
  --p-blue: #8ab4f8;
  --p-lavender: #a9b6f0;

  --p-accent: #8ab4f8;
  --p-accent-fill: #5b8dd9;
  --p-accent-ink: #0b1220;

  --p-tint: #17171d;
  --p-hairline: rgba(255, 255, 255, 0.05);
  --p-shadow: rgba(0, 0, 0, 0.7);
  --p-shadow-strong: rgba(0, 0, 0, 0.82);
  --p-veil: #000000;
  }
}

/* ----------------------------------------------------------------- Studio
   The staff pages' look, on both editions: the palettes below, and the shapes in "Studio" at the
   foot of this file. Keyed on data-app='staff', which the pre-paint script writes on every page but
   the five a guest sees (see themeScript in src/app.js) — so the guest pages keep the palettes
   above and their own looks, Spotlight and Aurora, and nothing here can reach them.

   Soft and tactile: surfaces layered rather than outlined, in neutral greys with no tint of their
   own, so the accent is the one colour a page has. Light is the same design in daylight rather than
   an inversion: white cards on a cool grey ground, and the page you are on raised in white.

   AND ITS OWN COLOURS, as drawn: a brighter blue for the accent, the three impact hues the mockups
   were drawn in, and a hue for each issue type. Two things outrank them, and both still do:

     - Your brand. The accent slots read the brand's tokens first and fall back to Studio's own, and
       the theme script writes those tokens only for a circuit on Your brand (see themeScript in
       src/app.js) — so the circuit's colour is on every button it was on before, and everybody
       else gets Studio's blue.
     - The colour-blind palettes, which rewrite the three status slots and are marked !important
       in the accessibility block for exactly this: a reader's own setting beats any palette,
       whichever block it sits in and however specific.

   Each block is one attribute more specific than the palette it replaces, so it wins wherever both
   match; the System copies are held identical to Dark and Light by test/studio.test.js, as the ones
   above are by test/theme.test.js. */
:root[data-app='staff'],
:root[data-app='staff'] [data-preview='dark'],
:root[data-app='staff'] [data-preview='custom'] {
  --p-crust: #101114;
  --p-mantle: #1a1b20;
  --p-base: #222329;
  --p-surface0: #2c2e35;
  --p-surface1: #3a3c44;
  --p-surface2: #4a4d57;
  --p-overlay0: #62666f;
  --p-overlay1: #7d818b;
  --p-subtext0: #a3a7b1;
  --p-subtext1: #c4c8d0;
  --p-text: #f4f5f7;
  --p-dim: #a3a7b1;
  --p-faint: #8b8f99;

  --p-tint: #131418;
  --p-hairline: rgba(255, 255, 255, 0.045);
  --p-shadow: rgba(0, 0, 0, 0.55);
  --p-shadow-strong: rgba(0, 0, 0, 0.66);

  /* Studio's own, which no theme has: the pill raised off a sunken track that marks where you
     are, the track itself, and the same mark on a surface — a rail's entry, the dock's tab. The
     two marks are one colour in the dark and two in daylight, where a white pill raised off a
     white card would be no mark at all. Tokens rather than palette slots, since only these
     blocks ever set them. */
  --raised: #34363e;
  --track: #16171b;
  --picked: #34363e;
  --lift-picked: 0 1px 0 rgba(255, 255, 255, 0.06) inset, 0 4px 14px -4px rgba(0, 0, 0, 0.66);

  /* The accent, as drawn: #5b8def filled, a lighter step of it for words and drawings on the
     dark, and a near-black ink on the fill (5.8:1, where white managed 3.2). */
  --p-accent: var(--brand-accent, #a0bdf6);
  --p-accent-fill: var(--brand-accent-fill, #5b8def);
  --p-accent-ink: var(--brand-accent-ink, #0b1220);

  /* Impact, as drawn: high, medium and low — and so bad, caution and good everywhere else. */
  --p-red: #ff6b6b;
  --p-yellow: #f5a524;
  --p-green: #4cd07d;

  /* A hue per issue type, for the tile on a case and the bar in a chart: identity, like a series
     in a chart, and never a status. Tokens rather than slots, since only these blocks set them. */
  --type-sound: #9d86ff;
  --type-picture: #4f9bf7;
  --type-seating: #d6a67a;
  --type-cleanliness: #2fc3d3;
  --type-temperature: #ec7db0;
  --type-concessions: #e3b341;
  --type-disruption: #f08a5d;
  --type-staff: #6fcf97;

  /* The Showtime scheduler's film colours, dealt one to a film so that no two films on a day
     share one (see filmColours in js/schedule-grid.js), and dealt in this order: ten vivid hues
     round the wheel — its red left to down rooms and the now line — then four pale ones, a grey
     and a brown. Each reads as the time on its own tinted pill (4.5:1 and up). The closest two of
     all sixteen are ΔE 8.6 apart in OKLab, the first twelve 10.8: the eight issue-type hues the
     films used to borrow put tan and orange 7.2 apart, and on a board the two read as one. */
  --film-1: #369bff;
  --film-2: #ff7800;
  --film-3: #36d261;
  --film-4: #e977ed;
  --film-5: #c8e928;
  --film-6: #40e2f3;
  --film-7: #a77eff;
  --film-8: #fc61a5;
  --film-9: #e3aa00;
  --film-10: #0eb09e;
  --film-11: #e7cafd;
  --film-12: #ffd1a9;
  --film-13: #baf5d4;
  --film-14: #addfff;
  --film-15: #adb1b9;
  --film-16: #b59069;
}

:root[data-app='staff'][data-theme='light'],
:root[data-app='staff'][data-theme='custom'][data-brand-base='light'],
:root[data-app='staff'] [data-preview='light'],
:root[data-app='staff'] [data-preview='custom'][data-brand-base='light'] {
  --p-crust: #f2f3f5;
  --p-mantle: #ffffff;
  --p-base: #f4f5f7;
  --p-surface0: #eceef2;
  --p-surface1: #dde0e6;
  --p-surface2: #c9cdd5;
  --p-overlay0: #a1a6b0;
  --p-overlay1: #868b96;
  --p-subtext0: #5e6470;
  --p-subtext1: #464b55;
  --p-text: #16181d;
  /* 6.9:1 and 5.3:1 on a card, 6.2:1 and 4.8:1 on the page: the two quiet levels are where this
     app keeps its hints, notes and captions, and both clear 4.5 on both grounds. */
  --p-dim: #555b66;
  --p-faint: #666c78;

  --p-tint: #fafbfc;
  --p-hairline: rgba(255, 255, 255, 0.9);
  --p-shadow: rgba(16, 24, 40, 0.1);
  --p-shadow-strong: rgba(16, 24, 40, 0.16);

  --raised: #ffffff;
  --track: #e7e9ee;
  --picked: #eceef2;
  --lift-picked: none;

  /* The same blue in daylight, deep enough to read on white (6:1) and to carry white words when
     filled (5.2:1). */
  --p-accent: var(--brand-accent, #1d5bd6);
  --p-accent-fill: var(--brand-accent-fill, #2563eb);
  --p-accent-ink: var(--brand-accent-ink, #ffffff);

  /* The impact hues a step deeper, so each clears 4.5:1 as the words on its own pill. */
  --p-red: #b3222e;
  --p-yellow: #8f4f00;
  --p-green: #11692f;

  /* And the types, deep enough to stand 3:1 off a white card as a bar. */
  --type-sound: #7a5af8;
  --type-picture: #2f74e0;
  --type-seating: #a86f3d;
  --type-cleanliness: #0a8fa0;
  --type-temperature: #d0488a;
  --type-concessions: #a87a00;
  --type-disruption: #d2602a;
  --type-staff: #23945a;

  /* And the films' in daylight: deep enough to read as the time on their tint (3.3:1 and up, as
     the type hues manage), the pale four gone deep, since a pale word cannot be read on white.
     Closest two ΔE 8.3 apart; the first twelve 9.8. */
  --film-1: #007ee1;
  --film-2: #c85d00;
  --film-3: #009038;
  --film-4: #99269e;
  --film-5: #4c5a00;
  --film-6: #008b96;
  --film-7: #6127b1;
  --film-8: #c72b78;
  --film-9: #946d00;
  --film-10: #005a50;
  --film-11: #491966;
  --film-12: #542d00;
  --film-13: #004128;
  --film-14: #003a55;
  --film-15: #5f636a;
  --film-16: #6c4b25;
}

/* Your brand keeps its own wash at the top of the page, which the blocks above would otherwise
   replace with Studio's grey: the one place the circuit's colour reaches the ground. */
:root[data-app='staff'][data-theme='custom'],
:root[data-app='staff'][data-theme='custom'][data-brand-base='light'],
:root[data-app='staff'] [data-preview='custom'],
:root[data-app='staff'] [data-preview='custom'][data-brand-base='light'] {
  --p-tint: var(--brand-tint, var(--p-crust));
}

@media (prefers-color-scheme: light) {
  :root[data-app='staff'][data-theme='system'],
  :root[data-app='staff'] [data-preview='system'] {
  --p-crust: #f2f3f5;
  --p-mantle: #ffffff;
  --p-base: #f4f5f7;
  --p-surface0: #eceef2;
  --p-surface1: #dde0e6;
  --p-surface2: #c9cdd5;
  --p-overlay0: #a1a6b0;
  --p-overlay1: #868b96;
  --p-subtext0: #5e6470;
  --p-subtext1: #464b55;
  --p-text: #16181d;
  --p-dim: #555b66;
  --p-faint: #666c78;

  --p-tint: #fafbfc;
  --p-hairline: rgba(255, 255, 255, 0.9);
  --p-shadow: rgba(16, 24, 40, 0.1);
  --p-shadow-strong: rgba(16, 24, 40, 0.16);

  --raised: #ffffff;
  --track: #e7e9ee;
  --picked: #eceef2;
  --lift-picked: none;

  /* The same blue in daylight, deep enough to read on white (6:1) and to carry white words when
     filled (5.2:1). */
  --p-accent: var(--brand-accent, #1d5bd6);
  --p-accent-fill: var(--brand-accent-fill, #2563eb);
  --p-accent-ink: var(--brand-accent-ink, #ffffff);

  /* The impact hues a step deeper, so each clears 4.5:1 as the words on its own pill. */
  --p-red: #b3222e;
  --p-yellow: #8f4f00;
  --p-green: #11692f;

  /* And the types, deep enough to stand 3:1 off a white card as a bar. */
  --type-sound: #7a5af8;
  --type-picture: #2f74e0;
  --type-seating: #a86f3d;
  --type-cleanliness: #0a8fa0;
  --type-temperature: #d0488a;
  --type-concessions: #a87a00;
  --type-disruption: #d2602a;
  --type-staff: #23945a;

  /* And the films' in daylight: deep enough to read as the time on their tint (3.3:1 and up, as
     the type hues manage), the pale four gone deep, since a pale word cannot be read on white.
     Closest two ΔE 8.3 apart; the first twelve 9.8. */
  --film-1: #007ee1;
  --film-2: #c85d00;
  --film-3: #009038;
  --film-4: #99269e;
  --film-5: #4c5a00;
  --film-6: #008b96;
  --film-7: #6127b1;
  --film-8: #c72b78;
  --film-9: #946d00;
  --film-10: #005a50;
  --film-11: #491966;
  --film-12: #542d00;
  --film-13: #004128;
  --film-14: #003a55;
  --film-15: #5f636a;
  --film-16: #6c4b25;
  }
}

@media (prefers-color-scheme: dark) {
  :root[data-app='staff'][data-theme='system'],
  :root[data-app='staff'] [data-preview='system'] {
  --p-crust: #101114;
  --p-mantle: #1a1b20;
  --p-base: #222329;
  --p-surface0: #2c2e35;
  --p-surface1: #3a3c44;
  --p-surface2: #4a4d57;
  --p-overlay0: #62666f;
  --p-overlay1: #7d818b;
  --p-subtext0: #a3a7b1;
  --p-subtext1: #c4c8d0;
  --p-text: #f4f5f7;
  --p-dim: #a3a7b1;
  --p-faint: #8b8f99;

  --p-tint: #131418;
  --p-hairline: rgba(255, 255, 255, 0.045);
  --p-shadow: rgba(0, 0, 0, 0.55);
  --p-shadow-strong: rgba(0, 0, 0, 0.66);

  --raised: #34363e;
  --track: #16171b;
  --picked: #34363e;
  --lift-picked: 0 1px 0 rgba(255, 255, 255, 0.06) inset, 0 4px 14px -4px rgba(0, 0, 0, 0.66);

  /* The accent, as drawn: #5b8def filled, a lighter step of it for words and drawings on the
     dark, and a near-black ink on the fill (5.8:1, where white managed 3.2). */
  --p-accent: var(--brand-accent, #a0bdf6);
  --p-accent-fill: var(--brand-accent-fill, #5b8def);
  --p-accent-ink: var(--brand-accent-ink, #0b1220);

  /* Impact, as drawn: high, medium and low — and so bad, caution and good everywhere else. */
  --p-red: #ff6b6b;
  --p-yellow: #f5a524;
  --p-green: #4cd07d;

  /* A hue per issue type, for the tile on a case and the bar in a chart: identity, like a series
     in a chart, and never a status. Tokens rather than slots, since only these blocks set them. */
  --type-sound: #9d86ff;
  --type-picture: #4f9bf7;
  --type-seating: #d6a67a;
  --type-cleanliness: #2fc3d3;
  --type-temperature: #ec7db0;
  --type-concessions: #e3b341;
  --type-disruption: #f08a5d;
  --type-staff: #6fcf97;

  /* The Showtime scheduler's film colours, dealt one to a film so that no two films on a day
     share one (see filmColours in js/schedule-grid.js), and dealt in this order: ten vivid hues
     round the wheel — its red left to down rooms and the now line — then four pale ones, a grey
     and a brown. Each reads as the time on its own tinted pill (4.5:1 and up). The closest two of
     all sixteen are ΔE 8.6 apart in OKLab, the first twelve 10.8: the eight issue-type hues the
     films used to borrow put tan and orange 7.2 apart, and on a board the two read as one. */
  --film-1: #369bff;
  --film-2: #ff7800;
  --film-3: #36d261;
  --film-4: #e977ed;
  --film-5: #c8e928;
  --film-6: #40e2f3;
  --film-7: #a77eff;
  --film-8: #fc61a5;
  --film-9: #e3aa00;
  --film-10: #0eb09e;
  --film-11: #e7cafd;
  --film-12: #ffd1a9;
  --film-13: #baf5d4;
  --film-14: #addfff;
  --film-15: #adb1b9;
  --film-16: #b59069;
  }
}

/* Studio's shapes, as tokens, so every surface that already reads them follows without a rule of
   its own: rounder corners, hairline edges drawn in the text colour so they suit both grounds,
   solid surfaces instead of glass, and a softer, lower lift. Figtree for the words; the wordmark
   keeps the face it has everywhere (see .wordmark in "Studio").

   Placed here, above the accessibility block, and no more specific than it: high contrast writes
   --border, --border-strong and the glass at the same weight further down, and must go on winning. */
:root[data-app='staff'],
:root[data-app='staff'] [data-preview] {
  --control: var(--p-surface0);
  /* Edges at a whisper: a Studio surface is set apart from the ground by its lift, as drawn,
     and these are what is left of the outline — enough to hold a field or a divider, too little
     to frame a card. High contrast turns both into real edges, further down. */
  --border: color-mix(in srgb, var(--p-text) 5%, transparent);
  --border-strong: color-mix(in srgb, var(--p-text) 7%, transparent);
  --glass: var(--surface);
  --glass-strong: color-mix(in srgb, var(--surface) 92%, transparent);
  --shadow: 0 1px 0 var(--p-hairline) inset, 0 12px 30px -14px var(--p-shadow);
  --shadow-lift: 0 1px 0 var(--p-hairline) inset, 0 22px 48px -18px var(--p-shadow-strong);
  /* The shadow above already carries the lit top edge, so the glass's own would draw it twice. */
  --rim: 0 0 transparent;
  /* A card's outline, which Studio draws as nothing: a surface is set apart by its lift. High
     contrast puts it back, below. */
  --surface-edge: transparent;
  --lift-raised: 0 1px 0 color-mix(in srgb, white 7%, transparent) inset, 0 4px 14px -4px var(--p-shadow-strong);
}

:root[data-app='staff'] {
  --font: 'Figtree', ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  --font-mark:
    ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, Roboto, Helvetica, Arial,
    sans-serif;
  --radius-lg: 22px;
  --radius: 16px;
  --radius-sm: 12px;

  /* Buttons: pills in the control grey, the danger one tinted rather than outlined, and the
     page's one bold action lifted on a glow of its own colour. Read by .btn and its variants. */
  --btn-radius: 999px;
  --btn-bg: var(--control);
  --btn-edge: transparent;
  --btn-quiet-bg: var(--control);
  --btn-quiet-hover: var(--p-surface1);
  --btn-quiet-ink: var(--text);
  --btn-danger-bg: color-mix(in oklab, var(--danger) 13%, transparent);
  --btn-danger-edge: transparent;
  --btn-cta-lift: 0 8px 20px -8px color-mix(in srgb, var(--accent-fill) 70%, transparent);

  /* Badges: pills, in the case they were written in. */
  --badge-size: 0.76rem;
  --badge-tracking: 0;
  --badge-case: none;
  --badge-radius: 999px;
}

/* High contrast gives the buttons back the edges Studio takes off them: an outline is exactly
   what somebody who asked for contrast is asking for. One attribute heavier than the block above,
   so it wins wherever both apply. */
:root[data-app='staff'][data-a11y-contrast='high'] {
  --btn-edge: var(--border-strong);
  --btn-danger-edge: var(--danger);
  --surface-edge: var(--border-strong);
}

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

/*
 * The browser hides [hidden] with `display: none` at the lowest specificity, so
 * ANY class rule setting display quietly beats it — and 57 rules here do. Every
 * page uses the hidden attribute to show and hide panels, which makes that a trap
 * waiting for whichever component grows a display rule next. One declaration
 * closes it for all of them.
 */
[hidden] {
  display: none !important;
}

/*
 * The page's ground is on the ROOT, not on body, and that is load-bearing twice over.
 *
 * On a phone it is the fix for a real artefact. Overscroll past the end of a page and the browser
 * rubber-bands, showing an area beyond the document — and what it paints there is the CANVAS, which
 * is the root element's background. With the background on body instead, it reaches the canvas only
 * by the propagation rule (a root with no background of its own borrows body's), and iOS does not
 * carry a `background-attachment: fixed` background through that reliably: the band came up in the
 * UA's own dark grey, which is LIGHTER than Crust, so the whole page appeared to lift and show
 * something paler underneath. Declared here, there is no propagation to get wrong.
 *
 * AND BODY MUST NOT HAVE ONE. That is not tidiness, it is paint order: the tint below sits at
 * `z-index: -2`, and the case monitor's triage network, Aurora's light and its grain at `-1` — and
 * negative-z-index layers paint after the root's background but BEFORE the in-flow block
 * backgrounds, so a background on body would paint over every one of them and they would silently
 * vanish. It works because body's background is propagated away to the canvas and body paints
 * nothing itself; move it here and that stops being true, so it has to move rather than be copied.
 */
/*
 * A COLOUR ONLY, and that is the fix for a strip along the bottom of every page on a phone.
 *
 * The tint used to be here too, with `background-attachment: fixed`, so its bright edge stayed at
 * the top of the screen rather than scrolling away with the top of the document. The intent was
 * right and the mechanism was not: a fixed background on the ROOT gives Safari a background layer
 * sized to the layout viewport, and the area exposed when the address bar collapses — or by a
 * rubber-band scroll past the end of the document — is outside it. What showed through was the
 * browser's own dark grey, around #2c2c2e against a crust of #11111b: lighter, full width, pinned
 * to the bottom of the window. The dust layer of the day was visible over it, which is what said it
 * was the page's own background missing rather than browser chrome.
 *
 * A plain `background-color` has none of that. The root's background is the CANVAS background, and
 * its painting area is defined to cover the whole canvas — every overscroll region included. So the
 * colour is here, where it can never fall short, and the tint moved to the pseudo-element below.
 */
html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  background-color: var(--bg);
}

/*
 * The scrollbar's space is reserved on EVERY page, whether that page scrolls or not.
 *
 * Without this the app moves sideways as you walk through it. The shell and the top bar
 * are both `max-width: 1180px; margin: 0 auto`, so their left edge is half of whatever is
 * left over — and what is left over changes by the width of a scrollbar depending on
 * whether the page you landed on is long enough to need one. Measured, with scrollbars
 * shown: the wordmark sits at x=65 on the overview and Location info, and at x=70 on
 * Settings and Needs review. Five pixels, on every navigation between a long page and a
 * short one, on the one element the eye is anchored to while a new page paints.
 *
 * The reticle is what made it obvious rather than what was wrong. Its ring is a mask on a
 * fractional box (0.82em — 14.95px in the bar), so a shift re-rasterises its edges as well
 * as moving it: it appears to change weight, which reads as the mark adjusting itself
 * rather than as the whole page sliding. The letters beside it re-hint and stay crisp.
 *
 * These scrollbars are CLASSIC ones — see the ::-webkit-scrollbar block below, which takes
 * layout width even on the platforms whose default is an overlay — so this is not a
 * Windows-only nicety. Headless Chromium hides scrollbars by default, which is why no test
 * saw it; test/browser/wordmark.browser.js now launches a browser that shows them.
 *
 * WIDE SCREENS ONLY, and the reason is what a reserved gutter costs. It comes out of the
 * layout viewport, so on a phone it would take ten of the 390 pixels the whole design is
 * cut to, push `calc(100vw - 32px)` dialogs into a sideways scroll, and buy nothing: a
 * phone browser draws overlay scrollbars that never took the width in the first place, so
 * there was no shift there to fix. The fault this cures is a desktop one.
 */
@media (min-width: 700px) {
  html {
    scrollbar-gutter: stable;
  }
}

/*
 * The tint, on a fixed layer of its own.
 *
 * Same result as `background-attachment: fixed` — the gradient is positioned against the viewport,
 * so its bright edge stays at the top of the screen — with the failure mode removed: if this box
 * falls short of the visual viewport during an overscroll, what is behind it is the root's own
 * crust rather than nothing.
 *
 * A pseudo-element rather than an element a script builds, so it is there for the first paint
 * rather than warming the page up a moment after loading.
 *
 * `-2` puts it behind the layers at `-1` — the triage network on the case monitor, Aurora's light
 * and grain on Foyer's guest pages — and behind everything in the flow. All of them are behind
 * content and above the canvas, which is the order they want.
 */
html::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -2;
  pointer-events: none;
  background-image: var(--bg-tint);
}

body {
  margin: 0;
  min-height: 100vh;
  font-family: var(--font);
  /*
   * 1rem, not 16px — identical at rest, and the difference is the whole of the text-size
   * accessibility option: every component here sizes itself in rem, so the root is the one
   * dial that scales the app. A hard 16px on the body left the plain prose behind while
   * headings, labels and figures grew around it, which reads as a broken page rather than a
   * larger one. See the accessibility block at the foot of this file.
   */
  font-size: 1rem;
  line-height: 1.6;
  color: var(--text);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  font-feature-settings: 'cv05', 'ss01';
}

/* ================================================================ the network
 * The case monitor's sky while AI TRIAGE works the queue: a CONSTELLATION over the page's
 * flat ground — nodes joined by faint lines, packets of light travelling link to link. The metaphor is the point: the reports are not drifting past anybody, they
 * are being carried through a working system, and the page says so the whole time it is
 * open. Built by weaveTriageNetwork in js/network.js, only on the case monitor, and only
 * while the agent is genuinely running (the queue response carries triageAvailability's
 * answer, not the bare setting — see review.js).
 *
 * Cheap by construction: hand-placed geometry rather than a canvas doing distance maths
 * every frame, and the only ANIMATED properties are transform and opacity — the two the
 * compositor runs without the main thread. The lines themselves never move; what animates is
 * eleven nodes breathing and fifteen four-pixel packets. The wash at the foot is static paint,
 * not a drifting window-sized layer — twenty open pages of those once took a frame from 17ms to
 * 237ms.
 *
 * Under prefers-reduced-motion the blanket rule zeroes the durations: packets and lines
 * rest at opacity 0 and vanish, and what remains is the faint nodes and the wash — a
 * still sky that still says the state. That is the right trade for a background that is
 * information: the agent has this queue.
 */
.network {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  overflow: clip;
  background: linear-gradient(
    to top,
    color-mix(in oklab, var(--p-accent) 16%, transparent),
    transparent 40%
  );
}

.network__node {
  position: absolute;
  width: var(--size);
  height: var(--size);
  /* Centred on its coordinate, so a node sits ON its lines' meeting point rather than
     hanging below-right of it — the lines are drawn centre to centre. */
  translate: -50% -50%;
  border-radius: 50%;
  background: var(--p-accent);
  /* Softer than the first cut: the nodes are the CONSTANT, there to give a surfacing line
     somewhere to come from, and eleven bright beacons were half of the busyness. */
  box-shadow:
    0 0 calc(var(--size) * 1.5) var(--p-accent),
    0 0 calc(var(--size) * 4) var(--p-accent),
    0 0 1px var(--p-accent);
  opacity: 0.3;
  animation: net-breathe var(--for) ease-in-out var(--delay) infinite;
}

/*
 * A line RESTS DARK. The first cut of this drew all fifteen at once, permanently, and a
 * whole mesh pulsing over the queue was a wall — busy exactly the way a background must
 * not be. Now each line is an EVENT: it fades in, its spark crosses, it fades out, and it
 * is gone for the rest of its cycle. The phasing in js/network.js guarantees two or three are
 * lit at any moment — never none, never a pile-up — with a fresh one surfacing somewhere
 * every four seconds or so.
 */
.network__link {
  position: absolute;
  height: 1px;
  /* Anchored at its left end and rotated onto its far node; the packet below rides this
     same rotation, which is what lets one translateX travel the whole line. */
  transform-origin: 0 50%;
  transform: rotate(var(--angle));
  background: linear-gradient(
    90deg,
    transparent,
    color-mix(in oklab, var(--p-accent) 55%, transparent) 16%,
    color-mix(in oklab, var(--p-accent) 55%, transparent) 84%,
    transparent
  );
  opacity: 0;
  animation: net-line var(--for) linear var(--delay) infinite;
}

/*
 * The packet: one spark per link, IN STEP with its line by construction — both animations
 * share the same duration and delay, so the spark gathers at the near node while the line
 * fades in (0–4%), crosses while it is fully lit (4–14%), and lands as it starts to fade.
 * A spark on an unlit line would be a dot crossing empty sky.
 */
.network__link::after {
  content: '';
  position: absolute;
  top: -1.5px;
  left: 0;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--p-accent);
  box-shadow: 0 0 9px var(--p-accent);
  opacity: 0;
  animation: net-packet var(--for) linear var(--delay) infinite;
}

@keyframes net-breathe {
  0%,
  100% {
    opacity: 0.18;
  }
  50% {
    opacity: 0.4;
  }
}

/* Lit for 18% of the cycle. The bound on how many show at once lives in the LINKS table's
   phases (js/network.js) — change this window and that arithmetic changes with it. */
@keyframes net-line {
  0% {
    opacity: 0;
  }
  3.5% {
    opacity: 0.3;
  }
  14% {
    opacity: 0.3;
  }
  18% {
    opacity: 0;
  }
  100% {
    opacity: 0;
  }
}

@keyframes net-packet {
  0% {
    transform: translateX(0);
    opacity: 0;
  }
  4% {
    transform: translateX(0);
  }
  5% {
    opacity: 0.85;
  }
  13% {
    opacity: 0.85;
  }
  14% {
    transform: translateX(var(--run));
  }
  15% {
    opacity: 0;
  }
  100% {
    transform: translateX(var(--run));
    opacity: 0;
  }
}

h1,
h2,
h3 {
  line-height: 1.2;
  margin: 0 0 0.4em;
  letter-spacing: -0.02em;
  font-weight: 650;
}

h1 {
  font-size: clamp(1.4rem, 1.2rem + 1vw, 1.85rem);
  font-weight: 750;
  letter-spacing: -0.025em;
}
h2 {
  font-size: 1.15rem;
}
h3 {
  font-size: 1rem;
}

p {
  margin: 0 0 1em;
}

a {
  color: var(--p-lavender);
  text-decoration-color: color-mix(in oklab, var(--p-lavender) 40%, transparent);
  text-underline-offset: 2px;
  transition: color 120ms var(--ease);
}

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

::selection {
  background: color-mix(in oklab, var(--accent) 35%, transparent);
  /* Not a fixed white: on the light theme that is invisible on its own tint. */
  color: var(--text);
}

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

/* Slim, unobtrusive scrollbars that match the theme. */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--p-surface1) transparent;
}

*::-webkit-scrollbar {
  width: 10px;
  height: 10px;
}

*::-webkit-scrollbar-thumb {
  background: var(--p-surface1);
  border-radius: 999px;
  border: 2px solid transparent;
  background-clip: content-box;
}

*::-webkit-scrollbar-thumb:hover {
  background: var(--p-surface2);
  background-clip: content-box;
}

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

/* ----------------------------------------------------------------- layout */
.shell {
  /*
   * Named, because one thing needs to escape it: the guest page's send bar is full
   * bleed and pulls itself back out by exactly this much. The value changes at a
   * breakpoint and again for print, and a negative margin written as a number was off
   * by four pixels on a phone — which is a page that scrolls sideways.
   */
  --shell-pad: 20px;
  max-width: 1180px;
  margin: 0 auto;
  padding: 0 var(--shell-pad) 80px;
}

/*
 * A PAGE WHOSE CARDS SIT EVENLY, however many dialogs are parked between them.
 *
 * The spacing everywhere else comes from `.card + .card`, which is an ADJACENT sibling rule — it
 * only fires when one card directly follows another. The staffing page keeps its four <dialog>
 * elements in the flow between the cards they belong to, and a closed dialog is display:none but
 * is still a sibling, so it breaks the adjacency: three of the cards there had no top margin at
 * all and sat flush against the one above, while the rest had their 16px. Reported as "some are
 * so close the menus are touching, and others are randomly spaced out", which is precisely what
 * a rule that can be interrupted by an invisible element looks like.
 *
 * A flex column with a gap cannot be interrupted that way: a display:none child is not a flex
 * item, so it contributes no gap and no break. The spacing then describes the page rather than
 * the order things happen to be written in.
 */
.shell--stack {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

/* The gap owns the spacing now, so the margin rule must not add to it. */
.shell--stack > .card + .card {
  margin-top: 0;
}

/* The header keeps the 30px it has always had under it: its own 14 plus the column's 16. Written
   as the remainder rather than left at 30, because the gap is now unavoidably part of that sum. */
.shell--stack > .page-head {
  margin-bottom: 14px;
}

.shell--narrow {
  max-width: 640px;
}

/*
 * Settings: wide enough that ten category pills wrap into two rows rather than
 * five, narrow enough that a line of help text stays readable.
 */
.shell--medium {
  max-width: 880px;
}

/*
 * ---------------------------------------------- changing pages by swiping sideways
 *
 * The gesture is in public/js/swipe.js, over the page header only, on a phone only.
 * These are the four states it puts the shell in.
 *
 * The shell rather than the whole page, so the top bar stays exactly where it is: what
 * moves is the page's content, which is what changed. The bar is the frame around it.
 *
 * Everything is transform and opacity, which the compositor can do on its own thread —
 * this animation runs while a document is being fetched and parsed, which is the worst
 * possible moment to be asking the main thread for layout.
 */

/* The spring back when a drag is released without going far enough. */
.shell {
  transition:
    transform 200ms var(--ease),
    opacity 200ms var(--ease);
}

/*
 * Following the finger. No transition, because the transform IS the finger's position —
 * easing it makes the content lag the thumb, which reads as a slow page rather than as a
 * drag.
 *
 * After the rule above and not before it: both selectors are one class, so this only wins
 * on being later. Written the other way round it silently loses, and the only symptom is
 * a drag that trails.
 */
.shell--dragging {
  transition: none;
}

/*
 * Leaving. The page goes the way the finger went, and the arriving page comes in behind it
 * from the other side, so one continuous movement crosses the two documents.
 *
 * Next goes LEFT and previous goes right, because the links in the bar above read left to
 * right and the pages are laid out in that order: moving to the next one slides this one
 * off to the left and brings that one in from the right. See `towards()` in
 * public/js/swipe.js, which is the same decision on the input side — the two have to
 * mirror each other or the page moves one way while the finger goes the other.
 *
 * Shorter and shallower than the entrance: the exit is time the reader is waiting for a
 * page, and the entrance is time they are being shown one.
 */
.shell--leaving-next,
.shell--leaving-prev {
  transition:
    transform 150ms var(--ease),
    opacity 150ms var(--ease);
  opacity: 0;
}

.shell--leaving-next {
  transform: translateX(-10%);
}

.shell--leaving-prev {
  transform: translateX(10%);
}

/*
 * Arriving.
 *
 * An animation rather than a transition, because there is no first state to transition
 * from — the document is new, and an animation carries its own starting point. The
 * attribute is set by /theme.js before first paint (see `themeScript` in src/app.js) and
 * removed by swipe.js once this has finished.
 */
@keyframes shell-swipe-in-next {
  from {
    transform: translateX(13%);
    opacity: 0;
  }
}

@keyframes shell-swipe-in-prev {
  from {
    transform: translateX(-13%);
    opacity: 0;
  }
}

html[data-swipe='next'] .shell {
  animation: shell-swipe-in-next 240ms var(--ease) both;
}

html[data-swipe='prev'] .shell {
  animation: shell-swipe-in-prev 240ms var(--ease) both;
}

/*
 * A shell pushed sideways is wider than the window, which would otherwise be a page that
 * can be scrolled sideways for the length of the animation — or worse, a scrollbar that
 * appears and goes again.
 *
 * `clip` rather than `hidden`: hidden makes this a scroll container, and a scroll
 * container is not something the sticky top bar can stick to. Applied only while
 * something is actually moving, so a legitimately wide child is never clipped at rest.
 */
html[data-swipe] body,
body:has(.shell--dragging),
body:has(.shell--leaving-next),
body:has(.shell--leaving-prev) {
  overflow-x: clip;
}

.topbar {
  position: sticky;
  top: 0;
  z-index: 20;
  background: color-mix(in oklab, var(--p-mantle) 82%, transparent);
  backdrop-filter: saturate(140%) blur(12px);
  -webkit-backdrop-filter: saturate(140%) blur(12px);
  border-bottom: 1px solid var(--border);
}

.topbar__inner {
  max-width: 1180px;
  margin: 0 auto;
  padding: 12px 20px;
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
}

.brand {
  display: flex;
  align-items: center;
  gap: 10px;
  font-weight: 650;
  font-size: 1rem;
  letter-spacing: -0.02em;
  text-decoration: none;
  color: var(--text);
}

.brand:hover {
  color: var(--text);
}

/* Whatever follows the wordmark — on a General Manager's bar, their cinema. */
/*
 * HOW BIG THE MARK IS DRAWN, which is not the same number for the two products.
 *
 * CINESC(O)PE is nine glyphs; F(O)YER is five. At one font size their cap heights are identical
 * — same typeface, same weights — but the short lockup covers barely half the width, and every
 * slot in this app was proportioned around the long one. So FOYER at 1.14rem is not smaller
 * type, it is a smaller OBJECT, and it reads as lost in a bar built for its predecessor.
 *
 * Corrected with a multiplier rather than a second set of sizes, so each slot keeps the decision
 * somebody made about it — the guest hero's mark is deliberately quiet, and must stay quiet, just
 * not quieter than CineScope's was there.
 *
 * 1.3 is a compromise, and it is worth being honest about which one. Measured in all three slots,
 * FOYER at 1.0 draws 58% of CINESCOPE's width at exactly its cap height; at 1.3 it draws 76% of
 * the width at 130% of the cap. Matching the width outright would take about 1.7, and a mark
 * two-thirds again as tall as the one it replaces would tower over the bar it sits in. So this
 * closes most of the gap in mass and accepts a slightly taller lockup, which is the trade that
 * looks right rather than the one that measures equal.
 *
 * What it costs, also measured: nothing in the top bar, whose height is fixed by its own padding
 * and does not move at any scale; one pixel on the guest header. In that header the mark ends up
 * at 18.7px against a 25.4px heading, so it is still the quieter of the two, which is the whole
 * point of the note above .guest-hero .wordmark.
 *
 * TWO SLOTS DELIBERATELY DO NOT TAKE IT, and both override the size with a literal below:
 *
 *   - the placard byline, where the mark sits inline in "Powered by ____" and has to match the
 *     words beside it rather than out-size them;
 *   - the sign-in page, whose mark is sized in vw and so already grows to fill its space
 *     whatever the glyph count — which is exactly why that one screen looked right when the
 *     fixed-size slots did not.
 */
:root {
  --mark-scale: 1;
}

html[data-edition='standalone'] {
  --mark-scale: 1.3;
}

/* ------------------------------------------------------------- the wordmark
   CINESC⌖PE — the O of SCOPE is a reticle, so the mark IS the name rather than
   a logo sitting beside it. Set in the UI font at two weights: CINE recedes,
   SCOPE holds, and the reticle lands where the eye is already going.

   The reticle is one empty <span>, not seven SVG elements repeated in eight
   pages: with no build step and no templating, markup duplicated per page is
   markup that drifts. The ring and ticks are a masked pseudo-element, so they
   take currentColor and recolour with the theme; the pupil is the other
   pseudo-element, and the one part of the mark that keeps its own colour. */
.wordmark {
  display: inline-flex;
  align-items: baseline;
  /* Sized for the desktop bar, and stepped down under 700px with the rest of the
     top bar — the phone version shares its row with the account button and has no
     room to grow. Everything in the reticle is em-based, so it follows. */
  font-size: calc(1.14rem * var(--mark-scale));
  letter-spacing: 0.06em;
  line-height: 1.35;
  white-space: nowrap;
  color: var(--text);
}

.wordmark__cine {
  font-weight: 400;
  color: var(--text-dim);
}

.wordmark__scope {
  font-weight: 600;
}

/*
 * Sized in em so it tracks the type it is standing in for, and placed off the
 * BASELINE rather than off the line box.
 *
 * It used to be `align-self: center`, which centres it in the flex line — and the
 * flex line's height is the text's line box rounded to whole device pixels, while
 * the ring is a fractional em. So the two rounded independently and the ring
 * landed a fraction of a pixel out by an amount that changed with the font size:
 * dead on at 110px, half a pixel low at 15px, which is the size the top bar
 * actually uses. On the sign-in page, where the mark is `clamp(1.7rem, 8.5vw, …)`,
 * it moved as the window was resized. Somebody noticed, which is the point at
 * which "close enough" stops being a defensible answer for a company's own name.
 *
 * Type is aligned on the baseline, so this is too:
 *
 *   `align-self: baseline` on an empty box makes its BOTTOM EDGE the baseline it
 *   is aligned by, which puts the ring's middle half its height up — 0.41em.
 *   A round letter's ink in this font runs from 0.7031em above the baseline to
 *   0.0156em below it, so an O's middle is 0.344em up. The difference is the
 *   0.066em it comes back down by.
 *
 * All three numbers are measured from the font rather than guessed. Pinned in
 * test/browser/wordmark.browser.js.
 *
 * AND IN WHOLE PIXELS, where the browser can round, because the letters are drawn on whole
 * pixels and the ring was not. A browser puts a line of text on a whole device pixel and snaps
 * the ring's box to one separately, and a box 14.96px tall hanging 1.2px below the baseline
 * rounded the other way from the text for part of every pixel. Wherever the mark landed in that
 * part, the ring sat a whole pixel below the letters, sharp enough to see. Reported from Firefox
 * at 100%, where the top bar's mark lands in it; Chromium does the same at other fractions of a
 * pixel. With the box and its drop both whole pixels, the ring and the letters round together
 * wherever the mark lands.
 *
 * The size is rounded first, and the drop is then whatever puts that box's middle nearest
 * 0.344em up, so the middle is never more than half a pixel from where the em numbers put it.
 *
 * A whole pixel is a DEVICE pixel in Firefox and a CSS pixel elsewhere, which is measured rather
 * than chosen: at 125%, 150% and 175%, Firefox lines the two up only in device pixels and
 * Chromium only in CSS pixels. /theme.js sets --device-px for Firefox (see src/app.js); every
 * other browser takes the 1px here, which is a whole number of device pixels at 100% and 200%.
 * Behind @supports because a round() the browser cannot read would drop the size altogether
 * rather than fall back to the em one above it.
 *
 * The mask is filled white, which shows through under both mask-mode: alpha and
 * luminance — black would be correct under one and invisible under the other.
 */
.wordmark__reticle {
  --reticle: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='50' r='38' fill='none' stroke='%23fff' stroke-width='12'/%3E%3Cg stroke='%23fff' stroke-width='8.5' stroke-linecap='round'%3E%3Cpath d='M50 6V29'/%3E%3Cpath d='M50 71V94'/%3E%3Cpath d='M6 50H29'/%3E%3Cpath d='M71 50H94'/%3E%3C/g%3E%3C/svg%3E");
  /* The pupil on the same 100-unit square as the ring, at the size the QR code's is (RETICLE in
     src/lib/qr.js). See ::after for why it is a drawing and not a gradient. */
  --pupil: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='50' r='10' fill='%23fff'/%3E%3C/svg%3E");

  position: relative;
  display: inline-block;
  width: 0.82em;
  height: 0.82em;
  margin: 0 0.05em;
  align-self: baseline;
  top: 0.066em;
  flex: none;
}

@supports (width: round(1em, 1px)) {
  .wordmark__reticle {
    --reticle-box: round(0.82em, var(--device-px, 1px));
    width: var(--reticle-box);
    height: var(--reticle-box);
    top: round(calc(var(--reticle-box) / 2 - 0.344em), var(--device-px, 1px));
  }
}

.wordmark__reticle::before {
  content: '';
  position: absolute;
  inset: 0;
  background: currentColor;
  -webkit-mask: var(--reticle) center / contain no-repeat;
  mask: var(--reticle) center / contain no-repeat;
}

/*
 * The pupil: a circle a fifth of the ring's width, cut by a mask over the whole ring rather than
 * drawn as a box with round corners. A box that small — 2.4px on the guest page — has its edges
 * snapped to whole pixels, and at some places on the pixel grid that made it 3 wide and 2 tall:
 * an amber dash in the middle of the O. A mask's circle is drawn rather than snapped, so it is
 * round wherever the mark lands.
 *
 * AND THE SAME KIND OF MASK AS THE RING, placed by the same words, so the two land on the pixel
 * grid together. It was a radial gradient, which was round, but a gradient and an image are not
 * snapped alike: where the mark fell at some fractions of a pixel — which is wherever the letters
 * before it put it, so it moved with the font — the ring moved half a pixel and the pupil did
 * not, and the amber sat up to a whole device pixel to one side of the ring's middle. Reported
 * from a desktop as the reticle off centre again. Two masks of one shape are drawn one way.
 * Measured, all three ways, in test/browser/wordmark.browser.js.
 */
.wordmark__reticle::after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--brand-pupil);
  -webkit-mask: var(--pupil) center / contain no-repeat;
  mask: var(--pupil) center / contain no-repeat;
}

/* ------------------------------------------------------------ main nav
   Exactly one link is ever highlighted: the page you are on. Home used to carry
   a permanent accent chip so that "how do I get back" needed no guess, but it
   read as "you are here" from every other page — two links looking picked out
   at once, one of them lying. Its icon is enough to make it findable. */
.nav {
  display: flex;
  align-items: center;
  gap: 4px;
  flex-wrap: wrap;
  padding-left: 6px;
  margin-left: 6px;
  border-left: 1px solid var(--border);
}

/* On desktop this is just a row inside a row, so the bar looks the same as if the
   links were direct children. It earns its keep on a phone — see the mobile block. */
.nav__rest {
  display: flex;
  align-items: center;
  gap: 4px;
}

/*
 * The menu button that folds the second row away, on a phone only.
 *
 * Not shown here: on desktop the links are a row inside a row with room for all of them,
 * so there is nothing to reveal and a button would be a control whose job is already done.
 * Built by wireNav() in js/util.js, which is also the thing that knows whether any links
 * survived the role filter.
 */
.nav__toggle {
  display: none;
}

.nav__link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px 12px;
  border-radius: 8px;
  border: 1px solid transparent;
  font-size: 0.87rem;
  font-weight: 550;
  color: var(--text-dim);
  text-decoration: none;
  white-space: nowrap;
  transition:
    background-color 140ms var(--ease),
    color 140ms var(--ease),
    border-color 140ms var(--ease);
}

/*
 * A ROLE-GATED LINK IS NOT DRAWN UNTIL THE ROLE IS KNOWN.
 *
 * Three pages carry every role's links and let wireNav in js/util.js remove the ones that are not
 * yours — the schedule board, Settings and the manager's dashboard — because one markup serving
 * several roles is one bar to keep right rather than four. The cost was a flash: /api/me answers a
 * few hundred milliseconds after the page paints, so a General Manager opening the showtime board
 * saw Case monitor, Location info and Active users appear and then vanish.
 *
 * Nothing was reachable — the server refuses those pages and their APIs, which is what actually
 * keeps somebody out — but a bar that offers a colleague's tools for half a second and takes them
 * back is the app telling them something untrue about their own account.
 *
 * Hiding them until wireNav had run fixed that and left the opposite complaint: the bar arrived as
 * Home alone and filled in a moment later, so every navigation announced itself as a page being
 * assembled. The links a reader has are the same on every page — they should look like furniture,
 * not like content.
 *
 * So `data-roles` is written onto <html> by the pre-paint script in src/app.js, next to the theme
 * and for the same reason, and these rules draw the right links in the FIRST paint. It carries the
 * set of guards the account satisfies rather than its role, which is why `~=` is the operator here:
 * a dev counts as an admin, and the sheet does not have to know that.
 *
 * `[data-nav='ready']` is still the second way in, and it is not redundant. It is what draws the
 * bar while a dev is borrowing an account, where the pre-paint script deliberately says nothing
 * because it cannot see whose dashboard is being looked at. By then wireNav has removed everything
 * that does not apply, so revealing the rest is safe.
 *
 * FAILS CLOSED either way: no flag and no module means Home alone — fewer links than somebody
 * should have, rather than more.
 */
.nav__link[data-role] {
  display: none;
}

html[data-roles~='admin'] .nav__link[data-role~='admin'],
html[data-roles~='gm'] .nav__link[data-role~='gm'],
html[data-roles~='maintenance'] .nav__link[data-role~='maintenance'],
html[data-nav='ready'] .nav__link[data-role] {
  display: inline-flex;
}

/*
 * A GRANTED link is hidden again unless the grant is on the document, and this rule has to come
 * AFTER the ones above because the specificities tie and order is what decides.
 *
 * `data-grant` marks a link its role alone does not earn — the review queue is a manager's page
 * only for a manager somebody has granted it to. Without this the rule above would draw it to
 * every manager on the first paint and wireNav would take it away a moment later, which is the
 * flash data-roles exists to prevent.
 *
 * It overrides `data-nav='ready'` too, deliberately. That flag means "the module has run and the
 * bar is whatever it made it", and the module has already removed this link if the grant is
 * missing — so the two agree, and on a page where the module never runs the safe answer is still
 * the one that shows fewer links rather than more.
 */
html:not([data-grants~='review']) .nav__link[data-grant='review'] {
  display: none;
}

/* And the link the grant replaces — see data-nogrant in wireNav. Same reasoning, opposite
   direction: without this the old Case monitor link is drawn for a granted manager and taken
   away once /api/me answers, which is the flash from the other side. */
html[data-grants~='review'] .nav__link[data-nogrant='review'] {
  display: none;
}

/*
 * The edition, over the roles. A standalone circuit's scheduler links stay hidden whatever
 * the role rules above decided — same first-paint reasoning as data-roles, with the flag
 * written unconditionally by the pre-paint script because it describes the circuit rather
 * than the account. AFTER the role rules on purpose: the specificities tie, so order is the
 * tiebreak, and this one has to win. wireNav then removes the links outright, which is what
 * keeps the data-nav='ready' fallback above safe on a borrowed view.
 */
html[data-edition='standalone'] .nav__link[data-edition='cinema'] {
  display: none;
}

.nav__link:hover {
  background: var(--surface-3);
  color: var(--text);
}

.nav__icon {
  flex: none;
  display: block;
}

/*
 * The page you are on, and nothing else.
 *
 * The accent rather than a grey chip, now that it is the only mark in the bar:
 * "you are here" is worth being unmistakable, and there is no longer a second
 * accented link for it to be confused with. Not a link offering to take you
 * anywhere either, so hover does not lift.
 */
.nav__link--current {
  color: var(--accent);
  background: color-mix(in oklab, var(--accent) 12%, transparent);
  border-color: color-mix(in oklab, var(--accent) 30%, transparent);
  font-weight: 620;
  cursor: default;
}

.nav__link--current:hover {
  background: color-mix(in oklab, var(--accent) 12%, transparent);
  color: var(--accent);
}

/*
 * A PAGE IN BETA SAYS SO IN THE BAR.
 *
 * Staffing is early, and the operator chose to say so rather than hide it (8 October 2026). The
 * Terms' section 23 covers features "labeled beta", so the label has to be where the feature is
 * used, not only on the website. Drawn from `data-beta` rather than written into the link: the dock
 * and the tests read a link's text as its name, and "Staffing" has to stay "Staffing". Generated
 * content is part of the link's accessible name, so a screen reader hears "Staffing Beta".
 */
.nav__link[data-beta] {
  position: relative;
}

/* A corner tag rather than a pill in the row: it takes no width, so the bar wraps exactly where it
   did without it (test/browser/nav-width.browser.js holds those widths). It sits in the link's top
   padding, clear of the word, and lets clicks through to the link. */
.nav__link[data-beta]::after {
  content: 'Beta';
  position: absolute;
  top: -5px;
  right: 2px;
  font-size: 0.5rem;
  font-weight: 700;
  line-height: 1.35;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 0 4px;
  border-radius: 4px;
  background: var(--info-soft);
  color: var(--info);
  border: 1px solid color-mix(in oklab, var(--info) 35%, transparent);
  pointer-events: none;
}

/*
 * THE TABLET'S STRIP, whose links are buttons.
 *
 * The back-office tablet's three destinations are panels on one page rather than pages —
 * the thirty-minute session and the sign-out that ends it belong to that page, and a tab
 * that navigated away would leave somebody's unlocked session on a counter. So its strip is
 * <button>s wearing .nav__link. Every rule that gives the class its look is class-keyed and
 * applies unchanged; these are the declarations a button needs and an anchor never did: the
 * UA's own background, its font — which does not inherit — and the hand an anchor gets free.
 */
.nav--floor .nav__link {
  background: none;
  font-family: inherit;
  cursor: pointer;
}

/* The tab you are on is not offering to take you anywhere. .nav__link--current says so
   already and would otherwise lose the cursor to the rule above on specificity. */
.nav--floor .nav__link--current {
  cursor: default;
}

.topbar__spacer {
  flex: 1 1 auto;
}

.topbar__who {
  font-size: 0.82rem;
  color: var(--text-dim);
  line-height: 1.35;
}

.topbar__who strong {
  display: block;
  color: var(--text);
  font-weight: 600;
}

/* --------------------------------------------------------- account menu
   Who you are, and Sign out. A small trigger in the sticky bar instead of a
   button, so scrolling on a phone does not drag a full-width Sign out along
   the top of the screen. */
.account {
  position: relative;
  flex: 0 0 auto;
}

.account__trigger {
  /* The box the announcement count on the avatar sits against. */
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 8px 3px 3px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--surface-2);
  color: var(--text);
  cursor: pointer;
  transition:
    background-color 140ms var(--ease),
    border-color 140ms var(--ease);
}

.account__trigger:hover {
  background: var(--surface-3);
  border-color: var(--border-strong);
}

.account__avatar {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: var(--accent-line);
  color: var(--accent-ink);
  display: grid;
  place-items: center;
  font-size: 11px;
  font-weight: 750;
  letter-spacing: -0.02em;
  flex: none;
}

.account__chevron {
  color: var(--text-faint);
  flex: none;
  transition: transform 160ms var(--ease);
}

/* ---------------------------------------------------------------- faces
   Somebody's profile picture, or their initials on the accent where they have not chosen one —
   beside a note they wrote, a question they asked, their row in the list of people. See face()
   in js/util.js. Always next to the name it belongs to, so it is never the only way to tell who;
   a screen reader skips it for that reason. */
.face {
  display: inline-grid;
  place-items: center;
  flex: none;
  overflow: hidden;
  border-radius: 50%;
  background: var(--accent-line);
  color: var(--accent-ink);
  font-weight: 750;
  letter-spacing: -0.02em;
  line-height: 1;
  user-select: none;
}

/* The initials, drawn from the attribute — see face() in js/util.js for why they are not text. */
.face[data-initials]::before {
  content: attr(data-initials);
}

/*
 * Beside who asked, on the questions at the top of the case monitor and in the question a Respond
 * answers. 26px, up from 18 at the operator's ask ("increase the profile picture size on the
 * cases so you can see the face more clearly"): a face at 18px was a coloured dot. Still no
 * taller than the buttons on the same row, so the row does not grow.
 */
/*
 * Who a case is with, on its meta line (ownerMark in js/tree.js): the ordinary ink, because most cases
 * are somebody's and that is no news; the reader's own in the accent, because "mine" is what a person
 * scans a queue for; and nobody's in the warm ink, because an unowned case is the one a manager is
 * looking for. Words first, colour second — each says it in full.
 */
.complaint__owner {
  color: var(--text-dim);
}

/*
 * After the meta line rather than inside it, in the meta line's own type: a name is the longest word
 * on the line, and inside the run it made the whole run too wide to sit beside the face of whoever
 * raised the case, so on a phone the line dropped under it. Out here it wraps alone.
 */
.complaint__top > .complaint__owner {
  font-size: 0.78rem;
  letter-spacing: 0.015em;
  white-space: nowrap;
}

/*
 * THE WORDS AS THE BUTTON that changes them (ownerControl in js/tree.js): the operator found a
 * button of its own in the action row "too cluttered", and asked for the wording to be the control.
 *
 * An outline in the words' own ink and a chevron, so it reads as something to press — a word that
 * opens a box and looks like a word is the text link the house rules turn down — and stays as quiet
 * as the meta line around it. No fill, so the impact pill keeps the one coloured block on the line.
 * As tall as the pills beside it, so the line does not grow; the colours are the owner's own, above.
 */
.complaint__owner--control {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 8px 2px 10px;
  border: 1px solid color-mix(in srgb, currentColor 38%, transparent);
  border-radius: 999px;
  background: transparent;
  font: inherit;
  font-size: 0.78rem;
  line-height: 1.3;
  letter-spacing: 0.015em;
  white-space: nowrap;
  cursor: pointer;
}

.complaint__owner--control::after {
  content: '';
  flex: none;
  width: 9px;
  height: 9px;
  background: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2.5 4.5L6 8l3.5-3.5'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2.5 4.5L6 8l3.5-3.5'/%3E%3C/svg%3E") center / contain no-repeat;
}

.complaint__owner--control:hover {
  background: color-mix(in srgb, currentColor 10%, transparent);
}

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

.complaint__owner--mine {
  color: var(--accent);
  font-weight: 600;
}

.complaint__owner--none {
  color: var(--p-peach, var(--text));
}

.face--xs {
  width: 26px;
  height: 26px;
  font-size: 10px;
}

/*
 * Beside a note on a case, at the top of the lines it starts, the way a message thread draws one.
 * 32px, up from 22 for the same ask: a face is how a run of notes reads as who said what, and at
 * 22px it could not be told from the next. js/picture-fit.js previews a picture at this size.
 */
.face--sm {
  width: 32px;
  height: 32px;
  font-size: 12.5px;
}

/* In Settings, where somebody is looking at their own. */
.face--lg {
  width: 72px;
  height: 72px;
  font-size: 24px;
}

/* Your own, in Settings: the face as colleagues will see it, and the buttons beside it. */
.profilepic {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 12px 16px;
}

.profilepic__side {
  display: grid;
  gap: 6px;
  min-width: 0;
}

.profilepic__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

/* The picture fills whichever circle it is in: a face, the account menu's, the people list's. */
.face__img {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  object-fit: cover;
}

/*
 * FIT YOUR PICTURE: the box a chosen photo opens in (js/picture-fit.js). The stage is the square
 * that is saved, with the face's circle marked on it, and the two faces beside the zoom are the
 * sizes colleagues see: Settings' and a note's, the .face--lg and .face--sm above, whose sizes
 * these repeat. The page draws each at whatever size this sheet gives it.
 */
.cropper {
  display: grid;
  gap: 14px;
}

.cropper .field__hint {
  margin: 0;
}

/*
 * Square at any width, and never wider than a phone held upright can show whole. `touch-action:
 * none` is what lets a finger drag the picture: without it the drag scrolls the page behind the
 * box instead, and two fingers zoom the whole page rather than the photo.
 */
.cropper__stage {
  display: block;
  justify-self: center;
  width: min(100%, 320px);
  aspect-ratio: 1;
  border-radius: var(--radius);
  background: var(--surface-2);
  touch-action: none;
  cursor: grab;
}

.cropper__stage:active {
  cursor: grabbing;
}

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

.cropper__zoom {
  display: flex;
  align-items: center;
  gap: 12px;
}

.cropper__label {
  flex: none;
  font-weight: 600;
}

.cropper__range {
  flex: 1 1 auto;
  min-width: 0;
  accent-color: var(--accent);
}

.cropper__preview {
  display: flex;
  align-items: center;
  gap: 12px;
}

.cropper__face {
  display: block;
  flex: none;
  border-radius: 50%;
  border: 1px solid var(--border);
}

.cropper__face--lg {
  width: 72px;
  height: 72px;
}

.cropper__face--sm {
  width: 32px;
  height: 32px;
}

/*
 * YOUR ISSUE TYPES, in Settings (js/case-types-controls.js): every type, or only the ticked ones.
 * The boxes rest ticked and faint while every type is chosen, so the list reads as "all of these"
 * rather than "none of these".
 */
.casetypes__modes {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 20px;
  margin: 10px 0 12px;
}

.casetypes__mode,
.casetypes__option {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

.casetypes__list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
  gap: 8px 16px;
}

.casetypes__list--off {
  opacity: 0.55;
}

.casetypes__gap {
  margin-top: 12px;
}

.account__trigger[aria-expanded='true'] .account__chevron {
  transform: rotate(180deg);
}

.account__menu {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  min-width: 235px;
  padding: 6px;
  /*
   * SOLID, unlike every other floating menu — and it cannot be otherwise. This menu
   * lives INSIDE the top bar, and the bar already runs a backdrop blur; backdrop
   * filters do not nest, so this element's own blur samples nothing and quietly does
   * not happen. As glass that left an 80% tint with no blur behind it, and the operator
   * read page text straight through the menu. Every menu that floats OUTSIDE a blurred
   * ancestor (the dock's More, the assistant panel) keeps the true glass; this one
   * wears the opaque surface, which over any page reads the same.
   */
  background: var(--surface-3);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lift);
  /* Above the page, but inside the topbar's own stacking context. */
  z-index: 30;
}

/*
 * THE ACCOUNT MENU OPENS OVER THE ASSISTANT, not under it.
 *
 * The menu lives inside the top bar, so it rises no higher than the bar does (20), and the
 * assistant's panel sits at 25 to float over the page. On a window short enough for the panel to
 * reach the top — 1280 by 720 is enough — the menu opened underneath it and Sign out could not be
 * pressed with the assistant open. Found while testing that signing out forgets the conversation.
 * While the menu is open the bar steps above the panel; closed, it goes back under it.
 */
.topbar:has(.account__trigger[aria-expanded='true']) {
  z-index: 26;
}

.account__menu .topbar__who {
  padding: 8px 10px 10px;
  border-bottom: 1px solid var(--border);
  margin-bottom: 6px;
}

/* A navigation item in the account menu, above Sign out. */
.account__item {
  display: block;
  padding: 9px 10px;
  border-radius: var(--radius-sm);
  color: var(--text);
  font-size: 0.88rem;
  font-weight: 600;
  text-decoration: none;
  transition: background-color 120ms var(--ease);
}

.account__item:hover {
  background: var(--surface-3);
  text-decoration: none;
}

.account__item--current {
  background: var(--surface-3);
  color: var(--accent);
}

/*
 * "Lock now", which is a <button> wearing the dress written for an <a>.
 *
 * It had this class and the class had no rule, so it rendered as what a button is when nobody
 * has said otherwise: Arial instead of the app's face, text centred, shrink-wrapped, and a
 * native grey box behind it — an OS push-button in a column of text links, sitting one row
 * above a Sign out that has had this same reset all along. It is also the deliberate lock,
 * which is a security control, and one that looks broken is one people do not press.
 *
 * The same fix as `button.stat--link` on the overview tiles, and for the same sentence: a
 * button brings its own font, centring and width that none of the <a>s have to undo.
 *
 * `font: inherit` and then the size and weight again, in that order, because the shorthand
 * resets both and this rule is written after `.account__item` — the two have equal specificity,
 * so whatever comes last is what the menu gets.
 */
.account__item--button {
  width: 100%;
  border: 0;
  background: none;
  font: inherit;
  font-size: 0.88rem;
  font-weight: 600;
  text-align: left;
  cursor: pointer;
  appearance: none;
}

/* Divides the destinations above from the one action below. */
.account__item + .account__signout {
  margin-top: 4px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
  border-radius: 0 0 var(--radius-sm) var(--radius-sm);
}

.account__signout {
  display: block;
  width: 100%;
  padding: 9px 10px;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--danger);
  font: inherit;
  font-size: 0.88rem;
  font-weight: 600;
  text-align: left;
  cursor: pointer;
  transition: background-color 120ms var(--ease);
}

.account__signout:hover {
  background: var(--danger-soft);
}

/*
 * The page header.
 *
 * Centred, with an icon beside the title. It was a bare left-aligned h1 on every page,
 * which is not wrong so much as unfinished — a heading with nothing to anchor it reads
 * like a placeholder somebody meant to come back to, and eight pages of it made the app
 * feel like scaffolding.
 *
 * Centring is the part that earns it. A left-aligned title over left-aligned content is
 * one long ragged column; a centred lockup with an icon is a header, and it gives the
 * page a top edge to start from. The content below stays left-aligned, because that is
 * where reading a list or a table actually begins.
 *
 * It is given room on all three sides. The header used to sit 34px under a sticky bar
 * and 22px above the first control, which on a busy page put the title in a sandwich —
 * close enough to the topbar to read as part of it, close enough to the filters to read
 * as their label. Air above separates it from the chrome; air below, and the rule, make
 * it a header rather than the first row of the content.
 */
.page-head {
  display: grid;
  justify-items: center;
  gap: 12px;
  padding: 46px 0 0;
  /*
   * Nothing below sets a top margin — every block in the shell spaces itself with
   * margin-bottom — so this is the whole gap between the rule and the first control,
   * not half of one.
   */
  margin-bottom: 30px;
  text-align: center;
}

/*
 * The rule under the header.
 *
 * A hairline that is brightest under the title and fades to nothing at both ends. A
 * border would have been the cheap way to do it, but a hard line wall to wall cuts the
 * page in two and makes the header look bolted on; fading out at the edges means the
 * separation is strongest exactly where the centred lockup is and has quietly stopped
 * by the time it reaches the margin.
 *
 * A pseudo-element rather than markup, so the eight pages carry nothing for it and a
 * ninth gets it for free. `justify-self: stretch` because the header centres its
 * children, and this is the one child that should be as wide as the page.
 */
.page-head::after {
  content: '';
  justify-self: stretch;
  height: 1px;
  /* On top of the 12px grid gap: the rule belongs to the space between, not to the
     paragraph above it. */
  margin-top: 14px;
  background: linear-gradient(
    90deg,
    transparent,
    color-mix(in oklab, var(--accent) 26%, var(--border)) 24%,
    color-mix(in oklab, var(--accent) 46%, var(--border)) 50%,
    color-mix(in oklab, var(--accent) 26%, var(--border)) 76%,
    transparent
  );
}

/* The icon and the words are one unit, so they wrap and centre together. */
.page-head__title {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 13px;
  min-width: 0;
}

.page-head__title h1 {
  margin: 0;
}

/*
 * The icon, in a tinted tile.
 *
 * A tile rather than a bare glyph: at this size a 1.7px stroke floating beside a 2rem
 * heading looks like it fell off something, and the tile gives it the same visual weight
 * as the word next to it. Accent-tinted rather than filled, so it sits with the heading
 * rather than competing with the page's one real call to action.
 *
 * Drawn per page and inline, like the nav's home icon — eight different glyphs is eight
 * different files or eight inline SVGs, and inline is the one that costs no request and
 * recolours with the theme for free.
 */
.page-head__icon {
  display: grid;
  place-items: center;
  flex: none;
  width: 42px;
  height: 42px;
  border-radius: 13px;
  color: var(--accent);
  background: color-mix(in oklab, var(--accent) 13%, var(--surface));
  border: 1px solid color-mix(in oklab, var(--accent) 26%, transparent);
  box-shadow: inset 0 1px 0 color-mix(in oklab, var(--accent) 18%, transparent);
}

/* One line under the title — today's date, the queue's counts — written by the page's script and
   absent until it is. Not a paragraph, so the page descriptions switch below never hides it. */
.page-head__sub {
  color: var(--text-dim);
}

.page-head__sub:empty {
  display: none;
}

.page-head p {
  color: var(--text-dim);
  margin: 0;
  /* Centred text needs a shorter measure than left-aligned: both edges are ragged, so a
     long line is harder to come back to at the start of the next one. */
  max-width: 58ch;
}

/*
 * "Show page descriptions", from Settings. Off unless you ask for it.
 *
 * One rule for every page, because every page's blurb is the paragraph under the
 * heading inside .page-head — so this cannot go stale as pages are added, and a new
 * page gets the behaviour without knowing about it. The paragraph stays in the
 * markup: it is what explains the page to somebody new, and a screen reader still
 * reaches it through Settings.
 *
 * Hidden by default rather than shown, because the descriptions earn their space in
 * the first week and cost it every week after. The heading keeps its own bottom
 * margin, so what is left is the title with the page's own spacing rather than a
 * heading jammed against the content.
 *
 * The attribute is written by /theme.js before first paint (see src/app.js), and
 * only for the accounts that turned them on. Applied on load instead, the paragraph
 * would render and then vanish, which is a page that moves under you as you start
 * reading it — and this way round, the flash cannot happen at all for the default.
 */
.page-head p {
  display: none;
}

:root[data-help='on'] .page-head p {
  display: block;
}

/*
 * And the sentence under each SETTING, on the same switch and for the same reason.
 *
 * The settings page carries a paragraph per control — twenty-odd of them, explaining what
 * moving each one does. Read once, they are scrollpast forever after, and a page that is
 * mostly explanation is a page an operator scrolls rather than uses: the label and the
 * bounds on the control say what it is. So they follow "Page descriptions", which already
 * exists, is already off by default, and is already where somebody who wants the detail
 * back would look for it.
 *
 * The hints that are a WARNING rather than a description keep their own class and stay —
 * see .field__hint--bad, which is what a refusal or a consequence is written in.
 */
.setting .field__hint:not(.field__hint--bad) {
  display: none;
}

:root[data-help='on'] .setting .field__hint:not(.field__hint--bad) {
  display: block;
}

/*
 * A row's own STATE, which is not a description and must not follow that switch.
 *
 * The rule above is about the paragraph under a control explaining what moving it does — read
 * once, scrollpast forever. This is the other thing a row can say: what it is doing right now.
 * "Connected to Apaleo. Last read 2 hours ago", "Last read failed: the property id was refused."
 *
 * The difference is whether the sentence is still true tomorrow. An explanation is; a status is
 * the answer to the question somebody opened the card to ask, and hiding it behind a preference
 * most people never find leaves a card that lists connections without saying whether any of them
 * work. It nearly shipped that way: written as a .field__hint, it was in the DOM, asserted by a
 * passing test, and painted nowhere — innerText() reads a display:none element quite happily.
 */
.setting__state {
  margin: 0.15rem 0 0;
  font-size: 0.82rem;
  line-height: 1.45;
  color: var(--text-dim);
}

/* A row whose state is a warning (AI triage when Jev cannot be asked, js/settings.js): the box
   clears the control above it, and the divider to the next row does the rest. */
.setting > .notice {
  margin: 12px 0 0;
}

/* ------------------------------------------------------------------ cards */
.card {
  background: var(--glass);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow), var(--rim);
  padding: 22px;
}

.card + .card {
  margin-top: 16px;
}

.card > h2:first-child {
  margin-bottom: 16px;
}

/* ------------------------------------------------------------------ forms */
.field {
  margin-bottom: 18px;
}

.field > label,
.field__legend {
  display: block;
  font-weight: 600;
  font-size: 0.9rem;
  margin-bottom: 7px;
  color: var(--p-subtext1);
}

.field__hint {
  font-size: 0.82rem;
  color: var(--text-dim);
  margin: 6px 0 0;
}

/* The same line, when it is a reason the value was refused. For askForText in js/util.js, which
   keeps the box open with the complaint under the field rather than closing and reopening. */
.field__hint--bad {
  color: var(--danger);
  font-weight: 600;
}

/*
 * A yes/no field inside an inline edit row. The label keeps the same position as
 * every text field's so the row's baselines line up, and the box sits under it in
 * place of an input rather than beside the text.
 */
.field--check > label {
  margin-bottom: 7px;
}

/* Matches an input's height so the box centres against the fields beside it. */
.field--check__box {
  display: flex;
  align-items: center;
  min-height: 42px;
}

.field--check input[type='checkbox'] {
  width: 18px;
  height: 18px;
  margin: 0;
  accent-color: var(--accent);
  cursor: pointer;
}

.field__optional {
  font-weight: 400;
  color: var(--text-faint);
}

/*
 * Every control the app has, listed by type rather than as a bare `input`, so a checkbox
 * or a radio is not given a 44px text box to live in.
 *
 * `date` earns a note. A type missing from this list gets no styling at all, which on a
 * phone is not a cosmetic problem: iOS Safari **zooms the page** when focus lands in a
 * field whose font-size is under 16px, and a browser's default for one of these is around
 * 13px. So an unlisted input does not merely look wrong — it throws the viewport at the
 * reader. That is what `input[type='date']` did the day it was added.
 */
input[type='text'],
input[type='email'],
input[type='tel'],
input[type='password'],
input[type='search'],
input[type='number'],
input[type='date'],
input[type='time'],
/* The colour well. In the shared list so it inherits the 16px floor and the focus ring like
   every other control — .brandpick__well then strips the browser's bevel and gives it a fixed
   size, which is the only part of this rule it overrides. */
input[type='color'],
select,
textarea {
  width: 100%;
  font: inherit;
  font-size: 1rem;
  color: var(--text);
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  padding: 11px 13px;
  min-height: 44px;
  transition:
    border-color 120ms var(--ease),
    box-shadow 120ms var(--ease),
    background-color 120ms var(--ease);
}

input::placeholder,
textarea::placeholder {
  color: var(--text-faint);
}

input:hover:not(:disabled),
select:hover:not(:disabled),
textarea:hover:not(:disabled) {
  border-color: var(--p-surface2);
}

input:focus,
select:focus,
textarea:focus {
  outline: none;
  /* The colour alone, never the `background` shorthand: that puts every other part of a field's
     background back to its default as well. A select lost its chevron the moment it had focus,
     and the Showtime scheduler's cinema picker, which draws its own, had it tiled across the
     whole pill. */
  background-color: var(--surface-3);
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--accent) 22%, transparent);
}

textarea {
  resize: vertical;
  min-height: 148px;
  line-height: 1.55;
}

select {
  appearance: none;
  background-image: linear-gradient(45deg, transparent 50%, var(--text-dim) 50%),
    linear-gradient(135deg, var(--text-dim) 50%, transparent 50%);
  background-position: right 18px center, right 13px center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  padding-right: 36px;
  cursor: pointer;
}

/*
 * The severity dropdown on a report card, which sits in a row of buttons rather than in a form.
 *
 * Shrink-wrapped instead of filling its line, so it reads as one control among the card's actions
 * and not as a field. A class beats the bare `select` selector above, so the narrower padding here
 * still wins.
 */
.severity-select {
  flex: 0 0 auto;
  width: auto;
  min-height: 36px;
  padding: 4px 28px 4px 10px;
  font-size: 0.87rem;
}

/*
 * A date field, made to obey the width it is given.
 *
 * WebKit sizes a native date control from its own content and lets that intrinsic width beat
 * `width: 100%`, so on iOS this came out visibly wider than every other control beside it and
 * gave the form a horizontal scrollbar — a box of stacked fields that slid sideways as well as
 * up and down. Dropping the native appearance is what makes it obey, which is the same thing
 * this sheet already does to `select` a few lines up.
 *
 * `min-width: 0` and `max-width: 100%` are the belt to that: whatever any engine thinks the
 * intrinsic minimum is, the used width cannot exceed the space available, so an overflow is
 * impossible rather than merely unlikely.
 */
input[type='date'],
input[type='time'] {
  appearance: none;
  -webkit-appearance: none;
  min-width: 0;
  max-width: 100%;
}

/*
 * The one part of the native control worth keeping: the button that opens the picker.
 *
 * `appearance: none` takes it away, and on a desktop that is the affordance — clicking the
 * glyph is how most people open a date picker, and without it the field looks like a text box
 * that happens to reject what you type. Its colour comes from `color-scheme`, which each
 * theme sets, so it stays legible on a dark surface without being recoloured here.
 */
input[type='date']::-webkit-calendar-picker-indicator,
input[type='time']::-webkit-calendar-picker-indicator {
  display: block;
  cursor: pointer;
  opacity: 0.7;
  transition: opacity 120ms var(--ease);
}

input[type='date']::-webkit-calendar-picker-indicator:hover,
input[type='time']::-webkit-calendar-picker-indicator:hover {
  opacity: 1;
}

input[aria-invalid='true'],
textarea[aria-invalid='true'],
select[aria-invalid='true'] {
  border-color: var(--danger);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--danger) 18%, transparent);
}

.counter {
  font-size: 0.78rem;
  color: var(--text-faint);
  text-align: right;
  margin-top: 6px;
  font-variant-numeric: tabular-nums;
}

/* Says what the form covers while only one category is rolled out, standing in
   for the picker rather than leaving an unexplained gap. */
.scope-note {
  margin: 0 0 20px;
  padding: 12px 14px;
  border-radius: var(--radius-sm);
  border: 1px solid color-mix(in oklab, var(--accent) 30%, transparent);
  background: var(--accent-soft);
  color: var(--p-subtext1);
  font-size: 0.9rem;
}

.scope-note[hidden] {
  display: none;
}

/*
 * Radio pill group — big tap targets for a phone held in one hand.
 *
 * A grid of equal tiles rather than a flow of differently-sized ones. Eight categories
 * of very different lengths — "Sound" beside "Concessions / queue" — wrapped into a
 * ragged staircase where no two rows lined up, which is the single thing that made this
 * page look thrown together. Equal columns cost a little width on the short labels and
 * buy an aligned block.
 *
 * `auto-fit` with a floor rather than a fixed count: two columns on a phone, three or
 * four on a tablet, and nothing to change if a category is added.
 */
.pills {
  border: 0;
  padding: 0;
  margin: 0;
  min-width: 0;
}

.pills__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  /*
   * Every ROW the same height as the tallest, not just every tile within its own row.
   *
   * `height: 100%` on the tile below stretches it to its row, which made the grid look even
   * only for as long as no label wrapped. One long label — "Concessions / queue" at a narrow
   * width — made its row taller than the others and the block went ragged, which is the exact
   * thing the tiles were made uniform to avoid. Found when a longer line elsewhere on the page
   * narrowed the grid by a scrollbar's width and the third row started wrapping.
   */
  grid-auto-rows: 1fr;
  gap: 8px;
}

.pill {
  position: relative;
}

.pill input {
  position: absolute;
  inset: 0;
  opacity: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  cursor: pointer;
}

.pill span {
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  /* Every tile the same height whatever its label wraps to, which is the other half of
     the grid reading as a block rather than as a list. */
  min-height: 52px;
  height: 100%;
  padding: 10px 14px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  font-size: 0.92rem;
  line-height: 1.25;
  background: var(--surface-2);
  color: var(--p-subtext1);
  user-select: none;
  transition:
    background-color 140ms var(--ease),
    border-color 140ms var(--ease),
    color 140ms var(--ease),
    transform 140ms var(--ease);
}

.pill:hover span {
  border-color: var(--p-surface2);
  color: var(--text);
}

.pill input:checked + span {
  background: var(--accent-fill);
  border-color: var(--accent-fill);
  color: var(--accent-ink);
  font-weight: 650;
}

.pill input:active + span {
  transform: scale(0.97);
}

.pill input:focus-visible + span {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* ---------------------------------------------------------------- buttons
   Three weights, so a screen full of actions does not shout:

     .btn            tonal — an accent-tinted fill with accent text. The default
                     primary. Reads clearly as the main action in a row without
                     a saturated block of colour, which matters on the review
                     queue where every complaint carries one.
     .btn--cta       solid fill. Reserved for the single main action on a page
                     (sign in, send my report), where boldness is the point.
     .btn--secondary neutral. Supporting actions.
     .btn--danger    tonal red. Destructive.

   Tonal and danger share a weight on purpose: in a list, "validate" and
   "reject" are peers, and neither should dominate the row.
   -------------------------------------------------------------------------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  font: inherit;
  font-size: 0.94rem;
  font-weight: 620;
  letter-spacing: -0.01em;
  min-height: 44px;
  padding: 11px 18px;
  /* The --btn-* hooks are Studio's (see "Studio" at the foot of this file): a pill in the
     control grey. Hooks rather than a second .btn rule down there, because a later rule of
     this weight would also beat every variant below it — the CTA's fill, the ghost's none. */
  border-radius: var(--btn-radius, var(--radius-sm));
  /* Glass body, accent voice: the fill is the app's material, the colour stays in the
     text so a button still reads as a button at a glance. */
  background: var(--btn-bg, var(--glass));
  color: var(--accent);
  border: 1px solid var(--btn-edge, var(--border-strong));
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
  transition:
    background-color 140ms var(--ease),
    border-color 140ms var(--ease),
    color 140ms var(--ease),
    transform 100ms var(--ease),
    opacity 140ms var(--ease);
}

.btn:hover:not(:disabled) {
  background: color-mix(in oklab, var(--accent) 26%, var(--surface));
  border-color: color-mix(in oklab, var(--accent) 55%, transparent);
  color: var(--accent-hover);
}

.btn:active:not(:disabled) {
  transform: translateY(1px);
}

.btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/* The one bold action on a page. Filled, but deepened off the raw palette
   value so it is confident rather than glaring. */
.btn--cta {
  background: var(--accent-fill);
  color: var(--accent-ink);
  border-color: transparent;
  box-shadow: var(--btn-cta-lift, none);
}

.btn--cta:hover:not(:disabled) {
  background: var(--accent-fill-hover);
  border-color: transparent;
  color: var(--accent-ink);
}

.btn--block {
  width: 100%;
}

/*
 * The primary look, named.
 *
 * It is what the base `.btn` already draws, so this restates rather than adds — and that is
 * the point. `btn--primary` was used with no rule behind it (home.js toggles it against
 * btn--secondary for "Edit dashboard"), which worked only because the base happens to be the
 * accent style. Change the base to something neutral one day and the toggle would have
 * silently stopped distinguishing the two modes, with no rule anywhere to notice.
 */
.btn--primary {
  background: color-mix(in oklab, var(--accent) 15%, var(--surface));
  color: var(--accent);
  border-color: color-mix(in oklab, var(--accent) 34%, transparent);
}

.btn--secondary {
  background: var(--btn-quiet-bg, var(--surface-2));
  color: var(--btn-quiet-ink, var(--p-subtext1));
  border-color: var(--btn-edge, var(--border-strong));
}

/*
 * The quiet one — and until now, the loudest one on the page.
 *
 * `btn--ghost` is used in five modules and had NO RULE ANYWHERE, so every button wearing it
 * fell through to the base `.btn`, which is the accent-tinted primary. The Remove beside each
 * QR placard, Back in the setup walkthrough, Dismiss and Allow-one-export on the console:
 * all of them drawn as the thing you are meant to press. That is what "a large remove button
 * next to it" was, and no test could see it — the class was on the element exactly as the
 * code intended, and the stylesheet simply had nothing to say about it.
 *
 * Found by screenshotting the console strip and noticing the wrong button was shouting.
 */
.btn--ghost {
  background: transparent;
  color: var(--p-subtext0);
  border-color: transparent;
}

.btn--ghost:hover:not(:disabled) {
  background: var(--surface-2);
  border-color: var(--border);
  color: var(--text);
  box-shadow: none;
}

.btn--secondary:hover:not(:disabled) {
  background: var(--btn-quiet-hover, var(--surface-3));
  border-color: var(--p-surface2);
  color: var(--text);
  box-shadow: none;
}

/* An icon inside a button. `.btn` is already a flex row with a gap, so the only thing to say is
   that the glyph does not get squashed when the label is the part under pressure. */
.btn__icon {
  flex: 0 0 auto;
}

.btn--danger {
  background: var(--btn-danger-bg, transparent);
  color: var(--danger);
  border-color: var(--btn-danger-edge, color-mix(in oklab, var(--danger) 45%, transparent));
}

.btn--danger:hover:not(:disabled) {
  background: var(--danger-soft);
  border-color: var(--danger);
  box-shadow: none;
}

.btn--sm {
  min-height: 34px;
  padding: 6px 12px;
  font-size: 0.85rem;
  border-radius: var(--btn-radius, 8px);
}

.btn--link {
  background: none;
  border: 0;
  color: var(--p-lavender);
  min-height: 0;
  padding: 0;
  font-weight: 500;
  font-size: 0.87rem;
  text-decoration: underline;
  text-underline-offset: 2px;
}

.btn--link:hover {
  color: var(--accent);
  background: none;
  box-shadow: none;
}

/* ----------------------------------------------------------------- badges */
.badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  /* The --badge-* hooks are Studio's: a pill in sentence case. Hooks for the reason the
     buttons have them — half the variants below set their own case, tracking or weight. */
  font-size: var(--badge-size, 0.72rem);
  font-weight: 650;
  letter-spacing: var(--badge-tracking, 0.04em);
  text-transform: var(--badge-case, uppercase);
  padding: 3px 9px;
  border-radius: var(--badge-radius, 7px);
  border: 1px solid transparent;
  white-space: nowrap;
}

.badge--pending_review,
.badge--open {
  background: var(--info-soft);
  color: var(--info);
  border-color: color-mix(in oklab, var(--info) 35%, transparent);
}

.badge--validated,
.badge--in_progress {
  background: var(--warn-soft);
  color: var(--warn);
  border-color: color-mix(in oklab, var(--warn) 35%, transparent);
}

/* Beside a page's own heading while it is in beta — the twin of .nav__link[data-beta]. */
.badge--beta {
  background: var(--info-soft);
  color: var(--info);
  border-color: color-mix(in oklab, var(--info) 35%, transparent);
  vertical-align: middle;
}

.badge--released,
.badge--resolved {
  background: var(--ok-soft);
  color: var(--ok);
  border-color: color-mix(in oklab, var(--ok) 35%, transparent);
}

.badge--rejected {
  background: var(--surface-3);
  color: var(--text-faint);
  border-color: var(--border-strong);
}

.badge--high {
  background: var(--danger-soft);
  color: var(--danger);
  border-color: color-mix(in oklab, var(--danger) 40%, transparent);
}

/* A step past high: filled rather than tinted, because it has to win against a card
   that already has three or four badges on it. */
.badge--emergency {
  background: var(--danger);
  color: var(--p-base);
  border-color: var(--danger);
  font-weight: 750;
  letter-spacing: 0.04em;
}

/*
 * A screen that is down, in the queue.
 *
 * The card is already first — the sort sees to that — so this is about being findable
 * when somebody scrolls past it later, not about shouting. A left edge and a tint, the
 * same language the app uses for a spotlighted report.
 *
 * Its colours live with the other report-card rules rather than here, because a card's
 * background is now set in one place and a tint declared up here would be overridden by
 * it further down. See `.complaint--report[data-severity='emergency']`.
 */

/*
 * And on the home page, above everything.
 *
 * Drawn only when a screen is actually down, so it can afford to be loud: an alert that
 * is usually empty is furniture, and this one is never empty for a good reason.
 */
.alarm {
  /*
   * The panel's temperature, as one token.
   *
   * Every rule below reads this rather than `--danger` directly, which is what lets the
   * inspection alert be `.alarm--due` — the same panel in amber — instead of a hundred and
   * fifty lines of near-identical CSS that drift apart the first time either is touched.
   */
  --alarm-ink: var(--danger);
  margin: 0 0 24px;
  padding: 16px 18px;
  border: 1px solid var(--alarm-ink);
  border-left: 4px solid var(--alarm-ink);
  border-radius: var(--radius);
  background: color-mix(in oklab, var(--alarm-ink) 10%, var(--surface));
}

/*
 * Inspections expected soon: the same panel, one temperature down.
 *
 * A screen that is down is red and costs money by the hour. A fire inspection due in a
 * fortnight costs nothing if somebody books it this week, and colouring the two the same
 * would teach a reader that red on this page does not necessarily mean now.
 */
/*
 * Questions waiting on the reader, at the top of a case page (js/asks.js). The info ink: a
 * person is waiting on them, which is worth the band, and nothing is broken, which is why it is
 * not the red or the amber.
 */
.alarm--asks {
  --alarm-ink: var(--info);
}

/* The reader's own questions, quieter again — they already know what they asked. */
.alarm--asked {
  --alarm-ink: var(--text-dim);
}

.alarm--due {
  --alarm-ink: var(--warn);
}

/*
 * The week ahead in reports (js/weather-ahead.js): the same panel in the info ink. Nothing is wrong
 * yet — it is a forecast, what the last days like it brought and what to expect — so it is neither
 * the red of a screen that is down nor the amber of something about to be overdue.
 */
.alarm--ahead {
  --alarm-ink: var(--info);
}

/*
 * News on the cases the reader started (js/case-notices.js): the asks' info ink, because it is the
 * same kind of thing — a colleague, about a case, for this one person — and nothing in it is broken.
 * The colour that matters is on each line's tag, so the panel stays the calm one.
 */
.alarm--notices {
  --alarm-ink: var(--info);
}

/*
 * What happened, as a tag at the start of the line, so the eye can run down the column for the one
 * it is waiting for: Closed in the colour of done, a fix claimed on the floor in the same colour with
 * its ring broken because nobody has signed it off, a reopening and a fix taken back in the warm ink,
 * and a note plain. Outlined like every reason chip on the programming page; mixed toward the text so
 * each holds its contrast on a light card as on a dark one.
 */
.case-notice__kind {
  flex: 0 0 auto;
  padding: 1px 8px;
  border: 1px solid currentcolor;
  border-radius: 999px;
  font-size: 0.72rem;
  font-weight: 650;
  white-space: nowrap;
}

.case-notice__kind--note {
  color: var(--text-dim);
}

.case-notice__kind--closed {
  color: color-mix(in oklab, var(--ok) 70%, var(--text));
}

.case-notice__kind--fixed {
  border-style: dashed;
  color: color-mix(in oklab, var(--ok) 70%, var(--text));
}

.case-notice__kind--reopened,
.case-notice__kind--unfixed {
  color: color-mix(in oklab, var(--p-peach, var(--warn)) 75%, var(--text));
}

/* A case somebody handed to the reader (src/case-assignee.js): the accent, because it is now theirs to do. */
.case-notice__kind--assigned {
  color: var(--accent);
}

/* How much more happened on the case than the line above says, and how many cases did not fit. */
.case-notice__earlier {
  margin: 4px 0 0;
  color: var(--text-dim);
  font-size: 0.82rem;
}

/* The sky's drawing at the head of a coming day, in the panel's ink rather than the page's grey. */
.alarm--ahead .wx__sky {
  color: var(--alarm-ink);
}

/*
 * The sky beside the day, inside the name's own box. A phone gives the name a line of its own (the
 * narrow breakpoint's .alarm__where), and with the drawing outside the box it sat alone on the
 * line above the day it was drawing.
 */
.alarm--ahead .alarm__where {
  display: inline-flex;
  align-items: center;
  gap: 10px;
}

/*
 * Every line whole. A fault's description is cut at two lines because the rest is on the case;
 * these lines are the whole of what Weather ahead says, and the figure is at the end of them —
 * cut, a phone read "rainy days bring about 2.3 times as…" and never how many days that was from.
 */
.alarm--ahead .alarm__what {
  display: block;
  -webkit-line-clamp: none;
  line-clamp: none;
  overflow: visible;
}

/*
 * And the inspections warning's, for the same reason: there is no case behind a line to read the
 * rest on, and no title carrying it either. Cut at two lines, a phone read "the certificate … was
 * handed in by Alpha Manager…" and never that it is waiting to be filed — the one thing the sentence
 * is there to say — and a booked visit lost who is coming the same way.
 */
.alarm--due .alarm__what {
  display: block;
  -webkit-line-clamp: none;
  line-clamp: none;
  overflow: visible;
}

/*
 * Gaps in the POS schedule (js/schedule-gaps.js): red while a gap starts tomorrow or the day after,
 * the amber of `.alarm--due` further out. Its line is the reason it is a gap — "the other 16 cinemas
 * are on sale through Thu, Oct 22" — and cut at two lines a phone lost the date at the end of it,
 * which is the half that says how far behind. So every line whole, in either colour.
 */
.alarm--gaps .alarm__what {
  display: block;
  -webkit-line-clamp: none;
  line-clamp: none;
  overflow: visible;
}

/*
 * The case monitor's Weather ahead (js/weather-ahead.js, renderWeatherPanel): the coming week, then
 * what the weather has done to reports so far as a table, which scrolls in its own box on a phone
 * rather than pushing the page sideways.
 */
.wxa__head {
  margin: 8px 0 0;
  font-size: 1rem;
}

.wxa__scroll {
  overflow-x: auto;
}

.wxa__table {
  min-width: 620px;
}

/* A dry, calm week draws no panel, and its empty box takes no gap in the stack. */
.wxa__coming:empty {
  display: none;
}

/*
 * A manager's copy does not fold, so it has no handle and needs no room for a chevron.
 * One line or three is not worth hiding, and a disclosure with two items in it is a
 * control that exists to be clicked twice.
 *
 * Two classes deep, both of them. `.alarm__head` sets the padding and draws the chevron
 * further down this file, so at equal specificity it won on source order and this left a
 * manager's panel indented behind an arrow that did nothing — the same trap `.modal--narrow`
 * fell into. A modifier beating its base should not depend on which came first.
 */
.alarm__head.alarm__head--static {
  padding-left: 0;
  cursor: default;
}

.alarm__head.alarm__head--static::before {
  content: none;
}

/*
 * Overdue is the one number in this panel that has stopped being a warning.
 *
 * Two classes, like every other modifier in this block: `.alarm__age` sets the colour
 * further down the file, so at equal specificity it won on source order and this shipped
 * bold but not amber — the weight came through because nothing else declares it, which is
 * exactly how the bug hides.
 */
.alarm__age.alarm__age--late {
  color: var(--alarm-ink);
  font-weight: 650;
}

/* Where a predicted date came from. Quieter than a reported fault, because it is the
   panel explaining itself rather than telling the reader something new. Two classes for
   the same reason as above. */
.alarm__what.alarm__what--quiet {
  color: var(--text-dim);
  font-size: 0.82rem;
}

/*
 * The headline, which is also the fold's handle.
 *
 * A <summary>, so the whole strip is the target rather than a small chevron beside it —
 * this is a panel somebody folds away once they have read it, and hunting for a 12px arrow
 * to do that is worse than not offering it. The native marker goes and a chevron of our
 * own takes its place, exactly as the auditorium lists and the settings sections do.
 *
 * No bottom margin while shut: the margin belongs between the headline and the first case,
 * and folded there is no first case — it was leaving a strip of empty red.
 */
.alarm__head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
  padding-left: 26px;
  position: relative;
  cursor: pointer;
  user-select: none;
  list-style: none;
}

.alarm[open] > .alarm__head {
  margin-bottom: 10px;
}

.alarm__head::-webkit-details-marker {
  display: none;
}

.alarm__head::before {
  content: '';
  position: absolute;
  left: 4px;
  top: 50%;
  width: 7px;
  height: 7px;
  margin-top: -5px;
  border-right: 2px solid var(--alarm-ink);
  border-bottom: 2px solid var(--alarm-ink);
  transform: rotate(-45deg);
  transition: transform 160ms var(--ease);
}

.alarm[open] > .alarm__head::before {
  transform: rotate(45deg);
  margin-top: -3px;
}

/* The focus ring goes round the strip, since the strip is the control. */
.alarm__head:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 3px;
  border-radius: 6px;
}

.alarm__tag {
  padding: 3px 10px;
  border-radius: 999px;
  background: var(--alarm-ink);
  color: var(--p-base);
  font-size: 0.72rem;
  font-weight: 750;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}

.alarm__title {
  margin: 0;
  font-size: 1.02rem;
}

/*
 * An admin's X on the overview's warnings (js/dismiss.js): the panel put away until there is
 * something new in it. Managers' and engineers' pages never draw one.
 *
 * Beside the panel, in a wrapper, rather than inside its headline: the headline is the fold's
 * <summary>, and a button inside it folded the panel as well as dismissing it. Placed over the far
 * end of that headline, where it reads as the panel's own, at the height of the headline's middle
 * — the panel's top border and padding (17px) and half a headline (12px), less half the button.
 */
.alarm-hold {
  position: relative;
}

/* Room at the end of the headline for the X, so a long title wraps before it rather than under it:
   the button reaches 23px into the headline, and the rest is the gap. */
.alarm-hold > .alarm > .alarm__head {
  padding-right: 32px;
}

/*
 * On a phone the title takes a line of its own, under the tag and the X, and the whole width of it.
 * It nearly always wrapped there anyway; reserving the X's room on its line as well cost the end of
 * a title a third line ("Thunderstorms today at Alpha / Cinema 4"), and a title short enough to sit
 * beside the tag could end under the X.
 */
@media (max-width: 560px) {
  .alarm-hold > .alarm > .alarm__head {
    padding-right: 0;
  }

  .alarm-hold > .alarm > .alarm__head > .alarm__title {
    flex-basis: 100%;
  }
}

.alarm__dismiss {
  position: absolute;
  top: 13px;
  right: 10px;
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  color: var(--text-dim);
}

/* A pointer like every other button here. Two classes deep, because the tooltip's attribute rule
   (`button[data-tip]`) reverts the cursor and outranks one class. */
.alarm-hold > .alarm__dismiss {
  cursor: pointer;
}

.alarm__dismiss svg {
  width: 16px;
  height: 16px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2.2;
  stroke-linecap: round;
}

.alarm__dismiss:hover {
  background: color-mix(in oklab, var(--text) 10%, transparent);
  color: var(--text);
}

/* Round the button and no further out, so the ring stays inside the panel it belongs to. */
.alarm__dismiss:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 0;
}

/*
 * One screen that is down: where it is, what is wrong, and where that has got to.
 *
 * A block rather than a row, because those are three different facts and the two that
 * matter most are sentences. Squeezed onto one line they each got a third of the width
 * and all three were ellipsed to uselessness.
 */
.alarm__case {
  padding: 10px 0 12px;
  border-top: 1px solid color-mix(in oklab, var(--alarm-ink) 25%, transparent);
}

/* The last one has no case under it to be separated from. */
.alarm__case:last-child {
  padding-bottom: 0;
}

.alarm__row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px 12px;
}

.alarm__where {
  font-weight: 640;
  flex: 1 1 auto;
  min-width: 0;
}

/* How long it has been dark, which is the number that decides what happens next. */
.alarm__age {
  flex: 0 0 auto;
  font-size: 0.83rem;
  color: var(--text-dim);
}

/* Who asked, or who is being waited on, with their face beside the words. */
.alarm__age--who {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

/*
 * The action, and it is the same button on every line whatever is beside it.
 *
 * Both halves of that matter. `flex: 0 0 auto` keeps it its own size — see the narrow
 * breakpoint, where the blanket `.btn { flex: 1 1 auto }` had otherwise made it as wide
 * as the leftovers. `margin-left: auto` keeps it flush right whether or not the row
 * wrapped, which it does when a cinema's name is long: without it the button landed
 * wherever the text before it ended, so a panel with three screens down had three
 * buttons at three different places down the page.
 */
.alarm__row .btn {
  flex: 0 0 auto;
  margin-left: auto;
}

/* A line's own buttons kept together at the end of the row — Remind beside Open, not spread
   across it by the auto margin every button in a row gets above. */
.alarm__actions {
  display: flex;
  flex: 0 0 auto;
  gap: 8px;
  margin-left: auto;
}

.alarm__row .alarm__actions .btn {
  margin-left: 0;
}

/*
 * What was reported, and what is being done about it.
 *
 * Two lines each at most. A fault worth a whole paragraph is rare and a single clipped
 * clause — "the projector will not" — helps nobody, so these get room to be a sentence
 * and are then cut. `line-clamp` rather than a character count, because the cut then
 * follows the width the reader actually has; the full text is on the element's title,
 * so a truncation is never a deletion.
 */
.alarm__what,
.alarm__update {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  margin: 5px 0 0;
  font-size: 0.86rem;
  line-height: 1.45;
}

.alarm__what {
  color: var(--text);
}

/*
 * The update reads as a reply to the line above it, so it is indented behind a rule in
 * the alert's own colour rather than being a second paragraph of equal weight.
 */
.alarm__update {
  padding-left: 10px;
  border-left: 2px solid color-mix(in oklab, var(--alarm-ink) 40%, transparent);
  color: var(--text-dim);
  font-size: 0.82rem;
}

/* Nobody having written anything is a status too, and it should look like the absence
   it is rather than like a note somebody left. */
.alarm__update--none {
  font-style: italic;
  border-left-color: color-mix(in oklab, var(--alarm-ink) 18%, transparent);
}

/*
 * The "is it down?" box in the report dialog.
 *
 * A checkbox rather than a fourth entry in the Impact select: it is not another shade
 * of bad, it is a different question — is this room usable — and a question deserves to
 * look like one.
 */
.down {
  border: 1px solid color-mix(in oklab, var(--danger) 35%, var(--border));
  border-radius: var(--radius);
  background: color-mix(in oklab, var(--danger) 6%, transparent);
  padding: 10px 12px;
}

/* Scoped to .down so it out-ranks `.field > label`, which is display:block and would
   otherwise stack the box above its own wording. */
.down .down__row {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  cursor: pointer;
  margin: 0;
  font-weight: 400;
  font-size: 0.92rem;
  color: var(--text);
  text-transform: none;
  letter-spacing: normal;
}

.down__row input {
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  margin: 2px 0 0;
  accent-color: var(--danger);
  cursor: pointer;
}

.down__label {
  display: block;
  min-width: 0;
}

.down__hint {
  display: block;
  margin-top: 2px;
  font-size: 0.8rem;
  color: var(--text-faint);
  font-weight: 400;
}

/*
 * One row per circuit on the deployment: what it is called, where it answers, the way in.
 *
 * Deliberately plain. This is a list a dev reads a few times a year, and it is one page's worth
 * of furniture away from the settings it sits beside — the same card, the same rhythm as a row of
 * facts, rather than a dashboard of its own.
 */
.circuits {
  display: grid;
  gap: 8px;
}

.circuit {
  display: flex;
  align-items: center;
  gap: 12px;
  justify-content: space-between;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

/* The one you are standing in, marked the way the accounts list marks "you". */
.circuit--here {
  background: color-mix(in oklab, var(--accent) 5%, transparent);
  border-color: color-mix(in oklab, var(--accent) 35%, var(--border));
}

.circuit__identity {
  display: grid;
  gap: 2px;
  min-width: 0;
}

/* Open, and Unmap where it is offered — beside each other rather than stacked, since the row is
   one line of text tall. */
.circuit__actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 0 0 auto;
}

/* Several hosts can name one circuit, so this line wraps rather than truncating one away. */
.circuit__identity .field__hint {
  overflow-wrap: anywhere;
}

/*
 * "Assign to someone else?" (js/raise.js) — the same shape as the two boxes around it and none of
 * their colour: whose a case is, is not good news or bad, so it sits in the quiet surface ink. The
 * picker under the tick is drawn only once it is ticked, so a form filed as it always was looks
 * as it always did, one line longer.
 */
.assign {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
  padding: 10px 12px;
}

/* Scoped as .done's is: `.field > label` is display:block and would stack the tick above its words. */
.assign .assign__row {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  cursor: pointer;
  margin: 0;
  font-weight: 400;
  font-size: 0.92rem;
  color: var(--text);
  text-transform: none;
  letter-spacing: normal;
}

.assign__row input {
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  margin: 2px 0 0;
  accent-color: var(--accent);
  cursor: pointer;
}

.assign__label {
  display: block;
  min-width: 0;
}

.assign__hint {
  display: block;
  margin-top: 2px;
  font-size: 0.8rem;
  color: var(--text-faint);
  font-weight: 400;
}

/* The picker, under the tick and lined up with its words rather than with the box. */
.assign__who {
  display: grid;
  gap: 6px;
  margin: 10px 0 0 28px;
}

/* Its label in the form's own label ink: nested, it is not a `.field > label` and would fall back
   to the browser's. */
.assign__who > label {
  font-weight: 600;
  font-size: 0.9rem;
  color: var(--p-subtext1);
}

/* The hint sits under the picker as every field's does, not at the grid's gap below it. */
.assign__who > .field__hint {
  margin: 0;
}

/*
 * "Is complete?" — the same box, the opposite meaning.
 *
 * Built like the one above rather than sharing its class, because the colour is the whole
 * difference between them: red for a room out of service, green for a job already done.
 * Two boxes that look alike and mean opposite things at the end of one form is how
 * somebody files an emergency they meant to close.
 */
.done {
  border: 1px solid color-mix(in oklab, var(--ok) 35%, var(--border));
  border-radius: var(--radius);
  background: color-mix(in oklab, var(--ok) 6%, transparent);
  padding: 10px 12px;
}

/* Scoped for the same reason .down is: `.field > label` is display:block and would stack
   the tick above its own wording. */
.done .done__row {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  cursor: pointer;
  margin: 0;
  font-weight: 400;
  font-size: 0.92rem;
  color: var(--text);
  text-transform: none;
  letter-spacing: normal;
}

.done__row input {
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  margin: 2px 0 0;
  accent-color: var(--ok);
  cursor: pointer;
}

.done__label {
  display: block;
  min-width: 0;
}

.done__hint {
  display: block;
  margin-top: 2px;
  font-size: 0.8rem;
  color: var(--text-faint);
  font-weight: 400;
}

/*
 * The confirmation is one short question, so it does not need the width of a form.
 *
 * Two classes deep, not one. `.modal` sets the default width and is declared further down
 * the file, so at equal specificity it won on source order and this rule did nothing at all —
 * for the screen-down confirmation, the report picker and the inspection form alike, all
 * three of which had been quietly 660px. Specificity rather than moving the rule, because
 * "the modifier beats the base" should not depend on which order the two happen to be in.
 */
.modal.modal--narrow {
  width: min(460px, calc(100vw - 32px));
}

.badge--medium {
  background: color-mix(in oklab, var(--p-peach) 12%, var(--surface));
  color: var(--p-peach);
  border-color: color-mix(in oklab, var(--p-peach) 32%, transparent);
}

.badge--low {
  background: var(--surface-3);
  color: var(--text-dim);
  border-color: var(--border-strong);
}

/*
 * "Set here" / "From the environment" — sentences, so they keep their own casing.
 * Every other badge is a one-word status that reads well shouted; these do not.
 */
.badge--source {
  background: var(--surface-3);
  color: var(--text-dim);
  border-color: var(--border-strong);
  text-transform: none;
  letter-spacing: 0;
  font-weight: 600;
}

.badge--source-saved {
  background: color-mix(in oklab, var(--p-teal) 10%, var(--surface));
  color: var(--p-teal);
  border-color: color-mix(in oklab, var(--p-teal) 28%, transparent);
}

.badge--cat {
  background: color-mix(in oklab, var(--p-teal) 10%, var(--surface));
  color: var(--p-teal);
  border-color: color-mix(in oklab, var(--p-teal) 28%, transparent);
  text-transform: capitalize;
  letter-spacing: 0;
  font-weight: 600;
}

/* A count of seats is a phrase, not a name, so it keeps the case it was written in: "48 seats with
   codes", where the capitals above made it "48 Seats With Codes". */
.badge--seats {
  text-transform: none;
}

/*
 * "AI reviewed": the AI Reviewer Agent read this report before a person confirmed it.
 *
 * FILLED, in the assistant launcher's own colour, and the fill is the load-bearing part.
 *
 * Every other badge on a card answers a question about the REPORT — how bad, what kind, what
 * stage — and wears the same soft tint with a matching outline. This one answers a question
 * about how the report was HANDLED, so it is a different KIND of chip rather than another
 * colour of the same one. That distinction has to survive the palette: in the light themes
 * --accent and --info are both blue within a few hex points of each other, so a tinted accent
 * badge would sit beside "Open" looking like a second status. Solid against outlined reads
 * apart at any hue, in all eight themes, without asking the palette for a colour it does not
 * have spare.
 *
 * Sentence case with the letter-spacing relaxed: "AI REVIEWED" tracked out like a status shouts,
 * and the fill is already doing the work of being seen.
 *
 * `cursor: help` for the same reason the 3D badge has it: it carries a tooltip saying what the
 * agent did and did not decide, and nothing happens if you click it.
 */
.badge--ai {
  background: var(--accent-fill);
  color: var(--accent-ink);
  border-color: transparent;
  text-transform: none;
  letter-spacing: 0.01em;
  font-weight: 600;
  cursor: help;
}

/*
 * The fold count: how many other reports this card answers.
 *
 * Quiet on purpose — an outline rather than a fill. It is a fact about the card's SIZE, not
 * about its urgency, and it sits in a row where severity and status are already competing to
 * be seen. `cursor: help` because it carries the sentence explaining what folding does, and
 * nothing happens if you click it.
 */
.badge--folded {
  background: transparent;
  border-color: var(--accent);
  color: var(--accent);
  text-transform: none;
  letter-spacing: 0.01em;
  font-weight: 600;
  cursor: help;
}

/* The star keeps its size wherever it sits — a flex item shrinks by default, and a squashed
   four-point spark reads as a smudge. `.btn` and `.badge` are both inline-flex with a gap
   already, so this one declaration is the whole of what the icon needs in either. */
.spark,
.badge__spark {
  flex: none;
}

/*
 * 3D capability, next to the Flat/Scope badge. A distinct colour because it
 * answers a different question — masking versus what kit is in the booth — and
 * two teal badges side by side would read as one fact split in half.
 *
 * Only rendered for a screen that has it, so its absence is the "not 3D" state
 * and there is nothing to hover on a screen that is not.
 */
.badge--3d {
  background: color-mix(in oklab, var(--p-pink) 12%, var(--surface));
  color: var(--p-pink);
  border-color: color-mix(in oklab, var(--p-pink) 32%, transparent);
  letter-spacing: 0.04em;
  font-weight: 700;
  /* It carries a tooltip and nothing else, so the cursor should not promise a click. */
  cursor: help;
}

/* ---------------------------------------------------------------- notices */
.notice {
  border: 1px solid var(--border-strong);
  border-left: 3px solid var(--border-strong);
  border-radius: var(--radius-sm);
  padding: 13px 15px;
  margin-bottom: 18px;
  font-size: 0.92rem;
}

/*
 * A page-level notice follows the reader down the page.
 *
 * Settings is one long column of collapsed cards, and its page-level message sits at the top
 * where the heading is. So a save that failed three cards down said so somewhere the reader
 * could not see: the button went back to normal, nothing else happened, and the only evidence
 * was a red box a full screen above them. People pressed it again.
 *
 * Sticky rather than a floating toast, because the message is often long — a licence refusal,
 * a restore that names four accounts — and a toast sized for a sentence either truncates those
 * or covers the form they are about. Pinned under the top bar it stays exactly as readable as
 * it was, in the place the reader already associates with "what just happened", and it is
 * still in the flow so nothing underneath is hidden by it.
 *
 * Opt-in per page rather than applied to every `.notice` in the app: most of them are already
 * beside the control they belong to, where following the reader would be following them away
 * from the thing the message is about.
 *
 * NOT called `notice--page`, and that is not a style preference. showNotice() in js/util.js
 * swaps the variant by stripping every class starting `notice--` and adding the new one, so a
 * placement class in that namespace would be deleted the first time a message was shown — the
 * sticky would work until it was needed. The helper's own comment describes it happening to
 * `modal__notice` once already.
 */
.page-notice {
  position: sticky;
  /* Under the top bar, not behind it. 63px is the bar MEASURED — 12px of padding either side of
     its row, plus the hairline — and the first guess at it was 56, which tucked the message
     seven pixels behind the thing it was supposed to sit below. The browser test asserts the
     message clears the bar's real bottom edge, so a bar that grows fails a test rather than
     quietly hiding a message again. */
  top: 71px;
  /* Below the bar (20) and above the cards, so it is never the thing a card scrolls over. */
  z-index: 15;
  box-shadow: var(--shadow-lift);
}

.notice--error {
  background: var(--danger-soft);
  border-color: color-mix(in oklab, var(--danger) 35%, transparent);
  border-left-color: var(--danger);
  color: var(--text);
}

.notice--ok {
  background: var(--ok-soft);
  border-color: color-mix(in oklab, var(--ok) 35%, transparent);
  border-left-color: var(--ok);
}

.notice--info {
  background: var(--info-soft);
  border-color: color-mix(in oklab, var(--info) 35%, transparent);
  border-left-color: var(--info);
}

/* A warning: not a failure and not good news — something the reader has to do for the thing to work. */
.notice--warn {
  background: var(--warn-soft);
  border-color: color-mix(in oklab, var(--warn) 45%, transparent);
  border-left-color: var(--warn);
}

/*
 * A line of live FACT under a control — what was last read, what a file did — set in a hint's
 * type but never folded away with the page descriptions: a description says what a control is
 * for, and this says what it did, which is the one line a reader deciding opens the card for.
 */
.setting__note {
  font-size: 0.82rem;
  color: var(--text-dim);
  margin: 6px 0 0;
}

/* The CSV import on the box office card: the file input under its name. */
.box-office-import {
  display: grid;
  gap: 6px;
}

/*
 * THE DISCOUNT NIGHT AT EACH CINEMA, under the setting (cheapNightControls in js/settings.js): a
 * cinema and what its discount night sells against its other weeknights, one line each.
 */
.cheap-night__list {
  list-style: none;
  margin: 8px 0 0;
  padding: 0;
  display: grid;
  gap: 4px;
}

.cheap-night__row {
  display: flex;
  flex-wrap: wrap;
  gap: 2px 10px;
  font-size: 0.84rem;
}

.cheap-night__cinema {
  font-weight: 600;
  min-width: 9rem;
}

.cheap-night__said {
  color: var(--text-dim);
}

/* A cinema where the night does nothing: said in full, and not dressed as the others. */
.cheap-night__row--flat .cheap-night__cinema {
  font-weight: 500;
  color: var(--text-dim);
}

/*
 * WHICH TITLES ARE ONE FILM, under the Programming lines (filmTitleControls in js/settings.js).
 * Three folded lists and a by-hand merge. Folded because the grouped list is as long as the
 * circuit's slate and is read once in a while, not every visit; "Possibly the same film" opens
 * by itself, since it is the one asking for an answer.
 */
.film-titles__group {
  margin-top: 10px;
  border-top: 1px solid var(--border);
  padding-top: 8px;
}

.film-titles__summary {
  cursor: pointer;
  font-weight: 600;
  font-size: 0.88rem;
}

.film-titles__list,
.film-titles__members {
  list-style: none;
  margin: 6px 0 0;
  padding: 0;
  display: grid;
  gap: 6px;
}

.film-titles__members {
  margin: 4px 0 0 14px;
}

.film-titles__name {
  font-weight: 600;
  font-size: 0.86rem;
}

/* A title and what to do about it on one line, the button dropping under it on a phone. */
.film-titles__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 6px 12px;
}

.film-titles__title {
  font-size: 0.84rem;
  color: var(--text-dim);
  overflow-wrap: anywhere;
}

/* A question rather than a fact: the pair is the point of the row, so it reads at full strength. */
.film-titles__row--ask .film-titles__title {
  color: inherit;
}

.film-titles__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.film-titles__merge {
  align-items: flex-end;
  margin-top: 12px;
}

.film-titles__pick {
  display: grid;
  gap: 4px;
  min-width: 0;
  flex: 1 1 200px;
}

.film-titles__select {
  max-width: 100%;
}

/* A button here is as wide as its words. Buttons grow by default (flex: 1 1 auto), and on a
   phone, wrapped under its title, that stretched one across the whole row with its label centred
   in the middle, where it read as belonging to the row below as much as to its own. */
.film-titles__row > .btn,
.film-titles__actions > .btn {
  flex: none;
}

/* A checkbox with its words beside it, inside a dialog's field. The app's other
   checkboxes are pills in a chooser or rows in the settings schema; this is the
   plain one — a single yes/no next to a sentence.

   Written as `.field > label.check` rather than `.check`, and that is the whole
   reason it works: `.field > label` above sets `display: block` at the same
   specificity a bare class has, wins on source order, and turned the flex row into
   a stacked block with the box jammed against its own words. A screenshot caught it.
   The margin goes too — the hint underneath supplies the spacing here. */
.field > label.check,
label.check {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 0;
  font-weight: 500;
  cursor: pointer;
}

.field > label.check input,
label.check input {
  flex: none;
  margin: 0;
}

/* The sentence under a tick box, which explains the box rather than the field. */
label.check + .field__hint {
  margin-top: 6px;
}

/*
 * Who to ask, in the note dialog (js/progress.js): a tick box per person who can already open
 * the case, with their role at the end of the line. Scrolls in its own box past eight or so, so
 * a circuit with a large office does not push the question out of the dialog.
 */
.askpick {
  display: grid;
  gap: 8px;
}

/*
 * One dropdown a row, with its Remove at the end. Flex rather than a grid, so a row whose Remove
 * is hidden gives the dropdown the whole width — a grid keeps the gap for the empty column and
 * leaves the dropdown 8px short of the note box under it.
 */
.askpick__row {
  display: flex;
  gap: 8px;
  align-items: center;
}

.askpick__select {
  flex: 1 1 auto;
  min-width: 0;
  width: auto;
}

.askpick__row > .btn {
  flex: none;
}

.askpick__more {
  margin-top: 10px;
}

.askpick + .field__hint,
.askpick__more + .field__hint {
  margin-top: 10px;
}

/*
 * What a Respond is about, above the question: the report, in the card's own label style, so the
 * box is not answered against a bare reference. Cut at four lines until Show all (js/progress.js),
 * because a report can run to 4,000 characters and the box to answer in is under it.
 */
/* Space above as well as below: without it the label reads as a second line of the case's. */
.askreport {
  margin: 12px 0 14px;
}

.askreport__label {
  margin: 0 0 4px;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-faint);
}

.askreport__words {
  margin: 0;
  white-space: pre-wrap;
  line-height: 1.45;
}

.askreport__words--clamped {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 4;
  line-clamp: 4;
  overflow: hidden;
}

.askreport__more {
  margin-top: 6px;
}

/*
 * The question a Respond answers, above the box it is answered in: set apart the way a quote is,
 * because it is somebody else's words and the box under it is the reader's.
 */
.askquote {
  margin: 0 0 16px;
  padding: 10px 14px;
  border-left: 3px solid var(--info);
  border-radius: 4px;
  background: color-mix(in oklab, var(--info) 8%, var(--surface));
}

.askquote__who {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0 0 4px;
  color: var(--text-dim);
  font-size: 0.82rem;
  font-weight: 600;
}

.askquote__words {
  margin: 0;
  white-space: pre-wrap;
}

/*
 * How many questions are waiting on the reader, on the link to their cases (js/dockbar.js).
 * A pill in the info ink; the words for a screen reader are in a visually-hidden span beside it.
 *
 * .menucount is the same pill for announcements waiting on a Got it, in the account menu (see
 * wireAccountMenu in js/util.js). Its own class because the dock's painter clears every .waitcount
 * on the page before drawing its own; one rule, so the two cannot come to look different.
 */
.waitcount,
.menucount {
  display: inline-grid;
  place-items: center;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: 999px;
  background: var(--info);
  color: var(--p-base);
  font-size: 0.7rem;
  font-weight: 750;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

/* On the dock the pill sits on the icon's shoulder, where a phone's own badges go, so the tab's
   label underneath keeps its line. The item is already the positioning box. */
.dockbar__item .waitcount {
  position: absolute;
  top: -3px;
  left: calc(50% + 6px);
}

/* Beside the word in the menu, and on the avatar's shoulder — the only part of the menu on screen. */
.account__item .menucount {
  margin-left: 8px;
  vertical-align: 1px;
}

.menucount--avatar {
  position: absolute;
  top: -5px;
  right: -5px;
}

/* ------------------------------------------------------------- announcements

   The history (announcements.html), the Got it banner on each first page
   (js/announce-banner.js), and the dialogs that write them. One column, not the rail: there is
   no filter for a rail to set, only a search, and the page is read top to bottom, newest first.
   See src/announcements.js for the rules these draw. */
.announce-search,
.announce-list {
  max-width: 760px;
  margin: 0 auto;
}

.announce-search {
  margin-bottom: 18px;
}

.announce-search input {
  width: 100%;
}

.announce-list {
  display: grid;
  gap: 16px;
}

.announce {
  padding: 18px 20px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  background: var(--surface);
}

/* The one a link named — the email's Read it — the way a case a link names is lit. */
.announce--spotlight {
  background: var(--accent-soft);
  box-shadow: inset 0 0 0 2px var(--accent);
}

.announce__title {
  margin: 0;
  font-size: 1.12rem;
}

.announce__meta {
  margin: 4px 0 0;
  color: var(--text-dim);
  font-size: 0.84rem;
}

/* Marked, never hidden: a changed sentence people already confirmed says it was changed. */
.announce__edited {
  margin-left: 8px;
  padding: 1px 7px;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  color: var(--text-dim);
  font-size: 0.72rem;
  font-weight: 650;
}

.announce__body {
  margin: 12px 0 0;
  white-space: pre-wrap;
  line-height: 1.5;
}

.announce__updates {
  display: grid;
  gap: 10px;
  margin: 14px 0 0;
  padding: 0;
  list-style: none;
}

/*
 * An announcement's pictures, under its message — on this page and in the Got it box. See
 * js/announce-pictures.js.
 *
 * Several sit side by side as equal squares, each picture WHOLE inside its square rather than
 * cropped to fill it: these are posters, floor plans and notices, and a crop cut the words off the
 * sides of every landscape one. One is drawn as large as the card allows at its own shape, capped in
 * height: a poster is tall, and uncapped it would push Got it off a phone's screen when the whole
 * point of the box is reading and then pressing. A tap opens it full size.
 */
.announce-pics {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 8px;
  margin: 12px 0 0;
}

.announce-pics--one {
  grid-template-columns: minmax(0, 1fr);
  justify-items: start;
}

.announce-pics__link {
  display: block;
  max-width: 100%;
  overflow: hidden;
  border-radius: var(--radius);
  border: 1px solid var(--border-strong);
  background: var(--surface-3);
  line-height: 0;
}

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

.announce-pics__img {
  display: block;
  width: 100%;
  aspect-ratio: 1;
  object-fit: contain;
}

.announce-pics--one .announce-pics__img {
  width: auto;
  max-width: 100%;
  max-height: 420px;
  aspect-ratio: auto;
  object-fit: contain;
}

/* The pictures chosen in New announcement, before they are sent: a thumbnail and a Remove each. */
.announce-shots {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin: 4px 0 10px;
}

.announce-shots__item {
  display: grid;
  justify-items: start;
  gap: 6px;
}

.announce-shots__img {
  display: block;
  height: 96px;
  width: auto;
  max-width: 160px;
  object-fit: cover;
  border-radius: var(--radius);
  border: 1px solid var(--border-strong);
  background: var(--surface-3);
}

/* Set apart like the question over the Respond box: somebody's later words under the first ones. */
.announce__update {
  padding: 10px 14px;
  border-left: 3px solid var(--info);
  border-radius: 4px;
  background: color-mix(in oklab, var(--info) 8%, var(--surface));
}

.announce__update-meta {
  margin: 0 0 4px;
  color: var(--text-dim);
  font-size: 0.8rem;
  font-weight: 600;
}

.announce__update-body {
  margin: 0;
  white-space: pre-wrap;
}

.announce__reader,
.announce__office {
  display: grid;
  gap: 10px;
  margin-top: 14px;
}

/* The office's half, under a rule: what only an admin sees is set apart from what everyone does. */
.announce__office {
  padding-top: 14px;
  border-top: 1px solid var(--border);
}

.announce__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}

.announce__done {
  margin: 0;
  color: var(--ok);
  font-size: 0.9rem;
  font-weight: 600;
}

.announce__note {
  margin: 0;
  color: var(--text-dim);
  font-size: 0.88rem;
}

.announce__asked,
.announce__who-list {
  display: grid;
  gap: 6px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.announce__asked li {
  display: grid;
  gap: 2px;
}

.announce__asked-when,
.announce__reminded,
.announce__who-detail {
  color: var(--text-dim);
  font-size: 0.84rem;
}

.announce__asked-body,
.announce__who-question {
  white-space: pre-wrap;
}

.announce__count {
  margin: 0;
}

/* Everyone asked has confirmed: the count in the colour of done, so a finished one reads as one. */
.announce__count--done strong {
  color: var(--ok);
}

.announce__who {
  display: grid;
  gap: 14px;
}

.announce__who-head {
  margin: 0 0 6px;
  color: var(--text-faint);
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.announce__who-list li {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 2px 10px;
}

.announce__who-name {
  font-weight: 600;
}

.announce__who-question {
  flex-basis: 100%;
}

/* The compose dialog's locations, a region to a group, with the region's all in one press. */
.announce-pick__group + .announce-pick__group {
  margin-top: 12px;
}

.announce-pick__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 8px;
}

.announce-pick__region {
  font-size: 0.86rem;
  font-weight: 650;
}

/*
 * The Got it banner: the alarm panel in the accent, which is the app's own voice — nothing is
 * broken and nobody is waiting on an answer, the office has said something. The message is whole
 * rather than cut to the alarm's two lines, because Got it means "I have read this".
 */
.alarm--announce {
  --alarm-ink: var(--accent);
}

.announce-due__body {
  margin: 8px 0 0;
  white-space: pre-wrap;
  line-height: 1.5;
}

.announce-due__update {
  margin: 6px 0 0;
  font-size: 0.9rem;
}

.announce-due__update-when {
  color: var(--text-dim);
  font-weight: 650;
}

/* ------------------------------------------------------------------- guides

   How things are done here (guides.html, drawn by js/guides.js; the rules are src/guides.js's).
   One column like announcements, for the same reason: a search and a shelf of titles, then one
   guide read top to bottom — the steps in order, each picture under the step it belongs to, and
   the video and the manual as buttons above the steps, because they are often the whole point. */

/* "Ask the AI Assistant" only where there is one: the stamp theme.js writes before paint. */
html:not([data-assistant]) .guides__ask {
  display: none;
}

.guides,
.guide {
  max-width: 760px;
  margin: 0 auto;
}

.guides__search {
  margin-bottom: 18px;
}

.guides__search input {
  width: 100%;
}

.guides__list {
  display: grid;
  gap: 10px;
}

/* A whole row is the link: a manager on a phone presses the title or anywhere near it. */
.guide-row {
  display: grid;
  gap: 4px;
  padding: 14px 18px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  background: var(--surface);
  color: var(--text);
  text-decoration: none;
}

.guide-row:hover {
  border-color: color-mix(in oklab, var(--accent) 45%, var(--border-strong));
}

.guide-row:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.guide-row__title {
  font-size: 1.02rem;
  font-weight: 700;
  text-wrap: balance;
}

.guide-row__facts {
  color: var(--text-dim);
  font-size: 0.84rem;
}

.guide {
  display: grid;
  gap: 14px;
}

.guide__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

.guide__tools {
  display: flex;
  gap: 8px;
}

/* Its own width on a phone too, where the generic `.btn { flex: 1 1 auto }` stretched "All guides"
   across the screen as if going back were the page's main action. */
.guide__bar .btn {
  flex: 0 0 auto;
}

.guide__title {
  margin: 0;
  font-size: 1.45rem;
  line-height: 1.25;
  text-wrap: balance;
}

.guide__meta {
  margin: -8px 0 0;
  color: var(--text-dim);
  font-size: 0.86rem;
}

.guide__links {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

/* A long label wraps inside its button rather than pushing the row off a phone. */
.guide__link {
  max-width: 100%;
  white-space: normal;
  text-align: left;
}

/* Before you start: what to wear, what to switch off. Set apart, because it is read first and
   skipping it is how somebody gets hurt. */
.guide__before {
  padding: 12px 16px 14px;
  border: 1px solid color-mix(in oklab, var(--warn) 45%, var(--border-strong));
  border-radius: var(--radius);
  background: var(--warn-soft);
}

.guide__before-title {
  margin: 0 0 4px;
  font-size: 0.9rem;
}

.guide__words {
  margin: 0;
  white-space: pre-wrap;
  line-height: 1.55;
}

/* The steps, numbered in the margin so the number is read before the words. */
.guide__steps {
  display: grid;
  gap: 14px;
  margin: 0;
  padding: 0;
  list-style: none;
  counter-reset: guide-step;
}

.guide__step {
  position: relative;
  display: grid;
  gap: 10px;
  padding: 14px 16px 14px 56px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  background: var(--surface);
  counter-increment: guide-step;
}

.guide__step::before {
  content: counter(guide-step);
  position: absolute;
  top: 12px;
  left: 14px;
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 750;
  font-size: 0.9rem;
  font-variant-numeric: tabular-nums;
}

/* A step's picture whole, never cropped: the latch is often at the edge of the photograph. */
.guide__shot {
  display: block;
  justify-self: start;
  max-width: 100%;
}

.guide__img {
  display: block;
  max-width: 100%;
  max-height: 360px;
  border-radius: var(--radius);
  object-fit: contain;
}

/* The editor, in the dialog: each step a block with its words, its picture and its order. */
.guide-edit__field {
  display: grid;
  gap: 8px;
}

.guide-edit__steps {
  display: grid;
  gap: 12px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.guide-edit__step {
  display: grid;
  gap: 8px;
  padding: 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
}

.guide-edit__label {
  font-weight: 700;
}

.guide-edit__shot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
}

.guide-edit__img {
  width: 96px;
  height: 72px;
  border-radius: 8px;
  object-fit: cover;
}

.guide-edit__order {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.guide-edit__links {
  display: grid;
  gap: 12px;
}

.guide-edit__link {
  display: grid;
  grid-template-columns: minmax(0, 3fr) minmax(0, 2fr) auto;
  align-items: end;
  gap: 10px;
}

.guide-edit__link .field {
  margin: 0;
}

@media (max-width: 640px) {
  .guide-edit__link {
    grid-template-columns: minmax(0, 1fr);
    align-items: stretch;
    padding-bottom: 12px;
    border-bottom: 1px solid var(--border);
  }

  .guide-edit__link .btn {
    justify-self: start;
  }

  .guide__step {
    padding-left: 50px;
  }
}

.guide-edit__pdf {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
}

.guide-edit__pdf-name {
  overflow-wrap: anywhere;
  font-weight: 600;
}

/* Under the PDF: "Let the AI Assistant read this PDF", Try again when a reading failed, then how it
   stands and what it costs, each on a line of its own below the controls. */
.guide-edit__ai {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 10px;
  margin-top: 12px;
}

.guide-edit__ai > .field__hint {
  flex-basis: 100%;
  margin: 0;
}

/* A switch whose label runs long wraps on a phone rather than pushing the box sideways. */
.guide-edit__ai .switch {
  max-width: 100%;
  white-space: normal;
  text-align: left;
}

.guide-edit__ai-state {
  flex-basis: 100%;
  margin: 0;
  font-size: 0.84rem;
  font-weight: 600;
}

/* A reading that failed says so in the warning colour, beside its Try again. */
.guide-edit__ai-state--failed {
  color: var(--warn);
}

/* On the guide itself: the assistant has read its PDF. Only where there is an assistant to ask. */
.guide__pdf-read {
  margin: 8px 0 0;
  color: var(--text-dim);
  font-size: 0.86rem;
}

html:not([data-assistant]) .guide__pdf-read {
  display: none;
}

.guide-edit__places {
  display: grid;
  gap: 10px;
  margin-top: 10px;
}

.guide-edit__region {
  display: grid;
  gap: 6px;
}

.guide-edit__region-name {
  color: var(--text-dim);
  font-size: 0.8rem;
  font-weight: 650;
}

/* ------------------------------------------------------------- the four pools

   One row per slot on the Thank-you codes card: what the slot is, how many codes
   are left in it, and the two things you can do about that. The count is the whole
   point of the card — a pool that empties takes the feature down silently, with the
   guest seeing the confirmation they always saw — so it gets a state of its own the
   moment a LIVE slot drops under the floor. */
.discount-pools {
  display: grid;
  gap: 10px;
  margin-top: 4px;
}

.discount-pools__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 10px 14px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
}

.discount-pools__what {
  display: grid;
  gap: 2px;
  min-width: 0;
}

.discount-pools__label {
  font-weight: 600;
}

.discount-pools__count {
  font-size: 0.86rem;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}

/* A live slot with nothing left to give. The one state on this card worth interrupting for. */
.discount-pools__count--low {
  color: var(--warn);
  font-weight: 600;
}

.discount-pools__acts {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}

.discount-pools__load {
  display: grid;
  gap: 6px;
}

.discount-find__head {
  margin-top: 16px;
}

.discount-find {
  display: flex;
  gap: 8px;
  align-items: center;
  flex-wrap: wrap;
}

.discount-find .input {
  flex: 1 1 220px;
  min-width: 0;
}

.notice[hidden] {
  display: none;
}

/* ------------------------------------------------------------ stat strip */
.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 12px;
  margin-bottom: 24px;
}

/*
 * CENTRED, and every tile the same.
 *
 * The row grew from six tiles to nine as sales arrived, and each one had ragged left-aligned
 * text of a different length sitting at a different height — a figure, a two-line label, a
 * note some tiles have and others do not. Left-aligned text only looks tidy when the blocks
 * are the same shape, and these never will be: "292" and "Auditorium 1" are not the same
 * width and no amount of care makes them look deliberate side by side. Centring both axes
 * makes every tile the same object regardless of what is in it — which is what the strip is,
 * a row of one kind of thing.
 */
.stat {
  position: relative;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  background: var(--glass);
  box-shadow: var(--rim);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  /* The extra at the bottom is the chevron's room, given to EVERY tile rather than only the
     ones that have a chevron: centred text runs to both edges, so a linked tile alone in
     wearing the padding would sit a few pixels higher than its neighbours in the same row. */
  padding: 14px 16px 20px;
  /* Background and transform for the three that are links — see .stat--link below. Listed
     here rather than there so a tile leaving a hover eases out as well as in. */
  transition:
    border-color 140ms var(--ease),
    background-color 140ms var(--ease),
    transform 110ms var(--ease);
}

/* A row with every tile hidden is no row at all, and an empty grid would still hold its
   bottom margin — a gap above the charts with nothing to explain it. */
.stats:empty {
  display: none;
}

/* A hairline of accent along the top edge, so the strip reads as one unit.

   The theme's own accent, solid: the fill its buttons wear, and on Your brand the circuit's
   own colour. It was the accent-to-pink gradient at half strength, which read as a purple and
   blue stripe on every tile of every page rather than as the theme, and the operator asked for
   the theme's colour instead. Full strength, because half of the accent is a paler colour that
   is not the accent either. */
.stat::before {
  content: '';
  position: absolute;
  inset: 0 0 auto 0;
  height: 2px;
  background: var(--accent-fill);
}

/*
 * ------------------------------------------------------- the three tiles that go somewhere
 *
 * Awaiting review, Open with managers and Oldest still unreviewed link to exactly the
 * reports they count. The other three cannot — see renderStats() in public/js/home.js, which
 * explains why — so everything that says "press me" belongs to this class alone.
 *
 * That is a correction as much as an addition: `.stat:hover` used to lift the border on all
 * six, which promised interaction on three tiles that had none. A hover state on something
 * inert is a worse lie than no hover state at all, because it is only discovered by trying.
 */
.stat--link {
  color: inherit;
  text-decoration: none;
  cursor: pointer;
}

/*
 * The occupancy tile is a <button> — it opens a popup on this page rather than going
 * somewhere — and a button brings its own font, centring and width that none of the
 * <a> tiles have to undo. Same dress, either element.
 */
button.stat--link {
  font: inherit;
  text-align: center;
  width: 100%;
  appearance: none;
}

.stat--link:hover {
  border-color: color-mix(in oklab, var(--accent) 45%, var(--border));
  background: color-mix(in oklab, var(--accent) 4%, var(--surface));
  /* Held against the global `a:hover`, which would otherwise recolour everything inside that
     has not set a colour of its own. See the value rule below — that one is deliberate. */
  color: inherit;
}

/*
 * The number goes accent on hover, and only the number.
 *
 * It is the thing being acted on, and the label and note keep their own dimmer colour so the
 * tile does not light up all over. This started as an accident — `a:hover` sets a colour, the
 * value is the one child that inherits it — and an inherited colour that happens to land in
 * the right place is not a decision, so it is stated. Focus gets it too, or a keyboard reader
 * would be the only one without the cue.
 */
.stat--link:hover .stat__value,
.stat--link:focus-visible .stat__value {
  color: var(--accent);
}

/* The accent hairline along the top edge is at half strength at rest; on hover it is the
   cheapest way to say the whole tile is one control. */
.stat--link:hover::before {
  opacity: 1;
}

.stat--link:focus-visible {
  /* Inside the tile rather than outside it, so a ring on a tile at the edge of the grid is
     not clipped by the shell's padding. */
  outline-offset: -3px;
}

/*
 * A chevron, bottom right, and only on the ones that move.
 *
 * At rest it is faint rather than absent: a control that only announces itself on hover
 * announces itself to nobody on a touchscreen, where there is no hover at all.
 */
.stat--link::after {
  content: '';
  position: absolute;
  right: 13px;
  bottom: 13px;
  width: 7px;
  height: 7px;
  border-right: 1.5px solid var(--text-faint);
  border-bottom: 1.5px solid var(--text-faint);
  transform: rotate(-45deg);
  opacity: 0.5;
  transition:
    opacity 140ms var(--ease),
    border-color 140ms var(--ease),
    transform 140ms var(--ease);
}

.stat--link:hover::after,
.stat--link:focus-visible::after {
  opacity: 1;
  border-right-color: var(--accent);
  border-bottom-color: var(--accent);
  /* A nudge in the direction it points, which is the direction it takes you. */
  transform: rotate(-45deg) translate(1.5px, 1.5px);
}

/* Pressed, on a finger: the tile takes the press rather than only the colour changing, which
   on a touchscreen is the whole of the feedback since there was never a hover. */
.stat--link:active {
  transform: scale(0.985);
}

/*
 * Proportional figures, not tabular. Equal-width digits are for columns that have
 * to line up vertically; at this size they make a number like 121 read loose,
 * because every digit takes a zero's width.
 */
/*
 * The drawing above a tile's figure — see js/tile-icons.js. In the accent, on a tint of it, so a
 * row of numbers can be told apart before any one of them is read. Only some tiles have one; the
 * figure below starts where it always did on the ones that do not.
 */
.stat__icon {
  display: inline-grid;
  place-items: center;
  flex: none;
  width: 34px;
  height: 34px;
  margin-bottom: 12px;
  border-radius: 11px;
  /* The fill's tint under the text accent: a deeper square with a lighter drawing on it, as
     drawn, where one colour for both left the drawing sitting in a wash of itself. */
  background: color-mix(in srgb, var(--accent-fill) 16%, transparent);
  color: var(--accent);
}

.stat__icon svg {
  width: 18px;
  height: 18px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.stat__value {
  font-size: 2.15rem;
  font-weight: 780;
  line-height: 1.05;
  letter-spacing: -0.045em;
  font-variant-numeric: tabular-nums;
  /* For the accent on a linked tile's hover; the global `a` transition covers the anchor, not
     a child that sets its own colour. */
  transition: color 140ms var(--ease);
}

/*
 * A tile whose value is a NAME rather than a number — Most reported says "Auditorium 4",
 * not 4. At the number's size and tracking it overflowed the tile, because a room's name is
 * three times the width of the widest figure any other tile shows. Smaller, allowed to wrap,
 * and without tabular figures, which only make sense on digits.
 */
.stat__value--text {
  font-size: 1.35rem;
  letter-spacing: -0.02em;
  line-height: 1.15;
  font-variant-numeric: normal;
  overflow-wrap: anywhere;
  text-wrap: balance;
}

.stat__label {
  font-size: 0.72rem;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-weight: 600;
  margin-top: 4px;
}

/* The one line of context a number needs — what it is measured against. */
.stat__note {
  font-size: 0.78rem;
  color: var(--text-dim);
  margin-top: 6px;
}

.stat__note--warn {
  color: var(--warn);
  font-weight: 600;
}

/*
 * A step past warn, for the one thing on the case monitor's overview that is not a
 * warning but a loss: an auditorium that cannot play. Amber is "look at this soon" and
 * it is the right weight for a queue ageing; a dark screen is money leaving the building
 * while the tile is on screen, and reading it in the same colour as "two days old" would
 * be the overview failing at the one job it has.
 */
.stat__note--bad {
  color: var(--danger);
  font-weight: 600;
}

/* The freshness line under a live figure — Current patrons says when its numbers were
   last synced. Fainter and smaller than the note above it: it is a caveat, not a fact
   of its own, and a timestamp that shouted would compete with the number it qualifies. */
.stat__sync {
  font-size: 0.68rem;
  color: var(--text-faint);
  margin-top: 4px;
}

/*
 * ============================================================ editing the dashboard
 *
 * One mode, both rows. A reader whose job is the till and a reader whose job is the queue
 * want different halves of this page, and the honest answer to that is not a shorter page
 * for everybody — it is letting each of them put away what they do not use.
 *
 * Everything here is scoped to `.is-editing` on <body> or to the edit-mode classes home.js
 * adds, so a dashboard being read carries none of it: no grips, no ✕, no dashed outlines.
 * The tray's + card is the one piece that only exists while editing, because a control that
 * re-adds hidden cards has nothing to say to somebody who is not hiding any.
 */

/* The card being edited: still readable, plainly in hand. The dashed edge is the one signal
   that says "this can be moved" without needing a hover to discover it. */
.stat--edit,
.is-editing .chart {
  border-style: dashed;
  border-color: color-mix(in oklab, var(--accent) 40%, var(--border));
}

/* Room at the top for the grip and the ✕, which are absolute and would otherwise sit on
   the figure. Only while editing, so a tile being read is never padded for controls it
   does not have. */
.stat--edit {
  padding-top: 32px;
}

/* The grip on a tile — the chart's own grip, at a tile's scale, in its top-left corner.
   Absolute, so it takes no room from the centred figure it sits beside. */
.stat__grip {
  position: absolute;
  top: 4px;
  left: 5px;
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  padding: 0;
  border: 1px solid transparent;
  border-radius: 7px;
  background: none;
  font: inherit;
  font-size: 0.9rem;
  line-height: 1;
  color: var(--text-faint);
  cursor: grab;
  /* Stops a drag on a touchscreen from scrolling the page instead. */
  touch-action: none;
  transition:
    background-color 140ms var(--ease),
    color 140ms var(--ease);
}

.stat__grip:hover,
.stat__grip:focus-visible {
  background: var(--surface-3);
  border-color: var(--border);
  color: var(--text);
}

.stat__grip:active {
  cursor: grabbing;
}

/*
 * The ✕ that puts a card away.
 *
 * Shared by the tiles and the charts, which is why it is `card__hide` and not either one's
 * class: the gesture is the dashboard's, and one spelling of it means one place to change
 * how hiding looks. On a tile it sits opposite the grip; on a chart it joins the head row
 * beside the grip that is already there.
 */
.card__hide {
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  flex: none;
  padding: 0;
  border: 1px solid transparent;
  border-radius: 7px;
  background: none;
  font: inherit;
  font-size: 0.78rem;
  line-height: 1;
  color: var(--text-faint);
  cursor: pointer;
  transition:
    background-color 140ms var(--ease),
    border-color 140ms var(--ease),
    color 140ms var(--ease);
}

.stat--edit .card__hide {
  position: absolute;
  top: 4px;
  right: 5px;
}

/* Hiding is destructive-ish — recoverable, but it takes something off the page — so the
   hover is the app's warn colour rather than its accent. */
.card__hide:hover,
.card__hide:focus-visible {
  background: color-mix(in oklab, var(--warn) 12%, var(--surface));
  border-color: color-mix(in oklab, var(--warn) 45%, var(--border));
  color: var(--warn);
}

/*
 * A chart's grip is full strength while editing rather than waiting for a hover.
 *
 * It is NOT hidden outside edit mode, and that is deliberate rather than an omission: the
 * same grip and the same reorder.js serve the manager's and engineer's overview, which has
 * no edit mode of its own — gating it on this class would take drag-to-rearrange away from
 * those pages entirely. So the charts keep the affordance they have always had, and edit
 * mode adds what only it can offer: the ✕, and the same grip on the tiles.
 */
.is-editing .chart__grip {
  opacity: 1;
}

/* ------------------------------------------------------------------- the tray */

/*
 * The + at the end of a row: a card-shaped disclosure holding what has been put away.
 *
 * A <details> because the app's other menus are, and because the keyboard and Escape come
 * free with it. Sized like the cards it sits among so the row keeps its rhythm — in the
 * charts grid it spans nothing and simply takes the next slot.
 */
.dash-add {
  position: relative;
  border: 1px dashed color-mix(in oklab, var(--accent) 35%, var(--border));
  border-radius: var(--radius);
  background: color-mix(in oklab, var(--accent) 3%, var(--surface));
  min-height: 84px;
}

.dash-add__summary {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  height: 100%;
  min-height: 84px;
  padding: 10px;
  cursor: pointer;
  color: var(--text-dim);
  list-style: none;
  text-align: center;
}

/* Safari draws its own triangle unless both of these are said. */
.dash-add__summary::-webkit-details-marker {
  display: none;
}

.dash-add__summary::marker {
  content: '';
}

.dash-add__summary:hover {
  color: var(--text);
  background: color-mix(in oklab, var(--accent) 6%, transparent);
  border-radius: var(--radius);
}

.dash-add__sign {
  font-size: 1.5rem;
  font-weight: 600;
  line-height: 1;
  color: var(--accent);
}

.dash-add__count {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-weight: 600;
}

/*
 * The menu itself, over the page rather than in the flow: a list that pushed the grid
 * around as it opened would move the very cards somebody is arranging.
 */
.dash-add__menu {
  position: absolute;
  z-index: 5;
  top: calc(100% + 6px);
  left: 0;
  min-width: max(100%, 220px);
  max-width: 320px;
  max-height: 320px;
  overflow-y: auto;
  padding: 6px;
  background: var(--surface-2, var(--surface));
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: 0 14px 34px rgb(0 0 0 / 28%);
}

/* Right-hand end of a row: a menu anchored left would hang off the page. */
.dash-add:last-child .dash-add__menu {
  left: auto;
  right: 0;
}

.dash-add__item {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 8px 10px;
  border: 1px solid transparent;
  border-radius: 8px;
  background: none;
  font: inherit;
  font-size: 0.86rem;
  color: var(--text);
  text-align: left;
  cursor: pointer;
  transition:
    background-color 120ms var(--ease),
    border-color 120ms var(--ease);
}

.dash-add__item:hover,
.dash-add__item:focus-visible {
  background: color-mix(in oklab, var(--accent) 8%, transparent);
  border-color: color-mix(in oklab, var(--accent) 40%, var(--border));
}

.dash-add__plus {
  color: var(--accent);
  font-weight: 600;
}

.dash-add__empty {
  margin: 0;
  padding: 8px 10px;
  font-size: 0.8rem;
  color: var(--text-dim);
}

/* ============================================================ home page
   Charts are hand-rolled SVG and HTML rather than a library, so their chrome
   lives here with everything else. Every mark reads a semantic token, which is
   what lets one chart look right in seven themes without any of them knowing.

   The rules the marks follow, in one place so they are not re-decided per chart:
   thin marks, hairline solid grid one step off the surface, a 2px surface GAP
   between touching fills rather than a stroke around them, and text in text
   tokens — never in the data colour. */

/* -------------------------------------------------------------- range bar */
.rangebar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  background: var(--glass);
  box-shadow: var(--rim);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  padding: 10px 14px;
  margin-bottom: 20px;
}

/*
 * THE RANGE, FOLDED AWAY ON A PHONE.
 *
 * Seven windows wrap to three rows — 115px of chips standing above every number on the
 * overview — and the operator's reading of it was the right one: somebody checking today's
 * figures through the day scrolls past a control they are not using, all day. On a phone the
 * chips go behind a summary that names the window showing, so the one fact the block carried
 * for a reader who is not changing anything survives the fold.
 *
 * On anything wider this element DISSOLVES. `display: contents` drops the details out of the
 * box tree and leaves the chips as direct children of the bar's flex row, laid out exactly as
 * they were before the fold existed — which is what keeps a mobile affordance from becoming a
 * desktop redesign. The summary is not drawn there at all.
 *
 * The markup ships `open` and js/range-fold.js closes it on a phone, rather than the other way
 * round: a shut <details> hides its children through the browser's own slot, and dissolving the
 * host with `display: contents` is not a dependable way to overrule that.
 */
.rangebar__fold {
  display: contents;
}

.rangebar__summary {
  display: none;
}

@media (max-width: 560px) {
  .rangebar__fold {
    display: block;
    /* Its own line in the bar's wrap, so the buttons beside it keep their places. */
    flex: 1 1 100%;
    min-width: 0;
  }

  .rangebar__summary {
    display: flex;
    align-items: center;
    gap: 8px;
    position: relative;
    /* Room on the right for the chevron, which holds that corner here exactly as it does
       on a location heading — one place for a fold's handle, page to page. */
    padding: 8px 32px 8px 12px;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    background: var(--surface-2);
    color: var(--text);
    font-size: 0.86rem;
    cursor: pointer;
    user-select: none;
    list-style: none;
  }

  .rangebar__summary::-webkit-details-marker {
    display: none;
  }

  .rangebar__summary::after {
    content: '';
    position: absolute;
    right: 13px;
    top: 50%;
    width: 6px;
    height: 6px;
    margin-top: -5px;
    border-right: 2px solid var(--text-dim);
    border-bottom: 2px solid var(--text-dim);
    transform: rotate(45deg);
    transition: transform 160ms var(--ease);
  }

  .rangebar__fold[open] > .rangebar__summary::after {
    transform: rotate(-135deg);
    margin-top: -2px;
  }

  .rangebar__summary-label {
    color: var(--text-faint);
  }

  /* The window showing, which is the whole reason this is a fold and not a hiding place. */
  .rangebar__current {
    font-weight: 600;
  }

  .rangebar__current[hidden] {
    display: none;
  }

  /* Only while open, so a shut fold is exactly one control tall. */
  .rangebar__fold[open] > .rangebar__options {
    margin-top: 10px;
  }

  /*
   * The fiscal quarters fold away with the chips.
   *
   * They are the same question — which window — and left outside the fold they were a row of
   * quarters standing permanently above the numbers on a phone, with no way back out of a
   * quarter that did not start by opening the fold to reach the chips. The operator found
   * both halves of that on their own phone.
   *
   * A SIBLING RULE rather than markup inside the fold, and the reason is worth writing down:
   * Chrome reports `display: contents` on a <details> and generates a box for it anyway, so a
   * fiscal row moved inside stopped being the bar's own full-width row on a wide screen and
   * became a line inside the fold's box with the buttons floating beside both. The row stays
   * where it was in the DOM — desktop untouched — and the phone hides it with the chips.
   */
  .rangebar__fold:not([open]) ~ #range-fiscal {
    display: none;
  }

  /* Open, it belongs with the windows rather than after the actions: the fold, its chips and
     the quarters read as one control, and Edit dashboard has nothing to do with any of them. */
  #range-fiscal {
    order: 0;
  }

  .rangebar .btn {
    order: 1;
  }

  /*
   * The bar's three actions keep ONE row on a phone.
   *
   * Edit dashboard, Detailed report and Refresh belong together — they are the three things
   * this bar does — and at the shared button padding the third wrapped onto a line of its
   * own, which read as Refresh having been separated from the other two. Trimming the side
   * padding is enough for all three to fit from 375px up (measured: every current iPhone,
   * and Android at 384 and wider), and they keep their size, weight and order — the same
   * format, on one line. Narrower than that they wrap again, which is the honest answer:
   * the labels are what they are, and clipping "Detailed report" to hold a row would cost
   * more than the row is worth. A fourth button (Reset layout, once a dashboard has been
   * arranged) wraps for the same reason.
   */
  .rangebar .btn {
    padding-left: 8px;
    padding-right: 8px;
  }
}

/* The narrow phones (360px and down) need the last few pixels to hold that one row: a
   tighter gap between the bar's own items. Below this the three wrap honestly rather than
   being squeezed into labels nobody can read. */
@media (max-width: 400px) {
  .rangebar {
    gap: 8px;
  }

  .rangebar .btn {
    padding-left: 6px;
    padding-right: 6px;
  }
}

.rangebar__options {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}

.rangebar__option {
  border: 1px solid transparent;
  background: none;
  border-radius: 8px;
  padding: 6px 11px;
  font: inherit;
  font-size: 0.84rem;
  font-weight: 550;
  color: var(--text-dim);
  cursor: pointer;
  white-space: nowrap;
  transition:
    background-color 140ms var(--ease),
    color 140ms var(--ease);
}

.rangebar__option:hover {
  background: var(--surface-3);
  color: var(--text);
}

.rangebar__option--on {
  color: var(--accent);
  background: color-mix(in oklab, var(--accent) 12%, transparent);
  border-color: color-mix(in oklab, var(--accent) 30%, transparent);
  font-weight: 620;
}

/*
 * A quarter nobody has reached yet.
 *
 * Shown and refused rather than hidden: the row is the circuit's year, and a year missing
 * its last two quarters reads as a broken control instead of as a year still running. The
 * cursor and the fade say it is not for pressing; the title says why.
 */
.rangebar__option[disabled] {
  opacity: 0.42;
  cursor: not-allowed;
}

.rangebar__option[disabled]:hover {
  background: none;
  color: var(--text-dim);
}

/*
 * The quarter the circuit is in right now, marked whether or not it is the one selected —
 * "which of these is now" is a different question from "which am I looking at", and on a
 * row of four identical chips there is otherwise nothing to answer it.
 */
.rangebar__option--now::after {
  content: '';
  display: inline-block;
  width: 5px;
  height: 5px;
  margin-left: 6px;
  border-radius: 50%;
  vertical-align: 1px;
  background: var(--accent);
}

/*
 * A full-width row inside the bar, for the two controls that only sometimes apply.
 *
 * `flex-basis: 100%` rather than a separate container, so they sit inside the bar's own
 * border and read as part of the same control — a second bordered box under the first
 * would look like a different setting.
 */
.rangebar__row {
  flex-basis: 100%;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
}

.rangebar__row[hidden] {
  display: none;
}

/* The year's own name, leading its quarters: "FY26 · Q1 Q2 Q3 Q4". */
.rangebar__lead {
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin-right: 2px;
}

.rangebar__date {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: 0.82rem;
  font-weight: 550;
  color: var(--text-dim);
}

.rangebar__date input {
  font: inherit;
  font-size: 0.84rem;
  color: var(--text);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 5px 8px;
}

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

/*
 * The cinema picker on the programming page: a labelled select on the bar's own row,
 * dressed like the date boxes beside it rather than like a form field. The shared `select`
 * rule above makes every dropdown a 44px full-width control, which is right in a form and
 * wrong in a toolbar; the class wins over the element selector, and the chevron is moved in
 * to sit inside the smaller padding.
 */
.rangebar__where {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: 0.82rem;
  font-weight: 550;
  color: var(--text-dim);
}

.rangebar__where[hidden] {
  display: none;
}

.rangebar__where select {
  width: auto;
  max-width: 260px;
  min-height: 32px;
  font-size: 0.84rem;
  border-radius: 8px;
  border-color: var(--border);
  padding: 5px 28px 5px 8px;
  background-position: right 14px center, right 9px center;
}

@media (max-width: 560px) {
  /* Under the folded range, sharing a line with Overview and Refresh: the select takes what
     the buttons leave, so a long cinema name has the width of the phone to be read in.
     A basis of ZERO, not auto: a select is as wide as its widest option, and on a circuit
     of twenty that is a name like "Station Square 4 · STA04" — measured from the content,
     the picker claimed the line and pushed Refresh onto a third row of the bar. Measured
     from nothing, it grows into whatever the button leaves and clips the name inside. */
  .rangebar__where {
    flex: 1 1 0;
    min-width: 0;
  }

  .rangebar__where select {
    flex: 1;
    min-width: 0;
    max-width: none;
  }

  /* Refresh keeps its own size beside a picker that is showing. The phone width's blanket
     `.btn { flex: 1 1 auto }` (below) would otherwise split the line's leftover with the
     picker — a 190px Refresh next to a 74px select holding a cinema's name. On a circuit
     of one the picker is hidden and the button stretches as it does on every other bar. */
  .rangebar__where:not([hidden]) + .btn {
    flex: 0 0 auto;
  }
}

/*
 * A LIST PAGE'S OVERVIEW, AT THE START OF THE CINEMA'S LINE — Programming, Sentiment, Staffing.
 *
 * Where the rail folds to a strip of chips, Overview was the first chip, and opening a list far
 * along the strip scrolled it out of sight: the way back to the overview was a hunt sideways. The
 * operator asked for it on the cinema's line instead, on the left, where it holds one place
 * whatever is open. It is the rail's own entry moved, so it is drawn as one of the strip's chips
 * and lit as they are, by aria-current while the overview is showing; the strip leaves its copy
 * out (below).
 *
 * Not a `.btn`, on purpose: a phone's bar sends every `.btn` to the end of its line (`order: 1`),
 * which would stand Overview after the cinema — and on Staffing, after the week. On a computer
 * the rail is a column with Overview at its head, always in view, and this is not drawn.
 */
.rangebar__overview {
  display: none;
}

@media (max-width: 860px) {
  .rangebar__overview {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    align-self: stretch;
    flex: none;
    padding: 6px 14px;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: none;
    color: var(--text-dim);
    font: inherit;
    font-size: 0.84rem;
    white-space: nowrap;
    cursor: pointer;
    transition:
      background-color 120ms var(--ease),
      color 120ms var(--ease);
  }

  .rangebar__overview:hover {
    background: var(--surface-3);
    color: var(--text);
  }

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

  .rangebar__overview[aria-current='page'] {
    background: color-mix(in oklab, var(--accent) 14%, transparent);
    border-color: color-mix(in oklab, var(--accent) 45%, var(--border));
    color: var(--text);
    font-weight: 650;
  }

  /* The strip's own Overview, which the bar carries here. listPage's rail is the only one whose
     Overview entry says `data-list=""`; the other rails keep theirs. */
  .market__rail > .market__railitem[data-list=''] {
    display: none;
  }

  /* The picker's word, "Cinema", is a screen reader's alone on this line: the select already
     reads "All cinemas" or the cinema's own name, and the operator wanted the room for it. Kept
     in the label rather than removed, so the select is still named. */
  .rangebar__where > span {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
  }

  /* ARRANGE THIS PAGE IS A COMPUTER'S. On a phone it was a full-width line of the bar over every
     overview, for a gesture made once, and the operator wanted the line back. An arrangement is
     the account's, so one made on a computer is what the phone shows. By id: the home pages'
     Reset layout wears `rangebar__reset` too, and keeps its place. */
  #arrange-page,
  #reset-arrangement {
    display: none;
  }
}

/* Why the two boxes are not yet a range. Empty until there is something to say. */
.rangebar__hint {
  margin: 0;
  font-size: 0.8rem;
  color: var(--warn);
}

.rangebar__hint:empty {
  display: none;
}

/*
 * The same periods, in the detailed report's dialog.
 *
 * A column of full-width choices rather than the bar's row of chips: this one is being
 * read and decided on rather than flicked through, the labels are the only content of
 * the dialog, and a row of five would wrap to two ragged lines in a narrow modal.
 */
.rangepick {
  display: grid;
  gap: 6px;
}

/*
 * The dialog is one explanatory line, one choice and one note, and the paragraphs above
 * and below the choice have no margin of their own — so the label sat straight on the
 * sentence before it. Spaced here rather than on `.field__hint` everywhere, because
 * every other use of that class is a hint sitting deliberately close to its control.
 */
#report-dialog .field {
  margin: 14px 0 4px;
}

/*
 * The fiscal row and the date boxes, inside the dialog rather than the range bar. Same
 * chips, but the bar's full-width rule and its divider are the bar's own: here they are
 * already the only thing on their line, and a rule between two halves of one control
 * would read as two settings.
 */
.rangebar__row--flush {
  flex-basis: auto;
  border-top: none;
  padding-top: 8px;
}

.rangepick__option {
  border: 1px solid var(--border);
  background: var(--surface-2);
  border-radius: 10px;
  padding: 10px 12px;
  font: inherit;
  font-size: 0.88rem;
  font-weight: 550;
  color: var(--text-dim);
  cursor: pointer;
  text-align: left;
  transition:
    background-color 140ms var(--ease),
    border-color 140ms var(--ease),
    color 140ms var(--ease);
}

.rangepick__option:hover {
  background: var(--surface-3);
  color: var(--text);
}

.rangepick__option--on {
  color: var(--accent);
  background: color-mix(in oklab, var(--accent) 12%, transparent);
  border-color: color-mix(in oklab, var(--accent) 45%, transparent);
  font-weight: 620;
}

/*
 * The buttons sit right, however many of them are showing. Pushed by the options
 * rather than pulled by the buttons: Reset layout appears and disappears, and two
 * elements each claiming `margin-left: auto` split the free space between them
 * instead of both going to the end.
 */
.rangebar__options {
  margin-right: auto;
}

/* ------------------------------------------------------- needs attention
   A .section disclosure like the Settings cards, but built open: this one is the
   page telling you something is wrong, and a warning you have to click to read is
   not a warning. Its rows are full-bleed and tinted, so they sit directly in the
   details rather than in a padded .section__body. */
.attention {
  margin-bottom: 24px;
}

.attention__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
  /* 22px to line up with the .section__body padding used elsewhere. */
  padding: 13px 22px;
  border-left: 3px solid transparent;
}

.attention__row + .attention__row {
  border-top: 1px solid var(--border);
}

.attention__row--danger {
  border-left-color: var(--danger);
  background: var(--danger-soft);
}

.attention__row--warn {
  border-left-color: var(--warn);
  background: var(--warn-soft);
}

.attention__text {
  flex: 1 1 320px;
  font-size: 0.9rem;
}

/* ------------------------------------------------------- getting started
   A new circuit's to-do list, above the tiles (renderGettingStarted in js/home.js). A card with a
   list in it rather than a row of tinted lines like Needs attention: nothing here is wrong, it is
   simply not done yet, and a list of jobs reads as one only when it looks like one. The ticks are
   the app's own green; the next job to do is the one filled button, so there is one place to start. */
.starting {
  margin-bottom: 24px;
  padding: 18px 22px 6px;
}

.starting__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
}

.starting__title {
  margin: 0;
  font-size: 1.05rem;
}

.starting__count {
  margin: 2px 0 0;
  font-size: 0.84rem;
  color: var(--text-dim);
}

.starting__hide {
  flex: none;
}

/* One segment per step, so the bar fills in the same steps the list does. */
.starting__bar {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: 4px;
  margin: 14px 0 4px;
}

.starting__seg {
  height: 4px;
  border-radius: 2px;
  background: var(--border-strong);
}

.starting__seg--done {
  background: var(--ok);
}

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

.starting__step {
  display: grid;
  grid-template-columns: 28px minmax(0, 1fr) auto;
  align-items: center;
  gap: 6px 14px;
  padding: 12px 0;
}

.starting__step + .starting__step {
  border-top: 1px solid var(--border);
}

.starting__mark {
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  border: 1.5px solid var(--border-strong);
  color: var(--text-dim);
  font-size: 0.8rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.starting__step--done .starting__mark {
  border-color: transparent;
  background: var(--ok-soft);
  color: var(--ok);
}

.starting__words {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.starting__name {
  font-size: 0.92rem;
  font-weight: 600;
}

/* A done step keeps its name and what it found, quieter, and drops the sentence it no longer needs. */
.starting__step--done .starting__name {
  font-weight: 500;
  color: var(--text-dim);
}

/* In one piece: "3 still without one" broken over two lines on a phone read as two facts. */
.starting__said {
  display: inline-block;
  margin-left: 8px;
  white-space: nowrap;
  font-size: 0.84rem;
  font-weight: 400;
  color: var(--text-dim);
}

.starting__note {
  font-size: 0.84rem;
  color: var(--text-dim);
}

.starting__go {
  white-space: nowrap;
}

@media (max-width: 560px) {
  .starting {
    padding: 16px 16px 4px;
  }

  /* The button goes under the words on a phone, lined up with them, rather than squeezing them. */
  .starting__step {
    grid-template-columns: 28px minmax(0, 1fr);
    align-items: start;
  }

  .starting__go {
    grid-column: 2;
    justify-self: start;
  }
}

/* ----------------------------------------------------------- chart layout */
.charts {
  display: grid;
  /* Wide enough that a 12-month trend's labels do not collide; anything narrower
     goes full width rather than being squeezed into a column. */
  /* min() so the track can go narrower than 340px when that is all there is: a
     flat 340px minimum overflowed a 320px phone by 36px and scrolled the page
     sideways. */
  grid-template-columns: repeat(auto-fit, minmax(min(340px, 100%), 1fr));
  gap: 16px;
  margin-bottom: 24px;
  /*
   * ONE SIZE OF CARD, which is the whole reason this row exists rather than `align-items:
   * start`.
   *
   * That was the right call when a card was as tall as its contents and stretching one meant
   * two-thirds of it being empty. The plots are now capped at a standard height (--plot-cap
   * below) and a chart with more to show offers to expand instead of growing, so the contents
   * of every card are the same height to within a line of subtitle — and stretching closes
   * that last gap instead of opening a big one. A grid of same-sized boxes is the thing that
   * makes this page scannable; ragged ones read as a pile.
   */
  align-items: stretch;
  /*
   * And a standard height for the ROW, so the promise holds down the page as well as across
   * it. Stretch alone matches the cards beside each other; without a floor, a row of two short
   * charts still sat sixty pixels shorter than the row above it. The floor is what a full card
   * comes to — head, a capped plot, and the numbers line — and `auto` above it is what lets an
   * expanded card, or the wide ticker, take the room it actually needs.
   */
  grid-auto-rows: minmax(340px, auto);
}

/* The trend is the page's one time series and reads across the full width. */
.chart--wide {
  grid-column: 1 / -1;
}

.chart {
  position: relative;
  margin: 0;
  /* A column, so the numbers disclosure can be pinned to the bottom of a stretched card
     rather than floating in the middle of whatever space is left over. */
  display: flex;
  flex-direction: column;
  background: var(--glass);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
  padding: 18px 20px 16px;
}

/*
 * How tall a plot is — every plot, in every card.
 *
 * The number is the column charts' own geometry: 8px of headroom, 150px of plot, 26px of axis
 * (COL in js/charts.js, which a test pins to this value). The bar charts used to be as tall as
 * their row count, which is what made a grid of cards ragged; they are clamped to this and
 * offer "Show all N" when there is more, so the standard size is the default and a taller card
 * is something the reader asked for.
 */
:root {
  --plot-cap: 184px;
}

.chart__plot--clamp {
  max-height: var(--plot-cap);
  overflow: hidden;
  /* Opening and shutting MOVES, like everything else on this page that changes size. The
     duration is here rather than in the script so one place decides it — which is also what
     makes reduced motion work on this without the script knowing that reduced motion exists. */
  transition: max-height 240ms var(--ease);
}

.chart__plot--open {
  max-height: none;
}

/*
 * The expander itself: quiet, full width, at the foot of the chart it belongs to.
 *
 * Not a link and not a chip — it does not go anywhere and it is not a filter. It reads as the
 * bottom edge of the card lifting, which is what it does.
 */
.chart__more {
  margin-top: 10px;
  padding: 6px 0;
  border: 0;
  border-top: 1px solid var(--border);
  background: none;
  font: inherit;
  font-size: 0.8rem;
  font-weight: 550;
  color: var(--text-dim);
  cursor: pointer;
  text-align: left;
  transition: color 140ms var(--ease);
}

.chart__more:hover,
.chart__more:focus-visible {
  color: var(--accent);
}

.chart__head {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  margin-bottom: 16px;
}

.chart__heading {
  min-width: 0;
  flex: 1 1 auto;
}

/*
 * How the live ticker stands against the same minute yesterday, at the top right of its card.
 *
 * A headcount on its own says nothing anybody can act on — eight hundred people is a good night
 * or a bad one depending entirely on what yesterday held. This is the half that makes it news,
 * so it sits where the eye lands after the title rather than under the plot.
 */
.chart__delta {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 1px;
  flex: 0 0 auto;
  text-align: right;
}

/* Two chips side by side in the one badge slot: the day's peak, and how now compares. */
.chart__chips {
  display: flex;
  align-items: flex-start;
  gap: 14px;
  flex: 0 0 auto;
}

.chart__peak {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 1px;
  flex: 0 0 auto;
  text-align: right;
}

/*
 * On a phone the figures go UNDER the title rather than beside it. Beside it they ate the
 * heading: on a week or a month the chips name a day and a window ("PEAK · SAT 19 SEP, 7:05 PM",
 * "daily peaks vs 11 – 17 Sep 2026"), and on a 390px screen that left the card's own title a
 * column one word wide. Under it, the title keeps its row with the grip, and the two figures
 * read left to right and wrap onto a second line of their own if they must.
 */
@media (max-width: 560px) {
  .chart__head:has(> .chart__chips) {
    flex-wrap: wrap;
  }

  /* From nothing rather than from its own width, so it shares the first row with the grip
     instead of taking the whole row and pushing the grip onto one of its own. */
  .chart__head:has(> .chart__chips) > .chart__heading {
    flex: 1 1 0;
  }

  .chart__head > .chart__chips {
    order: 3;
    flex: 1 1 100%;
    flex-wrap: wrap;
    justify-content: flex-start;
    row-gap: 8px;
  }

  .chart__head > .chart__chips > * {
    align-items: flex-start;
    text-align: left;
  }
}

.chart__peakfigure {
  font-size: 1.05rem;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

.chart__peaklabel {
  font-size: 0.68rem;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--text-faint);
}

/*
 * The measure switch, for a chart that can be read more than one way.
 *
 * In the head rather than under the plot, so it reads as part of the card's title — "how
 * full each cinema runs, BY tickets" — and so it cannot be mistaken for a control that
 * filters the page.
 */
.chart__modes {
  display: inline-flex;
  flex: 0 0 auto;
  padding: 2px;
  gap: 2px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
}

.chart__mode {
  font: inherit;
  font-size: 0.76rem;
  padding: 4px 10px;
  border: 0;
  border-radius: calc(var(--radius-sm) - 3px);
  background: none;
  color: var(--text-dim);
  cursor: pointer;
  transition:
    background-color 140ms var(--ease),
    color 140ms var(--ease);
}

.chart__mode:hover {
  color: var(--text);
}

.chart__mode--on {
  background: var(--surface);
  color: var(--text);
  box-shadow: var(--shadow-soft, 0 1px 2px rgb(0 0 0 / 0.25));
}

.chart__deltafigure {
  font-size: 1.05rem;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
}

.chart__deltalabel {
  font-size: 0.68rem;
  color: var(--text-faint);
}

/*
 * Colour SECONDS the sign, never carries it alone. The figure already reads +240 or −180, so a
 * reader on the colour-vision palette — or anybody glancing at a phone in a foyer — gets the
 * direction from the character whatever the hue is doing.
 */
.chart__delta--up .chart__deltafigure {
  color: var(--ok);
}

.chart__delta--down .chart__deltafigure {
  color: var(--danger);
}

.chart__delta--level .chart__deltafigure {
  color: var(--text-dim);
}

/* ----------------------------------------------------------- the drag grip
   Faint until the card is hovered or the grip itself is focused: five grips at
   full strength turn a page of charts into a page of controls. It stays reachable
   by keyboard the whole time — only its opacity changes, never its display. */
.chart__grip {
  flex: none;
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  margin: -4px -6px 0 0;
  padding: 0;
  border: 1px solid transparent;
  border-radius: 7px;
  background: none;
  color: var(--text-faint);
  cursor: grab;
  opacity: 0.45;
  /* Stops a drag on a touchscreen from scrolling the page instead. */
  touch-action: none;
  transition:
    opacity 140ms var(--ease),
    background-color 140ms var(--ease),
    color 140ms var(--ease);
}

.chart:hover .chart__grip,
.chart__grip:focus-visible {
  opacity: 1;
}

.chart__grip:hover {
  background: var(--surface-3);
  border-color: var(--border);
  color: var(--text);
}

.chart__grip:active {
  cursor: grabbing;
}

/*
 * The card being dragged.
 *
 * It stays in the flow and hops between slots rather than being carried under the
 * cursor — see reorder.js. Pointer-transparent so elementFromPoint finds the card
 * UNDERNEATH it, which is the whole mechanism for working out where to drop.
 */
/*
 * The card in hand: lifted off the page and carried by the pointer (see carry() in
 * js/reorder.js, which writes the translate and the two variables below).
 *
 * Solid rather than dimmed, and drawn over its neighbours, because it is the thing being
 * moved — a translucent rectangle reads as a card being deleted. It keeps its grid slot
 * while it travels, so the gap the other cards flow around is the real destination.
 */
.card--dragging {
  pointer-events: none;
  position: relative;
  z-index: 5;
  /*
   * OPAQUE while it is in the air, and this is not a style choice. Every card on this page
   * is glass — 55% of a surface over whatever is behind it — which is right for a card lying
   * on the page and wrong for one held over another card: the text underneath reads straight
   * through it and the thing in your hand looks like a hole. The glass is composited over a
   * solid ground rather than replaced, so the colour is the one the theme actually chose,
   * including the high-contrast setting that has already solidified the token.
   */
  background: linear-gradient(var(--glass), var(--glass)), var(--surface-2);
  border-color: var(--accent);
  box-shadow: 0 18px 38px -14px rgb(0 0 0 / 0.5);
  cursor: grabbing;
}

/*
 * And the outline left behind in the slot it came out of.
 *
 * The same box, moved back by exactly what the carry moved the card forward — so it is the
 * card's own geometry rather than a placeholder element that has to be kept in step with it.
 * Nothing is inserted into the grid, which is what keeps the column count from changing
 * under the reader mid-drag.
 */
.card--dragging::after {
  content: '';
  position: absolute;
  inset: 0;
  transform: translate(calc(var(--carry-x, 0px) * -1), calc(var(--carry-y, 0px) * -1));
  border: 1px dashed var(--accent);
  border-radius: inherit;
  opacity: 0.65;
  pointer-events: none;
}

/*
 * A card travelling to the slot a reorder gave it — see slide() in js/reorder.js, which puts
 * this on for exactly the length of one journey.
 *
 * Only `transform`, and that is what keeps it honest: the grid has already placed every card
 * before this rule sees any of them, so the animation cannot disagree with where a card
 * actually is. Short, and on the front-loaded curve the rest of the app's direct manipulation
 * uses — a reorder is a response to a hand that is still moving, not an entrance.
 */
.card--sliding {
  transition: transform 180ms var(--ease);
}

/* While a drag is live, nothing else on the page should look clickable or
   selectable — a half-selected chart title mid-drag reads as a bug. */
body.is-dragging {
  user-select: none;
  cursor: grabbing;
}

body.is-dragging .chart__tip {
  display: none;
}

.chart__title {
  margin: 0;
  font-size: 0.98rem;
}

.chart__sub {
  margin: 3px 0 0;
  font-size: 0.82rem;
  color: var(--text-dim);
}

.chart__empty {
  margin: 0;
  padding: 22px 0;
  text-align: center;
  font-size: 0.86rem;
  color: var(--text-faint);
}

/*
 * What a chart could not draw, under the chart.
 *
 * Dimmer than the marks and above the numbers table, because it is a caveat on the figures
 * rather than one of them — but it is not decoration either: this is the line that stops a
 * ranked list of nine cinemas from reading like a complete circuit of nine.
 */
.chart__footnote {
  margin: 10px 0 0;
  font-size: 0.78rem;
  line-height: 1.45;
  color: var(--text-dim);
}

/*
 * The plot is sized to include its own x-axis band. A container height that fits
 * only the marks gives the card a tiny nested scrollbar for its axis labels.
 */
/*
 * Height comes from the SVG, which is drawn at the container's real pixel size —
 * see columns() in charts.js. Fixing a height here and scaling into it is what
 * squashed the axis labels on a phone.
 */
.chart__plot--cols .plot {
  display: block;
  max-width: 100%;
}

.plot__grid {
  stroke: var(--border);
  stroke-width: 1;
  /* Solid, never dashed: dashing reads as a threshold or a projection when it is
     only a grid. */
  shape-rendering: crispEdges;
}

.plot__tick,
.plot__label {
  fill: var(--text-faint);
  font-size: 10px;
  font-family: var(--font);
  font-variant-numeric: tabular-nums;
}

.plot__col {
  fill: var(--accent-fill);
  transition: fill 140ms var(--ease);
}

/* The period still in progress. Lighter, so it is visibly not comparable to the
   completed ones beside it — the same hue, so it is still the same series. */
.plot__col--partial {
  fill: color-mix(in oklab, var(--accent-fill) 42%, var(--surface));
}

/*
 * A column's own tone, where a chart's columns are not one series but BANDS with a meaning —
 * the film index histogram on the programming page, whose low bands are the under list and
 * whose middle is nothing to act on. Warm and quiet rather than the status colours, because a
 * band is a reading and not an alarm; the key under the plot says which is which.
 */
.plot__col--warn {
  fill: var(--p-peach, var(--accent-fill));
}

.plot__col--quiet {
  fill: var(--p-surface1, var(--border-strong));
}

.plot__band:hover .plot__col--warn,
.plot__band:focus-within .plot__col--warn {
  fill: color-mix(in oklab, var(--p-peach, var(--accent)) 78%, var(--text));
}

.plot__band:hover .plot__col--quiet,
.plot__band:focus-within .plot__col--quiet {
  fill: var(--text-faint);
}

/* A threshold across a plot — the circuit's usual on the parts-of-the-day card. The one dashed
   line on any plot, which is what makes it read as a line to be measured against. */
.plot__ref {
  stroke: var(--text-faint);
  stroke-width: 1;
  stroke-dasharray: 4 4;
}

.plot__reflabel {
  fill: var(--text-faint);
  font-size: 10px;
  font-family: var(--font);
}

/*
 * The weather that met the buildings open, above its day's column (columns() in js/charts.js): the
 * sky's own drawing, stroked like every icon, in the text's quiet grey — a step above the axis's
 * fainter one, so a stroke scaled down to a 9px mark still reads. A note on the chart, not a series
 * in it, so it never takes a colour the data might be read as. The stroke is on the drawing's
 * 24-unit grid and scales with it, which keeps a 9px mark as light as a 14px one.
 */
.plot__mark path {
  fill: none;
  stroke: var(--text-dim);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}


/* -------------------------------------------------------------- the live ticker
 *
 * The patrons line (ticker() in js/charts.js): the day so far as one stroke, a soft
 * area under it for presence, and an orb on the newest point whose halo pulses —
 * the one deliberate piece of motion on the page, because this is the one number
 * that is genuinely moving while you watch. Same accent tokens as every other mark,
 * so the themes never have to know the ticker exists.
 */
.plot__line {
  fill: none;
  stroke: var(--accent);
  stroke-width: 2;
  stroke-linejoin: round;
  stroke-linecap: round;
}

.plot__area {
  fill: color-mix(in oklab, var(--accent-fill) 22%, transparent);
  stroke: none;
}

.plot__orb {
  fill: var(--accent);
}

/*
 * Each day's busiest moment, on the patrons chart over a week or a month (evenings() in
 * js/charts.js): a dot on the line, ringed in the card's own surface so it reads as a mark ON
 * the stroke rather than a thickening of it, and — where a day has the room — its number above.
 */
.plot__peak {
  fill: var(--accent);
  stroke: var(--surface);
  stroke-width: 2;
}

.plot__peakvalue {
  fill: var(--text-dim);
  font-size: 10px;
  font-weight: 600;
  font-family: var(--font);
  font-variant-numeric: tabular-nums;
}

.plot__orb-halo {
  fill: var(--accent);
  opacity: 0.45;
  transform-origin: center;
  transform-box: fill-box;
  animation: orb-pulse 2s var(--ease) infinite;
}

@keyframes orb-pulse {
  0% { transform: scale(1); opacity: 0.45; }
  70% { transform: scale(2.6); opacity: 0; }
  100% { transform: scale(2.6); opacity: 0; }
}

/* A reader who asked for no motion gets a still halo on the same spot — the chart
   itself, not a lesser one. The same preference every dialog on the page honours. */
@media (prefers-reduced-motion: reduce) {
  .plot__orb-halo {
    animation: none;
    opacity: 0.3;
  }
}

/* ---------------------------------------------------------------- several lines
 *
 * The admissions card (lines() in js/charts.js): the window as the accent, solid and strongest;
 * the window before it in the page's faint ink, dashed; the same dates last year in peach,
 * dotted. Three hues the palette check passes in pairs with the accent, and three dash
 * patterns so the series read apart in print and to a reader who sees two of the hues as one.
 * The legend repeats the pattern beside the name, so identity never rests on colour alone.
 */
.plot__line--solid {
  stroke-dasharray: none;
}

.plot__line--dashed {
  stroke-dasharray: 7 5;
}

.plot__line--dotted {
  stroke-dasharray: 2 4;
}

.plot__line--now {
  stroke: var(--accent);
  stroke-width: 2.5;
}

.plot__line--previous {
  stroke: var(--text-faint);
}

.plot__line--last-year {
  stroke: var(--p-peach, var(--text-dim));
}

/* A measured period standing alone between two gaps, which a line cannot reach. */
.plot__dot {
  fill: var(--accent);
}

.plot__dot--now {
  fill: var(--accent);
}

.plot__dot--previous {
  fill: var(--text-faint);
}

.plot__dot--last-year {
  fill: var(--p-peach, var(--text-dim));
}

/* The crosshair: a hairline on the hovered period, so three lines a few pixels apart are read
   as one column of figures in the tooltip. */
.plot__cross {
  stroke: var(--border-strong);
  stroke-width: 1;
  shape-rendering: crispEdges;
}

.plot__legend {
  list-style: none;
  margin: 10px 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 6px 16px;
  font-size: 0.78rem;
  color: var(--text-dim);
}

.plot__key {
  display: inline-flex;
  align-items: center;
  gap: 7px;
}

/* A swatch is a short piece of the line it stands for — the same colour and the same dash. */
.plot__swatch {
  flex: none;
  width: 18px;
  height: 0;
  border-top: 2px solid var(--text-dim);
}

.plot__swatch--solid {
  border-top-style: solid;
}

.plot__swatch--dashed {
  border-top-style: dashed;
}

.plot__swatch--dotted {
  border-top-style: dotted;
}

.plot__swatch--now {
  border-top-color: var(--accent);
}

.plot__swatch--previous {
  border-top-color: var(--text-faint);
}

.plot__swatch--last-year {
  border-top-color: var(--p-peach, var(--text-dim));
}

/* And for a key of column tones, a square of the fill rather than a piece of line. */
.plot__swatch--warn,
.plot__swatch--quiet,
.plot__swatch--accent {
  width: 10px;
  height: 10px;
  border: 0;
  border-radius: 3px;
}

.plot__swatch--warn {
  background: var(--p-peach, var(--accent-fill));
}

.plot__swatch--quiet {
  background: var(--p-surface1, var(--border-strong));
}

.plot__swatch--accent {
  background: var(--accent-fill);
}

.plot__key-label {
  color: var(--text-dim);
}

/* The band is the hit target, not the column: readers aim at a date. */
.plot__hit {
  fill: transparent;
  cursor: default;
}

.plot__hit:focus {
  outline: none;
}

/* The hovered column lifts — the mark responds, rather than every other one dimming. */
.plot__band:hover .plot__col,
.plot__band:focus-within .plot__col {
  fill: var(--accent);
}

/* Premium pass: carries each band's first-draw rise off its baseline — see the
   entrance block in js/charts.js. Redraws are born at scale 1, so nothing re-performs. */
.plot__band {
  transition: transform 540ms var(--ease);
}

/* Keyboard focus still needs a visible ring; the fill change alone is not enough. */
.plot__band:focus-within .plot__hit {
  stroke: var(--focus);
  stroke-width: 2;
}

/* ------------------------------------------------------- horizontal bars */
.bars {
  display: grid;
  gap: 3px;
}

.bars__row {
  display: grid;
  grid-template-columns: minmax(84px, 30%) 1fr auto;
  align-items: center;
  gap: 12px;
  /* Padding, so the hit and focus area is comfortably larger than the 10px bar. */
  padding: 5px 6px;
  margin: 0 -6px;
  border-radius: 7px;
  transition: background-color 120ms var(--ease);
}

.bars__row:hover,
.bars__row:focus-visible {
  background: var(--surface-3);
}

.bars__label {
  font-size: 0.85rem;
  color: var(--text-dim);
  overflow-wrap: anywhere;
}

.bars__track {
  /* For the tick a row can carry — see .bars__mark. */
  position: relative;
  height: 10px;
  border-radius: 5px;
  background: color-mix(in oklab, var(--p-surface0) 60%, transparent);
  overflow: hidden;
}

.bars__fill {
  height: 100%;
  /* Rounded at the data end, square at the baseline, so the bar stays anchored. */
  border-radius: 0 4px 4px 0;
  background: var(--accent-fill);
  transition: width 260ms var(--ease);
}

/* A bar short of its own tick — a building under its usual. Warm rather than the warn colour,
   because it is a reading and not an alarm; the value beside it says both numbers. */
.bars__fill--under {
  background: var(--p-peach, var(--accent-fill));
}

/* And the other side of a tick: a person past their weekly ceiling on the staffing page's
   hours chart — the same warm ink, because on that chart over the mark is the finding. */
.bars__fill--over {
  background: var(--p-peach, var(--accent-fill));
}

/* The tick: a second value on the bar's own scale, standing on the track — a building's usual
   fullness beside how full it ran. In text ink, so it reads as a reference, not a second bar. */
.bars__mark {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 2px;
  margin-left: -1px;
  border-radius: 1px;
  background: var(--text);
  opacity: 0.85;
}

/*
 * A two-sided track, for a value that can be below nought — a net promoter score. The zero is
 * the middle; a positive bar grows right from it, a negative one left, and the negative side is
 * warm because it is the reading a manager is here for. See `signed` in js/charts.js.
 */
.bars__track--signed {
  background: color-mix(in oklab, var(--p-surface0) 45%, transparent);
}

.bars__zero {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: 1px;
  margin-left: -0.5px;
  background: var(--text);
  opacity: 0.4;
}

.bars__fill--pos {
  position: absolute;
  top: 0;
  left: 50%;
}

.bars__fill--neg {
  position: absolute;
  top: 0;
  right: 50%;
  border-radius: 4px 0 0 4px;
  background: var(--p-peach, var(--accent-fill));
}

/*
 * The folded tail, as a line of text rather than a bar. Drawn as one it set the
 * scale — eight cinemas add up to more than the busiest single one — and squashed
 * every real bar against it.
 */
.bars__more {
  display: flex;
  align-items: baseline;
  gap: 12px;
  margin: 8px 0 0;
  padding-top: 9px;
  border-top: 1px solid var(--border);
  font-size: 0.82rem;
  color: var(--text-faint);
}

.bars__more .bars__value {
  margin-left: auto;
  color: var(--text-dim);
}

.bars__value {
  font-size: 0.85rem;
  font-weight: 620;
  font-variant-numeric: tabular-nums;
  min-width: 2.5ch;
  text-align: right;
}

/*
 * A chart whose names are the reading — a film, the room it played in and the building — gives
 * each name the row's whole width, on a line of its own over its bar and figure, and lets it wrap
 * rather than cutting it: the part cut off was the room and the building, which is what the card
 * is glanced at for. Two classes, so it outweighs every look's one-line column. See `stacked` in
 * js/charts.js.
 */
.bars--stacked .bars__row {
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas:
    'label label'
    'track value';
  row-gap: 3px;
  padding-top: 5px;
  padding-bottom: 5px;
  line-height: 1.35;
}

.bars--stacked .bars__label {
  grid-area: label;
  white-space: normal;
  overflow: visible;
  overflow-wrap: anywhere;
}

.bars--stacked .bars__track {
  grid-area: track;
}

.bars--stacked .bars__value {
  grid-area: value;
}

/*
 * And on a phone, room for about as many of them as a one-line chart shows. A stacked row is two
 * lines, so the standard plot held three; the cards there are one column, so a taller one makes no
 * row ragged — the reason every card is one height does not apply. Not once it is opened: Show all
 * takes the limit off, and this must not put it back.
 */
@media screen and (max-width: 700px) {
  .chart__plot--clamp:not(.chart__plot--open):has(> .bars--stacked) {
    max-height: calc(var(--plot-cap) * 1.45);
  }
}

/* --------------------------------------------------------- stacked strip */
.stack {
  display: flex;
  height: 14px;
  border-radius: 7px;
  overflow: hidden;
  /* The gap between segments is the surface showing through, not a stroke around
     each one — a border would add data-weight ink that is not data. */
  gap: 2px;
  background: var(--surface);
}

.stack__part {
  min-width: 3px;
  transition: filter 140ms var(--ease);
}

.stack__part:hover,
.stack__part:focus-visible {
  filter: brightness(1.15);
  outline: none;
}

.stack__part--danger {
  background: var(--danger);
}
.stack__part--warn {
  background: var(--warn);
}
.stack__part--ok {
  background: var(--ok);
}
.stack__part--accent {
  background: var(--accent-fill);
}
.stack__part--accent-soft {
  background: color-mix(in oklab, var(--accent-fill) 42%, var(--surface));
}

/* Always present, so identity never rests on colour alone. */
.stack__legend {
  list-style: none;
  margin: 14px 0 0;
  padding: 0;
  display: grid;
  gap: 7px;
}

.stack__key {
  display: flex;
  align-items: center;
  gap: 9px;
  font-size: 0.85rem;
}

.stack__swatch {
  width: 10px;
  height: 10px;
  border-radius: 3px;
  flex: none;
}

.stack__swatch--danger {
  background: var(--danger);
}
.stack__swatch--warn {
  background: var(--warn);
}
.stack__swatch--ok {
  background: var(--ok);
}
.stack__swatch--accent {
  background: var(--accent-fill);
}
.stack__swatch--accent-soft {
  background: color-mix(in oklab, var(--accent-fill) 42%, var(--surface));
}

.stack__key-label {
  color: var(--text-dim);
}

.stack__key-value {
  margin-left: auto;
  font-weight: 620;
  font-variant-numeric: tabular-nums;
}

/*
 * A bar row that names an issue type, with the type's drawing on a tile before its name — see
 * `icon` in drawBars. One more track, at the front, for the tile; the name keeps the width it had.
 */
.bars__row--marked {
  grid-template-columns: 28px minmax(84px, 30%) 1fr auto;
}

.bars__icon {
  display: inline-grid;
  place-items: center;
  width: 28px;
  height: 28px;
  border-radius: 9px;
  --glyph: 15px;
  color: var(--text-dim);
  background: color-mix(in oklab, var(--p-surface0) 60%, transparent);
}

.bars__icon .glyph--letter {
  border: 0;
  font-size: 0.78rem;
}

/*
 * One whole as a ring, with its key beside it — see `form: 'ring'` in stack(). The ring is a
 * fixed size, so a card's text size cannot squash it into an oval; the key takes what is left.
 */
.ring {
  display: grid;
  grid-template-columns: 120px minmax(0, 1fr);
  align-items: center;
  gap: 24px;
  padding: 6px 0 4px;
}

.ring__dial {
  position: relative;
  width: 120px;
  height: 120px;
}

.ring__svg {
  display: block;
  width: 100%;
  height: 100%;
  overflow: visible;
}

.ring__arc {
  fill: none;
  stroke-width: 14;
  transition: filter 140ms var(--ease);
}

.ring__arc:hover,
.ring__arc:focus-visible {
  filter: brightness(1.15);
  outline: none;
}

.ring__arc--danger {
  stroke: var(--danger);
}
.ring__arc--warn {
  stroke: var(--warn);
}
.ring__arc--ok {
  stroke: var(--ok);
}
.ring__arc--accent {
  stroke: var(--accent-fill);
}
.ring__arc--accent-soft {
  stroke: color-mix(in oklab, var(--accent-fill) 42%, var(--surface));
}

/* The total, in the hole. The ring's marks take the pointer; the words under them do not. */
.ring__hole {
  position: absolute;
  inset: 0;
  display: grid;
  place-content: center;
  justify-items: center;
  pointer-events: none;
}

.ring__total {
  font-size: 1.6rem;
  font-weight: 800;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}

.ring__noun {
  margin-top: 4px;
  font-size: 0.75rem;
  color: var(--text-dim);
}

/* Beside the ring rather than under it, and dots rather than squares: the key to a round thing. */
.stack__legend--ring {
  margin: 0;
  gap: 12px;
}

.stack__legend--ring .stack__swatch {
  width: 8px;
  height: 8px;
  border-radius: 50%;
}

/* A phone gives the ring its own line, and the key the full width under it. */
@media (max-width: 380px) {
  .ring {
    grid-template-columns: 1fr;
    justify-items: center;
  }

  .stack__legend--ring {
    width: 100%;
  }
}

/* ------------------------------------------------------------ token bar
 *
 * How many AI tokens are left, as one track that empties.
 *
 * It borrows the stacked strip's shape above rather than inventing a second bar language on
 * the same page — same rounded track, same swatch-and-number legend — but it is NOT a stack:
 * a stack draws parts of a whole that is always full, and this one is deliberately not full.
 * The gap on the right IS the reading. So the fills are laid at the left of a track that
 * shows through, instead of dividing it up.
 */
.tokenbar__body {
  /*
   * Centred in what the head leaves behind. The cards in this grid share a floor of 340px so
   * a row of them lines up; a bar is a shorter thing than a plot, and pinned to the top it
   * would sit in a card that was three-quarters empty. See grid-auto-rows on #charts.
   */
  margin: auto 0;
}

.tokenbar__track {
  display: flex;
  height: 18px;
  border-radius: 9px;
  overflow: hidden;
  /*
   * The empty part, and the only part of this card that is load-bearing on its own. Darker
   * than the card so the boundary between spent and left is a real edge rather than a change
   * of tint — on a glass card, a fill against a near-identical ground reads as full.
   */
  background: var(--surface);
  box-shadow: 0 0 0 1px var(--border) inset;
}

.tokenbar__fill {
  /*
   * Both fills grow from the left of the same track, so the bought pool sits directly against
   * the included one and the two together read as a single remaining length. Widths are set
   * in the script from the month's capacity.
   */
  min-width: 0;
  /* MOVES when the number moves — the dashboard redraws on a poll, and a bar that jumped
     would be the only thing on the page that did. */
  transition: width 320ms var(--ease);
}

/* What the licence includes: the accent, because it is the ordinary state. */
.tokenbar__fill--included {
  background: var(--accent-fill);
}

/*
 * What somebody bought: a second colour, which is the whole point of the two pools being
 * drawn apart. Green rather than a paler accent — a tint of the same hue would read as
 * "some of the accent" at a glance, and these are two different things with two different
 * rules (one resets, one never expires).
 */
.tokenbar__fill--bought {
  background: var(--ok);
}

/*
 * Rounded on the OUTSIDE edges only, so the pair looks like one bar and not two pills. The
 * last fill in the track is whichever one is actually drawn — the bought span is left out at
 * zero (see token-bar.js), which is what lets `:last-child` mean "the end of the bar" rather
 * than "the bought pool, whether or not it exists".
 */
.tokenbar__fill:first-child {
  border-start-start-radius: 9px;
  border-end-start-radius: 9px;
}

.tokenbar__fill:last-child {
  border-start-end-radius: 9px;
  border-end-end-radius: 9px;
}

/* Names both colours, so identity never rests on colour alone — as the stack legend does. */
.tokenbar__legend {
  list-style: none;
  margin: 14px 0 0;
  padding: 0;
  display: grid;
  gap: 7px;
}

.tokenbar__key {
  display: flex;
  align-items: center;
  gap: 9px;
  font-size: 0.85rem;
}

.tokenbar__swatch {
  width: 10px;
  height: 10px;
  border-radius: 3px;
  flex: none;
}

.tokenbar__swatch--included {
  background: var(--accent-fill);
}

.tokenbar__swatch--bought {
  background: var(--ok);
}

.tokenbar__key-label {
  color: var(--text-dim);
}

.tokenbar__key-value {
  margin-left: auto;
  font-weight: 620;
  font-variant-numeric: tabular-nums;
}

/*
 * The bottom of the card: the one rule the bar cannot draw, and the way to buy more when
 * there is a reason to. Pinned to the foot rather than following the legend, so the card has
 * a bottom edge whether or not the button is there.
 */
.tokenbar__foot {
  margin-top: auto;
  padding-top: 14px;
  display: flex;
  align-items: flex-end;
  gap: 12px;
}

.tokenbar__reset {
  margin: 0;
  font-size: 0.78rem;
  line-height: 1.45;
  color: var(--text-dim);
}

.tokenbar__top-up {
  flex: none;
  margin-left: auto;
}

/* --------------------------------------------------------- the pack picker
 *
 * What a circuit can buy, when the bar has raised the question. Each pack is a link to a
 * Stripe Payment Link — see js/token-packs.js — so these are anchors that look like choices
 * rather than buttons that take a payment.
 */
.packs {
  display: grid;
  gap: 10px;
  margin: 16px 0;
}

.pack {
  display: flex;
  align-items: baseline;
  gap: 12px;
  padding: 14px 16px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--glass);
  color: var(--text);
  text-decoration: none;
  transition: border-color 140ms var(--ease), background 140ms var(--ease),
    transform 140ms var(--ease);
}

.pack:hover,
.pack:focus-visible {
  border-color: var(--accent);
  background: var(--accent-soft);
  /* A pack lifts under the pointer the way every other pressable row in the app does. */
  transform: translateY(-1px);
  outline: none;
}

.pack__tokens {
  font-weight: 620;
  font-variant-numeric: tabular-nums;
}

.pack__price {
  margin-left: auto;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}

@media (prefers-reduced-motion: reduce) {
  .tokenbar__fill {
    transition: none;
  }

  .pack {
    transition: none;
  }

  .pack:hover,
  .pack:focus-visible {
    transform: none;
  }
}

/* --------------------------------------------------------------- tooltip */
/*
 * Enhances, never gates: everything it says is also in the table twin below the
 * chart, so a reader who cannot hover loses nothing.
 */
.chart__tip {
  position: absolute;
  z-index: 5;
  pointer-events: none;
  background: var(--surface-3);
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  box-shadow: var(--shadow-lift);
  padding: 6px 10px;
  display: flex;
  flex-direction: column;
  gap: 1px;
  white-space: nowrap;
}

/* The value leads — the reader already knows which mark they are pointing at. */
.chart__tip-value {
  font-size: 0.88rem;
  font-weight: 650;
}

.chart__tip-label {
  font-size: 0.76rem;
  color: var(--text-dim);
}

/* A readout of several series at one period: swatch, name, value, one line each. */
.chart__tip-row {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: 0.8rem;
}

.chart__tip-name {
  color: var(--text-dim);
}

.chart__tip-row .chart__tip-value {
  margin-left: auto;
  font-size: 0.8rem;
}

/* ------------------------------------------------------- the table twin */
.chart__data {
  /* `auto` rather than a fixed margin: on a stretched card the slack goes here, which puts the
     disclosure on the bottom edge of every card in a row instead of leaving each one a
     different gap underneath it. */
  margin-top: auto;
  padding-top: 10px;
  border-top: 1px solid var(--border);
}

/* Except directly under the expander, which already drew the line. */
.chart__more + .chart__data {
  margin-top: 0;
  border-top: 0;
  padding-top: 0;
}

.chart__data > summary {
  list-style: none;
  cursor: pointer;
  font-size: 0.8rem;
  color: var(--text-dim);
  padding: 2px 0;
}

.chart__data > summary::-webkit-details-marker {
  display: none;
}

.chart__data > summary:hover {
  color: var(--text);
}

.chart__scroll {
  /* Wide content scrolls inside its own box; the page never scrolls sideways. */
  overflow-x: auto;
  margin-top: 8px;
  max-height: 260px;
  overflow-y: auto;
}

.table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.82rem;
}

.table th,
.table td {
  text-align: left;
  padding: 6px 10px 6px 0;
  border-bottom: 1px solid var(--border);
  font-weight: 500;
}

.table thead th {
  position: sticky;
  top: 0;
  background: var(--surface);
  color: var(--text-dim);
  font-size: 0.74rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  font-weight: 650;
}

.table tbody th {
  color: var(--text-dim);
  font-weight: 500;
}

.table td {
  font-variant-numeric: tabular-nums;
  font-weight: 620;
}

/* Refetch holds the previous render rather than flashing a skeleton — no layout
   jump, and the numbers you were reading stay put while the new ones arrive. */
body.is-loading .stats,
body.is-loading .charts,
body.is-loading #attention {
  opacity: 0.55;
  transition: opacity 120ms var(--ease);
}

/* --------------------------------------------------------- filter strip */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: flex-end;
  background: var(--glass);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  padding: 16px;
  margin-bottom: 24px;
}

.toolbar .field {
  margin: 0;
  flex: 1 1 190px;
  min-width: 0;
}

/*
 * A tickbox in a toolbar, which the ordinary .field is not shaped for: that stacks a label above
 * its control, and a checkbox wants its label beside it. It also does not stretch — a tickbox
 * given a third of the bar puts its label a hand's width from the box it belongs to.
 *
 * `align-items: flex-end` on the toolbar lines it up with the bottom of the select beside it,
 * which is where the button sits too, so the row reads as one line of controls.
 */
.field--check {
  display: flex;
  flex: 0 0 auto;
  align-items: center;
  gap: 8px;
  padding-bottom: 9px;
}

/*
 * Sentence case, against the small-caps the toolbar gives every other label.
 *
 * Those are captions naming a control below them — "CINEMA", over a select. This one is the
 * control's own words, read as a sentence next to the box you tick: "CODE AND NAME ONLY" beside
 * a checkbox reads as another caption for something that is not there.
 *
 * Both classes in the selector on purpose. `.toolbar .field > label` is the small-caps rule and
 * it is written LOWER DOWN this file, so at equal specificity it would win on source order — and
 * did, until this had the extra class on it.
 */
.toolbar .field.field--check > label {
  margin: 0;
  font-weight: 500;
  font-size: 0.88rem;
  letter-spacing: normal;
  text-transform: none;
  color: var(--text-dim);
  cursor: pointer;
}

.field--check > input {
  width: 17px;
  height: 17px;
  accent-color: var(--accent);
  cursor: pointer;
  flex: none;
}

/* Long option labels ("Validated, not yet sent") clip rather than overflow, and
   read as deliberately truncated instead of cut off. */
.toolbar select {
  text-overflow: ellipsis;
}

.toolbar .field > label {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-faint);
  font-weight: 600;
}

/* ------------------------------------------------------------ account rows
   One row per staff account on Active users. Same card shell as a cinema, so
   the page reads as part of the same app rather than a bolted-on table. */

/*
 * What a role group says when nobody is in it. Both groups are always drawn, because
 * each one holds the button that creates its own kind of account — so the group that
 * is empty is exactly the group somebody is about to need.
 */
.group__empty {
  margin: 0;
  padding: 18px 20px;
  color: var(--text-dim);
  font-size: 0.92rem;
}


.person {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 20px;
  border-bottom: 1px solid var(--border);
  flex-wrap: wrap;
}

.person:last-child {
  border-bottom: 0;
}

/* A suspended account is dimmed rather than hidden: it still holds history, and
   an admin looking for "why can this person not sign in" needs to find it. */
.person--suspended {
  background: color-mix(in oklab, var(--danger) 4%, transparent);
}

.person--suspended .person__avatar {
  background: var(--p-surface1);
  color: var(--text-dim);
}

.person--suspended .person__identity {
  opacity: 0.72;
}

.person__avatar {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--accent-line);
  color: var(--accent-ink);
  display: grid;
  place-items: center;
  font-size: 12px;
  font-weight: 750;
  letter-spacing: -0.02em;
  flex: none;
}

/*
 * 200px to start from, and it grows into whatever the row has left — so this number decides
 * only when a row wraps, never how wide the name is on one that does not. It was 220, which
 * left a manager's row on a dev's screen at 1440 exactly 35px spare: less than any badge plus
 * its gap, so the notifications bell alone put View dashboard and Edit on a second line.
 */
.person__identity {
  flex: 1 1 200px;
  min-width: 0;
}

.person__name {
  display: flex;
  align-items: center;
  gap: 8px;
  font-weight: 620;
  font-size: 0.95rem;
}

.person__email {
  font-family: var(--mono);
  font-size: 0.78rem;
  color: var(--text-dim);
  text-decoration: none;
  overflow-wrap: anywhere;
}

.person__email:hover {
  color: var(--p-lavender);
}

/* The actions on an account row — View dashboard, then Edit — and the panel Edit opens. */
.person__actions {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 8px;
}

.person--editing {
  display: block;
  background: var(--surface-2);
}

.person--editing .toolbar--bare {
  align-items: flex-end;
}

.person--editing .field {
  margin: 0;
  flex: 1 1 200px;
  max-width: 320px;
  min-width: 0;
}

.person--editing .field > label {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-faint);
  font-weight: 600;
}

/*
 * Which issue types an account is emailed about, inside its edit form.
 *
 * Its own block under the name and email rather than another field on that row: ten
 * pills do not belong in a line of text inputs, and the choice is a different kind of
 * thing from the account's identity.
 */
.notify {
  margin-top: 14px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
}

/*
 * What a maintenance account covers: locations, then issue types.
 *
 * Two halves of one answer, so they are one block with a rule between them rather than
 * two settings that happen to sit together — an account is defined by the pair, and
 * reading one without the other describes it wrongly.
 */
.scope__half + .scope__half {
  margin-top: 16px;
  padding-top: 16px;
  border-top: 1px dashed var(--border);
}

/*
 * The "every location" / "every issue type" toggle, on the same row as the label it
 * qualifies. It is not one of the pills below it: those are a list of things, and this is
 * the answer that makes the list unnecessary — see scopePicker() for why an open-ended
 * "every" is not the same as ticking all of them today.
 */
.scope__all {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: 0.87rem;
  color: var(--text-dim);
  cursor: pointer;
  user-select: none;
}

.scope__all input {
  margin: 0;
}

.notify__head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 10px;
  margin-bottom: 10px;
}

.notify__label {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-faint);
  font-weight: 600;
}

/* "3 of 10 issue types" — the answer without counting ticked pills. */
.notify__summary {
  flex: 1 1 auto;
  font-size: 0.83rem;
  color: var(--text-dim);
}

/* -------------------------------------------------------------- photographs */

/*
 * Choosing one, in a dialog.
 *
 * The <input type=file> itself is never shown — every browser draws it differently and
 * none of them draw it like the rest of this app. The <label for> beside it is the
 * button, which is a plain HTML behaviour rather than a click handler that has to
 * remember to fire.
 */
.photo__input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

/*
 * Positioned, so the hidden input above is placed inside its row rather than against whatever
 * positioned box is further out. Inside a dialog that box is the dialog itself, outside the part
 * that scrolls: the input sat where the scrolling could not move it, and the moment it took focus —
 * a Tab from the field before it — the browser scrolled the DIALOG to show it, which slid the whole
 * form up and left its buttons floating halfway down an empty box. Found on the inspection form.
 */
.photo__row {
  position: relative;
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}

/* A label is not a button, so it needs telling that it behaves like one. */
.photo__pick {
  cursor: pointer;
}

/* The input is what takes keyboard focus, and it is never seen — so the ring goes on the label
   beside it, the control a keyboard user is actually looking at. */
.photo__input:focus-visible + .photo__pick {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.photo__size {
  font-size: 0.78rem;
  color: var(--text-faint);
}

/* Big enough to check you photographed the right thing, small enough to sit in a form. */
.photo__thumb {
  display: block;
  margin-top: 10px;
  max-width: 100%;
  max-height: 180px;
  border-radius: var(--radius);
  border: 1px solid var(--border-strong);
}

/*
 * The pictures on a report card, grouped by what they are of.
 *
 * Fixed-height thumbnails in a row that scrolls sideways rather than wrapping into a
 * grid: the card is a paragraph of text with an attachment, not a gallery, and three
 * photographs should not push the buttons off the bottom of it.
 */
.shots {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  margin-top: 12px;
}

.shots__group {
  min-width: 0;
}

.shots__label {
  display: block;
  margin-bottom: 6px;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-faint);
}

.shots__row {
  display: flex;
  gap: 8px;
  overflow-x: auto;
  padding-bottom: 2px;
}

.shots__link {
  flex: none;
  /*
   * A COLUMN now, not a bare block: the thumbnail with what the check said under it. Capped at
   * the thumbnail's own widest, so a sentence wraps under its picture instead of stretching the
   * strip and pushing the next photo off the row.
   */
  display: flex;
  flex-direction: column;
  gap: 5px;
  max-width: 160px;
  border-radius: var(--radius);
  outline-offset: 3px;
  /*
   * ON THE ANCHOR, not on the caption inside it. Text decoration is drawn by the ancestor that
   * declares it and a descendant cannot cancel it — so `text-decoration: none` on the caption
   * span did nothing at all, and the description under each thumbnail was drawn underlined as
   * though the sentence were itself a link somewhere. The tile used to hold only an image, which
   * is why the link never needed this before.
   */
  text-decoration: none;
  /* It opens the viewer rather than following the href, so say so under the pointer. */
  cursor: zoom-in;
}

/*
 * What the check made of the picture — see shotSays in js/tree.js.
 *
 * It lives INSIDE the thumbnail's anchor, so it inherits the link's underline unless told not
 * to — which drew the description as though the sentence itself were a second link to somewhere
 * else. The whole tile stays clickable (the words belong to the picture above them, and a target
 * that includes its own caption is easier to hit on a phone); it just stops looking like prose
 * that has been linked.
 */
.shots__says {
  display: flex;
  flex-direction: column;
  gap: 3px;
  text-decoration: none;
}

.shots__what {
  font-size: 0.78rem;
  line-height: 1.35;
  color: var(--text-dim);
}

/*
 * The hint, in the app's own severity colours but NEVER the report's badge shape.
 *
 * complaints.severity is a person's decision and wears a filled chip; this is a model's read of
 * one photograph and wears a bare dot and a word. Different enough that nobody scanning a card
 * mistakes the second for the first — which matters, because the two can legitimately disagree
 * and the person is right.
 */
.shots__level {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--text-dim);
}

.shots__level::before {
  content: '';
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: currentColor;
}

.shots__level--low {
  color: var(--text-dim);
}

.shots__level--medium {
  color: var(--warn);
}

.shots__level--high {
  color: var(--danger);
}

.shots__thumb {
  display: block;
  height: 92px;
  width: auto;
  max-width: 160px;
  object-fit: cover;
  border-radius: var(--radius);
  border: 1px solid var(--border-strong);
  background: var(--surface-3);
  transition: border-color 140ms var(--ease), transform 140ms var(--ease);
}

.shots__link:hover .shots__thumb {
  border-color: var(--accent-line);
  transform: translateY(-1px);
}

/*
 * The photo viewer: a picture over the page, rather than a trip to another tab.
 *
 * Bigger than .modal and shaped by the image rather than by a column width — this is
 * one thing to look at, not a form. `display` is on [open] only, for the same reason it
 * is on .modal[open]: an author's `display` outranks the browser's own
 * `dialog:not([open]) { display: none }`, and getting that wrong leaves the viewer
 * unfolded at the bottom of every page.
 */
.viewer {
  width: min(1100px, calc(100vw - 32px));
  max-height: min(92vh, 900px);
  max-height: min(92dvh, 900px);
  padding: 0;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  background: var(--glass-strong);
  -webkit-backdrop-filter: blur(16px);
  backdrop-filter: blur(16px);
  color: var(--text);
  box-shadow: var(--shadow-lift);
  overflow: hidden;
}

.viewer[open] {
  display: flex;
  flex-direction: column;
}

.viewer::backdrop {
  background: rgba(6, 6, 10, 0.82);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
}

/* Dark behind the picture whatever the theme: a photograph of a dim auditorium on a
   white page reads as a mistake, and letterboxing has to look deliberate. */
.viewer__frame {
  position: relative;
  flex: 1 1 auto;
  /* min-height: 0 lets it shrink to the room left over, and the hidden overflow is the
     guarantee: a portrait photograph letterboxes inside the frame instead of growing it
     and pushing the caption bar off the bottom of the dialog. */
  min-height: 0;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #0b0b10;
}

/* Against the frame rather than against a guess at what is left after the bar — which
   is what the first version did, and it cropped every portrait photo. */
.viewer__img {
  display: block;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
}

.viewer__x {
  position: absolute;
  top: 8px;
  right: 8px;
  background: rgba(10, 10, 16, 0.6);
  color: #f4f4f6;
}

.viewer__x:hover {
  background: rgba(10, 10, 16, 0.85);
  color: #ffffff;
}

/* Arrows only when there is more than one, and out of the way of the picture. */
.viewer__step {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 44px;
  height: 64px;
  display: grid;
  place-items: center;
  font-size: 30px;
  line-height: 1;
  border: 0;
  border-radius: 10px;
  background: rgba(10, 10, 16, 0.55);
  color: #f4f4f6;
  cursor: pointer;
  transition: background-color 140ms var(--ease);
}

.viewer__step:hover {
  background: rgba(10, 10, 16, 0.85);
}

.viewer__step--prev {
  left: 8px;
}

.viewer__step--next {
  right: 8px;
}

.viewer__bar {
  flex: none;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 16px;
  border-top: 1px solid var(--border);
  background: var(--surface-2);
  font-size: 0.83rem;
}

.viewer__caption {
  flex: 1 1 auto;
  margin: 0;
  color: var(--text-dim);
  min-width: 0;
}

.viewer__count {
  flex: none;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}

/* Still offered, for somebody who wants the file itself. */
.viewer__full {
  flex: none;
  color: var(--accent);
}

@media (max-width: 560px) {
  .viewer__step {
    width: 36px;
    height: 52px;
    font-size: 24px;
  }

  /* The count is the one thing that can go: the caption says what you are looking at. */
  .viewer__count {
    display: none;
  }
}

.notify__quick {
  display: flex;
  gap: 8px;
  flex: 0 0 auto;
}

/* Smaller than the settings page's, because there are ten of them inside a row. */
.notify__pills .pill span {
  padding: 7px 12px;
  font-size: 0.85rem;
}

/* On a collapsed row: nothing for the ordinary case, a line when it is not. Its own
   line, because the email above it is a link and running the two together reads as
   one long address. */
.person__notify {
  display: block;
  margin-top: 2px;
  font-size: 0.78rem;
  color: var(--text-faint);
  font-style: italic;
}

/* The address is the thing above it, so it needs to end. */
.person__email {
  display: inline-block;
}

/* "also at Aviator 10" — one manager, several buildings. */
.manager__also {
  font-size: 0.78rem;
  color: var(--text-faint);
  font-style: italic;
}

.person__scope {
  flex: 0 1 190px;
  font-size: 0.83rem;
  color: var(--text-dim);
}

.person__state {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: 0 0 auto;
}

/*
 * An ordinary account has nothing to say here, and an empty box is not nothing.
 *
 * `.person` is a flex row with a 12px gap, so a zero-width child still costs a gap on each
 * side — which would leave the spacing between the scope and the last-seen column different
 * on the rows that carry a badge from the rows that do not. Since dropping the permanent
 * "Active" badge made most rows empty here, that would be most of the list.
 */
.person__state:empty {
  display: none;
}

/*
 * Notifications are on for this account: a bell rather than the word, because the word was the
 * widest badge on the row and wrapped every manager's buttons onto a second line for a dev. No
 * count beside it — a bell with a number reads as that many unread. The count is in its tooltip.
 */
.badge--bell {
  padding: 3px 6px;
  background: var(--ok-soft);
  color: var(--ok);
  border-color: color-mix(in oklab, var(--ok) 35%, transparent);
}

.badge--bell svg {
  display: block;
}

.person__seen {
  flex: 0 0 130px;
  text-align: right;
  font-size: 0.8rem;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}

/* Distinct from "Active": one is whether the account works, the other is
   whether they are actually holding a session right now. */
.badge--live {
  background: color-mix(in oklab, var(--info) 18%, transparent);
  border-color: color-mix(in oklab, var(--info) 45%, transparent);
  color: var(--info);
}

.person__never {
  color: var(--warn);
}

/* ------------------------------------------------------------------- tabs
   Where a report is in its life, as the case monitor's main axis. This was an
   option in a select inside the collapsed panel below — fine once you know the
   app, no help at all before that. The count on each is what earns the space:
   "nothing is waiting" should be readable without clicking anything. */
/*
 * The day strip with a week arrow at each end.
 *
 * The arrows keep their size and the strip between them takes the rest — which is what makes them
 * usable on a phone, where the seven day tabs scroll: the way to next week does not scroll away
 * with them.
 */
.weeknav {
  display: flex;
  align-items: stretch;
  gap: 4px;
}

/* --------------------------------------------------------------- the agent row
 *
 * The AI Reviewer Agent, on a line between the filter controls and the tab strip.
 *
 * LEFT-ALIGNED, under Advanced filters rather than under Refresh: the eye comes down the
 * left edge from the filters button, and a lone right-aligned button reads as a footer to
 * the controls above rather than a thing about the list below.
 *
 * The .filters block owns 24px of margin under itself, which is the gap between the controls
 * and whatever follows. This row lives inside that gap, so it pulls up into it and keeps its
 * own smaller breath above the tabs — otherwise the button floated in the middle of a
 * two-row space, attached to neither.
 */
.agentrow {
  display: flex;
  /*
   * Closer to the controls above than to the tabs below, because that is which group it is in:
   * the eye comes down the left edge from Advanced filters and finds this on the next line.
   *
   * Both numbers are MEASURED rather than reasoned about, and they had to be re-measured: this
   * whole rule was dead until now — an orphaned `.weeknav` selector above the comment block had
   * silently turned it into `.weeknav .agentrow`, which matches nothing — so the row had been
   * laying itself out as a plain block with no margins at all, flush against the tab strip. The
   * -14px top written for it had never once been applied, and applying it put the button hard
   * against Advanced filters. 4px leaves ten clear pixels above.
   *
   * The 18 below is the one the reader notices: the tab strip's hover and selected states paint
   * a filled box that starts 10px above the tab's own text, so at the old spacing that fill met
   * the button's border with nothing between them and the two read as one shape.
   */
  margin: -4px 0 18px;
  align-items: center;
  gap: 10px;
}

.agentrow[hidden] {
  display: none;
}

/* Create new, at the row's far end across from the agent, on a desk (see placeRaise in
   js/review.js) — where the queue row under it ends too, so the two right edges are one. */
.agentrow > #raise {
  margin-left: auto;
}

/* A row with nothing showing on it takes no room: the agent is not offered on every tab or to
   every reader, and a phone keeps Create new on the filters' row instead. */
.agentrow:not(:has(> :not([hidden]))) {
  display: none;
}

@media (max-width: 560px) {
  /* Before the page's script has moved it to the filters' row, Create new is not drawn up here
     for a moment only to jump down; and the row is the agent's alone on a phone. */
  .agentrow > #raise {
    display: none;
  }

  .agentrow:not(:has(> .agentrow__btn:not([hidden]))) {
    display: none;
  }
}

/* The spark sits with the words rather than beside them: a gap the button's own padding
   does not already provide would read as two controls that happen to touch. */
.agentrow__btn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
}

/*
 * And it wears the accent, like everything else generative in this app.
 *
 * It was `btn--secondary` — the neutral grey every supporting control on the page uses — which
 * put the one AI control on the case monitor in the same clothes as Refresh. The assistant
 * launcher is accent, the "AI reviewed" tag is accent, the Auto-translated tag is accent; a
 * reader who has learned that colour means "a model does this" was getting no help at all from
 * the button that runs one.
 *
 * A TINT rather than the solid fill the tags wear, because this is a button among buttons and
 * a solid accent chip is what `btn--cta` means — the thing you are meant to press. This one is
 * offered, not urged, so it takes the accent's quieter register: the same recipe as
 * `.btn--primary`, reached through its own class so the two can part company later without
 * either of them silently changing.
 */
.agentrow__btn.btn--secondary {
  background: color-mix(in oklab, var(--accent) 13%, var(--surface));
  color: var(--accent);
  border-color: color-mix(in oklab, var(--accent) 32%, transparent);
}

.agentrow__btn.btn--secondary:hover {
  background: color-mix(in oklab, var(--accent) 20%, var(--surface));
  border-color: color-mix(in oklab, var(--accent) 45%, transparent);
}

/*
 * The same button while AI TRIAGE runs, when its words change to say so (dressForTriage in
 * js/review.js). One register up from the tint — a firmer border and a soft glow, the
 * button-sized echo of the network behind it — and still short of `btn--cta`, because it is
 * a state being worn, not a press being urged. It still opens the same box: the reports
 * triage defers are in there waiting for exactly this person.
 */
.agentrow__btn--live.btn--secondary {
  background: color-mix(in oklab, var(--accent) 22%, var(--surface));
  border-color: color-mix(in oklab, var(--accent) 60%, transparent);
  /*
   * Lit from WITHIN, not haloed.
   *
   * The glow was `0 0 14px` and spread that far past every edge, including the left — and
   * an eye reads the halo's edge as the button's edge, so this row looked misaligned with
   * Advanced filters directly above it even though both boxes start at exactly the same x
   * (measured: 130px each). The operator saw it as two controls that would not line up.
   * Inset keeps the lit state and keeps the edge: nothing is painted outside the border box,
   * so the two rows agree by construction rather than by luck.
   */
  box-shadow: inset 0 0 12px color-mix(in oklab, var(--accent) 30%, transparent);
}

.agentrow__spark {
  flex: none;
}

/*
 * A button whose words change when it is pressed, the width of the longer of the two in both states.
 *
 * The scheduling agent's button says "Hide" while its picture is on the grid, and the extra word
 * made it wider at the moment it was pressed. On a phone that was too wide to stay beside the
 * Optimizer's button, so it dropped to a line of its own under the finger that had pressed it.
 * The words it is NOT showing (data-spare, set by paintAdvice) sit in the same grid cell as the
 * words it is, invisibly, so the cell is always as wide as the longer and the button never moves.
 * Invisible rather than absent, so it takes up room without being read out or painted.
 *
 * The spark travels with the words as one face, centred in the cell, so the shorter label keeps
 * its spark beside it rather than stranded at the edge. The spare is padded by the spark and the
 * gap (14px + 7px) because the face it stands in for carries them both.
 */
.agentrow__btn--steady {
  display: inline-grid;
  justify-content: center;
}

.agentrow__btn--steady > .agentrow__face,
.agentrow__btn--steady::after {
  grid-area: 1 / 1;
}

.agentrow__face {
  display: inline-flex;
  align-items: center;
  justify-self: center;
  gap: 7px;
}

.agentrow__btn--steady::after {
  content: attr(data-spare);
  padding-left: 21px;
  visibility: hidden;
}

.tabs {
  flex: 1 1 auto;
  min-width: 0;
}

.weeknav__arrow {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  /* 40px: a comfortable thumb target that still reads as part of the strip rather than as a
     button parked beside it. */
  width: 40px;
  margin-bottom: 6px;
  padding: 0;
  background: none;
  border: 0;
  border-bottom: 1px solid var(--border);
  border-radius: 0;
  color: var(--text-dim);
  cursor: pointer;
}

.weeknav__arrow:hover {
  color: var(--accent);
}

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

.tabs {
  display: flex;
  gap: 6px;
  margin-bottom: 6px;
  padding-bottom: 2px;
  border-bottom: 1px solid var(--border);
  /* Four tabs with counts do not fit 360px, and wrapping them onto two lines makes
     the strip read as two groups. It scrolls instead. */
  overflow-x: auto;
  scrollbar-width: none;
}

.tabs::-webkit-scrollbar {
  display: none;
}

.tab {
  flex: none;
  position: relative;
  /* Clips the glow below to the tab's own box, exactly as the dock's pill clips its
     light: what shows is the upper slice of something big and soft rising from the
     bottom edge, never the outline of a shape. */
  overflow: hidden;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 14px;
  border: 0;
  border-bottom: 2px solid transparent;
  border-radius: var(--radius) var(--radius) 0 0;
  background: none;
  color: var(--text-dim);
  font: inherit;
  font-weight: 600;
  font-size: 0.92rem;
  cursor: pointer;
  white-space: nowrap;
  transition:
    color 140ms var(--ease),
    border-color 140ms var(--ease);
}

/* The dock's under-rim light, at a tab's scale — the same soft ellipse whose centre sits
   below the bottom edge, so the light rises off the underline rather than pooling behind
   the words. One drawing shared by both states below; only its strength differs. */
.tab::after {
  content: '';
  position: absolute;
  left: 50%;
  bottom: -30px;
  transform: translateX(-50%);
  width: 120%;
  height: 52px;
  border-radius: 50%;
  background: radial-gradient(
    closest-side,
    color-mix(in srgb, var(--text) 15%, transparent),
    transparent
  );
  opacity: 0;
  transition: opacity 160ms var(--ease);
  pointer-events: none;
}

/* Hover borrows the glow at part strength — an invitation, not a claim. Gated on a
   device that can actually hover, because iOS leaves :hover asserted after a tap until
   the next touch — the wash this replaces used to sit lit on the tab somebody had just
   pressed. Same rule the list rows state at their own hover. */
@media (hover: hover) {
  .tab:hover {
    color: var(--text);
  }

  .tab:hover::after {
    opacity: 0.6;
  }
}

/* The line is what says "you are here" — the page's own ink, not the accent, with the
   glow at full strength rising off it: the dock's language for the lit tab, spoken by
   the strip. */
.tab--on {
  color: var(--text);
  border-bottom-color: var(--text);
}

.tab--on::after {
  opacity: 1;
}

.tab__count {
  padding: 1px 8px;
  border-radius: 999px;
  background: var(--surface-3);
  color: var(--text-dim);
  font-size: 0.78rem;
  /* Tabular, so 9 becoming 10 is the only thing that changes width — and 11 becoming 12
     changes nothing at all. */
  font-variant-numeric: tabular-nums;
  transition: opacity 160ms var(--ease);
}

/* The count on the lit tab wears the same quiet pill as every other — chosen by the
   operator over an accent circle (which fought the monochrome line and glow) and over an
   ink chip (louder than a number needs to be). Only the numeral brightens, the same step
   the tab's own words take. */
.tab--on .tab__count {
  background: var(--surface-3);
  color: var(--text);
}

/* Nothing in this stage is worth saying quietly rather than not at all — a missing
   number reads as "not loaded yet", which is a different thing. */
.tab__count--zero {
  opacity: 0.55;
}

/*
 * THE RECOMMENDATION DOT: this tab has something suggested on it.
 *
 * The scheduler's day strip wears it while an overlay is on the grid, because the grid draws one
 * day and the overlay argues about seven — without it, a plan whose changes fall on Tuesday and
 * Thursday is invisible from Monday and reads as though the plan did nothing.
 *
 * A DOT rather than a second number, and the accent rather than the strip's own ink. The count
 * beside it is the day's showings, which is a fact about the schedule; this is a suggestion about
 * it, and the two should not look like two of the same thing. Its text is the number in words for
 * a reader who cannot see colour at all — clipped rather than hidden, so it is announced with the
 * day's name instead of being a coloured circle that says nothing.
 */
.tab__dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent);
  /* The words inside, off the screen and still in the accessibility tree. */
  overflow: hidden;
  white-space: nowrap;
  text-indent: 100%;
  flex: none;
}

/* The day itself brightens a step, so the strip reads at a glance rather than by hunting for
   six pixels — and the dot above is what says which of the bright ones is which. */
.tab--advised {
  color: var(--text);
}

/*
 * A tab's two names, of which the strip shows one: the scheduler's days carry a short one, "Fri
 * 25", for a computer's row too narrow for seven at full length (see fitWeekNav in
 * js/schedule-grid.js, and .weeknav--stacked in the Studio section). Both are text, so the one
 * showing is the one a screen reader reads.
 */
.tab__short {
  display: none;
}

.tabs--short .tab__long {
  display: none;
}

.tabs--short .tab__short {
  display: inline;
}

/*
 * A count that is being recalculated: the number it had, dimmed, until the new one arrives.
 *
 * It used to be taken off the strip while the request was out, which made all four badges
 * disappear and come back on every tab change — the bar shrank and grew again, and on the
 * control people press most often that reads as a glitch rather than as loading. Holding
 * the old number costs nothing in truthfulness on a tab change (switching tab does not
 * change any of these counts) and one dim digit is the honest signal while a filter is.
 *
 * The fade is on `.tab__count` itself, above, so this is one badge changing rather than two
 * swapping — and it lands after `--zero`, so an empty stage that is also being recalculated
 * reads as recalculating.
 */
.tab__count--stale {
  opacity: 0.4;
}

/* The line under the tabs, and the fold control at the other end of it. */
/* Wrapping, because this row grew a third thing in it.
   A hint that takes the slack, then Collapse all, the Cards / List switch and Case history. Two
   of those fitted beside the sentence on a phone; four do not, and without a wrap they simply
   overflowed — the switch came to rest ON TOP of the Case history button, which stayed visible
   and enabled and could no longer be clicked. Caught by the phone-width case history test in
   test/browser/flows.browser.js, which is the only place that presses that button at 400px. */
/* Centred, not baseline-aligned: the row is buttons now, and two of them — the plain
   Collapse all and the bordered Cards / List group — have different box chrome around
   the same size of text, so lining up their BASELINES left their boxes visibly off by
   the difference. Centring their boxes is what reads as aligned on a row of controls;
   the hint is one line of text and centres against them the same way.
   The top margin is the row's own air under the tab strip — the strip only carries 6px
   of its own, cut for the pages where nothing follows it this closely. */
.tabs__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
  margin: 8px 0 16px;
}

/* With nothing showing in it — one cinema, and not the Resolved tab — the foot takes no room. */
.tabs__foot:not(:has(> :not([hidden]))) {
  display: none;
}

/*
 * The case monitor's row: the stages on the left, the filters and the view switch across from
 * them. It wraps rather than squeezing, so on a phone the tools take a line under the stages.
 * Positioned, so the filter panel can open under the whole row rather than under its button.
 */
.queuebar {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px 12px;
  margin-bottom: 14px;
}

.queuebar > .tabs {
  flex: 0 1 auto;
  margin-bottom: 0;
}

.queuebar__tools {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-left: auto;
}

.queuebar .filters--advanced {
  margin-bottom: 0;
}

/* The panel opens under the row and over the list, as wide as the row, rather than pushing the
   queue down by a row of controls most visits never open. */
.queuebar .filters--advanced .filters__body {
  position: absolute;
  top: calc(100% + 8px);
  left: 0;
  right: 0;
  z-index: 30;
  margin-top: 0;
  box-shadow: var(--shadow-lift);
}

/* And it arrives from its button, as the account menu does, rather than blinking on over the
   list. Out of the row's flow, the box it belongs to does not grow as it opens — which is how
   every other fold here moves — so without this nothing would. Reduced motion zeroes it with
   every other animation, and the panel is simply there. */
.queuebar .filters--advanced[open] .filters__body {
  transform-origin: top right;
  animation: menu-down 150ms var(--ease);
}

/* One line saying what is in the tab you are on. The page description above says how
   the queue works; this says what you are looking at right now. */
.tabs__hint {
  flex: 1 1 auto;
  margin: 0;
  font-size: 0.86rem;
  color: var(--text-faint);
}

.tabs__foot .btn {
  flex: 0 0 auto;
}

/* ------------------------------------------------------- collapsible filters
   The review-queue filters, folded into a single bar. The <summary> is the whole
   bar rather than just a label, so it can carry the state of the filters and the
   Refresh button while the controls themselves are put away. */
.filters {
  background: var(--glass);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  margin-bottom: 24px;
}

.filters__summary {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 16px 10px 14px;
  cursor: pointer;
  user-select: none;
  list-style: none;
  border-radius: var(--radius);
  transition: background-color 140ms var(--ease);
}

.filters__summary::-webkit-details-marker {
  display: none;
}

/*
 * The case monitor's variant: a button rather than a bar.
 *
 * Cinema, issue type, severity and search cut across every stage and most visits use
 * none of them, so a full-width strip above the queue was paying rent on something
 * rarely read. The frame comes off, the label becomes a button, and the row carries
 * only what has to stay visible: whether anything is filtering, and the way to undo it.
 */
.filters--advanced {
  background: none;
  border: 0;
  border-radius: 0;
  margin-bottom: 14px;
}

.filters--advanced > .filters__summary {
  padding: 0;
  gap: 12px;
}

.filters--advanced > .filters__summary:hover,
/*
 * Two `.filters` classes deep, not one. `.filters[open] > .filters__summary` puts a rule
 * under the strip further down the file and matches with the same specificity, so it won on
 * source order — and the advanced bar has been carrying a border it was written not to have
 * on the manager and maintenance pages ever since. The `background: none` beside it always
 * worked, because nothing else declares that: a half-applied rule is how this hides.
 */
.filters.filters--advanced[open] > .filters__summary {
  background: none;
  border-bottom: 0;
}

.filters--advanced > .filters__summary:focus-visible {
  outline: none;
}

/* The part that looks and behaves like a button. Focus lands here rather than on the
   whole row, because the row is as wide as the page and a ring around it says nothing
   about what pressing Enter would do. */
.filters__button {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 7px 13px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface-2);
  color: var(--text);
  font-size: 0.86rem;
  transition:
    background-color 140ms var(--ease),
    border-color 140ms var(--ease),
    color 140ms var(--ease);
}

.filters__summary:hover .filters__button {
  border-color: var(--p-surface2);
  background: var(--surface-3);
}

.filters__summary:focus-visible .filters__button {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* Open, or filtering: either way the button says so without being read. */
.filters--advanced[open] .filters__button,
.filters--advanced .filters__button:has(.filters__count:not([hidden])) {
  border-color: var(--accent);
  color: var(--accent);
}

.filters--advanced .filters__body {
  margin-top: 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}

.filters__summary:hover {
  background: var(--surface-3);
}

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

.filters[open] > .filters__summary {
  border-bottom: 1px solid var(--border);
  border-radius: var(--radius) var(--radius) 0 0;
}

.filters__icon {
  color: var(--text-faint);
  flex: 0 0 auto;
  transition: color 140ms var(--ease), transform 160ms var(--ease);
}

.filters__summary:hover .filters__icon,
.filters[open] > .filters__summary .filters__icon {
  color: var(--accent);
}

.filters__label {
  font-weight: 640;
  font-size: 0.9rem;
  flex: 0 0 auto;
}

/* How many controls are narrowing the queue right now. */
.filters__count {
  flex: 0 0 auto;
  min-width: 18px;
  padding: 1px 6px;
  border-radius: 999px;
  background: color-mix(in oklab, var(--accent) 24%, transparent);
  border: 1px solid color-mix(in oklab, var(--accent) 45%, transparent);
  color: var(--text);
  font-size: 0.72rem;
  font-weight: 700;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

/* Reads out what is filtering the queue while the panel is shut. Truncates
   rather than wrapping, so the bar stays one line however many filters are on. */
.filters__active {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 0.83rem;
  color: var(--text-dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.filters__summary .btn {
  flex: 0 0 auto;
}

/*
 * The summary's buttons hold the RIGHT EDGE, whatever else is on the row.
 *
 * They used to be pushed there by the active-filter label, which grows to fill — so once
 * that label went silent at rest there was nothing doing the pushing, and Create new and
 * Refresh sat halfway across the row beside the filters button. Turning one filter on then
 * moved them seven hundred pixels. The operator's rule for this app, given for the fold
 * glyph and applied here: a control lives in ONE place, so that whatever is happening, the
 * reader knows without looking where Create new and Refresh are.
 *
 * The auto margin is claimed by the first button actually RENDERED and dropped by every
 * button after it, so the group travels together whether or not Clear filters is showing.
 * `:not([hidden])` is the whole trick — an earlier note here rejected auto margins because
 * "two elements each claiming it would split the free space", which is true of two VISIBLE
 * claimants and is exactly what the hidden test rules out.
 */
.filters--advanced > .filters__summary > .btn {
  margin-left: auto;
}

.filters--advanced > .filters__summary > .btn:not([hidden]) ~ .btn {
  margin-left: 0;
}

.filters__body {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: flex-end;
  padding: 16px;
}

.filters__body .field {
  margin: 0;
  flex: 1 1 190px;
  min-width: 0;
}

.filters__body select {
  text-overflow: ellipsis;
}

.filters__body .field > label {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-faint);
  font-weight: 600;
}

/* A toolbar with no chrome of its own, for forms nested inside a card. */
.toolbar--bare {
  border: 0;
  padding: 0;
  margin: 0;
  box-shadow: none;
  background: none;
}

/* --------------------------------------- cinema > auditorium > complaint */
.theatre {
  background: var(--glass);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
  margin-bottom: 20px;
  overflow: hidden;
}

.theatre__head {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 16px 20px;
  background: linear-gradient(180deg, var(--surface-3), var(--surface-2));
  border-bottom: 1px solid var(--border);
  flex-wrap: wrap;
}

.theatre__name {
  margin: 0;
  font-size: 1.1rem;
}

.theatre__meta {
  font-size: 0.83rem;
  color: var(--text-dim);
}

.theatre__count {
  margin-left: auto;
  font-size: 0.83rem;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}

/* Each cinema is a disclosure on the case monitor, which spans every building.
   The header keeps the same layout as the static version — it is a <summary>
   rather than a <div>, so the native marker goes and a chevron takes its place. */
.theatre--toggle > .theatre__head {
  border-bottom: 0;
}

.theatre--toggle[open] > .theatre__head {
  border-bottom: 1px solid var(--border);
}

.theatre__head--toggle {
  padding-left: 42px;
  position: relative;
  cursor: pointer;
  user-select: none;
  list-style: none;
  transition: background-color 140ms var(--ease);
}

.theatre__head--toggle::-webkit-details-marker {
  display: none;
}

.theatre__head--toggle::before {
  content: '';
  position: absolute;
  left: 20px;
  top: 50%;
  width: 7px;
  height: 7px;
  margin-top: -5px;
  border-right: 2px solid var(--text-dim);
  border-bottom: 2px solid var(--text-dim);
  transform: rotate(-45deg);
  transition: transform 160ms var(--ease);
}

.theatre--toggle[open] > .theatre__head--toggle::before {
  transform: rotate(45deg);
  margin-top: -6px;
}

/* Gated on `hover: hover` — see the note on the case rows below. A tap on iOS asserts
   :hover and leaves it asserted, so an unguarded wash here left every cinema somebody had
   opened looking selected until they touched something else. */
@media (hover: hover) {
  .theatre__head--toggle:hover {
    background: var(--surface-3);
  }
}

.theatre__head--toggle:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: -3px;
}

/* Address and phone. Takes the full width below the cinema name so a long
   street address never squeezes the counts out of the header. */
.contact {
  flex: 1 0 100%;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 6px 18px;
  margin-top: -2px;
  font-size: 0.83rem;
}

.contact__link {
  color: var(--text-dim);
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: color 120ms var(--ease), border-color 120ms var(--ease);
}

.contact__link:hover {
  color: var(--text);
  border-bottom-color: var(--border-strong);
}

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

.contact__link--phone {
  font-variant-numeric: tabular-nums;
}

.auditorium {
  border-bottom: 1px solid var(--border);
}

.auditorium:last-child {
  border-bottom: 0;
}

.auditorium__head {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 11px 20px;
  background: color-mix(in oklab, var(--p-base) 45%, transparent);
  border-bottom: 1px solid var(--border);
}

/*
 * The area's header as a disclosure summary.
 *
 * Mirrors .theatre__head--toggle above it rather than sharing a class: the two sit at different
 * depths with different padding and different type, and one selector serving both would be a rule
 * full of exceptions. What they do share is the affordance — a chevron on the left that turns —
 * because a reader should not have to learn that two folds in one page work differently.
 *
 * `padding-left` alone rather than a fresh `padding`, so the base rule above stays in charge of the
 * other three sides. Declared after it, which is what lets it win.
 */
.auditorium__head--toggle {
  padding-left: 42px;
  position: relative;
  cursor: pointer;
  user-select: none;
  /* The browser's own triangle, removed in both spellings — `list-style` covers the standard
     marker, the pseudo-element covers WebKit's. */
  list-style: none;
  transition: background-color 140ms var(--ease);
}

.auditorium__head--toggle::-webkit-details-marker {
  display: none;
}

.auditorium__head--toggle::before {
  content: '';
  position: absolute;
  left: 22px;
  top: 50%;
  width: 6px;
  height: 6px;
  margin-top: -4px;
  border-right: 2px solid var(--text-dim);
  border-bottom: 2px solid var(--text-dim);
  transform: rotate(-45deg);
  transition: transform 160ms var(--ease);
}

.auditorium--toggle[open] > .auditorium__head--toggle::before {
  transform: rotate(45deg);
  margin-top: -6px;
}

/* Gated with the cinema's, and for the same reason: a room is the other thing a finger
   opens on this page, and it stuck exactly the same way. */
@media (hover: hover) {
  .auditorium__head--toggle:hover {
    background: color-mix(in oklab, var(--p-base) 70%, transparent);
  }
}

/* A shut area has no body to draw a line under, so the header's own line would hang. */
.auditorium--toggle:not([open]) > .auditorium__head--toggle {
  border-bottom: 0;
}

.auditorium__number {
  font-weight: 680;
  font-size: 0.95rem;
  letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums;
}

.auditorium__meta {
  font-size: 0.8rem;
  color: var(--text-faint);
}

.auditorium__count {
  margin-left: auto;
  font-size: 0.8rem;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}

.complaint {
  padding: 16px 20px;
  border-bottom: 1px solid var(--border);
  transition: background-color 140ms var(--ease);
}

.complaint:last-child {
  border-bottom: 0;
}

/* ------------------------------------------------------------- report cards
 * A report hangs off its RAIL — the severity edge grown into the card's one piece of
 * structure — and the queue reads as a timeline of them.
 *
 * This is the queue's calm-down, chosen by the operator from four mocked skins. The well
 * and the border that used to frame each card carried real weight (the notes on the old
 * design were right that a page needs to say where one report ends), but they carried it
 * with chrome; the rail carries it with the one colour the card was already wearing. The
 * body fades to a faint tint — enough to hold the card together as a surface — and the
 * separating is done by the rail, the gap, and the asymmetric radius that makes each card
 * visibly hang from its left edge.
 */
.auditorium__body {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 8px 4px;
  background: transparent;
}

.auditorium__body:empty {
  display: none;
}

/* ------------------------------------------------------------- the glass rows
 *
 * On the case pages a location is a ROW OF GLASS — the dock's own material, as a slim
 * contained strip — and each auditorium is still an island of its own beneath it.
 *
 * Second treatment to hold this ground. The first (the hanging tree) took the name OUT of
 * every container and set it straight on the page, which read as confident against the flat
 * page it was designed on — and read as unfinished the day the dock, the bar and the
 * assistant's tile all became glass objects. The operator said so, looking at a collapsed
 * queue: bare names with the background showing through them looked cheap beside the new
 * chrome. Containing them in the same glass closes that gap, and keeps what the hanging
 * style was actually FOR: a location can never be mistaken for a report, because the row
 * and the island are different weights of the same material.
 *
 * Scoped to `[data-caseview]`, which renderTree stamps on every container it draws —
 * and nothing else does. That boundary matters: the Location info page borrows the same
 * .theatre__head classes for its management rows and edit-in-place forms, and those keep
 * the boxed look these rules would break.
 */
[data-caseview] .theatre {
  background: none;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  overflow: visible;
  margin-bottom: 18px;
}

[data-caseview] .theatre > .theatre__head {
  background: var(--glass);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  box-shadow: var(--rim);
  border: 1px solid var(--border-strong);
  border-radius: 14px;
  /*
   * ONE padding declaration for this head, and the right inset is the chevron's lane —
   * the rule that survives from the hanging era, because the bug it pins (two paddings
   * disagreeing, the name sliding under the glyph) does not care what the head looks like.
   */
  padding: 14px 44px 14px 18px;
}

/* Open, the row becomes the cap of what it holds: square shoulders toward its islands,
   and a small step of air before the first one. */
/* Open, the row and its reports are ONE object: the cap squares its shoulders and sits
   flush on the stack — the operator disliked the air that used to float between them, and
   the seam is the cap's own bottom border. */
[data-caseview] .theatre--toggle[open] > .theatre__head {
  border-radius: 14px 14px 0 0;
  margin-bottom: 0;
}

[data-caseview] .theatre__name {
  font-size: 1.35rem;
}

/*
 * The chevron holds the top-right corner of the row — the same side as a card's own
 * control, with a fixed top rather than centring: this heading wraps on a phone, and a
 * centred glyph would drift to the middle of the block instead of holding the corner.
 */
[data-caseview] .theatre__head--toggle::before {
  left: auto;
  right: 16px;
  top: 21px;
  margin-top: 0;
}

/* The open state's nudge belonged to the old centring; against a fixed top it would only
   push the glyph off its line. */
[data-caseview] .theatre--toggle[open] > .theatre__head--toggle::before {
  margin-top: 0;
}

/*
 * The hover wash: the glass fills in a step, nothing more — and gated on `hover: hover`,
 * like the case rows further down this file. iOS leaves :hover asserted after a tap until
 * something else is touched, so an unguarded wash left every opened cinema looking
 * selected. A device that cannot hover should never enter a hover state at all.
 */
@media (hover: hover) {
  [data-caseview] .theatre__head--toggle:hover {
    background: color-mix(in srgb, var(--surface-3) 78%, transparent);
  }
}

/* Each auditorium is the island it became under the hanging style, unchanged: the row and
   the island are different weights of one material, which is the whole hierarchy. */
[data-caseview] .auditorium {
  background: var(--glass);
  border: 1px solid var(--border-strong);
  border-top: 0;
  border-radius: 0;
  box-shadow: var(--shadow), var(--rim);
  margin-bottom: 0;
  overflow: hidden;
}

/* The stack ends the way the cap began: rounded, closed, one object. */
[data-caseview] .auditorium:last-child {
  border-bottom: 1px solid var(--border-strong);
  border-radius: 0 0 var(--radius-lg) var(--radius-lg);
  margin-bottom: 0;
}

/* No phone block of its own, as before: one set of rules serves every width, so the two
   copies that once disagreed about the chevron cannot come back. */
/*
 * One exception to the no-phone-block rule, and it is DENSITY, not geometry: the same glass
 * row, tighter. At 390px the head's pieces stack, and desktop's padding turned a two-line
 * row into a slab — the operator preferred the narrow footprint the hanging head had. The
 * chevron moves with the padding, in the same declaration block, so the two cannot disagree
 * the way the old phone copy famously did.
 */
@media (max-width: 700px) {
  [data-caseview] .theatre > .theatre__head {
    padding: 10px 38px 10px 14px;
    row-gap: 2px;
  }

  [data-caseview] .theatre__name {
    font-size: 1.15rem;
  }

  [data-caseview] .theatre__head--toggle::before {
    top: 14px;
    right: 12px;
  }
}

.complaint--report {
  background-color: color-mix(in oklab, var(--surface-2) 55%, transparent);
  border: 0;
  /* Premium pass: the rail as a FALL of the severity's light: full colour where
     the card hangs, letting go toward its foot. Painted over a transparent border so the
     geometry (and the uneven radius that makes the card hang) is exactly as before. */
  border-left: 4px solid transparent;
  background-image: linear-gradient(
    to bottom,
    var(--card-edge, var(--border-strong)) 0%,
    color-mix(in srgb, var(--card-edge, var(--border-strong)) 50%, transparent) 60%,
    transparent 94%
  );
  background-origin: border-box;
  background-clip: border-box;
  background-size: 4px 100%;
  background-repeat: no-repeat;
  border-radius: 6px 12px 12px 6px;
  padding: 12px 16px 10px;
}

.complaint--report[data-severity='low'] {
  --card-edge: var(--border-strong);
}

.complaint--report[data-severity='medium'] {
  --card-edge: var(--p-peach);
}

.complaint--report[data-severity='high'] {
  --card-edge: var(--danger);
}

/* A screen down takes the same edge as high impact — there is no colour past red — and
   keeps it whatever became of the report, because the badge still says "Screen down". */
.complaint--report[data-severity='emergency'] {
  --card-edge: var(--danger);
}

/*
 * The tint, though, is only for a screen that is down NOW. `--emergency` is the server's
 * answer to that question — severity is emergency AND the report is neither resolved nor
 * rejected — so a room that came back keeps its red edge and its badge but stops being
 * coloured like an alarm. Keying the tint off the severity attribute instead would leave
 * a fixed screen looking like a broken one for as long as the record exists.
 */
.complaint--report.complaint--emergency {
  /* Mixed toward transparent rather than toward a surface: the rail card's own body is
     translucent, and an opaque alarm block among translucent neighbours would read as a
     different KIND of card rather than a card in a different state. The rail is already
     full danger; this is the wash behind it. */
  background: color-mix(in oklab, var(--danger) 11%, transparent);
}

/* The review note keeps its panel — it is a quoted block, and quoting needs a surface.
   The progress run gave its box up for a rail of its own; see .progress below. */
.complaint--report .complaint__note {
  background: var(--surface-3);
}

/* ------------------------------------------------------------- deep links
   Arriving from a link that named one report — Needs attention on the home page
   does — the queue scrolls to it and marks it. Inset, so the ring reads as being
   around this row rather than as a border between two of them.

   It fades after a few seconds (removed by review.js): the mark says where to
   look on arrival, and a highlight that never leaves becomes furniture. */
.complaint--spotlight {
  background: var(--accent-soft);
  box-shadow: inset 0 0 0 2px var(--accent);
  border-radius: var(--radius);
}

/*
 * Why the queue is narrowed, when the reader did not narrow it themselves. The
 * collapsed filter bar already names the active filters, but somebody who got here
 * by clicking "Review now" did not choose them — and a short queue with no
 * explanation reads as an empty one.
 */
/*
 * "3 new reports — show them."
 *
 * Deliberately quieter than an alert and louder than a hint: something has changed and
 * it is worth knowing, but nothing is wrong and nobody has to act now. It sits above the
 * list rather than floating over it, so it pushes the content down once and then stays
 * still — a toast that appears over the first card would cover the row somebody is
 * reading, which is the thing this whole approach exists to avoid.
 */
.fresh {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px 14px;
  margin-bottom: 16px;
  padding: 10px 16px;
  border: 1px solid color-mix(in oklab, var(--ok) 40%, transparent);
  border-left: 3px solid var(--ok);
  border-radius: var(--radius);
  background: color-mix(in oklab, var(--ok) 10%, var(--surface));
}

.fresh[hidden] {
  display: none;
}

.fresh__text {
  flex: 1 1 220px;
  font-size: 0.88rem;
  font-weight: 550;
}

.deeplink {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
  margin-bottom: 16px;
  padding: 12px 18px;
  border: 1px solid color-mix(in oklab, var(--accent) 35%, transparent);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius);
  background: var(--accent-soft);
}

.deeplink__text {
  flex: 1 1 300px;
  font-size: 0.88rem;
}

/*
 * THE ISSUE TYPES AN ADMIN LOOKS AFTER, as a chip at the end of the case monitor's line under its
 * title, whenever they have chosen some (see paintTypesScope in js/review.js).
 *
 * It was a banner above the queue in the deep link's shape, with the whole list in it on every
 * visit. The operator: "a set it and forget it feature", and a banner you are always looking at
 * gets tiresome. What has to stay on screen is only that the page is narrowed, and two words
 * beside the counts they narrow say that. The list, Show all and the way to Settings are one
 * press away, in a panel that opens over the page rather than pushing it down.
 *
 * The panel hangs from the LINE, not from the chip. The line starts at the page's own edge, so a
 * panel no wider than the screen less its two gutters cannot run off the side of a phone,
 * wherever the chip has wrapped to.
 */
.page-head__sub:has(> .typescope) {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 12px;
}

.typescope__chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 9px 3px 11px;
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--text-dim);
  font-size: 0.8rem;
  font-weight: 600;
  line-height: 1.45;
  cursor: pointer;
  user-select: none;
  list-style: none;
  transition:
    background-color 140ms var(--ease),
    color 140ms var(--ease),
    box-shadow 140ms var(--ease);
}

.typescope__chip::-webkit-details-marker {
  display: none;
}

.typescope__chip:hover {
  background: var(--surface-3);
  color: var(--text);
}

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

/*
 * Open, or showing every type. The second lasts until the page is opened again, so it is the one
 * state the chip does not leave quiet: a wider list than usual should not pass for the usual one.
 */
.typescope[open] > .typescope__chip,
.typescope--all > .typescope__chip {
  color: var(--accent);
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--accent) 55%, transparent);
}

.typescope__chevron {
  flex: none;
  transition: transform 160ms var(--ease);
}

.typescope[open] .typescope__chevron {
  transform: rotate(180deg);
}

/* Over the page, from the line, as the filter panel opens from its row — and arriving the same
   way. Left-aligned whatever the head does, since it is a paragraph and a pair of buttons. */
.typescope__panel {
  position: absolute;
  top: calc(100% + 8px);
  left: 0;
  z-index: 30;
  display: grid;
  gap: 12px;
  width: min(26rem, calc(100vw - 32px));
  padding: 14px 16px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--shadow-lift);
  color: var(--text);
  font-size: 0.88rem;
  line-height: 1.5;
  text-align: left;
  transform-origin: top left;
  animation: menu-down 150ms var(--ease);
}

.typescope__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.complaint__top {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-bottom: 7px;
}

/* Where and what, as the card's title, with which report and when across from it. */
.complaint__head {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 4px 12px;
  margin-bottom: 6px;
}

.complaint__title {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0 8px;
  min-width: 0;
}

.complaint__place {
  font-weight: 700;
}

/* A place with one report has no heading of its own — its card names it (see renderAuditorium) —
   so its well starts at the top rather than under a header that is not there. */
.auditorium--single > .auditorium__body {
  padding-top: 0;
}

/* The issue type's tile on a report card (see tree.js), which only Studio draws — see "the case
   monitor" there. Hidden everywhere else rather than left as a bare drawing in the card's flow,
   and on paper. */
.complaint__type {
  display: none;
}

/*
 * The meta line: what used to be a row of pills, as one run of quiet words.
 *
 * Part of the queue's calm-down. Five pills per card — each its own colour, fill and
 * size — made every report a small argument for attention. The words carry the same
 * facts in one size and one ink, dotted apart, and exactly one of them keeps a colour:
 * the severity, wearing the hue of the card's own rail, so a card has one accent and a
 * reader scanning for how bad still gets the answer at arm's length twice over — the
 * rail for the glance, the word for the confirmation.
 */
.complaint__meta {
  display: flex;
  align-items: baseline;
  gap: 8px;
  flex-wrap: wrap;
  min-width: 0;
  font-size: 0.78rem;
  color: var(--text-dim);
  letter-spacing: 0.015em;
}

.meta__word {
  white-space: nowrap;
}

.meta__word--severity {
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  /* The card's own rail colour, inherited from the [data-severity] rules above. The
     fallback is for the one place a meta line renders outside a report card. */
  color: var(--card-edge, var(--text-dim));
}

/*
 * EXCEPT ON A LOW-IMPACT CARD, WHERE THAT COLOUR IS NOT A TEXT COLOUR.
 *
 * Borrowing `--card-edge` for the word is right for the three severities whose edge is a
 * real hue: peach and red read perfectly well as type. Low is the one whose edge is
 * deliberately NOT a hue — it is `--border-strong`, a line colour, chosen so a low-impact
 * card has a quiet stripe rather than a coloured one.
 *
 * Used as ink that lands at about 1.5:1 against the card: #3a3a44 on #1c1c21 in the dark
 * theme, and #bcc0cc on #e6e9ef in the light one. Both are well under the 4.5:1 a small
 * bold label needs, and the operator's report was the plain reading of it — "the low impact
 * lettering becomes almost invisible on the cases themselves".
 *
 * So the stripe keeps the line colour and the word takes the ordinary quiet ink, which is
 * what every other muted word in the meta line already uses. The two stop matching, which
 * is the point: they were only ever meant to match where the edge had a colour to lend.
 */
[data-severity='low'] .meta__word--severity {
  color: var(--text-dim);
}

/* Carries the folded-count tooltip, so the cursor says there is one. */
.meta__word--folded {
  cursor: help;
}

/*
 * Fixed on the floor, waiting for the cinema to sign it off. Coloured, unlike its neighbours
 * in the meta line, because it is the one word there that means somebody is waiting on the
 * READER — the rest describe the case. The tooltip names who, so the cursor says there is one.
 */
.meta__word--fixed {
  color: var(--p-green, var(--success, var(--accent)));
  font-weight: 650;
  cursor: help;
}

/*
 * What AI triage made of a report it left for a person (js/tree.js). Quiet, like the status it
 * qualifies: it explains why the report is waiting rather than asking anything of the reader. The
 * app's spark says it is a model's reading; the tooltip names the model.
 */
.meta__word--jev {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  color: var(--text-dim);
  cursor: help;
}

/*
 * Waiting on somebody: a question asked on the case that nobody has answered (src/mentions.js).
 * Coloured for the reason sign-off is — it is the case waiting on a PERSON — and in the info ink
 * rather than a warning's, because nothing is wrong with the case. The tooltip says who asked.
 */
.meta__word--waiting {
  color: var(--info);
  font-weight: 650;
  cursor: help;
}

/* The same fact in the closing dialog (js/progress.js), in the same ink as the card's mark, with
   room under it before the question it sits above. */
.field__hint--emailing {
  margin-bottom: 14px;
  color: var(--accent);
  font-weight: 600;
}

/*
 * The guest will be emailed when this is resolved (js/tree.js, src/guest-mail.js). In the accent,
 * not a status colour: nothing is wrong and nobody is waiting — it tells whoever closes the case
 * that a guest will read the result. The tooltip says what the email holds, so the cursor says
 * there is one.
 */
.meta__word--emailing {
  color: var(--accent);
  font-weight: 650;
  cursor: help;
}

.meta__sep {
  opacity: 0.45;
}

/* The agent's mark: the spark alone, riding with the reference. Provenance, not status —
   the title carries the sentence for whoever hovers to ask. */
.complaint__spark {
  display: inline-flex;
  color: var(--accent);
  margin-right: 2px;
  vertical-align: -2px;
  cursor: help;
}

/*
 * One piece, always. A reference is read out and typed back — into search, over the phone to a
 * contractor — and its hyphens are break points, so on a phone beside a time given as a full date
 * (anything older than a day) it split as "CQ-HNGE-" over "0417". The time beside it wraps instead.
 */
.complaint__ref {
  font-family: var(--mono);
  font-size: 0.78rem;
  color: var(--text-faint);
  letter-spacing: 0.02em;
  white-space: nowrap;
}

.complaint__when {
  margin-left: auto;
  font-size: 0.78rem;
  color: var(--text-faint);
}

/* Reference and time as one group at the end of the badge row: two small grey things
   that answer "which report, and when" belong next to each other, and together they
   wrap onto one line instead of two. The `auto` margin moves from the time to the
   group, so the pair sits right while the badges stay left. */
.complaint__id {
  display: flex;
  align-items: baseline;
  gap: 10px;
  margin-left: auto;
}

.complaint__id .complaint__when {
  margin-left: 0;
}

.complaint__body {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  margin: 0 0 12px;
  color: var(--text);
}

.complaint__facts {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 18px;
  font-size: 0.84rem;
  color: var(--text-dim);
  margin-bottom: 12px;
}

/*
 * Details, on a phone: the seat, the unit and its serial, the film and the showtime behind one
 * quiet line at the end of the row that says who started it (factsLine in js/tree.js). A desk has
 * the width to show them and never sees the line.
 *
 * The app's own quiet-disclosure shape — a muted line, a turning arrow — as "▸ 2 earlier notes" a
 * little further down the same card has it, and not a pill: it shows more of what the card already
 * holds, the way that one does, and two of them in two shapes on one card would be two ideas.
 * At the row's right edge, so it is in the same place on every card whatever the name beside it,
 * and as tall as the face it sits level with, which is a thumb's target for a word this small.
 */
.complaint__details-btn {
  display: none;
}

@media (max-width: 560px) {
  .complaint__details-btn {
    display: inline-flex;
    align-items: center;
    flex: none;
    min-height: 32px;
    margin-left: auto;
    padding: 0 0 0 8px;
    border: 0;
    background: none;
    font: inherit;
    font-size: 0.8rem;
    color: var(--text-dim);
    cursor: pointer;
  }

  .complaint__details-btn::before {
    content: '▸\00a0';
  }

  .complaint__details-btn[aria-expanded='true']::before {
    content: '▾\00a0';
  }

  /* No lead to sit beside — a case whose source this reader is not shown: first, at the left. */
  .complaint__details-btn:first-child {
    margin-left: 0;
    padding-left: 0;
  }

  /* One to a line, opened: a phone's line holds one of them, and a fact half on the Details
     row and half under it read as two. An inspection's date, which stays out, likewise. */
  .complaint__facts > .complaint__detail,
  .complaint__facts > .complaint__visit {
    flex-basis: 100%;
  }

  .complaint__facts:not(.complaint__facts--open) > .complaint__detail {
    display: none;
  }
}

/* An inspection's action past the day it was due by, still open: the one line on the card that
   somebody is being held to, so it says so in the colour that means late (visitFact in js/tree.js). */
.complaint__visit--late {
  color: var(--danger);
  font-weight: 600;
}

/* A colleague who raised the report, their face at the start of the line as their notes have it
   (see the Staff fact in js/tree.js), so a staff case reads as a person's before a word of it is.
   A guest's report has the same line with the code they scanned in the face's place (qrLine). */
.complaint__raiser,
.complaint__source {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

/* The code a guest scanned, on a tile the size of the face a colleague's report wears in the same
   place. Square and tinted like the issue type's tile in the card's corner, not round and coloured
   like a face: it is a code and not a person, and in the face's colours it read as one more face. */
.complaint__qr {
  display: inline-grid;
  place-items: center;
  flex: none;
  width: 32px;
  height: 32px;
  border-radius: 9px;
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  color: var(--accent);
  --glyph: 18px;
}

/*
 * Who raised it, on the part of the card that is always showing (raisedMark in js/tree.js): the
 * face or the code that leads the line under the words, at 26px — a face's size beside who asked,
 * a little taller than the pills it leads — so a shut card and a row in the list say whose it is.
 * The operator: "as cases start to build up you can see at a glance who made the case/if it was a
 * QR report".
 */
.complaint__by {
  display: inline-flex;
  flex: none;
  align-items: center;
}

.complaint__by > .complaint__qr {
  width: 26px;
  height: 26px;
  border-radius: 8px;
  --glyph: 15px;
}

/* An open card says it in full under the words, with the name, so it is not said twice. A row keeps
   it: there it is a column, and a column that came and went would move all the others. */
.complaint--fold[open] > .complaint__face:not(.complaint__row) .complaint__by {
  display: none;
}

/*
 * The run of notes on a released report.
 *
 * One panel with a line per note, rather than a stack of note boxes: this is a
 * conversation about one job — "awaiting parts", then "engineer booked" — and boxing
 * each remark separately would make three sentences look like three events.
 */
/*
 * The progress run: a rail of its own instead of a boxed panel.
 *
 * The box was a card inside a card — a second border, a second background, a second
 * radius, all inside a card that just gave up its own. The notes hang off a thin rail
 * now, tinted faintly with the card's edge colour so the story visibly belongs to the
 * report it advances, and the block's weight is its words.
 */
.progress {
  background: transparent;
  border: 0;
  border-left: 2px solid color-mix(in oklab, var(--card-edge, var(--border)) 45%, transparent);
  border-radius: 0;
  padding: 2px 0 2px 14px;
  margin: 0 0 10px 2px;
  font-size: 0.88rem;
}

/* The tag on a status-change line — "Started", "Resolved" — as a quiet word, not a chip:
   it labels the line, it does not compete with it. */
.progress__tag {
  font-size: 0.72rem;
  font-weight: 650;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-dim);
  flex: none;
}

/*
 * The earlier notes, folded. What a card answers in passing is "where is this NOW", and
 * that is always the latest line — so it stands alone and the rest of the story waits
 * behind a disclosure that says how much of it there is. The summary is the app's own
 * quiet-disclosure shape (see .complaint__original): a muted line, a turning arrow.
 */
.progress__earlier {
  margin: 2px 0 6px;
}

.progress__earlier > summary {
  cursor: pointer;
  list-style: none;
  font-size: 0.78rem;
  color: var(--text-dim);
}

.progress__earlier > summary::-webkit-details-marker {
  display: none;
}

.progress__earlier > summary::before {
  content: '▸ ';
}

.progress__earlier[open] > summary::before {
  content: '▾ ';
}

.progress__label {
  display: inline-block;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-faint);
  margin-bottom: 6px;
}

/* Said plainly rather than hidden: a card showing four of eleven notes without
   mentioning the other seven is the wrong kind of quiet. */
.progress__more {
  margin-left: 8px;
  font-size: 0.72rem;
  color: var(--text-faint);
  font-style: italic;
}

/*
 * A face, then the line: the tag, the words, who and when. The face sits at the top so a note
 * that wraps to three lines still has it beside its first, the way a message does.
 */
.progress__row {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: start;
  gap: 8px;
  padding: 4px 0;
  border-top: 1px solid var(--border);
}

.progress__line {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 4px 8px;
  min-width: 0;
}

.progress__row:first-of-type {
  border-top: 0;
}

.progress__text {
  flex: 1 1 240px;
  min-width: 0;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* Who and when, at the end of the line and out of the way of the words. */
.progress__by {
  flex: 0 0 auto;
  font-size: 0.76rem;
  color: var(--text-faint);
}

.complaint__note {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-left: 3px solid var(--p-surface2);
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  padding: 10px 14px;
  font-size: 0.88rem;
  margin: 0 0 12px;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.complaint__note strong {
  display: block;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-faint);
  font-weight: 650;
  margin-bottom: 2px;
}

.complaint__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  align-items: center;
  /* A gap of its own, so the buttons are not tight against whatever the card ended
     with — which is a paragraph on most cards and a row of photographs on some. */
  margin-top: 12px;
}

/*
 * The row's geography: doing on the left, exits on the right.
 *
 * Same fault and Reject take a report somewhere ELSE — into another report's job, or out
 * of the queue — and they used to stand shoulder to shoulder with Add note and Mark
 * resolved, four buttons in one strip with the destructive one a slot from the primary
 * one. The first `.act--away` takes the free space as its margin and carries the rest of
 * the exits with it; on a narrow screen the row wraps into a doing line and an exits
 * line, which is the same statement made vertically.
 */
.complaint__actions > .act--away {
  margin-left: auto;
}

.complaint__actions > .act--away ~ .act--away {
  margin-left: 0;
}

/* Same fault: the fold glyph and its words, small. Its rarity is why it is quiet — a button
   that shouted would have missed the point — but it was the glyph alone for a while, with what
   it does said only in a tooltip, and a phone has no hover. The words are the button's now; the
   accessible name still carries the count. */
.complaint__fold-btn {
  padding: 6px 9px;
  gap: 5px;
}

.complaint__fold-btn svg {
  display: block;
  flex: none;
}

.complaint__fold-word {
  font-size: 0.75rem;
  font-weight: 600;
  white-space: nowrap;
}

/* PINNED to the card's top-right corner, just left of the collapse chevron — the same
   spot on every card, whatever the words on it do. It stood at the end of the meta words
   first, and the operator's verdict was exact: a control that shifts with each card's
   words cannot be aimed for without reading, so it holds the corner where the card's
   OTHER control already lives, centered on the chevron's line. The "+N same fault" word
   in the meta line goes on carrying the count for readers; in the DOM the button still
   rides that line, which keeps it beside the words in reading order. Absolute also puts
   it beyond the reach of the phone width's generic `.btn { flex: 1 1 auto }` stretch,
   which once grew it into the line's leftover width. */
.complaint__meta > .complaint__fold-btn {
  padding: 2px 7px;
  position: absolute;
  top: 1px;
  right: 24px;
  /* The .btn family's tap-target minimum would make this a 34px slab beside 17px words —
     visibly hanging below the line it belongs to. Hugging the glyph, its centre sits on
     the words' own centre, level with the chevron beside it. */
  min-height: 0;
}

/* The corner is the glyph's now, so the top row's own content stops short of it — the
   face's 22px already clears the chevron; this clears the button beside it. */
.complaint__top {
  padding-right: 36px;
}

/*
 * The released row on a phone: Add note and Ask side by side on a line of their own, then
 * the two verdicts — Mark resolved and Reject — sharing the line under it, equal halves too.
 * The doing-left / exits-right geography needs width to say anything; at phone width the
 * wrap landed arbitrarily, and the operator chose this arrangement instead — first with Add
 * note and Ask stacked a full line each, then, from their phone, side by side.
 *
 * Half the line each, less half the 8px gap, so the two FILL it: nothing is left beside them
 * for Mark resolved to squeeze into, whatever the words on the buttons measure.
 */
@media (max-width: 560px) {
  .complaint__actions > .act--note {
    flex: 1 1 calc(50% - 4px);
  }

  .complaint__actions > .act--pair {
    flex: 1 1 0;
    margin-left: 0;
  }

  /*
   * The impact, on a line of its own ABOVE the decision. It is chosen first — Validate & send to
   * GM sends the report at whatever level it reads — and as the row's last item it wrapped to a
   * line under the buttons, so the reader met the button before the choice it acts on. As wide as
   * the buttons and as tall, so the two lines read as one block: how bad, then what to do. Translate
   * stays above it (same order, earlier in the row): it reads the words, and they are above both.
   */
  .complaint__actions > .severity-select {
    order: -1;
    flex: 1 1 100%;
    min-height: 40px;
  }
}

/* The record line — who sent this where — full-width under the buttons rather than
   jammed beside them at button height. `order` keeps it last however the row wraps. */
.complaint__actions > .complaint__sent {
  flex-basis: 100%;
  order: 10;
  margin: 2px 0 0;
}

/*
 * Translate, as a reading aid rather than a peer of the case buttons.
 *
 * It acts on the WORDS — the one control here that does — so it takes a quiet full-width
 * line above the row instead of standing in it: on a phone it was the fifth button that
 * turned the geography back into a wall. Accent ink and the spark say what it is (the
 * control that spends on a model), the stripped box says what it is not (a lifecycle
 * action). `order: -1` puts it first however the row wraps, and it is rare besides — the
 * short-English fix keeps it off plain English, so what remains is the held tab and the
 * genuinely ambiguous.
 */
.complaint__actions > .complaint__translate {
  flex-basis: 100%;
  order: -1;
  justify-content: flex-start;
  gap: 6px;
  margin: 0;
  padding: 2px 0;
  background: transparent;
  border: 0;
  box-shadow: none;
  color: var(--accent);
  font-weight: 600;
}

.complaint__actions > .complaint__translate:hover:not(:disabled) {
  background: transparent;
  text-decoration: underline;
}

/* ------------------------------------------------------- the card that folds
 * A report card shut is its badges, its reference, its time and two lines of what
 * happened. Everything else — the guest's details, the notes, the photographs, the
 * buttons — is behind the fold.
 *
 * This is the other half of the page-height fix (see PAGE_SIZE in src/complaints.js).
 * Five hundred open cards was 191,982px on a phone: 246 screens. A third of that was
 * progress notes and action buttons, neither of which anybody reads while scrolling.
 *
 * Cards only ARRIVE shut past CARDS_OPEN_UPTO in public/js/tree.js. A short list is
 * somebody working through it, and shut cards would be a click each to see what was
 * already visible.
 */
.complaint--fold > .complaint__face {
  position: relative;
  cursor: pointer;
  /* Room for the chevron, so a long badge row cannot run underneath it. */
  padding-right: 22px;
  /* NOT user-select: none, unlike the cinema header above. The face carries the
     reference and the description, which are the two things people copy out of this
     app — so the text stays selectable, and disclose.js ignores a click that ends a
     selection rather than folding the card away mid-copy. */
  list-style: none;
}

.complaint--fold > .complaint__face::-webkit-details-marker {
  display: none;
}

.complaint--fold > .complaint__face::after {
  content: '';
  position: absolute;
  right: 2px;
  top: 9px;
  width: 6px;
  height: 6px;
  border-right: 2px solid var(--text-faint);
  border-bottom: 2px solid var(--text-faint);
  transform: rotate(45deg);
  transition: transform 160ms var(--ease);
}

.complaint--fold[open] > .complaint__face::after {
  transform: rotate(-135deg);
}

/* Two lines of the description while shut, and no gap under it — the card ends there. */
.complaint--fold:not([open]) > .complaint__face > .complaint__body {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  margin-bottom: 0;
}

/* Shut, the badge row's own gap is the only space inside the card. */
.complaint--fold:not([open]) > .complaint__face > .complaint__top {
  margin-bottom: 6px;
}

/* The last thing in the fold should not add a gap below itself — the card's padding is
   already there, and doubling it made every card taller than it reads. */
.complaint__rest > :last-child {
  margin-bottom: 0;
}

.complaint--busy {
  opacity: 0.45;
  pointer-events: none;
}

/* ============================================== the list view: reports as rows

   The other arrangement of the same report — see renderComplaint in js/tree.js, and
   js/view-mode.js for why the app offers two.

   ## Everything here is behind one attribute

   `data-caseview` is written on the results container by renderTree, and every rule below is
   scoped under `='list'`. Cards are what nearly everybody sees and they should not start
   paying for a second arrangement existing: in that mode not one selector here matches, and
   the card rules above are reached exactly as they were before this section was written.

   ## Why the columns are fixed widths

   Because each row is its own grid. Two sibling grids do not share track sizes, so
   `max-content` would size every row to its own contents and the colours would stagger down
   the page — which is precisely the thing rows exist to avoid. Stated lengths are what makes
   a severity at the top of the list sit above the severity below it, and that alignment is
   the entire feature. `subgrid` would express it more honestly and is not used: the summary
   is two levels below the well, so it would need the <details> to be a subgrid item too, and
   that is a lot of fragility to buy a tidier declaration.

   The last track is `minmax(0, 1fr)`, not `1fr` — a bare `1fr` has a min of auto, so a long
   description would refuse to shrink and push the row wider than its container. */

/* The well stops being a tray of cards and becomes a table body: flush, no gaps, hairlines
   between rows.

   This used to carry `container-type: inline-size`, so the rows answered to the width of the WELL
   rather than of the window — because the same tree is drawn inside the case history dialog. It is
   gone, for two reasons. It was not needed: the dialog is `min(1040px, 100vw - 32px)`, so it is
   narrow exactly when the window is, and a media query gets the same answer at every width (
   measured — a 1440px window gives that dialog a 956px well, and a phone gives it a narrow one).
   And `container-type` is not free: it applies `contain: layout style inline-size`, which makes
   this a containment context and changes how the rows inside it are composited while scrolling.
   That is the leading suspect for the severity edge blinking in and out on a phone, which is a
   list-only symptom and this was a list-only rule. */
[data-caseview='list'] .auditorium__body {
  gap: 0;
  padding: 0;
  background: var(--surface-2);
}

/* The card's own frame goes; the hairline above each row replaces it.
   The severity edge stays, because reading down a column of coloured edges is the one card
   affordance that gets better in a list — but it is drawn differently here. See below. */
[data-caseview='list'] .complaint--report {
  padding: 0;
  border: 0;
  border-top: 1px solid var(--border);
  border-radius: 0;
  background-color: transparent;
  /*
   * The severity edge as a BACKGROUND rather than an inset shadow.
   *
   * `.complaint--report` draws it with `box-shadow: inset 3px 0 0`, which is right on a card: the
   * card has its own opaque background, so the shadow composites against something. A row does
   * not — it is transparent, sitting on the well — so the same shadow has to be composited
   * against whatever is behind it on every frame, and on a phone that showed as the coloured edge
   * blinking in and out while scrolling. A gradient is part of the element's own background and is
   * painted with it.
   *
   * Written with four stops rather than the two-position shorthand, which is newer syntax than
   * anything else this stylesheet relies on.
   */
  background-image: linear-gradient(
    to right,
    var(--card-edge, transparent) 0,
    var(--card-edge, transparent) 3px,
    transparent 3px
  );
  box-shadow: none;
}

[data-caseview='list'] .auditorium__body > .complaint--report:first-child {
  border-top: 0;
}

/* A SCREEN THAT IS DOWN keeps its tint, which the rule above had quietly taken away.
   `.complaint--report.complaint--emergency` and `[data-caseview='list'] .complaint--report` are
   both two-selector-units of specificity, so the tie went to whichever came later in the file —
   and that is this section. The alarm colour is the one thing on a report that must survive a
   change of arrangement: an auditorium nobody can watch a film in is exactly what somebody
   scanning forty rows is scanning FOR, and it was coming out the same colour as a cold room.

   Only the live ones, as in card view: `--emergency` is the server's answer to "is this screen
   down NOW", so a room that came back keeps its red edge and its badge and stops being coloured
   like an alarm.

   `background-color`, not the `background` shorthand: the shorthand would reset the edge gradient
   above, so a screen that is down would be the one row in the list with no severity edge — the
   opposite of what this rule is for. */
[data-caseview='list'] .complaint--report.complaint--emergency {
  background-color: color-mix(in oklab, var(--danger) 9%, var(--surface-2));
  border-top-color: color-mix(in oklab, var(--danger) 45%, var(--border));
}

/* A row is a control, so it has to answer the pointer. Cards say so with their own edges and
   their spacing; a run of flush rows needs the hover to show which one a click would open.

   ## Two things here were wrong, and this is what they looked like

   Reported from a phone: expand a case and the coloured edge "jumps down to the expanded part",
   then comes back the moment you touch the screen to scroll. Both halves are this rule.

   The edge is drawn on the BLOCK, and the row is a child of it, so an opaque background on the row
   paints straight over it — for exactly the row's own height, which is why the colour appeared to
   start below the fold and not above it. An earlier comment here claimed `background-color` rather
   than the `background` shorthand kept the edge visible underneath; that was simply wrong.
   Shorthand or longhand makes no difference when the thing covering it is a child. So the row
   redraws the same edge on top of its own hover colour: `background-color` paints first,
   `background-image` over it, both on one element. `--card-edge` is a custom property, so it
   inherits from the block and the two gradients cannot disagree.

   And it is gated on `hover: hover`, which is what makes the phone right rather than merely
   consistent. iOS leaves `:hover` asserted after a tap until you touch something else — so
   expanding a case left the row hovered, and scrolling was what cleared it. A device that cannot
   hover should never enter a hover state at all. */
@media (hover: hover) {
  [data-caseview='list'] .complaint--fold > .complaint__row:hover {
    background-color: var(--surface-3);
    background-image: linear-gradient(
      to right,
      var(--card-edge, transparent) 0,
      var(--card-edge, transparent) 3px,
      transparent 3px
    );
  }
}

/* What is behind the fold lines up with the row that opened it.
   A card pads its whole box, so its face and its innards share one left edge for free. A row
   pads itself instead — the block has to be flush for the hairlines to run the width of the
   list — which leaves the notes and the buttons starting fifteen pixels to the left of the
   badges above them, reading as a second column that is not one. */
[data-caseview='list'] .complaint__rest {
  padding: 0 24px 12px 15px;
}

.complaint__row {
  display: grid;
  /* Severity, issue type, stage, reference, when, the marks, and the words.
   *
   * FIXED, and every number below was measured rather than guessed — see the note at the end of
   * this comment for why it cannot be `fit-content` or `auto`.
   *
   * The widths these replace were each cut for the worst case a column could hold, estimated by
   * eye, and measuring them found the estimates wrong in both directions at once:
   *
   *   - Issue type was 132px for a longest label that measures 108 and a typical one that
   *     measures 56. Most of why a page of ordinary reports read as five badges strung across a
   *     thousand pixels rather than as a row.
   *   - Stage was 106px for a "Pending review" badge that measures 128, so the one tab where
   *     that badge distinguishes anything printed "PENDING R…" on every single row. Shortened
   *     to "Pending" in this view rather than paid for in width — see complaintParts in
   *     js/tree.js — which drops the column's worst case to "In progress" at 106.
   *   - The reference was 88px for twelve monospaced characters that need 104, so the thing a
   *     reader would copy to name a case was never shown whole.
   *
   * So: the slack comes out, both truncations go, and the gap drops from 14 to 10. The badges
   * sit as a row now instead of as five things that happen to share a line.
   *
   * Each track is its column's measured worst case plus a few pixels — not an estimate of one.
   * The estimates these replace were wrong in both directions at once, which is what an
   * estimate of a text width usually is.
   *
   * WHY NOT fit-content. It is the obvious answer — size each track to the content actually in
   * it — and it is wrong here for a structural reason: every `.complaint__row` is its OWN grid
   * container, one per report, so a content-sized track is sized per row rather than per column.
   * Tried and screenshotted: the badges pack tightly and the columns stop lining up down the
   * page, each row starting its second badge wherever its first one happened to end. Reading
   * DOWN a column is the entire reason this view exists; a packed ragged list is the card view
   * with worse manners. Fixed tracks are what make the columns columns. */
  /*      severity  type    stage   ref     when     marks           the words
     worst   95       90      88     104      95   — re-measured when the cells stopped
     holding badges and started holding words: the ~18px of pill padding and border came
     out of the first three tracks, and the columns closed up by exactly the chrome the
     quiet-card change removed. The reference and time were never badges and keep their
     measure. Who raised it leads them all, at the face's own 26px (raisedMark in js/tree.js). */
  grid-template-columns: 26px 98px 94px 92px 104px 98px minmax(0, auto) minmax(0, 1fr);
  align-items: center;
  /* 10px, matching the flowed layout under 660px below. */
  gap: 0 10px;
  /* Right padding clears the chevron, which is positioned against this box. */
  padding: 7px 24px 7px 15px;
}

/* Each badge starts at its cell's left edge instead of wherever its own text ends. This is
   the line that makes the columns line up. */
[data-caseview='list'] .complaint__cell {
  display: block;
  min-width: 0;
  overflow: hidden;
}

/* A renamed issue type can be longer than its column. Clipped at the cell edge with an
   ellipsis rather than allowed to overlap the next one — the full text is in the card view
   and in the report itself, and a column that shoves its neighbour sideways has stopped
   being a column.

   `inline-block`, and that word is the whole rule. A badge is `inline-flex`, and the cell is
   a block: `display: block` here stretched every badge to the full width of its track, so the
   list came out as a grid of empty outlined boxes with a word at the left of each. Shrink-wrap
   is what makes a badge read as a badge. It also has to be a block container of SOME kind for
   `text-overflow` to mean anything — an ellipsis does not apply to the anonymous text item
   inside a flex box — so `inline-block` is the one value that satisfies both. */
[data-caseview='list'] .complaint__cell > .badge {
  display: inline-block;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  vertical-align: middle;
}

/* Who raised it, centred in its track: a face has no line of text to sit on. */
[data-caseview='list'] .complaint__cell--by {
  display: flex;
  align-items: center;
  align-self: center;
}

/* The marks cell can hold two badges, so it lays them out rather than leaving them to sit
   against each other. Capped, because its track is `auto` and a triage reason runs to a
   sentence — uncapped, "Held: contains a link and three phone numbers" would take its width
   out of the description, which is the column actually being read. */
[data-caseview='list'] .complaint__cell--marks {
  display: flex;
  align-items: center;
  gap: 6px;
  max-width: 260px;
}

/* Undoing the inline-block above for the agent's mark: the spark is an icon, and an
   inline-block box would leave it sitting on the text baseline instead of centred
   against it. It is small and never needs the ellipsis the rule exists for. */
[data-caseview='list'] .complaint__cell--marks > .complaint__spark {
  display: inline-flex;
  overflow: visible;
}

/* The reference and the time are their own columns here rather than a pair at the end of the
   badge row, so `.complaint__id`'s auto margin is not involved and neither needs the group. */
[data-caseview='list'] .complaint__row > .complaint__when {
  margin-left: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

[data-caseview='list'] .complaint__row > .complaint__ref {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* One line while shut, ellipsised — the row's height is what keeps forty of them scannable,
   so the description gets exactly the space left over. Beats the card's two-line clamp above
   on specificity and on source order, and unsets it explicitly rather than relying on that:
   `-webkit-box` plus a line clamp is a different layout mode, and leaving it on would give a
   box that ignores `text-overflow`. */
[data-caseview='list'] .complaint--fold:not([open]) > .complaint__row > .complaint__body,
[data-caseview='list'] .complaint__row > .complaint__body {
  /* THE LAST TRACK, named from the end so it stays right if the columns are ever re-cut.
     Explicit rather than left to auto-placement, because the marks cell before it is only
     sometimes there: without this the description would sit in the marks track on an ordinary
     report and in the next row on a held one, so the list's own column would move depending on
     what the triage happened to think of the report. */
  grid-column: -2 / -1;
  display: block;
  margin: 0;
  -webkit-line-clamp: none;
  line-clamp: none;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  font-size: 0.88rem;
}

/* Open, the description is the thing being read, so it wraps in full. The row grows and the
   cells move to the top of it — badges centred against a five-line paragraph would leave the
   severity floating in the middle of nothing. */
[data-caseview='list'] .complaint--fold[open] > .complaint__row {
  align-items: start;
}

[data-caseview='list'] .complaint--fold[open] > .complaint__row > .complaint__body {
  white-space: pre-wrap;
  overflow: visible;
  text-overflow: clip;
}

/* The chevron is positioned against the row box, whose height changes with the fold, so it is
   pinned rather than centred: `50%` of an open row holding a long description would put it
   halfway down a paragraph. 14px is the middle of a shut row, and stays beside the first line
   of an open one. */
[data-caseview='list'] .complaint--fold > .complaint__row::after {
  top: 14px;
}

/* A well too narrow for six columns goes back to flowing, which is a card in all but name —
   and that is the honest answer rather than six columns of four characters each.

   660px of WINDOW, which is about 620px of well: the shell and the panel take ~40px between them
   at every width that matters. Measured against the container query this replaces, so the
   behaviour is the same on both sides of it — a 640px window flows and a 720px window keeps its
   columns, exactly as before. The case history dialog needs no special case: it is
   `min(1040px, 100vw - 32px)`, so it is narrow precisely when the window is. */
@media (max-width: 660px) {
  .complaint__row {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 6px 10px;
    padding: 10px 24px 10px 15px;
  }

  /* The description takes its own line under the badges. */
  [data-caseview='list'] .complaint__row > .complaint__body {
    flex: 1 0 100%;
  }

  /* Packed left beside the reference rather than pushed to the right edge. `margin-left: auto`
     would be right if the badges left room on the line and wrong when they do not — and at
     this width they usually do not, so it bought a line of its own for four words. */
  [data-caseview='list'] .complaint__row > .complaint__when {
    margin-left: 0;
  }

  /* The Same fault glyph holds the top-right corner here too — the same spot the card
     view pins it to, for the same reason: a control that never moves can be aimed for
     without reading. At this width the row flows like a card in all but name and the
     action row wraps, which dropped the glyph BETWEEN Mark resolved and Reject — a
     one-glyph button adrift in the middle of the verdicts, reported from the operator's
     phone. The button still lives in the opened actions in the DOM (a shut row has no
     room on its face), so it appears only while the row is open — pinned against the
     report's own box, level with the chevron beside it. */
  [data-caseview='list'] .complaint--fold {
    position: relative;
  }

  [data-caseview='list'] .complaint__actions > .complaint__fold-btn {
    position: absolute;
    top: 6px;
    right: 14px;
    margin-left: 0;
    padding: 2px 7px;
    /* Same exception as the card pin above: the .btn tap-target minimum would hang a
       34px slab beside the row's first line instead of sitting level with it. */
    min-height: 0;
  }

  /* And the open row's first line stops short of the corner the glyph now owns — the
     24px that clears the chevron does not clear a button beside it. */
  [data-caseview='list'] .complaint--fold[open] > .complaint__row {
    padding-right: 56px;
  }

  /* Reject keeps the row's right edge. Above this width the GLYPH carries the row's
     `margin-left: auto` and Reject rides flush behind it (see the act--away pair rule);
     with the glyph pinned out of the flow its auto margin pushes nothing, and Reject —
     still matching `.act--away ~ .act--away` in the DOM — packed in on the left with the
     doing buttons. Handing the auto margin to Reject itself restores the exits-right
     geography. Harmless on the released row's phone halves: both verdicts are
     `flex: 1 1 0`, so the grow absorbs the line's free space before an auto margin can. */
  [data-caseview='list'] .complaint__actions > .complaint__fold-btn ~ .act--away {
    margin-left: auto;
  }
}

/* -------------------------------------------------------- the Cards / List switch

   A segmented pair rather than one button that toggles. A single control reading "List view"
   cannot say whether that is what you are looking at or what you would get by pressing it,
   and a reader who has to press it to find out has been handed a puzzle. */
.viewswitch {
  display: flex;
  flex: 0 0 auto;
  gap: 2px;
  padding: 2px;
  border: 1px solid var(--border);
  border-radius: 9px;
  background: var(--surface-2);
}

.viewswitch[hidden] {
  display: none;
}

.viewswitch__option {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  border: 0;
  border-radius: 7px;
  padding: 5px 10px;
  background: none;
  font: inherit;
  font-size: 0.8rem;
  font-weight: 550;
  color: var(--text-dim);
  cursor: pointer;
  white-space: nowrap;
  transition:
    background-color 140ms var(--ease),
    color 140ms var(--ease);
}

.viewswitch__option:hover {
  color: var(--text);
}

/* The pressed half, in the accent — the same language the range bar on the home page uses for
   the period that is showing, so a reader who has met one control has met both. Colour-mixed
   against the theme's own surface rather than given a fixed shadow: this app ships seven
   themes, three of them light, and a hardcoded shadow is legible in about half of them. */
.viewswitch__option--on {
  background: color-mix(in oklab, var(--accent) 14%, var(--surface));
  color: var(--accent);
  font-weight: 620;
}

.viewswitch__icon {
  flex: 0 0 auto;
}

/* --------------------------------------------------- archived / editing */

/* An archived cinema or screen is dimmed but still fully readable — its
   complaint history is intact and admins still need to work with it. */
.theatre--archived .theatre__head {
  background: repeating-linear-gradient(
    135deg,
    var(--surface-2),
    var(--surface-2) 10px,
    var(--surface) 10px,
    var(--surface) 20px
  );
}

.theatre--archived .theatre__name {
  color: var(--text-dim);
}

/* Inline edit rows: the form takes the place of the row it replaces. */
.theatre__head--editing,
.complaint--editing {
  background: var(--accent-soft);
  border-bottom: 1px solid var(--border);
  display: block;
}

.theatre__head--editing {
  display: flex;
  align-items: flex-end;
  gap: 12px;
  flex-wrap: wrap;
}

.theatre__head--editing .field {
  margin: 0;
  flex: 1 1 150px;
  /* Without a cap, whichever field lands alone on a wrapped row stretches to
     the full width and stops looking like a sibling of the others. */
  max-width: 280px;
  min-width: 0;
}

/* A street address needs more room than a ZIP or a region. */
.theatre__head--editing .field--wide {
  flex: 2 1 220px;
  max-width: 380px;
}

/* And the ordering link a row of its own: a store's address with its query is long, and a box that
   shows a third of it cannot be checked by eye. */
.theatre__head--editing .field--link {
  flex: 1 1 100%;
  max-width: 760px;
}

.theatre__head--editing .field > label,
.toolbar--bare .field > label {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-faint);
  font-weight: 600;
}

.theatre__head--editing input,
.toolbar--bare input {
  min-height: 38px;
  padding: 7px 11px;
  font-size: 0.95rem;
}

/* ------------------------------------------------------- page head + modal */

/* Heading on the left, the page's one create action on the right. */
/*
 * The page's own action, under the header rather than opposite it.
 *
 * There was a `--split` modifier that put the title left and the button right. With the
 * title centred that arrangement has nothing to balance against, and a button pinned to
 * one corner of a symmetrical header is the thing that would look unfinished. Centred
 * below, it reads as the page's one obvious next step.
 *
 * Below the rule, not above it. `order` rather than moving it in the markup, because the
 * heading has to come first in the document for a reader going through it in order —
 * what the rule separates is the page's name from the page's work, and a button is work.
 * Sitting above the line it read as a stray piece of the title block.
 *
 * `.page-head > .btn` as well as the wrapper: two pages put the button in the header
 * directly rather than in a group of one.
 */
.page-head__actions,
.page-head > .btn {
  order: 1;
  display: flex;
  justify-content: center;
  gap: 10px;
  flex-wrap: wrap;
  /* On top of the 12px grid gap. Nearer the rule than the content is to it, so the
     button belongs to the page rather than to whatever list starts underneath. */
  margin-top: 8px;
}

/* The "Add new…" dialog. Native <dialog>, so focus trapping, Escape and
   inertness of the page behind come from the browser. */
.modal {
  width: min(660px, calc(100vw - 32px));
  max-height: min(86vh, 760px);
  /* dvh where it exists: on a phone, vh is the tallest the viewport ever gets, so a
     dialog sized in vh hides its own bottom edge behind the browser's toolbar. */
  max-height: min(86dvh, 760px);
  padding: 0;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  background: var(--glass-strong);
  -webkit-backdrop-filter: blur(16px);
  backdrop-filter: blur(16px);
  color: var(--text);
  box-shadow: var(--shadow-lift);
  overflow: hidden;
}

/*
 * Case history: the same box, given the width its contents need.
 *
 * After `.modal` rather than before it, and that ordering is the whole rule — both are a
 * single class, so specificity ties and source order decides. Written above, it lost to the
 * width it was meant to override and the box opened at 660px.
 *
 * 660px is right for a form; a column of fields reads badly wider. This one holds the queue's
 * own report cards, whose head is a row of badges plus a reference and a time, and at 660px
 * that wrapped onto three ragged lines on every card.
 */
.modal--history {
  width: min(1040px, calc(100vw - 32px));
}

/* How many closures are on screen. Between the filters and the list, so a narrowed view says
   what it narrowed to rather than leaving the reader to count cards. */
.history__count {
  margin: 0 0 4px;
}

/*
 * A column, so the head, the scrolling middle and the footer each get a row of their own.
 * Before this the body carried `max-height: calc(86vh - 150px)` — a guess at the height of
 * everything else in the box — and a form taller than the guess put its sticky footer on
 * top of a field instead of below it.
 *
 * On `[open]`, and this is not a detail. The browser hides a closed dialog with
 * `dialog:not([open]) { display: none }` from its own stylesheet, which any `display` an
 * author sets on `.modal` outranks — so declaring `display: flex` unconditionally left
 * every dialog on the page permanently unfolded at the bottom of it.
 */
.modal[open] {
  display: flex;
  flex-direction: column;
}

.modal::backdrop {
  background: rgba(10, 10, 16, 0.66);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
}

/*
 * No ring around the box itself.
 *
 * A pop-up whose first field is a `<select>` holds focus on the box rather than the control —
 * see `arrive()` in raise.js, where a ringed select read as an already-open menu. The catch is
 * that a 2px outline around a 660px box is far louder than the thing it was avoiding, and
 * Chromium matches `:focus-visible` for this programmatic focus, so `:not(:focus-visible)`
 * does not get rid of it (measured — that was the first attempt).
 *
 * Unconditional is safe here rather than the usual anti-pattern, for a reason specific to this
 * element: the dialog carries `tabindex="-1"`, so it is not in the tab order and no keyboard
 * reader can ever arrive on it — the only thing that focuses it is the line above. Every
 * control inside keeps its ring, and the box being modal is what says where you are.
 */
dialog:focus {
  outline: none;
}

/*
 * ------------------------------------------------- every pop-up box, arriving and leaving
 *
 * Driven by public/js/dialogs.js, which wraps showModal/close to put these classes on and —
 * for the way out — to hold the native close until the animation has finished. A box that
 * has already been hidden cannot be watched leaving.
 *
 * `dialog` rather than `.modal`, so the photo viewer is included and so is any box added
 * later. There is exactly one mechanism for all of them.
 *
 * A short rise and a small scale rather than a bigger one: these boxes are up to 660px wide
 * and 86vh tall, and a box that size growing from 90% reads as a zoom rather than as
 * something arriving. 3% and 8px is enough to say which direction it came from.
 *
 * The backdrop fades on its own timing. Sharing the box's would have the page behind darken
 * in step with the box growing, which is two things happening at once; the dark arriving
 * first is what makes the box look like it is on top of something.
 */
/*
 * The starting state, applied for one frame before the animation begins — see ARM in
 * dialogs.js for why. This and the keyframe's `from` below must stay identical: a difference
 * between them is a visible jump on the frame the two swap over.
 */
dialog.is-arming {
  opacity: 0;
  transform: translateY(12px) scale(0.96);
}

dialog.is-arming::backdrop {
  opacity: 0;
}

/*
 * No blur until the box has landed, and this is the single biggest thing about how the
 * entrance reads.
 *
 * `backdrop-filter` on a full-viewport pseudo-element has to sample and blur the whole page,
 * and the first frame of it measured 44ms on this app — three frames, gone, out of an
 * animation that is fifteen. With the blur deferred the entry runs at a frame every 16ms
 * from the first one; with it on, the box was invisible and then most of the way there.
 *
 * The blur arrives when the class comes off, a frame after the movement has finished, where
 * paying for it costs nothing anybody can see: a stutter matters while something is moving
 * and not at all once it has stopped.
 *
 * The exit is on the list for a reason that is not obvious and was measured, not assumed. The
 * backdrop is long since composited by then, so the blur itself is free — but the exit
 * animates its *opacity*, and a blurred layer changing opacity has to be re-blurred every
 * frame. Left on, the close stalled for 72ms and on one run showed no intermediate frames at
 * all. Under a dark layer that is on its way out, nobody can tell it stopped blurring.
 *
 * And taking the blur OFF is itself a repaint of the whole page behind the box, which is why
 * the exit is armed for a frame (is-leaving, in dialogs.js) exactly as the entry is: the box
 * sits still at full opacity while that repaint is paid, and the fade starts on the frame
 * after. Measured frame by frame, the fade started 40 to 110ms late without it — often after
 * the timer had already shut the box.
 */
dialog.is-arming::backdrop,
dialog.is-leaving::backdrop,
dialog.is-opening::backdrop,
dialog.is-closing::backdrop {
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

/*
 * Nothing styles the box itself under is-leaving, and a test pins that nothing ever does: a
 * visible change there would be a jump on the frame the classes swap. (A will-change hint
 * was tried here and measured: it moved nothing, because the time after the class lands is
 * the display compositor drawing the frame, not the box being rasterised — so it is not
 * here, and the fade is allowed its own clock in dialogs.js instead.)
 */

@keyframes dialog-in {
  from {
    opacity: 0;
    transform: translateY(12px) scale(0.96);
  }
}

@keyframes dialog-out {
  to {
    opacity: 0;
    transform: translateY(4px) scale(0.985);
  }
}

@keyframes dialog-backdrop-in {
  from {
    opacity: 0;
  }
}

@keyframes dialog-backdrop-out {
  to {
    opacity: 0;
  }
}

/* 240ms and 140ms are also in dialogs.js, where the timer for the close lives. A test fails
   if the two ever disagree. Longer than the exit, and longer than it was: an entry that has to
   be noticed needs more room than one the reader has already decided about. */
dialog.is-opening {
  animation: dialog-in 240ms var(--ease-arrive) both;
}

dialog.is-closing {
  animation: dialog-out 140ms var(--ease-out) both;
}

/*
 * A BOX THAT OPENS STRAIGHT INTO TYPING DOES NOT MOVE ON A TOUCH SCREEN — it fades.
 *
 * Nearly every box here focuses its first field in the same tap that opens it, which is what brings
 * the keyboard up at once: iOS raises it only for a focus inside the tap. That tap is also the first
 * frame of the entrance above, with the box 12px low and at 96% around the press — and iOS draws its
 * caret where the field was when it took focus, and leaves it there until something is typed. On
 * the operator's phone the caret sat right of and below where the typing starts, in the search bar
 * and in Add a note alike, and jumped into place at the first key.
 *
 * So while a field that shows a caret is focused inside a box, the box is not transformed at all.
 * !important because it has to outrank the animation, and an animation outranks every rule that is
 * not important. The opacity half of the entrance still plays, so the box still fades in, and the
 * field is where it will stay from the first frame. A touch screen only, where the caret is the
 * operating system's to draw; a mouse keeps the rise, and a box that opens without a field focused
 * keeps it everywhere. The setup flow's steps slide in under the same focus, and are held the same.
 */
@media (any-pointer: coarse) {
  :is(dialog, .starter__step):has(
      :is(
          textarea,
          input:not([type]),
          input:is([type='text'], [type='search'], [type='email'], [type='url'], [type='tel'], [type='password'], [type='number']),
          [contenteditable]:not([contenteditable='false'])
        ):focus
    ) {
    transform: none !important;
  }
}

/* Slightly ahead of the box on the way in, and behind it on the way out, so the page is
   already dim when the box lands and still dim as it goes. */
dialog.is-opening::backdrop {
  animation: dialog-backdrop-in 190ms var(--ease-arrive) both;
}

dialog.is-closing::backdrop {
  animation: dialog-backdrop-out 190ms var(--ease-out) both;
}

.modal__head {
  flex: none;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 18px 20px;
  border-bottom: 1px solid var(--border);
  background: linear-gradient(180deg, var(--surface-3), var(--surface-2));
}

.modal__title {
  margin: 0;
  font-size: 1.05rem;
  flex: 1 1 auto;
}

/*
 * The guest form's one dialog: the personal-information refusal.
 *
 * Its own layout rather than the head/body/foot the staff dialogs are built from, because it
 * has none of those parts — it is a heading, two sentences and the way back. `.modal` sets
 * `padding: 0` for a dialog with its own scrolling body, so the padding is put back here.
 *
 * Sized for a phone held one-handed in a dark auditorium, which is where every one of these
 * is read.
 */
.modal--privacy {
  width: min(420px, calc(100vw - 32px));
  padding: 22px;
  text-align: center;
}

.modal--privacy .modal__title {
  margin-bottom: 10px;
  font-size: 1.15rem;
  color: var(--warn);
}

.modal--privacy .modal__body {
  padding: 0 0 14px;
  overflow: visible;
  font-size: 0.9rem;
  line-height: 1.5;
  color: var(--text-dim);
}

/*
 * The rule said where it would be broken.
 *
 * Warm rather than red: this is guidance being read BEFORE anything has gone wrong, and a
 * form that opens already showing an error-coloured line reads as broken. The refusal itself
 * is where the alarm belongs.
 */
.field__hint--privacy {
  margin-top: 6px;
  color: var(--warn);
}

.modal__x {
  flex: none;
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  font-size: 22px;
  line-height: 1;
  border-radius: 8px;
  border: 1px solid transparent;
  background: none;
  color: var(--text-dim);
  cursor: pointer;
  transition:
    background-color 140ms var(--ease),
    color 140ms var(--ease);
}

.modal__x:hover {
  background: var(--surface-3);
  color: var(--text);
}

.modal__notice {
  flex: none;
  margin: 16px 20px 0;
}

/* The middle row: it takes what is left and scrolls, rather than being told in advance
   how much room there is. min-height: 0 is what lets a flex item shrink below its
   content — without it the form keeps its full height and pushes the footer out. */
.modal__body {
  flex: 1 1 auto;
  min-height: 0;
  padding: 20px;
  overflow-y: auto;
  /*
   * Defence, not a fix for anything observed here.
   *
   * A flick that outruns the content can hand the remainder to whatever is behind the dialog, so
   * the page moves under a box that has supposedly taken over. Chromium under a synthetic wheel
   * would not reproduce it either way, so this is unproven rather than verified — kept because it
   * is the standard guard for a scroll region inside a modal and costs nothing. The scroll bug
   * this app actually had was structural: see the note in public/js/case-history.js.
   */
  overscroll-behavior: contain;
}

.modal__body[hidden] {
  display: none;
}

/*
 * A form whose footer must never sit on a field.
 *
 * The fields scroll inside `.modal__scroll` and the footer is a row of the form rather
 * than something floating over the end of it. Sticky was fine for the short add-a-cinema
 * form and wrong for the six-field report form, where it covered whatever field happened
 * to be last on screen.
 */
.modal__body--flow {
  display: flex;
  flex-direction: column;
  padding: 0;
  overflow: hidden;
}

.modal__scroll {
  flex: 1 1 auto;
  min-height: 0;
  padding: 20px;
  overflow-y: auto;
  /* The same unproven guard as `.modal__body` above. */
  overscroll-behavior: contain;
}

.modal__body--flow .modal__foot {
  position: static;
  flex: none;
  margin: 0;
  padding: 16px 20px;
}

/* ------------------------------------------------------- the layout preset box
 *
 * Three stacked choices, each a button carrying its own name and the sentence that says what
 * it does. Full-width and stacked rather than three across: the hints are the point — a row of
 * three bare words would be asking somebody to guess what "Review focus" puts away — and three
 * columns of prose on a phone is three columns of one word each.
 *
 * Left-aligned text inside a button, which the app's buttons otherwise centre. These are
 * choices to read rather than actions to press, and centred prose in a 400px box wanders.
 */
.preset__choices {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.preset__choice {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  width: 100%;
  padding: 12px 14px;
  text-align: left;
  /* A card's corners, not a button's pill: three and four lines of hint in a pill ran into its
     curve at the first and last line, most of all on a phone. */
  border-radius: var(--radius);
}

.preset__label {
  font-weight: 600;
}

.preset__hint {
  color: var(--text-dim);
  font-size: 0.9em;
  font-weight: 400;
  /* Wraps rather than ellipsing: a hint cut off mid-sentence is worse than a taller button. */
  white-space: normal;
  line-height: 1.35;
}

/* The promise, spaced away from the choices so it reads as a note about all three. */
.preset__promise {
  margin-top: 16px;
}

/* ------------------------------------------------- occupancy, year over year
 *
 * The occupancy tile's popup: two tables and a caption. The change chips carry the one
 * judgement the reader came for — up or down on last year — so they get the two status
 * colours; everything else is the table dress the rest of the app already wears.
 */
.compare__table {
  width: 100%;
  margin-top: 12px;
}

.compare__heading {
  margin: 20px 0 0;
  font-size: 0.95rem;
  color: var(--text-dim);
}

.compare__empty {
  margin: 14px 0 0;
  color: var(--text-dim);
}

.compare__shift {
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}

.compare__shift--up {
  color: var(--ok);
}

.compare__shift--down {
  color: var(--warn);
}

/* The background sweep's health line on the POS card (js/settings.js): grey when the
   clock is delivering, the warn voice when it is late — late is the one state that
   changes what every number on the dashboard means. */
.pos-sweep--late {
  color: var(--warn);
  font-weight: 600;
}

/* Three states, not two, and this is the middle one: the pass completed, so the figures
   above it are current, but the watchdog is what ran it — the clock behind them is dead
   and the app is only keeping up because somebody is looking. The warn colour without the
   weight is the hierarchy: late means the numbers are wrong NOW, rescued means they will
   be by morning. It went a long time with no dress at all, which is how a deployment
   whose alarms had entirely stopped kept reading as well. */
.pos-sweep--rescued {
  color: var(--warn);
}

/* ------------------------------------------------------------- the case peek
 *
 * The quick look behind a dashboard tile (js/case-peek.js): the reports a number counts,
 * as a list of rows in the app's own dress. Only the hot-impact chip gets colour, and it
 * is the warn token the tiles themselves use — a peek that shouted louder than the queue
 * would misreport the queue.
 *
 * ## Why this is not a table any more
 *
 * It was: seven nowrap columns, which is a shape that cannot be made to fit. A 660px
 * dialog gave it a horizontal scrollbar and the reader a sideways drag to see the last
 * three facts of every row; on a phone the same drag in a third of the width. Worse, it
 * did not LOOK like this app — every other list here is a stack of rows with a border
 * between them, and a spreadsheet inside a card reads as something borrowed from
 * elsewhere. So the seven facts stack the way `.attn` and `.oblig` stack theirs, three
 * lines to a case, and the row reflows instead of scrolling at every width there is.
 *
 * `.peek__table` and `.peek__hot` stay: AI verification's own table still wears them.
 */
.peek__table {
  width: 100%;
  margin-top: 4px;
}

.peek__table td,
.peek__table th {
  white-space: nowrap;
}

.peeklist {
  margin-top: 4px;
}

/* Between rows and not above the first: the hint over this list is often empty, and a
   border under nothing is a line the reader has to account for. */
.peekcase {
  padding: 11px 0;
}

.peekcase + .peekcase {
  border-top: 1px solid var(--border);
}

/* The reference leads and the two chips sit at the far end, so a column of rows has one
   ragged edge rather than two. Wraps on a narrow phone, at which point the chips take the
   next line whole instead of squeezing the reference to three characters. */
.peekcase__top {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 4px 12px;
}

.peekcase__ref {
  font-family: var(--mono);
  font-size: 0.82rem;
  font-weight: 600;
  overflow-wrap: anywhere;
}

.peekcase__tags {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

/* The pill this app uses everywhere something is being labelled rather than said. */
.peekcase__tag {
  padding: 2px 9px;
  border-radius: 999px;
  background: var(--surface-3);
  color: var(--text-dim);
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  white-space: nowrap;
}

.peekcase__tag--hot {
  background: var(--warn-soft);
  color: var(--warn);
}

.peekcase__where {
  margin: 3px 0 0;
  font-size: 0.9rem;
  font-weight: 600;
}

/* What it is, and how long it has been waiting — the two facts that answer "does this
   need me now", on one line with the age pushed to the end of it. */
.peekcase__what {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 2px 12px;
  margin: 2px 0 0;
  color: var(--text-dim);
  font-size: 0.86rem;
}

.peekcase__when {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/*
 * Three buttons in a footer built for two.
 *
 * The agent takes the left end and the two ways out keep the right, because it is a
 * different KIND of thing from Close and Open in Case monitor — it acts on the list you
 * are looking at rather than deciding where you go next, and a row of three identical
 * right-aligned buttons hides that. `margin-right: auto` on the first rather than
 * `space-between` on the row, so the layout is unchanged on every peek that has no agent
 * button: two buttons, right, exactly as before.
 *
 * And it wraps, because at 390px "AI Reviewer Agent" + "Close" + "Open in Case monitor" is
 * wider than the phone. Wrapped, the agent gets a line and the exits keep theirs.
 */
.peek__foot {
  flex-wrap: wrap;
}

.peek__foot .agentrow__btn {
  margin-right: auto;
}

/*
 * The AI Reviewer Agent popup. Wider than the standard dialog because it is a seven-column
 * table being read across, and .modal's 660px folded the reason column to a word a line.
 * Everything else — colors, radius, the open animation — is .modal's own.
 *
 * ITS OWN NAME, and that is the point of this rule rather than an incidental detail. This
 * dialog opened life as `.modal--wide`, which the starter's seven-tile chooser also claims —
 * and that one is written as `.modal.modal--wide` a few thousand lines further down, so it won
 * on specificity regardless of order and this box quietly opened at 820px from the day it
 * shipped. "Wide" is not a name; it is an adjective two unrelated dialogs will both reach for
 * and then disagree about. Named for the thing it is, it cannot be taken.
 */
.modal--verify {
  width: min(1080px, calc(100vw - 32px));
}

/*
 * Where and Issue type are facts that fit on one line; the two prose columns are the reading.
 *
 * Auto table layout gives a nowrap cell whatever width its longest content wants, so
 * "Auditorium 1 · Alpha Cinema 4" was claiming a quarter of the dialog while "What it says"
 * and "Agent's reason" folded to three words a line beside it — the two columns an admin is
 * actually here to read, squeezed by the two they glance at. Fixed layout with declared widths
 * puts the space where the reading is, and holds the columns still from row to row.
 */
/*
 * The widths belong to the TABLE, so they are declared only where there is one.
 *
 * Below the breakpoint each row becomes a card and every cell becomes a block — at which point
 * a `width: 20%` stops meaning "a fifth of the table" and starts meaning "a fifth of the card",
 * which is 60px, which is one word per line. The phone layout set `width: auto` on `th` and
 * lost to `th:nth-child(1)` on specificity, so "Auditorium 1 · Alpha Cinema 4" came down as a
 * five-line ribbon. Fencing the widths off by width is the fix that cannot be out-specified.
 */
@media (min-width: 701px) {
  .verify__table {
    table-layout: fixed;
  }

  /*
   * Rebalanced when the reason column stopped being one sentence.
   *
   * On the group list that cell now carries four things — which job this rides with, whether
   * that job is already out with the cinema, what the job itself says, and the agent's reason
   * — and at 20% the quoted line broke to three. The width comes off the four columns that are
   * glanced at rather than read; "What it says" keeps most of its own, because the two prose
   * columns are what the decision is actually made from.
   */
  .verify__table th:nth-child(1) { width: 17%; }  /* Where */
  .verify__table th:nth-child(2) { width: 11%; }  /* Issue type */
  .verify__table th:nth-child(3) { width: 8%; }   /* Impact */
  .verify__table th:nth-child(4) { width: 25%; }  /* What it says */
  .verify__table th:nth-child(5) { width: 28%; }  /* Agent’s reason */
  .verify__table th:nth-child(6) { width: 11%; }  /* the Move button */

  /* Fixed layout would otherwise clip these two mid-word: they are the columns that wrap. */
  .verify__table th.verify__where,
  .verify__table td.verify__type {
    white-space: normal;
  }
}

/*
 * ---------------------------------------------------- the same list, as cards, on a phone
 *
 * Six columns of prose do not survive 390px, and the first attempt proved it: the header ran
 * together into "CASWHERE ISSUEIMPWHAT IT SAYSAGENT'S RI", the description came down in a
 * three-character ribbon, and the Move button hung off the right edge of the dialog.
 *
 * A table is a shape for COMPARING rows — read down a column, spot the outlier — and that is a
 * thing a phone cannot offer at any font size. So on a phone it stops pretending: each report
 * becomes a card that is read on its own, in the order the eye wants it. Where it happened,
 * what kind and how bad, what the guest said, what the agent made of it, and the one button
 * that overrules it.
 *
 * 700px rather than the app's usual 660: this table is six columns of sentences where the case
 * list's six are short values, and it was already tight at 660 before it broke.
 */
@media (max-width: 700px) {
  .verify__table,
  .verify__table tbody,
  .verify__table tr,
  .verify__table th,
  .verify__table td {
    display: block;
    width: auto;
  }

  /* The headings are the card's own layout now — "Auditorium 2 · Durango 9" needs no label
     saying "Where", and six labels per card is more furniture than content. */
  .verify__table thead {
    display: none;
  }

  .verify__table tbody tr {
    padding: 12px 14px;
    margin: 0 0 10px;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface-2, var(--surface));
  }

  /* Nothing is nowrap in a card: every cell here is prose and gets the width it needs. */
  .verify__table th,
  .verify__table td {
    white-space: normal;
    padding: 0;
    border: 0;
  }

  .verify__table th.verify__where {
    font-weight: 650;
    color: var(--text);
    margin-bottom: 4px;
  }

  /* The two short facts share a line — stacked, they were two lines saying three words. */
  .verify__table td.verify__type,
  .verify__table td.verify__impact {
    display: inline-block;
  }

  .verify__table td.verify__type::after {
    content: '·';
    margin: 0 7px;
    opacity: 0.5;
  }

  .verify__table td.verify__said {
    margin: 8px 0 0;
    color: var(--text);
  }

  /*
   * Labelled, because "Off-topic comment, no cinema fault named" under a guest's own words is
   * otherwise two paragraphs with no indication of who wrote which. The column heading did
   * this job on a desktop; on a card the prefix does it.
   *
   * On the REASON, not on the cell around it. The cell holds three voices now — which job this
   * rides with, where that job already is, and what the job itself says — and a prefix on the
   * cell put "Agent:" above another guest's quoted words, which is the one thing it must not
   * claim. See the group list in js/ai-verify.js.
   */
  .verify__table td.verify__why .verify__reason::before {
    content: 'Agent: ';
    font-weight: 650;
    color: var(--text-muted, var(--text));
  }

  .verify__table td.verify__why {
    margin-top: 6px;
  }

  .verify__table td.verify__actions {
    margin-top: 10px;
    text-align: left;
  }

  .verify__table td.verify__actions .btn {
    width: 100%;
  }
}

.verify__tabs {
  display: flex;
  gap: 8px;
  margin: 4px 0 10px;
}

/* The two text columns wrap; the fact columns stay one line like the peek's. The widths are
   declared with the table's fixed layout above — a max-width here would fight it. */
.verify__table td.verify__said,
.verify__table td.verify__why {
  white-space: normal;
}

.verify__why {
  color: var(--text-dim);
}

/*
 * The agent's own sentence, as its own line.
 *
 * A bare span would run on from the quoted words above it — "…queues for the doors." Same
 * floor by the same doors — which is two people's writing in one paragraph. It is also what
 * the phone layout hangs its "Agent:" prefix on, so it has to be a block for the prefix to
 * lead a line rather than appear mid-sentence.
 */
.verify__reason {
  display: block;
}

/*
 * "Same fault as CQ-…", above the agent's own words rather than beside them.
 *
 * A block, because the two spans in this cell answer different questions — WHICH job this
 * rides with, and WHY the agent thought so — and inline they ran together into one unreadable
 * string ("CQ-DEMO-6Same floor, same evening"). The reference is the part an admin acts on,
 * so it leads, in the app's own accent.
 */
.verify__fold {
  display: block;
  color: var(--accent);
  font-weight: 600;
}

/* -------------------------------------------------- the agent, visibly at work
 *
 * The model reasons over every waiting report at high effort, which on a morning queue is tens
 * of seconds. What stood here was one line of static text over an empty box, and a still page
 * during a long wait does not read as "working" — it reads as broken.
 *
 * So the app's own spark breathes: the same mark on the button that opened the box and on the
 * tag every report leaves wearing, scaled and dimmed on a slow cycle with a halo behind it on
 * a longer one, so the two never sit in step and the whole thing never looks like a spinner
 * counting down to something.
 */

.verify__working {
  display: grid;
  justify-items: center;
  gap: 4px;
  padding: 54px 20px 58px;
  text-align: center;
}

.verify__working-mark {
  position: relative;
  display: grid;
  place-items: center;
  width: 76px;
  height: 76px;
  margin-bottom: 10px;
  color: var(--accent);
}

/* The halo: a soft disc that swells behind the star, on a longer cycle than the star's own. */
.verify__working-mark::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: radial-gradient(
    circle,
    color-mix(in oklab, var(--accent) 30%, transparent),
    transparent 68%
  );
  animation: verify-halo 2.9s ease-in-out infinite;
}

.verify__working-spark {
  position: relative;
  animation: verify-spark 1.7s ease-in-out infinite;
}

@keyframes verify-spark {
  0%,
  100% {
    transform: scale(0.88) rotate(-6deg);
    opacity: 0.62;
  }
  50% {
    transform: scale(1.06) rotate(6deg);
    opacity: 1;
  }
}

@keyframes verify-halo {
  0%,
  100% {
    transform: scale(0.72);
    opacity: 0.25;
  }
  50% {
    transform: scale(1);
    opacity: 0.6;
  }
}

.verify__working-said {
  margin: 0;
  font-size: 1rem;
  font-weight: 650;
  color: var(--text);
}

.verify__working-note {
  margin: 0;
  font-size: 0.88rem;
  color: var(--text-dim);
}

/*
 * A reader who asked for no motion gets the same star on the same spot, still and legible —
 * not a lesser screen. The sentence under it was always the thing carrying the meaning.
 */
@media (prefers-reduced-motion: reduce) {
  .verify__working-spark,
  .verify__working-mark::before {
    animation: none;
  }

  .verify__working-mark::before {
    opacity: 0.35;
  }
}

/*
 * "Already sent to the cinema", under the reference it names.
 *
 * A different fact from the two spans around it — not which job, and not why the agent thinks
 * so, but WHERE that job already is — so it gets its own line rather than joining either. In
 * the warning colour and not the accent, because this is the row on the list an admin should
 * read twice before a bulk Confirm: agreeing hands a guest's report to a building that may be
 * halfway through the repair.
 */
.verify__open {
  display: block;
  color: var(--warn);
  font-weight: 600;
  font-size: 0.85em;
}

/*
 * What the job it would be folded into actually says, quoted.
 *
 * A reference alone asked an admin to trust the grouping or go and look the case up in another
 * tab — and the one they most need to check is the one they cannot see, because a job already
 * out with the cinema is on no list in this box. Set apart from the agent's own reason under it
 * by the quotation marks and the italic: three different voices share this cell — which job,
 * where that job is, and what the agent made of it — and a reader has to tell them apart at a
 * glance.
 */
.verify__quote {
  display: block;
  margin: 2px 0;
  color: var(--text);
  font-style: italic;
}

.verify__actions {
  text-align: right;
}

.peek__hot {
  color: var(--warn);
  font-weight: 600;
}

.peek__empty {
  margin: 14px 0 0;
  color: var(--text-dim);
}

/* ---------------------------------------------------------------------------------------
   The Same fault box: folding two reports of one fault together by hand (js/same-fault.js).

   A LIST, not a table, and that is a decision rather than laziness. The agent's box beside it
   is a table because every row there carries the same five facts and the reader is comparing
   them down a column. This one is read row by row — "is THIS the report I am looking at" —
   and the answer is a room, a sentence, and a button. A table would have bought column
   alignment nobody needs and a phone layout that has to be rebuilt cell by cell (see the
   .verify__table media query above, which is 70 lines of exactly that).
   --------------------------------------------------------------------------------------- */

.fold__heading {
  margin: 18px 0 8px;
  font-size: 0.95rem;
  font-weight: 650;
}

/* The first heading follows the explanation rather than a list, so it needs less air above
   it — but not none, or the two paragraphs read as one. */
.fold__heading:first-child {
  margin-top: 10px;
}

.fold__find {
  width: 100%;
  margin: 0 0 10px;
}

.fold__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.fold__row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-2, var(--surface));
}

/* The words take the room; the button takes what it needs. Without min-width a long
   description refuses to wrap and pushes the button off the edge of the dialog. */
.fold__what {
  flex: 1;
  min-width: 0;
}

.fold__where {
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 6px;
  font-weight: 600;
}

.fold__sep {
  opacity: 0.45;
  font-weight: 400;
}

/* Already carrying riders — the same words as the card badge, in the same accent, so the
   two are recognisably one fact in two places. */
.fold__riders {
  color: var(--accent);
  font-weight: 600;
  font-size: 0.85em;
}

/*
 * The guest's own words, clamped to two lines.
 *
 * Full text would make one long report taller than the six around it and turn a scan into a
 * scroll — and the point of this list is to find a match in it. Two lines is enough to
 * recognise a fault somebody has already read once today.
 */
.fold__said {
  margin: 4px 0 0;
  color: var(--text);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.fold__when {
  margin: 4px 0 0;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  color: var(--text-dim);
  font-size: 0.85em;
}

.fold__ref {
  font-variant-numeric: tabular-nums;
}

.fold__go {
  flex: none;
}

/*
 * "One more report is folded in that is still with head office for review."
 *
 * A note about the LIST rather than a row in it, so it reads as one: dimmed and set slightly
 * back from the rows below. It appears only on a manager's copy of this box, where the count
 * in the heading above is larger than the number of rows underneath — see the same-fault route
 * in src/routes/gm.js for why they are shown a number and not the report.
 */
.fold__hidden {
  margin: 0 0 10px;
  color: var(--text-dim);
  font-size: 0.88rem;
}

.fold__already {
  margin: 4px 0 10px;
  color: var(--text);
}

.fold__standalone {
  margin: 0 0 4px;
}

/* On a phone the button goes under the words at full width, the same shape every other
   action in the app takes down there. */
@media (max-width: 700px) {
  .fold__row {
    flex-direction: column;
    align-items: stretch;
  }

  .fold__go .btn {
    width: 100%;
  }
}

.modal__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 0 18px;
}

.modal__grid .field:last-child {
  grid-column: 1 / -1;
}

/* Sticky, so the primary action stays reachable when a form scrolls — which it
   does on a phone. */
.modal__foot {
  position: sticky;
  bottom: -20px;
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  margin-top: 8px;
  padding: 16px 0;
  border-top: 1px solid var(--border);
  background: var(--surface);
}

/*
 * A footer that is the dialog's own child, not the body's.
 *
 * The zero horizontal padding above assumes the foot sits INSIDE .modal__body and inherits
 * its 20px inset — true of the confirm boxes, and not of the dialogs built head/body/foot
 * (the case peek, AI verification): there the buttons sat flush against the dialog border.
 * Scoped with a child combinator so the nested feet, which are already inset by the body,
 * are not indented twice.
 *
 * AND ONE LEVEL DOWN, inside a form that wraps the whole box. The text box every page asks with
 * (askForText), the equipment box on Location info and the two on the showtime scheduler are
 * dialog > form > head, body, foot — so the foot was neither the dialog's child nor inside the
 * body, matched neither rule, and every one of those four put its Add button hard against the
 * edge of the box. The operator spotted it on the equipment box; it had been on the others all
 * along. A form that IS the body (`form.modal__body`, the add-an-account box) is left out, because
 * its foot is inset by it already.
 */
.modal > .modal__foot,
.modal > form:not(.modal__body) > .modal__foot {
  padding: 16px 20px;
  margin-top: 0;
  bottom: 0;
}

/*
 * And the same boxes' MIDDLE, which has to be the part that scrolls.
 *
 * The dialog is a column with a height limit and `overflow: hidden`; its head, body and foot are
 * meant to split that height, with the body scrolling (`.modal__body`). One level down, inside a
 * form, they did not: the form was a plain block as tall as everything in it, so a body longer than
 * the box was cut off at the dialog's edge — nothing scrolled, and what was under the cut could not
 * be reached on a phone at all. Found on the Equipment box on Location info, whose list of what is
 * in a screen and what was before outgrows a phone; the seat box was one long seating plan from the
 * same thing. So a form that holds a body is a column that shrinks to the box, and the body takes
 * what is left. Only such a form: one laid out any other way is left as it was.
 */
.modal > form:has(> .modal__body) {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}

/* ------------------------------------------------------ inspection compliance
 *
 * Which inspections a building has a current pass for, and until when, in the pop-up the
 * Location info button opens. Four columns, because that is the question the page is asked: what
 * is it, where does it stand, when was it last done — and the way to record the next visit.
 *
 * A grid rather than a <table>, and for one reason: on a phone each inspection has to stop
 * being a row of cells and become a small block with its parts stacked and labelled. A table
 * cannot do that without `display: block` on every part of it, which throws away the semantics
 * the table was for; a grid of four columns collapsing to two is the same layout said in a way
 * that reflows.
 */
/*
 * One grid, and each row a subgrid of it: the columns are the box's, not each row's. They used to
 * be each row's own, and the button column is sized by its button — so the row whose button said
 * "Record the re-inspection" drew its pill and its date a column-width to the left of every other
 * row's, and the heading, with no button at all, lined up with none of them.
 */
.compliance {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr) minmax(0, 1fr) auto;
  gap: 8px 10px;
  margin: 4px 0 16px;
}

.compliance__summary {
  margin: 0 0 12px;
  font-size: 0.88rem;
  color: var(--text-dim);
}

/* The one number in the box worth a colour: how much is missing. */
.compliance__summary--due {
  color: var(--warn);
  font-weight: 550;
}

.compliance__head,
.compliance__row {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: subgrid;
  row-gap: 10px;
  align-items: center;
}

.compliance__head {
  padding: 0 12px;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-faint);
  font-weight: 650;
}

.compliance__row {
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
}

.compliance__name {
  display: flex;
  flex-direction: column;
  gap: 1px;
  font-weight: 600;
  font-size: 0.9rem;
}

/* How often it comes round, under its name: the thing that says why a row went amber in
   October when its neighbours are good until next spring. */
.compliance__every {
  font-size: 0.72rem;
  font-weight: 500;
  color: var(--text-faint);
}

/*
 * Where it stands, as a pill: the one part of the row read before any of its words. Shaped like
 * a badge and in sentence case, coloured by what it asks of the reader — nothing, soon, or now.
 * Overdue and Failed share the danger colour because they ask the same thing; their words say
 * which it is.
 */
.standing {
  align-self: start;
  display: inline-flex;
  align-items: center;
  padding: 2px 9px;
  border: 1px solid transparent;
  border-radius: var(--badge-radius, 7px);
  font-size: 0.74rem;
  font-weight: 650;
  white-space: nowrap;
}

.standing--current {
  background: var(--ok-soft);
  color: var(--ok);
  border-color: color-mix(in oklab, var(--ok) 35%, transparent);
}

.standing--due {
  background: var(--warn-soft);
  color: var(--warn);
  border-color: color-mix(in oklab, var(--warn) 35%, transparent);
}

.standing--overdue,
.standing--failed {
  background: var(--danger-soft);
  color: var(--danger);
  border-color: color-mix(in oklab, var(--danger) 40%, transparent);
}

.standing--none {
  background: var(--surface-3);
  color: var(--text-dim);
  border-color: var(--border-strong);
}

/* The date the pill turns on, under it: "Good until", "Valid until" or "Ran out". */
.compliance__until {
  margin-top: 3px;
  font-variant-numeric: tabular-nums;
  font-size: 0.78rem;
  color: var(--text-dim);
}

/* A date is one word: the line may break before it, never inside it. */
.compliance__day,
.compliance__date {
  white-space: nowrap;
}

.compliance__none {
  font-size: 0.82rem;
  color: var(--text-faint);
}

/* The record button keeps to the right of the row, the end a reader's eye leaves it from. */
.compliance__act {
  display: flex;
  justify-content: flex-end;
  min-width: 0;
}

/*
 * The booking, for an inspection whose visits are agreed ahead of time: a line across the foot of
 * the row rather than a fifth column, because most rows have none and a column of blanks would
 * squeeze the four that every row uses. Dashed above, so it reads as part of the row and not as the
 * start of the next one.
 */
.compliance__booking {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 10px;
  padding-top: 8px;
  border-top: 1px dashed var(--border);
  font-size: 0.82rem;
}

.compliance__booked {
  font-variant-numeric: tabular-nums;
  color: var(--text-dim);
}

/* A booked day gone by with nothing recorded, and an inspection waited on with nobody booked: the
   two lines here that ask somebody to pick up the phone. */
.compliance__booked--missed,
.compliance__booked--none {
  color: var(--warn);
  font-weight: 550;
}

.compliance__who {
  color: var(--text-faint);
  font-weight: 400;
}

.compliance__booking-acts {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-left: auto;
}

/* What the booking box is about, above its two fields: the one thing it cannot leave unsaid. */
.compliance__booking-what {
  margin: 0 0 12px;
  font-weight: 600;
}

/*
 * The reports that count towards this inspection since its last visit: a line across the foot of the
 * row like the booking, for the same reason — only the rows Settings gave issue types to carry one.
 * Quiet when nothing was reported; in the text's own ink when something was, because three exit
 * signs reported out is the thing to put right before the inspector comes, not a footnote.
 */
.compliance__reports {
  grid-column: 1 / -1;
  margin: 0;
  padding-top: 8px;
  border-top: 1px dashed var(--border);
  font-size: 0.82rem;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}

.compliance__reports--some {
  color: var(--text);
  font-weight: 550;
}

.compliance__cell {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
}

.compliance__date {
  font-variant-numeric: tabular-nums;
  font-size: 0.88rem;
}

.compliance__cert {
  font-family: var(--mono);
  font-size: 0.72rem;
  color: var(--text-faint);
  overflow-wrap: anywhere;
}

/* Sized to the words rather than stretched across the cell: a full-width button in each of
   ten cells is a wall of buttons, and this one is an invitation, not the row's purpose.
   But never wider than the cell: "Record the re-inspection" in a phone's half-width column
   ran out of its card and off the screen, so a label that long wraps inside it instead. */
.compliance__add {
  align-self: start;
  flex: 0 0 auto;
  max-width: 100%;
  white-space: normal;
}

/* How many of a visit's actions are still open, kept to one line: "2 of 2 open" broken after
   the first 2 read as a stray number with a sentence fragment under it. */
.compliance__count {
  white-space: nowrap;
}

/*
 * This box's foot carries three buttons — the PDF, the PDF with its documents, and Record another —
 * and on a phone they are wider than the box. The foot is a row pushed to the right, so whatever
 * did not fit went off the LEFT edge, where nobody looks for it: Download PDF was half a word. They
 * wrap instead, still in their order and still to the right.
 */
#inspections-dialog .modal__foot {
  flex-wrap: wrap;
}

/*
 * The notes field is short here, unlike every other textarea in the app.
 *
 * The app-wide `min-height: 148px` is right for the ones it was written for — a guest
 * describing what went wrong, a manager writing up progress — where somebody is there to
 * type a paragraph and a box that starts small says "keep it brief". This one is an aside on
 * a form whose point is a date, and at full height it was the tallest thing in a box that
 * already needed two screens of scrolling on a phone.
 *
 * Scoped to the dialog rather than made a modifier class, following `#report-dialog .field`
 * further up: it is one box's opinion about its own contents, not a kind of field.
 */
#inspection-form-dialog textarea {
  min-height: 84px;
}

/*
 * And the closing line, for the same reason and by the same route.
 *
 * It sits directly under the team's note in the "Mark resolved" dialog, and at the app-wide
 * height the two were identical boxes — which invites the same kind of writing in both, when
 * the whole point of there being two is that one is a paragraph for colleagues and the other is
 * one sentence for a stranger. Capped at 240 characters server-side (LIMITS.guestOutcome); a box
 * about three lines tall is what says so before anybody hits the cap.
 */
#progress-outcome {
  min-height: 76px;
}

/* Two fields to a row where they fit, one where they do not. Not `.modal__grid`: that
   deliberately spans its last child, which is right for a form ending in a description and
   wrong for one whose last field is a reference number. */
.compliance__form-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 0 16px;
}

/* Every record, behind a disclosure — shut, because the grid is what the box was opened
   for and eleven years of records above it would push the answer off the screen. */
.record-list {
  border-top: 1px solid var(--border);
  margin-top: 4px;
}

.record-list[hidden] {
  display: none;
}

.record-list__summary {
  padding: 12px 2px;
  cursor: pointer;
  list-style: none;
  position: relative;
  padding-left: 20px;
}

.record-list__summary::-webkit-details-marker {
  display: none;
}

.record-list__summary::before {
  content: '';
  position: absolute;
  left: 3px;
  top: 50%;
  width: 6px;
  height: 6px;
  margin-top: -4px;
  border-right: 2px solid var(--text-dim);
  border-bottom: 2px solid var(--text-dim);
  transform: rotate(-45deg);
  transition: transform 160ms var(--ease);
}

.record-list[open] > .record-list__summary::before {
  transform: rotate(45deg);
  margin-top: -5px;
}

.record-list__label {
  font-size: 0.88rem;
  font-weight: 600;
}

.record-list__body {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 0 0 12px;
}

.record {
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
}

.record__main {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 10px;
}

.record__name {
  font-weight: 550;
  font-size: 0.9rem;
}

.record__date {
  font-variant-numeric: tabular-nums;
  font-size: 0.82rem;
  color: var(--text-dim);
}

.record__cert {
  font-family: var(--mono);
  font-size: 0.74rem;
  color: var(--text-faint);
}

.record__valid {
  font-variant-numeric: tabular-nums;
  font-size: 0.78rem;
  color: var(--text-dim);
}

/* Who did the visit: the firm, in the record's quiet ink beside the verdict. */
.record__doneby {
  font-size: 0.78rem;
  color: var(--text-dim);
  overflow-wrap: anywhere;
}

/*
 * The bill that paid for a visit, under its documents: what it came to, then the button that links
 * one or changes it. The chooser takes the button's place on the line while it is open, and wraps
 * under the words on a phone rather than pushing the record wider than the box.
 */
.record__bill {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 10px;
  margin-top: 8px;
}

.record__invoice {
  font-variant-numeric: tabular-nums;
  font-size: 0.82rem;
  overflow-wrap: anywhere;
}

.record__link {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  min-width: 0;
}

.record__invoice-pick {
  min-width: 0;
  max-width: 100%;
  font-size: 0.82rem;
}

.record__notes {
  margin: 6px 0 0;
  font-size: 0.85rem;
  color: var(--text-dim);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.record__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-top: 8px;
}

.record__by {
  font-size: 0.76rem;
  color: var(--text-faint);
}

/*
 * What a visit came to, beside its date in the grid — said only where it was something other than
 * a plain pass, which is what the column has always meant. A failure in the danger colour, because
 * it leaves the year to be done; open actions in the warning colour, done with work outstanding; a
 * pass whose actions are all closed in the quiet ink, because there is nothing left to do.
 */
.compliance__outcome {
  font-size: 0.74rem;
  font-weight: 600;
  color: var(--text-dim);
}

.compliance__outcome--failed {
  color: var(--danger);
}

.compliance__outcome--open {
  color: var(--warn);
}

/* The record's documents, under its reference: a link to the one certificate, or how many. */
.compliance__doc {
  font-size: 0.74rem;
  color: var(--text-dim);
}

/* In the history: the verdict beside the date, a failure in the colour that says it. */
.record__outcome {
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--text-dim);
}

.record__outcome--failed {
  color: var(--danger);
}

/*
 * The cases a visit raised, and the documents filed with it — each a short line under the record,
 * the case's reference first because it is what opens it. A closed one dims: it is history, and the
 * open ones are what the eye should land on.
 */
.record__actions,
.record__docs {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-top: 8px;
}

.record__actions-head {
  margin: 0;
  font-size: 0.76rem;
  font-weight: 650;
  color: var(--text-dim);
}

.record__case,
.record__doc {
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 2px 6px;
  font-size: 0.82rem;
  overflow-wrap: anywhere;
}

.record__case--closed {
  color: var(--text-faint);
}

.record__size {
  color: var(--text-faint);
  font-size: 0.76rem;
}

/* A document on record with nothing behind it (restored from a backup, which carries a place in the
   bucket and never the bytes): said in the warning ink, since it is a gap in the evidence. */
.record__gone {
  color: var(--warn);
  font-size: 0.76rem;
}

.record__acts {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

/*
 * What was removed, under the records that stand: a heading of its own so the two lists are never
 * read as one, and each entry drawn quieter than a record — dashed and on the page's own ground —
 * because it is the note that a record went, not a record.
 */
.record-list__sub {
  margin: 10px 0 0;
  font-size: 0.8rem;
  font-weight: 650;
  color: var(--text-dim);
}

.record--removed {
  border-style: dashed;
  background: transparent;
}

.record--removed .record__name {
  color: var(--text-dim);
}

/* Why it went, in the remover's words: the line an auditor reads in the record's place. */
.record__why {
  margin: 6px 0 0;
  font-size: 0.85rem;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/*
 * What the inspector asked for, on the form: a fieldset set in from the rest, as the equipment box
 * on Mark resolved is, because these lines become cases and should read as their own question. Off
 * (and disabled, so nothing in it can hold the form up) unless the visit asked for anything.
 *
 * Each line is the words on a line of their own, with the issue type and a quiet Remove under them —
 * at every width. The form is a narrow box even on a desktop, and set beside the type the words got
 * a sliver of it: "Replace the" was all an admin could see of what they had typed. The gap between
 * one action and the next is wider than the gap inside one, so which type belongs to which words is
 * plain without a box round each.
 */
.visit-actions {
  min-width: 0;
  margin: 0 0 18px;
  padding: 0 0 0 14px;
  border: 0;
  border-left: 2px solid var(--border);
}

.visit-actions__rows {
  display: flex;
  flex-direction: column;
  gap: 16px;
  margin: 8px 0 12px;
}

.visit-actions__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 6px 8px;
  align-items: center;
}

.visit-actions__text {
  grid-column: 1 / -1;
}

.visit-actions__text,
.visit-actions__type {
  min-width: 0;
}

.visit-actions__remove {
  justify-self: end;
}

/* Room under Add an action, so Due by reads as the next question rather than the button's caption. */
.visit-actions__add {
  margin-bottom: 18px;
}

/* The documents chosen, named under the picker, so what will be filed is what the reader sees. */
.visit-docs__list {
  overflow-wrap: anywhere;
}

/*
 * WHAT MANAGERS HAVE HANDED IN, at the top of the compliance box: the paper waiting for this admin
 * to file. A band in the info tint rather than more rows of the grid, because it is not where an
 * inspection stands — it is what came in about one. Each hand-in in it is a .handin, below.
 */
.compliance__handed {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin: 0 0 14px;
  padding: 12px;
  border: 1px solid color-mix(in oklab, var(--info) 30%, transparent);
  border-radius: var(--radius-sm);
  background: color-mix(in oklab, var(--info) 7%, transparent);
}

.compliance__handed[hidden] {
  display: none;
}

.compliance__handed-title {
  margin: 0;
  font-size: 0.85rem;
  font-weight: 650;
}

/* Filing a hand-in, at the top of the record form: whose paper it is, and that it goes onto the record. */
.compliance__filing {
  margin: 0 0 14px;
  padding: 10px 12px;
  border: 1px solid color-mix(in oklab, var(--info) 30%, transparent);
  border-radius: var(--radius-sm);
  background: color-mix(in oklab, var(--info) 7%, transparent);
}

.compliance__filing-what {
  margin: 0;
  font-size: 0.85rem;
}

.compliance__filing-notes {
  margin: 6px 0 0;
  font-size: 0.82rem;
  color: var(--text-dim);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/*
 * What reading the certificate filled in (js/inspections.js, applyReading) — the invoice screen's
 * read line, in this form. Quiet, because it is a note on how the boxes came to be filled rather than
 * a result. Where the paper disagrees with something already in the form, that is the one thing it
 * says that somebody must not read past, so it — and only it — is loud, on a line of its own: the
 * whole line in bold amber was four lines on a phone with the disagreement somewhere inside them.
 * The whole line is loud only when it is one short sentence about a document that was refused.
 */
.compliance__read {
  margin: 0 0 14px;
  font-size: 0.85rem;
  line-height: 1.5;
  color: var(--text-dim);
  overflow-wrap: anywhere;
}

.compliance__read--busy {
  font-style: italic;
}

.compliance__read--warn {
  color: var(--warn);
  font-weight: 600;
}

.compliance__read-differs {
  display: block;
  color: var(--warn);
  font-weight: 600;
}

/* ---------------------------------------------------------------------------------------------
 * THE COMPLIANCE PAGE (public/compliance.html, js/compliance.js): every building's inspections.
 *
 * The rail, the tiles and the cards are the app's own; what is new is the strip and a building's
 * table. The pills inside are the inspection box's own `.standing`, so a state looks one way
 * wherever it is shown.
 * ------------------------------------------------------------------------------------------- */

/*
 * THE NEXT TWELVE MONTHS: a building a row, a month a column. Squares rather than a chart, because
 * the question is "which building, which month" and both are read off the edges. A phone scrolls it
 * sideways inside its own box, with the buildings held at the left.
 */
.runout > h2 {
  margin-bottom: 6px;
}

.runout__lede {
  margin: 0 0 14px;
  font-size: 0.86rem;
  color: var(--text-dim);
}

.runout__scroll {
  overflow-x: auto;
}

/* The card's width, the months sharing what the names leave: a strip that stopped two-thirds of
   the way across a wide card read as one that had not finished drawing. A phone's is wider than its
   box, and scrolls. */
.runout__table {
  width: 100%;
  border-collapse: separate;
  border-spacing: 3px;
  font-variant-numeric: tabular-nums;
}

.runout__table th,
.runout__table td {
  padding: 0;
}

.runout__corner,
.runout__head {
  padding-bottom: 4px;
  font-size: 0.72rem;
  font-weight: 600;
  color: var(--text-dim);
  line-height: 1.25;
  vertical-align: bottom;
}

.runout__corner {
  text-align: left;
}

/* As narrow as the names: the months take the rest. */
.runout__corner,
.runout__place {
  width: 1%;
  white-space: nowrap;
}

.runout__head {
  min-width: 34px;
  text-align: center;
}

.runout__head--now {
  color: var(--danger);
}

.runout__year {
  display: block;
  font-size: 0.66rem;
  font-weight: 500;
}

/* The building's name, held at the left while the months scroll under it on a phone. */
.runout__place,
.runout__corner,
.runout__foot {
  position: sticky;
  left: 0;
  z-index: 1;
  background: var(--surface);
}

.runout__go {
  display: block;
  max-width: 190px;
  padding: 6px 10px 6px 2px;
  border: 0;
  background: none;
  color: var(--text);
  font: inherit;
  font-size: 0.84rem;
  font-weight: 600;
  text-align: left;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  cursor: pointer;
}

.runout__go:hover,
.runout__go:focus-visible {
  color: var(--accent);
  text-decoration: underline;
}

.runout__cell {
  height: 30px;
  min-width: 34px;
  border-radius: 6px;
  background: var(--surface-2);
  color: var(--text);
  font-size: 0.8rem;
  font-weight: 650;
  text-align: center;
}

/* Each colour once, for the square and for its swatch in the key, so the two cannot drift apart. */
.runout__cell--later,
.runout__swatch--later {
  background: color-mix(in oklab, var(--accent) 24%, var(--surface-2));
}

.runout__cell--due,
.runout__swatch--due {
  background: var(--warn-soft);
  color: var(--warn);
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--warn) 40%, transparent);
}

.runout__cell--now,
.runout__swatch--now {
  background: var(--danger-soft);
  color: var(--danger);
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--danger) 40%, transparent);
}

/* What the colours mean, under the strip. */
.runout__key {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 18px;
  margin: 14px 0 0;
  padding: 0;
  list-style: none;
  font-size: 0.78rem;
  color: var(--text-dim);
}

.runout__key[hidden] {
  display: none;
}

.runout__key li {
  display: flex;
  align-items: center;
  gap: 7px;
}

.runout__swatch {
  width: 14px;
  height: 14px;
  border-radius: 4px;
}

.runout__foot {
  padding-top: 4px;
  font-size: 0.74rem;
  font-weight: 600;
  color: var(--text-dim);
  text-align: left;
}

.runout__total {
  padding-top: 4px;
  font-size: 0.74rem;
  font-weight: 600;
  color: var(--text-dim);
  text-align: center;
}

/* ONE BUILDING: where each of its inspections stands, and the box that records them. */
.estate__head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
}

.estate__name {
  margin: 0;
}

.estate__where {
  margin: 4px 0 0;
  font-size: 0.84rem;
  color: var(--text-dim);
}

.estate__todo {
  font-weight: 650;
}

.estate__line {
  margin: 14px 0 0;
  font-size: 0.9rem;
}

/* What a building's visits of the last twelve months cost: under its line, in the same quiet size. */
.estate__cost {
  margin: 4px 0 0;
  font-size: 0.85rem;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}

.estate__handed {
  margin: 6px 0 0;
  font-size: 0.86rem;
  color: var(--warn);
  font-weight: 600;
}

.estate__scroll {
  margin-top: 14px;
  overflow-x: auto;
}

.estate__table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.88rem;
  font-variant-numeric: tabular-nums;
}

.estate__table th {
  padding: 8px 10px;
  border-bottom: 1px solid var(--border);
  font-size: 0.74rem;
  font-weight: 600;
  color: var(--text-dim);
  text-align: left;
}

.estate__table td {
  padding: 10px;
  border-bottom: 1px solid var(--border);
  vertical-align: top;
}

.estate__table tr:last-child td {
  border-bottom: 0;
}

.estate__kind {
  font-weight: 600;
}

.estate__sub {
  display: block;
  margin-top: 4px;
  font-size: 0.78rem;
  color: var(--text-dim);
}

.estate__sub--late {
  color: var(--warn);
  font-weight: 600;
}

.estate__none {
  color: var(--text-dim);
}

/* On a phone, a row is a block and each cell says what it is: four columns do not fit 390px. */
@media (max-width: 700px) {
  .estate__table thead {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
  }

  .estate__table tr {
    display: block;
    padding: 10px 0;
    border-bottom: 1px solid var(--border);
  }

  .estate__table tr:last-child {
    border-bottom: 0;
  }

  .estate__table td {
    display: grid;
    grid-template-columns: 7.5rem minmax(0, 1fr);
    gap: 8px;
    padding: 4px 0;
    border-bottom: 0;
  }

  .estate__table td::before {
    content: attr(data-label);
    font-size: 0.74rem;
    font-weight: 600;
    color: var(--text-dim);
  }

  /* Everything a cell holds in the value column, its second line under its first. Placed by the
     grid alone, a cell's second line ("Every year", "Valid until …") took the next free square,
     which is under the label; and a pill, stretched to the column, read as a bar. */
  .estate__table td > * {
    grid-column: 2;
    justify-self: start;
  }
}

/*
 * ONE CERTIFICATE HANDED IN (js/handin-entry.js) — the same object on a manager's My cinemas and in
 * an admin's compliance box, so the paper reads as itself on its way up and on its way in. Shaped
 * like a record in the history, because it is a record that has not been made yet.
 */
.handin {
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
}

.handin__main {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 10px;
}

.handin__name {
  font-weight: 550;
  font-size: 0.9rem;
}

/* Where it has got to: shaped like the standing pills, and coloured by whose move it is. */
.handin__pill {
  display: inline-flex;
  align-items: center;
  padding: 1px 8px;
  border: 1px solid transparent;
  border-radius: var(--badge-radius, 7px);
  font-size: 0.72rem;
  font-weight: 650;
  white-space: nowrap;
}

/* Waiting is somebody's move and nobody's fault: the info colour, not the warning one. */
.handin__pill--waiting {
  background: var(--info-soft);
  color: var(--info);
  border-color: color-mix(in oklab, var(--info) 35%, transparent);
}

.handin__pill--filed {
  background: var(--ok-soft);
  color: var(--ok);
  border-color: color-mix(in oklab, var(--ok) 35%, transparent);
}

.handin__pill--turned {
  background: var(--danger-soft);
  color: var(--danger);
  border-color: color-mix(in oklab, var(--danger) 40%, transparent);
}

.handin__meta,
.handin__decided {
  margin: 4px 0 0;
  font-size: 0.8rem;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}

/* A date is one word: the line may break before it, never inside it. */
.handin__day,
.handins__day {
  white-space: nowrap;
}

/* The admin's reason for turning one away — the words the manager came to this line to read. */
.handin__reason {
  color: var(--text);
  font-weight: 550;
}

.handin__notes {
  margin: 6px 0 0;
  font-size: 0.85rem;
  color: var(--text-dim);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.handin__docs {
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-top: 8px;
}

.handin__doc {
  margin: 0;
  font-size: 0.82rem;
  overflow-wrap: anywhere;
}

.handin__size {
  color: var(--text-faint);
  font-size: 0.76rem;
}

/* A hand-in with nothing in it: only its own manager ever sees one, and only they can mend it. */
.handin__empty {
  margin: 6px 0 0;
  font-size: 0.8rem;
  color: var(--warn);
}

.handin__acts {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 10px;
}

/* The hand-in box: which building it is for, over its fields. */
.handin__for {
  margin: 0 0 12px;
  font-weight: 600;
}

/* Two fields to a row where they fit, one where they do not — .compliance__form-row's rule. */
.handin__row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 0 16px;
}

.handin__chosen {
  overflow-wrap: anywhere;
}

/* A note for the admin is an aside, as on the record form: short, not the app-wide 148px. */
#handin-dialog textarea {
  min-height: 84px;
}

/*
 * A MANAGER'S INSPECTIONS, at the foot of each building on My cinemas: where each one stands, the
 * way to hand in a certificate, and what became of each handed in. Set off from the screens above by
 * a rule and the card's own side padding.
 */
.handins {
  padding: 14px 20px 18px;
  border-top: 1px solid var(--border);
}

.handins__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 8px 12px;
  margin-bottom: 6px;
}

.handins__title {
  margin: 0;
  font-size: 0.95rem;
}

/* One row per inspection, on one shared grid, so the pills and their dates line up down the list. */
.handins__lines {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  margin: 0;
  padding: 0;
  list-style: none;
}

.handins__line {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: subgrid;
  align-items: center;
  gap: 4px 14px;
  padding: 9px 0;
  border-top: 1px solid var(--border);
}

.handins__line:first-child {
  border-top: 0;
}

/* The pill sits on the row's line here; in the compliance grid it tops a cell of several lines. */
.handins__line > .standing {
  align-self: center;
}

.handins__name {
  min-width: 0;
  font-weight: 550;
  font-size: 0.9rem;
  overflow-wrap: anywhere;
}

.handins__until {
  font-size: 0.8rem;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}

/* The booked day, and the certificate already in, on a line of their own under the row. */
.handins__also {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  gap: 2px 14px;
  margin: 0;
  font-size: 0.8rem;
}

.handins__flag {
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}

/* A booked day gone by with nothing recorded: the one that asks somebody to pick up the phone. */
.handins__flag--late {
  color: var(--warn);
  font-weight: 550;
}

/* The certificate is in: said in the info colour its pill wears below, as the answer to the row. */
.handins__flag--handed {
  color: var(--info);
  font-weight: 550;
}

.handins__sub {
  margin: 16px 0 8px;
  font-size: 0.82rem;
  font-weight: 650;
  color: var(--text-dim);
}

.handins__list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* A phone: the name on a line of its own, with the pill and its date under it. */
@media (max-width: 560px) {
  .handins__lines {
    grid-template-columns: auto minmax(0, 1fr);
  }

  .handins__name {
    grid-column: 1 / -1;
  }
}

/* A count riding on a button, the same badge the tab strip uses. */
.btn__count {
  margin-left: 7px;
  padding: 1px 7px;
  border-radius: 999px;
  background: var(--warn-soft);
  color: var(--warn);
  font-size: 0.74rem;
  font-weight: 650;
  font-variant-numeric: tabular-nums;
}

@media (max-width: 560px) {
  /* The tile and the gap give up a few pixels, so "Printable QR placards" and its icon
     still sit inside a 320px phone with something either side of them. */
  .page-head__title {
    gap: 10px;
  }

  .page-head__icon {
    width: 36px;
    height: 36px;
    border-radius: 11px;
  }

  .page-head__icon svg {
    width: 19px;
    height: 19px;
  }

  /* Full width on a phone, which is what a page's one action should be there. Stacked
     rather than side by side: two full-width buttons on one row is two half-width
     buttons, and the labels wrap to three lines each. */
  .page-head__actions,
  .page-head > .btn {
    width: 100%;
    flex-direction: column;
  }

  .page-head__actions .btn,
  .page-head > .btn {
    width: 100%;
  }
}

/* --------------------------------------------- collapsible screens section
   A 16-screen cinema would push everything below it off the page, so the
   auditorium list is a disclosure that starts closed. */
.screens {
  border-bottom: 1px solid var(--border);
}

.screens:last-child {
  border-bottom: 0;
}

.screens__summary {
  display: flex;
  align-items: baseline;
  gap: 10px;
  flex-wrap: wrap;
  padding: 12px 20px 12px 42px;
  position: relative;
  cursor: pointer;
  background: color-mix(in oklab, var(--p-base) 45%, transparent);
  user-select: none;
  transition: background-color 140ms var(--ease);
}

/* Replace the native triangle with a chevron we can animate. */
.screens__summary::-webkit-details-marker {
  display: none;
}

.screens__summary {
  list-style: none;
}

.screens__summary::before {
  content: '';
  position: absolute;
  left: 20px;
  top: 50%;
  width: 7px;
  height: 7px;
  margin-top: -5px;
  border-right: 2px solid var(--text-dim);
  border-bottom: 2px solid var(--text-dim);
  transform: rotate(-45deg);
  transition: transform 160ms var(--ease);
}

.screens[open] > .screens__summary::before {
  transform: rotate(45deg);
  margin-top: -6px;
}

.screens__summary:hover {
  background: var(--surface-3);
}

.screens[open] > .screens__summary {
  border-bottom: 1px solid var(--border);
}

.screens__title {
  font-weight: 680;
  font-size: 0.95rem;
  letter-spacing: -0.01em;
}

.screens__meta {
  font-size: 0.83rem;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}

/* Unsaved deletions are the one thing that must be obvious while closed. */
.screens__meta--warn {
  color: var(--danger);
  font-weight: 600;
}

/* Guidance only matters once the list is open. */
.screens__hint {
  margin-left: auto;
  display: none;
}

.screens[open] > .screens__summary .screens__hint {
  display: inline;
}

/* ------------------------------------------------------ the holiday shelf
   The rule days as a ticked list a year, the typed ranges under them, and the
   form. A disclosure like the auditoriums, because a year of holidays under
   every cinema is a page nobody asked for. See holidayList in js/locations.js. */
.holidays__list {
  list-style: none;
  margin: 8px 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 4px 18px;
}

.holidays__list--ranges {
  grid-template-columns: 1fr;
}

.holidays__row {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 28px;
  font-size: 0.9rem;
}

.holidays__tick {
  display: flex;
  align-items: center;
  gap: 10px;
  cursor: pointer;
}

.holidays__tick input {
  margin: 0;
  accent-color: var(--accent);
}

.holidays__day {
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
  min-width: 6.5em;
}

/* A day this building does not observe stays listed — struck, so the tick can go back on. */
.holidays__row--skipped .holidays__name {
  text-decoration: line-through;
  color: var(--text-faint);
}

.holidays__row .btn {
  margin-left: auto;
}

.holidays__form {
  padding: 14px 20px 18px;
}

.holidays__fields {
  display: flex;
  flex-wrap: wrap;
  gap: 12px 16px;
  align-items: flex-end;
  margin-top: 10px;
}

.holidays__fields .field {
  margin: 0;
  min-width: 160px;
  flex: 1 1 160px;
}

.holidays__fields select {
  width: 100%;
}

.holidays__submit {
  flex: 0 0 auto;
  min-width: 0;
}

/* ------------------------------------------------- revealed edit actions
   Rows show a single Edit button. The panel it opens holds the fields plus
   everything else you can do to that row, separated by a rule so the
   destructive options are not mistaken for part of the form. */
.edit-more {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  width: 100%;
  margin-top: 14px;
  padding-top: 14px;
  border-top: 1px dashed var(--border-strong);
}

.edit-more__label {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-weight: 600;
  color: var(--text-faint);
  margin-right: 2px;
}

/* -------------------------------------------------- managers on a cinema */
.managers {
  border-bottom: 1px solid var(--border);
}

/* A cinema with nobody assigned is a real problem — validated reports would
   have nowhere to go — so it is called out rather than stated flatly. */
.managers__warn {
  color: var(--warn);
  font-weight: 600;
}

.managers__head {
  display: flex;
  align-items: baseline;
  gap: 10px;
  flex-wrap: wrap;
  padding: 11px 20px;
  background: color-mix(in oklab, var(--p-base) 45%, transparent);
  border-bottom: 1px solid var(--border);
}

/* A select used as an action inside a row of buttons, rather than a form field:
   sized to its content and matched to .btn--sm. */
.inline-select {
  width: auto;
  flex: 0 0 auto;
  min-height: 34px;
  padding: 5px 30px 5px 11px;
  font-size: 0.85rem;
  border-radius: 8px;
  background-position: right 13px center, right 8px center;
}

.inline-select:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/* ------------------------------------------- staged (unsaved) deletions */

/* A row marked for deletion but not yet saved. Tinted and struck through so it
   is obviously different from a row that is simply archived. */
.complaint--staged {
  background: var(--danger-soft);
}

.auditorium__number--struck {
  text-decoration: line-through;
  text-decoration-thickness: 2px;
  text-decoration-color: color-mix(in oklab, var(--danger) 70%, transparent);
  color: var(--text-dim);
}

.badge--pending-delete {
  background: transparent;
  color: var(--danger);
  border-color: var(--danger);
  border-style: dashed;
}

/* The commit point for staged deletions. */
.savebar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 14px;
  padding: 16px 20px;
  background: linear-gradient(180deg, var(--danger-soft), var(--surface-2));
  border-top: 1px solid color-mix(in oklab, var(--danger) 45%, transparent);
}

.savebar__text {
  flex: 1 1 260px;
  min-width: 0;
}

/*
 * The Settings page's save bar follows you down the page. Its neutral tint is not
 * the dashed-danger one the locations page uses — nothing here is destructive,
 * and borrowing the red would cry wolf.
 */
.savebar--sticky {
  position: sticky;
  /* Clear of the very bottom so it reads as floating over the page, not welded on. */
  bottom: 16px;
  z-index: 20;
  margin-top: 16px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  background: var(--glass-strong);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  box-shadow: var(--shadow-lift);
}

/*
 * Room to scroll the last row clear of the pinned bar — but only while there IS a
 * bar. It appears on the first change and nowhere else, so holding 180px of empty
 * page open for it the rest of the time would be worse than the bar was.
 *
 * A sticky element still occupies its place in the flow, so the page has to be
 * scrollable far enough to reach it, or the bar stays stuck over the content above
 * it. Sized for its tallest form, the phone layout where the text and buttons wrap
 * onto separate rows.
 */
.shell--medium:has(#savebar:not([hidden])) {
  padding-bottom: 180px;
}

/* ------------------------------------------------- settings: one shelf at a time
 *
 * The rail — shared with the console, see .railed further down — chooses a group, and
 * everything not on it comes off the page rather than being dimmed. A settings page's
 * problem IS how much is on it, and a greyed row still costs a line to read past.
 *
 * Two rules and no layout in JavaScript. The shell widens by exactly the rail's width
 * plus its gap, so the column the settings live in stays the 880px it has always been
 * instead of losing 200 of them to the menu. And a reader with only one shelf — a
 * general manager has only their own — gets no rail and no column, which is precisely
 * the page they had before any of this existed.
 */
.shell--medium:has(.rail:not([hidden])) {
  max-width: 1100px;
}

.railed:has(> .rail[hidden]) {
  grid-template-columns: minmax(0, 1fr);
}

/*
 * Not the `hidden` attribute, deliberately. That one is how a ROLE takes a section away
 * entirely — a manager has no business seeing Circuits — and it is read all over
 * settings.js to mean exactly that. Two mechanisms writing one attribute would let
 * "you may not see this" and "this is on another shelf" overwrite each other, and the
 * loser would be a section somebody is permitted to reach and cannot.
 */
.section--elsewhere {
  display: none;
}

/* ------------------------------------------------------------- settings page */
/*
 * Every section of the page is a disclosure, closed by default.
 *
 * Open all at once, the page is something you scroll to read rather than something
 * you use: the theme swatches alone are two rows tall, and what you came for is
 * usually one row in one section. Closed, the page is a short list of what is here,
 * and each summary carries a count so a shut section still says how much is inside.
 *
 * Native <details>, the same as the auditorium list on the locations page — so
 * keyboard support, find-in-page opening the right section, and the browser's own
 * expand-all come for free rather than being reimplemented.
 */
.section {
  /* The summary tints the full width, so the card's padding moves inwards to it. */
  padding: 0;
  /* Clips that tint to the rounded corners. */
  overflow: hidden;
}

.section__summary {
  list-style: none;
  display: flex;
  align-items: baseline;
  gap: 10px;
  flex-wrap: wrap;
  padding: 16px 22px 16px 44px;
  position: relative;
  cursor: pointer;
  user-select: none;
  transition: background-color 140ms var(--ease);
}

/* Replace the native triangle with a chevron we can animate. */
.section__summary::-webkit-details-marker {
  display: none;
}

.section__summary::before {
  content: '';
  position: absolute;
  left: 22px;
  top: 50%;
  width: 7px;
  height: 7px;
  margin-top: -5px;
  border-right: 2px solid var(--text-dim);
  border-bottom: 2px solid var(--text-dim);
  transform: rotate(-45deg);
  transition: transform 160ms var(--ease);
}

.section[open] > .section__summary::before {
  transform: rotate(45deg);
  margin-top: -6px;
}

.section__summary:hover {
  background: var(--surface-3);
}

/* Inset, because the card clips its overflow and a +2px offset ring would be cut
   off on three sides. */
.section__summary:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: -3px;
}

.section[open] > .section__summary {
  border-bottom: 1px solid var(--border);
}

/*
 * Still a real <h2> inside the summary — which HTML allows — so a screen reader
 * user can jump between sections by heading instead of tabbing through five
 * disclosures to find the one they want.
 */
.section__title {
  margin: 0;
  font-weight: 680;
  font-size: 1.05rem;
  letter-spacing: -0.01em;
}

.section__meta {
  font-size: 0.83rem;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}

/*
 * An edit hidden inside a closed section is the one thing collapsing must not do
 * quietly, so it is marked on the summary as well as counted in the save bar.
 * Accent rather than danger: nothing here is destructive, and the locations page's
 * red belongs to staged deletions.
 */
.section__meta--dirty {
  color: var(--accent);
  font-weight: 600;
}

/*
 * How many things are waiting, on the home page's Needs attention panel — the one
 * part of a closed disclosure that has to carry weight, since closing it is how you
 * put the warnings away. Declared here beside the other modifier rather than with
 * the panel: both are one class deep, so the plain .section__meta below would win
 * on source order and quietly grey it out.
 */
.section__meta--alert {
  color: var(--warn);
  font-weight: 620;
}

.section__body {
  padding: 20px 22px 22px;
}

#settings-form {
  margin: 0;
}

/*
 * `.card + .card` cannot reach across the form the schema's sections live in, so the
 * gap around it is stated. Only while it holds sections: a General Manager's form is
 * empty, and an empty form should take up no room at all.
 */
#settings-form:has(.section) {
  margin: 16px 0;
}

.setting + .setting,
/* The theme card's circuit-wide rows sit in a wrapper of their own, so the divider
   between "Your theme" and what follows has to cross it — going in, and coming out
   again to whatever the card ends with. Adjacency cannot see through an element, and
   the row after that wrapper is a sibling of the WRAPPER rather than of a setting. */
#theme-circuit > .setting:first-child,
#theme-circuit + .setting {
  margin-top: 22px;
  padding-top: 22px;
  border-top: 1px solid var(--border);
}

/* A section that opens with a line about itself before its first control. Without
   this the intro and the first setting's label stack as one four-line paragraph. */
.section__body > .field__hint:first-child + .setting {
  margin-top: 18px;
}

.setting__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin-bottom: 4px;
}

.setting__label {
  font-weight: 650;
  font-size: 0.95rem;
  color: var(--text);
}

/* Pushed to the right so the labels down the page stay a readable column. */
.setting__source {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  white-space: nowrap;
}

.setting__reset {
  border: 0;
  background: none;
  padding: 2px 4px;
  color: var(--text-dim);
  font: inherit;
  font-size: 0.8rem;
  text-decoration: underline;
  text-decoration-color: var(--border-strong);
  cursor: pointer;
}

.setting__reset:hover {
  color: var(--accent);
}

.setting__control {
  margin-top: 10px;
}

.setting__control input[type='text'],
.setting__control input[type='email'] {
  width: 100%;
}

.setting__control--inline {
  display: flex;
  align-items: center;
  gap: 9px;
}

/* A picker whose choices are longer than half a phone: its button goes under it on a narrow
   screen rather than cutting short the words it is there to show ("Kitchen hood suppression").
   On a desk the two still share a line. */
.setting__control--wrap {
  flex-wrap: wrap;
}

.setting__control--wrap select {
  flex: 1 1 20rem;
  min-width: 0;
}

.setting__control--inline input[type='checkbox'] {
  width: 18px;
  height: 18px;
  margin: 0;
  accent-color: var(--accent);
  cursor: pointer;
}

.setting__inline-label {
  font-size: 0.9rem;
  color: var(--text-dim);
  cursor: pointer;
}

.setting__pills {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

/*
 * The last remaining issue type is disabled: a form with nothing to submit is a
 * broken form, and finding that out from a guest holding a phone is too late.
 */
.pill input:disabled + span {
  cursor: not-allowed;
}

.pill input:disabled:checked + span {
  opacity: 0.75;
}

/* ------------------------------------------------------------ theme picker
   Swatches, not a dropdown: the thing being chosen is how it looks, and a list of
   names tells you nothing about which one you want.

   Each card carries data-preview="<id>", and the theme blocks at the top of this
   file are re-applied to it below — so a preview is drawn with the real theme's
   own tokens and cannot drift from what selecting it does. */
/*
 * A ROW OF CHIPS, where this was a grid of cards.
 *
 * It held eight themes, each a card with a swatch and a sentence of prose — a block of reading
 * standing between a reader and a choice they make once. Four remain and the names say what
 * they are. Both theme controls on the page use it: see themeSwatch.
 *
 * AND OPEN, where each chip used to paint itself in the theme it offers. On a light page that
 * was three black slabs and one pale one, which the operator called ugly, and it was: the eye
 * read the row as four heavy objects before it read it as a choice. A chip is now drawn in
 * the page's own colours, and only its swatch — the one small filled thing on it — is the
 * theme on offer.
 *
 * The page's colours have to be taken HERE, on the row. Each chip carries data-preview, which
 * re-declares every token for its theme, so var(--text) inside a Dark chip is Dark's text. A
 * custom property is resolved where it is declared and inherited as that value, so these four
 * still read as the page's inside a chip that has redefined everything else.
 */
.themes {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 12px;
  --themes-ink: var(--text);
  --themes-dim: var(--text-dim);
  --themes-line: var(--border-strong);
  --themes-accent: var(--accent);
}

.theme {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 7px 11px 7px 8px;
  text-align: left;
  font: inherit;
  cursor: pointer;
  border: 1px solid var(--themes-line);
  border-radius: var(--radius);
  background: transparent;
  color: var(--themes-ink);
  transition:
    border-color 140ms var(--ease),
    transform 140ms var(--ease);
}

.theme:hover {
  border-color: var(--themes-accent);
  transform: translateY(-1px);
}

.theme--chosen {
  border-color: var(--themes-accent);
  box-shadow: 0 0 0 1px var(--themes-accent) inset;
}

/*
 * The theme itself, small: its ground, and on it three colours — a card, the accent, the text.
 * The ground is the swatch's own fill and its ring is the page's line, so a dark swatch on a
 * dark page and a pale one on a pale page both still have an edge.
 */
.theme__swatch {
  display: flex;
  gap: 3px;
  padding: 4px;
  border-radius: 8px;
  background: var(--bg);
  box-shadow: 0 0 0 1px var(--themes-line);
}

.theme__chip {
  width: 13px;
  height: 13px;
  border-radius: 4px;
  border: 1px solid var(--border-strong);
}

.theme__chip--surface {
  background: var(--surface-2);
}

.theme__chip--accent {
  background: var(--p-accent-fill);
  border-color: var(--p-accent-fill);
}

.theme__chip--text {
  background: var(--p-text);
  border-color: var(--p-text);
}

.theme__name {
  display: flex;
  align-items: center;
  gap: 8px;
  font-weight: 650;
  font-size: 0.92rem;
}

.theme__default {
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 2px 6px;
  border-radius: 5px;
  color: var(--themes-dim);
  border: 1px solid var(--themes-line);
}

.theme__note {
  font-size: 0.8rem;
  color: var(--themes-dim);
  line-height: 1.45;
}

/* Which half of System the chip happens to be showing — the one theme that cannot preview
   itself, since it is whichever of the other two the device is in. */
.theme__note--live {
  color: var(--themes-accent);
  font-weight: 600;
  font-size: 0.76rem;
}


/* Not a preview of anything, and dashed, because it is not a theme: it is "stop choosing". */
.theme--reset {
  align-items: flex-start;
  justify-content: center;
  padding: 8px 11px;
  color: var(--text-dim);
  border-style: dashed;
  font-size: 0.86rem;
  font-weight: 600;
}

.theme--reset:hover {
  color: var(--text);
}

/* --------------------------------------------------------- read-only facts */
.facts {
  margin: 16px 0 0;
}

.facts__row {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 16px;
  padding: 9px 0;
  border-top: 1px solid var(--border);
}

.facts dt {
  flex: 0 0 200px;
  color: var(--text-dim);
  font-size: 0.88rem;
}

.facts dd {
  flex: 1 1 200px;
  margin: 0;
  font-size: 0.88rem;
  font-weight: 600;
  overflow-wrap: anywhere;
}

.facts__warn {
  color: var(--warn);
}

.savebar__text strong {
  display: block;
  font-size: 0.95rem;
}

.savebar .field__hint {
  margin: 3px 0 0;
}

/* ---------------------------------------------------------------- states */
.empty {
  text-align: center;
  /* Premium pass: the brand's own mark where there is nothing to do — the
     moments the queue is clear are exactly where personality is affordable. */
  --reticle: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cg stroke='black' fill='none' stroke-linecap='round'%3E%3Ccircle cx='16' cy='16' r='9' stroke-width='3'/%3E%3Cpath d='M16 3.4V7.4' stroke-width='2.4'/%3E%3Cpath d='M16 24.6V28.6' stroke-width='2.4'/%3E%3Cpath d='M3.4 16H7.4' stroke-width='2.4'/%3E%3Cpath d='M24.6 16H28.6' stroke-width='2.4'/%3E%3C/g%3E%3Ccircle cx='16' cy='16' r='3.2' fill='black'/%3E%3C/svg%3E");
  padding: 56px 24px;
  color: var(--text-dim);
  background: var(--glass);
  box-shadow: var(--rim);
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius-lg);
}

.empty::before {
  content: '';
  display: block;
  width: 42px;
  height: 42px;
  margin: 0 auto 16px;
  background-color: var(--text-faint);
  -webkit-mask: var(--reticle) center / contain no-repeat;
  mask: var(--reticle) center / contain no-repeat;
}

.empty h2 {
  color: var(--text);
}

.empty p {
  margin: 0 auto;
  max-width: 46ch;
}

/*
 * The 404 page, where this box is the whole page rather than a gap in a list.
 *
 * A class rather than the `style="margin-top: 64px"` it used to be. That one attribute was the
 * only thing on any served page that needed 'unsafe-inline' on style-src, so it cost the entire
 * app the tightest directive in its CSP — see SECURITY_HEADERS in src/app.js.
 */
.empty--standalone {
  margin-top: 64px;
}

/* -------------------------------------------------------------- skeleton loaders
   What a page shows while its data is on the wire: grey shapes where the content will
   be, not a spinner beside where it will not. The shapes are honest about layout — a
   queue draws card-shaped ghosts, a table row-shaped ones — so the page does not jump
   when the real thing lands, and the reader already knows what kind of thing is coming.

   The shimmer is one sweeping gradient, and it inherits the app-wide reduced-motion
   rule further down this file: for readers who asked for stillness it degrades to a
   plain grey shape, which is the whole point of doing this in CSS.

   The wrapper carries role="status" and an aria-label, because the shapes themselves
   are silence: a screen reader gets the same "loading" a sighted reader infers. */

.skeleton {
  display: grid;
  gap: 16px;
}

.skel {
  position: relative;
  overflow: hidden;
  height: 12px;
  border-radius: 7px;
  background: color-mix(in oklab, var(--text) 9%, var(--surface-2));
}

.skel::after {
  content: '';
  position: absolute;
  inset: 0;
  transform: translateX(-100%);
  background: linear-gradient(
    90deg,
    transparent,
    color-mix(in oklab, var(--text) 8%, transparent),
    transparent
  );
  animation: skel-sweep 1.5s ease-in-out infinite;
}

@keyframes skel-sweep {
  to {
    transform: translateX(100%);
  }
}

/* The vocabulary: a title, lines of two lengths, a stat-sized block, an icon chip.
   Widths are classes rather than style attributes because the CSP refuses inline
   styles — and a skeleton is the one thing guaranteed to render before any script. */
.skel--title {
  height: 15px;
  width: 38%;
  max-width: 220px;
}

.skel--line-mid {
  width: 65%;
}

.skel--line-short {
  width: 40%;
}

.skel--chip {
  width: 40px;
  height: 40px;
  border-radius: 10px;
}

.skel--tile {
  height: 54px;
}

/* A ghost of a card, holding ghost lines the way the card will hold text. */
.skelcard {
  display: grid;
  gap: 12px;
  padding: 18px 20px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
}

/* A chip beside lines — the shape of a stat card or an account row. */
.skelcard--row {
  grid-template-columns: auto 1fr;
  align-items: center;
}

/* Lines stacked inside a card sit closer than cards sit to each other. */
.skelcard .skeleton {
  gap: 8px;
}

/* A grid of small tiles — the shape of a fact grid or a sheet of QR codes. */
.skel-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  gap: 10px;
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }

  /*
   * Delays too, not only durations.
   *
   * The nav strip's `visibility` is transitioned with a delay rather than a duration, so
   * that it stays visible for the length of the close and the motion is actually seen.
   * Zeroing durations leaves that delay in place, which would keep hidden links in the
   * tab order for a fifth of a second after a reader who asked for no motion had already
   * seen them go.
   */
  * {
    transition-delay: 0s !important;
  }
}

.timeline {
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: 0.85rem;
}

.timeline li {
  padding: 7px 0 7px 14px;
  border-left: 2px solid var(--border-strong);
  color: var(--text-dim);
}

.timeline li strong {
  color: var(--text);
}

details.reveal {
  margin-bottom: 12px;
  border-top: 1px solid var(--border);
  padding-top: 12px;
}

details.reveal > summary {
  cursor: pointer;
  font-size: 0.88rem;
  color: var(--p-lavender);
  font-weight: 550;
  padding: 2px 0;
}

details.reveal > summary:hover {
  color: var(--accent);
}

details.reveal > div {
  padding-top: 14px;
}

/*
 * --------------------------------------------- every collapsing box, while it is moving
 *
 * The height is animated in public/js/disclose.js, which cannot be done in CSS alone yet:
 * `interpolate-size` is Chromium-only and `grid-template-rows: 0fr → 1fr` needs Safari
 * 17.5. What CSS does own is the fade, and it is worth having — a box that only grows
 * reveals its contents by uncovering them, which reads as the panel sliding out from
 * behind the header rather than as the panel arriving.
 *
 * `> :not(summary)` rather than a wrapper, because these eight boxes do not agree on
 * having one: a settings section has a single `.section__body`, but a cinema in the tree
 * has one child per auditorium and the alert has one per screen that is down. One class on
 * the details, and the engine fades however many children there turn out to be.
 *
 * Opacity only — no lift. A transform here would make each child a containing block for
 * the length of the animation, and some of them hold a photograph that opens over the page.
 * The box growing is already the movement; this only has to keep the contents from being
 * fully legible before there is room for them.
 */
@keyframes disclose-in {
  from {
    opacity: 0;
  }
}

@keyframes disclose-out {
  to {
    opacity: 0;
  }
}

details.is-expanding > :not(summary) {
  /* Shorter than the box, and `both`, so the contents are solid a moment before the
     movement stops rather than still arriving after it has. */
  animation: disclose-in 160ms var(--ease) both;
}

details.is-collapsing > :not(summary) {
  /* `--ease-out`, not `--ease`: run forwards, `--ease` puts a 130ms fade at 11% after two
     frames, so the contents were gone before the box had started moving. */
  animation: disclose-out 130ms var(--ease-out) both;
}

/* ------------------------------------------------------- guest form page

   The one page a member of the public ever sees, on a phone, usually in the dark,
   usually annoyed. Everything below is in service of two things: looking like somebody
   built it on purpose, and being usable one-handed without reading anything twice.

   It is deliberately the most designed page in the app. A staff page is furniture for
   somebody who is here every day; this is the whole of a cinema's software as far as
   the person holding it is concerned, and a plain stack of grey boxes says the cinema
   does not much care what they thought. */

.guest {
  /* The hero is a full-bleed band, so the page's own padding cannot indent it. */
  display: flex;
  flex-direction: column;
  min-height: 100dvh;
}

.guest .shell {
  flex: 1 0 auto;
}

.guest-hero {
  position: relative;
  overflow: hidden;
  /*
   * Trimmed once already. At 26px/24px with a larger heading the band took a fifth of a
   * phone screen before the first question, which made the form under it feel cramped
   * even though the form itself had not changed. The mark and the location chip are what
   * this band is for; the space around them was not carrying anything.
   */
  padding: 18px 20px 16px;
  /*
   * Two washes rather than one — a strong one from the top left and a fainter one from
   * the right — which keeps a large flat area from banding on an OLED phone.
   *
   * Both are the accent. The second was a different palette slot for one revision, and
   * the theme test caught it: a slot the stylesheet reads has to exist in all seven
   * themes, and inventing one here would have left a hole in six of them.
   */
  background:
    radial-gradient(760px 300px at 8% -40%, color-mix(in oklab, var(--accent) 30%, transparent), transparent),
    radial-gradient(520px 240px at 108% 10%, color-mix(in oklab, var(--accent) 13%, transparent), transparent),
    linear-gradient(180deg, var(--surface-2), var(--bg));
  border-bottom: 1px solid var(--border);
}

/* Gradient rule along the bottom edge, in place of a heavy colour band. */
.guest-hero::after {
  content: '';
  position: absolute;
  inset: auto 0 0 0;
  height: 2px;
  background: var(--accent-line);
}

.guest-hero__inner {
  position: relative;
  max-width: 640px;
  margin: 0 auto;
}

/*
 * The mark, small. It is here so a guest can tell what they are using, not so the
 * software can introduce itself — the heading below it is what the page is about, and
 * the mark should not out-shout it.
 */
.guest-hero .wordmark {
  font-size: calc(0.9rem * var(--mark-scale));
  letter-spacing: 0.1em;
  opacity: 0.85;
}

.guest-hero h1 {
  margin: 11px 0 10px;
  font-size: clamp(1.3rem, 1.15rem + 1.8vw, 1.6rem);
  letter-spacing: -0.02em;
  line-height: 1.15;
}

/*
 * Where they are, as a chip.
 *
 * The placard has already told us the cinema and the screen, and showing that back is
 * the thing that makes the rest of the form feel short — the reader can see we know
 * where they are, so they do not have to explain it.
 */
.guest-hero__where {
  display: inline-flex;
  /* Its own space, so the chip is not read as attached to whatever is above it. */
  margin-top: 2px;
  align-items: center;
  gap: 8px;
  margin: 0;
  padding: 6px 13px 6px 10px;
  border-radius: 999px;
  border: 1px solid color-mix(in oklab, var(--accent) 30%, var(--border));
  background: color-mix(in oklab, var(--accent) 10%, var(--surface));
  font-size: 0.86rem;
  font-weight: 550;
  color: var(--text);
}

/*
 * What this cinema's own record says — the one line on the page that is about the BUILDING
 * rather than about the form. See src/record.js.
 *
 * Deliberately quieter than the chip above it. The place chip is orientation: it tells a guest
 * we know where they are, and it has to be read. This is a reason to bother, which is a
 * different job — read once, believed, and then not competing with the heading somebody came
 * here to answer. It reads as a caption on the chip, which is what it is.
 *
 * No colour of its own, no icon, no box. A claim about somebody's responsiveness dressed up as
 * a badge starts looking like an advertisement, and an advertisement is the one thing that
 * would make a guest doubt it.
 */
.guest-hero__record {
  margin: 9px 0 0;
  font-size: 0.82rem;
  line-height: 1.35;
  color: var(--text-dim);
}

/* The same line after the send, where it answers "did that go anywhere" instead of "is this
   worth thirty seconds". Set on its own row under the note that says nobody will be in touch,
   so the two are read together rather than one qualifying the other. */
/* What became of the photograph, under the heading and before the reference — one line, quiet,
   and never a reason to doubt the report it sits on. */
.thanks__photo {
  margin: 4px 0 12px;
  font-size: 0.9rem;
  color: var(--text-dim);
}

.thanks__record {
  margin: 10px 0 0;
  font-size: 0.82rem;
  line-height: 1.35;
  color: var(--text-dim);
}

/* ------------------------------------------------------- a guest watching their own report
 *
 * Three steps, as a list rather than a progress bar. A report is not sixty per cent done, and a
 * bar invites somebody to read a rate out of it — "it moved a third in two days, so…" — which is
 * a promise this page must not make. A step has happened or it has not.
 *
 * The line down the left is drawn on the dots rather than as a border on the list, so the last
 * step has nothing hanging below it.
 */
/*
 * The trace page sits in the middle of what it has, rather than under the header.
 *
 * It is one short panel on an otherwise empty screen. Pinned to the top it reads as the top of
 * something longer that failed to load — the eye keeps going down the page looking for the rest
 * of it. This is the whole document, so it should look like the whole document.
 *
 * `svh` rather than `vh`: on a phone `vh` is the height with the address bar HIDDEN, so a page
 * measured in it is taller than the screen until you scroll, and the thing you centred starts
 * slightly below the middle and jumps as the bar collapses. `svh` is the smallest viewport, which
 * is the one that is actually there when the page loads. `vh` first, for anything that has not
 * heard of `svh`.
 *
 * Centred with auto margins rather than `justify-content`, and that is not a preference: a flex
 * item centred by `justify-content` that grows taller than its container overflows in BOTH
 * directions, and the top of it cannot be scrolled to. Auto margins collapse to nothing in that
 * case and the panel simply scrolls, which is what a long panel on a small phone needs to do.
 */
.guest--trace {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  min-height: 100svh;
}

.guest--trace .guest-hero {
  flex: none;
}

.guest--trace #main {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

/*
 * Above centre, by SPLITTING WHAT IS SPARE rather than by reserving a fixed amount below.
 *
 * Two flexible spacers, one either side, growing at 1 and 1.7. Whatever room the panel does not
 * need is divided in that ratio, so the panel sits a little above the middle at every height and
 * the gap above never collapses.
 *
 * A fixed `padding-bottom` was the first attempt and it is wrong in a way worth writing down: it
 * does not scale. Tuned to look right on a tall phone it was 190px, which on a 667px screen is
 * most of the space there is — the panel ended up jammed under the header with a void beneath
 * it, which is the same complaint it was meant to fix, upside down. A ratio has no such tuning
 * to get wrong.
 *
 * `flex: N 0 0` — grow, never shrink, no basis — so when there is nothing spare both spacers are
 * zero and the panel simply scrolls, which is what a long panel on a short phone must do.
 */
.guest--trace #main::before,
.guest--trace #main::after {
  content: '';
  flex: 1 0 0;
}

/*
 * A floor under the gap above, not just a share of what is spare.
 *
 * The ratio alone is only as good as there being something to divide: adding the case number
 * made the panel taller than a 667px phone has room for, the spare space went to zero, and the
 * panel sat one pixel under the header again — the exact complaint, reintroduced by adding
 * content rather than by changing the layout. Its own test caught it.
 *
 * `flex-shrink: 0` above means this floor holds: when the page no longer fits, it scrolls rather
 * than closing the gap, which is the right way round.
 */
.guest--trace #main::before {
  min-height: 28px;
}

/*
 * 1 : 2.2, chosen by measuring rather than by taste. On the phone heights people actually hold
 * — 667 to 900 CSS pixels — that puts the panel's middle between 45% and 51% of the screen:
 * above centre on every one of them, and never so high it reads as pushed. A larger ratio was
 * tried and lands a tall phone at 43%, which starts to look like a mistake in the other
 * direction.
 */
.guest--trace #main::after {
  flex-grow: 2.2;
}

.guest--trace #main > .card {
  width: 100%;
  padding: 26px 22px 22px;
}

/* =========================================================================
   The tracker on a guest's own report

   Three states drawn as a rail: an orb per step, and a thread between two orbs
   that EXISTS ONLY once the second of them has been reached.

   That is the whole idea. A greyed-out line to a step that has not happened
   draws the future as though it were already half there — the eye reads a
   four-fifths-complete rail and the words underneath have to argue it back.
   Nothing ahead is drawn at all, so the line a guest sees is exactly the
   distance their report has actually travelled.

   One orb pulses: the step it is on now. The halo is the ticker's, keyframes
   and all — see .plot__orb-halo, which is the live patron line on the staff
   overview. It is the same statement in both places: this is the part that is
   moving while you look at it. Borrowing it rather than writing a second one
   means there is one piece of deliberate motion in this product and one place
   to change its mind about it.

   Resolved recolours the whole rail green, top to bottom, and the last orb
   keeps pulsing. Green is doing real work here rather than decorating: it is
   the only state a guest is hoping for, and it is legible at arm's length on a
   phone held by somebody who is walking.
   ========================================================================= */
/*
 * The rail's geometry, on the CARD rather than on the list.
 *
 * It was on the list, which was right while the list was the only thing that used it. The
 * closing line underneath has to indent to the same text column, and a sibling cannot read a
 * custom property declared on the <ol> — so it would have been a second copy of the rail width,
 * silently diverging the first time anybody tuned one of them.
 */
.trace-card {
  /* The rail's width, the orb, and the ink. Named so the connector geometry
     below is arithmetic rather than four numbers that have to be kept in step. */
  --trace-rail: 30px;
  --trace-orb: 14px;
  --trace-ink: var(--accent);
  /* How far the thread stops short of an orb, so the two read as joined rather
     than as one shape. */
  --trace-clear: 5px;
  /* The gap between the rail column and the words, shared by the steps' grid and by the
     closing line's indent — the two have to line up and this is the one place it is set. */
  --trace-gap: 12px;
}

.trace {
  list-style: none;
  margin: 0 0 4px;
  padding: 0;
  display: grid;
  /* No gap. The steps' own padding sets the spacing, and the two halves of a
     connector have to meet exactly at the row boundary — a gap between rows is
     a hairline of background straight through the middle of the thread. */
  gap: 0;
}

/* Resolved: the same rail in the one colour a guest was hoping for. */
.trace--complete {
  --trace-ink: var(--ok);
}

.trace__step {
  position: relative;
  display: grid;
  grid-template-columns: var(--trace-rail) 1fr auto;
  align-items: center;
  column-gap: var(--trace-gap);
  padding: 15px 0;
  color: var(--text-faint);
}

/*
 * The orb. Hollow until its step happens, and it is the ONLY thing drawn for a
 * step that has not.
 */
.trace__orb {
  position: relative;
  justify-self: center;
  width: var(--trace-orb);
  height: var(--trace-orb);
  border-radius: 50%;
  border: 2px solid var(--border);
  background: var(--surface);
}

.trace__step--done {
  color: var(--text);
}

.trace__step--done .trace__orb {
  border-color: var(--trace-ink);
  background: var(--trace-ink);
}

/*
 * The thread, in two halves that meet at an orb's row boundary.
 *
 * Two halves rather than one line hung off a single step, because a step's
 * height is not a number this file gets to know: the label wraps on a narrow
 * phone and in a language with longer words. Each step draws the half inside
 * its own box, so the join lands correctly at any height.
 *
 * `--done` draws the half ABOVE its orb, which is what makes the thread appear
 * only when the step it leads to has been reached. `--joined` draws the half
 * below, and is set on a step whose successor is done. Both are set in
 * public/js/trace.js, deliberately, rather than derived here with :has() — a
 * guest's phone may be old, and a selector that silently does nothing on it
 * would leave the rail in pieces on exactly the devices least able to say so.
 */
.trace__step--done:not(:first-child)::before,
.trace__step--joined:not(:last-child)::after {
  content: '';
  position: absolute;
  left: calc(var(--trace-rail) / 2);
  width: 2px;
  transform: translateX(-50%);
  border-radius: 1px;
  background: var(--trace-ink);
}

/*
 * The two halves overlap by a pixel where they meet, and that pixel is not cosmetic.
 *
 * A step boundary lands wherever the type puts it — 427.9px on the phone this was drawn on —
 * so both halves antialias their own edge against a fraction of a device pixel, and the two
 * partial coverages do not add up to a solid one. The result is a faint notch in the middle of
 * every connector: invisible at 1×, plain at 3×, and exactly the sort of thing that looks like
 * a rendering fault on somebody's good phone.
 *
 * Overlapping is the fix rather than rounding the layout, because the layout has no business
 * being on whole pixels: the rail follows the text, and the text is where it is. Both halves
 * are the same opaque colour, so an overlap cannot show.
 */
.trace__step--done:not(:first-child)::before {
  top: -1px;
  bottom: calc(50% + var(--trace-orb) / 2 + var(--trace-clear));
}

.trace__step--joined:not(:last-child)::after {
  top: calc(50% + var(--trace-orb) / 2 + var(--trace-clear));
  bottom: -1px;
}

/*
 * The step it is on now, and only that one.
 *
 * The halo is a sibling of the orb rather than a shadow on it, because it has
 * to scale past its own box without pushing anything: an absolutely positioned
 * ::after inside a 14px orb can grow to 2.6× and disturb no layout at all.
 */
.trace__step--current .trace__orb::after {
  content: '';
  position: absolute;
  inset: -2px;
  border-radius: 50%;
  background: var(--trace-ink);
  opacity: 0.45;
  animation: orb-pulse 2s var(--ease) infinite;
}

/* The same answer the ticker gives: a still halo in the same place, not a
   lesser tracker. Somebody who asked for no motion still gets to see which
   step they are on. */
@media (prefers-reduced-motion: reduce) {
  .trace__step--current .trace__orb::after {
    animation: none;
    opacity: 0.28;
  }
}

.trace__what {
  font-weight: 600;
  font-size: 0.95rem;
}

/* The step being worked on now, said in weight rather than in another colour —
   the orb is already carrying the colour, and two signals for one fact is how a
   panel starts looking busy. */
.trace__step--current .trace__what {
  font-weight: 700;
}

.trace__when {
  font-size: 0.8rem;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}

/*
 * The closing line, under the rail and lined up with the words rather than with the orbs.
 *
 * The indent is the rail width plus the step's own column gap, which is what puts it under
 * "Resolved" instead of under the thread — the arithmetic reads off the same two values the
 * steps use, so the three of them cannot drift apart.
 *
 * No border, no tint, no card. It is one sentence in a panel that is already a list of short
 * lines, and giving it a box of its own would make the smallest thing on the page the loudest.
 */
.trace__outcome {
  /* Room underneath, because what follows is the anonymity note — a different voice entirely,
     and the two run together at the rail's own spacing. */
  margin: 2px 0 14px;
  padding-left: calc(var(--trace-rail) + var(--trace-gap));
}

/* The label reads as a caption, not as a fourth step — smaller, uppercase, and in the faint
   ink the dates use, so nothing about it competes with the three words above. */
.trace__outcomeLabel {
  display: block;
  font-size: 0.7rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.trace__outcomeText {
  margin: 3px 0 0;
  font-size: 0.9rem;
  line-height: 1.45;
  color: var(--text);
  /* A sentence somebody typed into a dashboard, on a phone. Long words break rather than
     push the panel sideways — the one thing a guest's page must never do. */
  overflow-wrap: anywhere;
}

/*
 * The link back, on the confirmation. Set apart by a rule rather than a tint: it is the one
 * thing on that panel somebody has to act on, and it is asking them to keep something.
 */
.thanks__trace {
  margin-top: 16px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
}

.thanks__trace .field__hint {
  margin: 9px 0 0;
}

/* ---------------------------------------------------------------- thank-you code

   The customer's own discount code, on the four pages that can hand one over: the
   report confirmation, the film card, the exit-door card and the tracking page.

   Deliberately the loudest thing in its panel and deliberately not a button. It is
   read aloud at a counter or typed into a checkout, so it wants the mono face, room
   to breathe and selectable text — and nothing that looks pressable, because there
   is nowhere for a press to go. The accent tint is what makes it findable again on
   a tracking page somebody reopened a week later looking for exactly this. */
.thanks__code {
  margin-top: 16px;
  padding: 14px 16px;
  border-radius: var(--radius);
  border: 1px solid color-mix(in oklab, var(--accent) 34%, var(--border));
  background: var(--accent-soft);
  display: grid;
  gap: 8px;
  text-align: center;
}

/* The caption over a thank-you code. Reads exactly like .trace__outcomeLabel beside it on the
   tracking page, and is its OWN class rather than that one reused — a test selects that class
   expecting the single label above the closing line, and borrowing it made two. Shared looks are
   not a reason to share a name. */
.thanks__code-label {
  display: block;
  font-size: 0.7rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.thanks__code-words {
  margin: 0;
  font-size: 0.94rem;
  color: var(--text);
}

/* Two codes on a tracking page — one from filing, one from the fix — stack with the same
   gap the panel uses between its own rows. */
.thanks__code-list {
  display: grid;
  gap: 8px;
}

.thanks__code-value {
  margin: 0;
  font-family: var(--mono);
  font-size: 1.34rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  /* --accent, not --accent-fill: the accent is the slot tuned for text on the background, and
     this sits on --accent-soft which is that background with a little of the accent in it. */
  color: var(--accent);
  /* Long codes exist and a phone is narrow. Breaking anywhere beats overflowing the card. */
  overflow-wrap: anywhere;
  /* The one place in the app where selecting the text IS the interaction. */
  user-select: all;
  -webkit-user-select: all;
}

/* A map pin, drawn rather than fetched: one more request on a phone in a cinema's
   basement wifi is one more thing that can fail to arrive. */
.guest-hero__pin {
  width: 9px;
  height: 9px;
  flex: none;
  border-radius: 50% 50% 50% 0;
  transform: rotate(-45deg);
  background: var(--accent);
}

/* --------------------------------------------------------------- the form */

.guest-form {
  counter-reset: step;
  display: grid;
  gap: 26px;
  /*
   * A little air under the hero. The shell has no top padding, so the first question sat
   * directly against the band's edge — some of what the band gave up is better spent
   * here than left in it.
   */
  padding-top: 20px;
}

/*
 * A numbered step.
 *
 * Counted in CSS rather than written into the markup, because two of them come out
 * depending on the placard: a restroom has no seat, and a cinema still rolling out one
 * category at a time has nothing to choose. Hard-coded numbers would read 1, 3 — and a
 * page that cannot count is the opposite of the impression this is for. An element with
 * `display: none` generates no box and so increments nothing, which is exactly the rule
 * this leans on.
 */
.step {
  counter-increment: step;
  border: 0;
  padding: 0;
  margin: 0;
  min-width: 0;
}

.step[hidden] {
  display: none;
}

.step__title {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 0;
  margin: 0 0 12px;
  font-size: 1rem;
  font-weight: 620;
  letter-spacing: -0.01em;
  color: var(--text);
}

.step__title label {
  margin: 0;
  font: inherit;
  color: inherit;
}

.step__title::before {
  content: counter(step);
  flex: none;
  width: 24px;
  height: 24px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: color-mix(in oklab, var(--accent) 18%, var(--surface-3));
  border: 1px solid color-mix(in oklab, var(--accent) 30%, transparent);
  color: var(--accent);
  font-size: 0.78rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* The fields are the page here, not rows inside a card, so they carry the weight the
   card used to. Bigger targets, and a focus ring that is obvious in a dark room. */
.guest-form input[type='text'],
.guest-form input[type='email'],
.thanks__email input[type='email'],
.guest-form textarea {
  width: 100%;
  padding: 14px 15px;
  font-size: 1rem;
  border-radius: var(--radius);
  border: 1px solid var(--border-strong);
  background: var(--surface);
  color: var(--text);
  transition:
    border-color 140ms var(--ease),
    box-shadow 140ms var(--ease);
}

.guest-form textarea {
  min-height: 132px;
  line-height: 1.5;
  resize: vertical;
}

.guest-form input[type='text']:focus,
.guest-form input[type='email']:focus,
.thanks__email input[type='email']:focus,
.guest-form textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in oklab, var(--accent) 22%, transparent);
}

/* The hint and the counter share a line: two stacked rows of small grey text under a
   textarea is most of a screen's height spent on furniture. */
.guest-form__meta {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin-top: 8px;
}

.guest-form__meta .field__hint {
  margin: 0;
  flex: 1 1 auto;
}

.guest-form__meta .counter {
  flex: none;
}

.guest-form .reveal {
  margin-top: 18px;
}

/* The grid gap is doing the spacing here, so the note drops its own — otherwise the
   restroom form opens with forty-six pixels of nothing under its first sentence. */
.guest-form .scope-note {
  margin: 0;
}

/* --------------------------------------------------------------- sending */

/*
 * The send button rides the bottom of the screen.
 *
 * On a phone this form is two screens long, so the action spent the whole time somebody
 * was filling it in below the fold — and a button you have to go looking for reads as a
 * page that is not sure it wants your report.
 *
 * `sticky`, not `fixed`: it stays in the flow, so it cannot cover the last field, and
 * the page's own scroll end is still the end of the form. The safe-area padding is what
 * keeps it clear of an iPhone's home indicator.
 */
.guest-send {
  position: sticky;
  bottom: 0;
  z-index: 2;
  margin: 6px calc(-1 * var(--shell-pad)) 0;
  padding: 26px var(--shell-pad) calc(14px + env(safe-area-inset-bottom, 0px));
  /*
   * A long fade rather than a short one. Reaching full opacity a fifth of the way down
   * put a hard edge across whatever heading happened to be behind it, which reads as a
   * rendering fault; over half the height it reads as what it is, content passing under
   * a bar.
   */
  background: linear-gradient(180deg, transparent, color-mix(in oklab, var(--bg) 88%, transparent) 42%, var(--bg) 72%);
}

.guest-send__note {
  margin: 9px 0 0;
  text-align: center;
  font-size: 0.78rem;
  color: var(--text-dim);
}

/* ----------------------------------------------------------- the thank you */

.thanks {
  text-align: center;
  padding: 30px 22px 26px;
}

.thanks__mark {
  width: 62px;
  height: 62px;
  border-radius: 50%;
  background: var(--ok-soft);
  color: var(--ok);
  border: 1px solid color-mix(in oklab, var(--ok) 40%, transparent);
  display: grid;
  place-items: center;
  margin: 0 auto 18px;
  box-shadow: 0 0 0 9px color-mix(in oklab, var(--ok) 7%, transparent);
}

.thanks .btn {
  margin-top: 18px;
}

.thanks h2 {
  margin: 0 0 6px;
  font-size: 1.45rem;
  letter-spacing: -0.02em;
}

.thanks__where {
  margin: 0 0 20px;
  color: var(--text-dim);
  font-size: 0.92rem;
}

/*
 * The one email, offered again under the note (see report.html): the form's own field with its
 * button beside it, wrapping under it where the card is too narrow for both. Centred as the rest of
 * the card is; the address itself starts at the field's left edge, as it does on the form.
 */
.thanks__email {
  display: grid;
  gap: 8px;
  margin: 16px 0 0;
}

.thanks__email-label {
  font-size: 0.92rem;
  font-weight: 650;
}

.thanks__email-row {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.thanks__email-row input[type='email'] {
  flex: 1 1 13rem;
  min-width: 0;
  text-align: left;
}

/* Level with the field: the card's own rule puts space above every button it holds. */
.thanks .thanks__email-row .btn {
  flex: 0 0 auto;
  margin-top: 0;
}

.thanks__email .field__hint {
  margin: 0;
  /* Two even lines rather than a full one and a word: the card is wide on a desk. */
  text-wrap: balance;
}

/* What became of it. Focused when the box goes, so a keyboard is not left at the top of the page. */
.thanks__email-done {
  margin: 14px 0 0;
  font-size: 0.95rem;
  font-weight: 600;
}

/* An address to look at again: the one line here the guest has something to fix in. */
.thanks__email-done--fix {
  color: var(--danger);
}

.thanks__email-done:focus {
  outline: none;
}

/*
 * The reference, as something that looks kept.
 *
 * A guest may well show this to whoever is on the desk on the way out, so it is a block
 * with a label and a copy button rather than a line of monospace in a paragraph — which
 * reads as output, not as a thing to hold on to.
 */
.ticket {
  display: grid;
  gap: 8px;
  justify-items: center;
  margin: 0 0 20px;
  padding: 16px;
  border-radius: var(--radius);
  border: 1px dashed color-mix(in oklab, var(--accent) 45%, transparent);
  background: var(--accent-soft);
}

.ticket__label {
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.thanks__ref {
  font-family: var(--mono);
  font-size: 1.35rem;
  font-weight: 650;
  letter-spacing: 0.08em;
  color: var(--accent);
  /* Selectable in one gesture, for the browsers with no clipboard API and the people
     who would rather not press a button they do not recognise. */
  user-select: all;
}

.ticket__copy {
  border: 1px solid color-mix(in oklab, var(--accent) 34%, transparent);
  background: none;
  border-radius: 999px;
  padding: 5px 14px;
  font: inherit;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--accent);
  cursor: pointer;
  transition:
    background-color 140ms var(--ease),
    color 140ms var(--ease);
}

.ticket__copy:hover {
  background: color-mix(in oklab, var(--accent) 14%, transparent);
}

.ticket__copy--done {
  color: var(--ok);
  border-color: color-mix(in oklab, var(--ok) 40%, transparent);
}

.ticket__copy[hidden] {
  display: none;
}

/* ---------------------------------------------------------------- signed
 *
 * The credit lives under the send button now — see `.guest-send__by`. It was a footer at the
 * foot of the document, which on a form this short meant below the fold and read by nobody.
 */

/* --------------------------------------------------------------- mobile */
@media (max-width: 700px) {
  .wordmark {
    font-size: calc(0.95rem * var(--mark-scale));
  }

  /*
   * The header is the swipe surface — see public/js/swipe.js, which gates on this same
   * 700px so the declaration and the gesture cannot come apart.
   *
   * `pan-y` hands the browser the vertical and keeps the horizontal for us, which is what
   * lets the drag move the page without cancelling a single event: there is no scroll
   * left to compete with. `pinch-zoom` is spelled out because `pan-y` alone would also
   * take zooming away, and zooming is somebody reading.
   */
  .page-head {
    touch-action: pan-y pinch-zoom;
  }

  /*
   * Home joins the wordmark on the first row; the rest goes on one line below it.
   *
   * The nav used to drop to its own line whole, which put Home on a second row and
   * the remaining links on a third — three rows of sticky top bar covering the
   * reports somebody came to read. Home is the one link that belongs beside the
   * logo anyway: it is where the logo goes.
   *
   * `display: contents` is how one child of the nav can be lifted onto the row
   * above it — the nav's box disappears and its links become flex items of the top
   * bar, so each can be ordered on its own. The explicit role="navigation" in the
   * markup is what keeps the landmark: an element with no box has, in some
   * browsers, lost the role its tag name implied.
   *
   * Losing the nav's box also loses its left divider, which is the point: on this
   * row Home sits WITH the wordmark, not across a rule from it.
   */
  .nav {
    display: contents;
  }

  #nav-home {
    order: 1;
  }

  /* Right of the first row, opposite the brand. The auto margin also eats the free
     space, which is what pushes the strip below onto its own row. */
  .account {
    order: 2;
    margin-left: auto;
  }

  /*
   * A full-width item of no height: it wraps to a row of its own, so everything after it
   * starts below. Pure layout, and deliberately without a border.
   *
   * It used to carry the rule under the first row, which was right while the strip below
   * was permanent and wrong the moment it could fold: collapsed, that rule sat a dozen
   * pixels above the top bar's own bottom border and the two read as a double line with a
   * band of nothing between them. The rule belongs to the strip it separates, so it moved
   * there — see .nav__rest — and folded there is one line, which is the bar's own.
   */
  .topbar__spacer {
    order: 3;
    flex: 0 0 100%;
    height: 0;
  }

  /*
   * The rest of the nav, on one line of its own.
   *
   * Wrapping was the alternative and it defeated the point: at 390px the three of
   * them need more width than there is, so they took two rows and the bar was as
   * tall as it had been before Home moved. One row that scrolls when it has to is a
   * shorter bar at every width — and the thin scrollbar the stylesheet already gives
   * everything is the affordance that says there is more.
   */
  /*
   * The button, immediately right of Home — see wireNav(), which inserts it there.
   *
   * Sized like a nav link rather than like an icon button, so the row reads as two
   * controls of the same kind rather than a link with a widget bolted beside it. The tap
   * target is the full 34px whatever the glyph inside measures.
   */
  .nav__toggle {
    order: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 34px;
    padding: 0;
    border: 1px solid transparent;
    border-radius: 8px;
    background: none;
    color: var(--text-dim);
    cursor: pointer;
    transition:
      background-color 140ms var(--ease),
      border-color 140ms var(--ease),
      color 140ms var(--ease);
  }

  .nav__toggle:hover {
    color: var(--text);
    background: var(--surface-2);
  }

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

  /* Open, it reads as the pressed control it is — the same treatment the current nav
     link gets, so "this is on" looks the same everywhere in the bar. */
  .nav__toggle[aria-expanded='true'] {
    color: var(--accent);
    background: var(--accent-soft);
    border-color: color-mix(in oklab, var(--accent) 40%, transparent);
  }

  /*
   * The rest of the nav, on one line of its own — folded away until the button is pressed.
   *
   * Wrapping was the alternative to one scrolling line and it defeated the point: at 390px
   * the three of them need more width than there is, so they took two rows and the bar was
   * as tall as it had been before Home moved. One row that scrolls when it has to is a
   * shorter bar at every width — and the thin scrollbar the stylesheet already gives
   * everything is the affordance that says there is more.
   *
   * `max-height` rather than `grid-template-rows: 0fr → 1fr`, which is the tidier modern
   * way to animate to a content height: that needs Safari 17.5, and this is the part of
   * the app most likely to be opened on an older phone. The strip is always exactly one
   * line, so a fixed ceiling is not a guess — and the opacity and the lift are what the
   * eye actually reads as the motion, which is why they carry the shorter duration.
   *
   * `visibility` rather than opacity alone: a strip at zero opacity is still in the tab
   * order, and a keyboard reaching links nobody can see is worse than no animation. It
   * flips at the end of the close so the transition still plays, and at the start of the
   * open so the links are focusable the moment they move.
   */
  .nav__rest {
    order: 4;
    flex: 0 0 100%;
    gap: 2px;
    overflow-x: auto;
    /* Vertical is never wanted here, and `auto` on one axis implies it on the other
       unless it is said. */
    overflow-y: hidden;

    max-height: 0;
    /* Every pixel of the gap between this and the row above lives here, so the fold takes
       it with it — the row-gap is 0 at this width, further down this block. */
    margin-top: 0;
    padding-top: 0;
    padding-bottom: 0;
    /* The rule under the first row. Zero-width rather than transparent so a folded strip
       is exactly nothing, and animated so the line arrives with the links rather than
       snapping in ahead of them. */
    border-top: 0 solid var(--border);
    opacity: 0;
    visibility: hidden;
    transform: translateY(-4px);
    transition:
      max-height 220ms var(--ease),
      margin-top 220ms var(--ease),
      padding-top 220ms var(--ease),
      padding-bottom 220ms var(--ease),
      border-top-width 220ms var(--ease),
      opacity 150ms var(--ease),
      transform 180ms var(--ease),
      visibility 0s linear 220ms;
  }

  .nav__rest--open {
    /* Covers a 34px line plus its padding with room to spare, and nothing more: the
       ceiling only has to be reachable, and a generous one makes the last few frames of
       the open animation move nothing. */
    max-height: 3.25rem;
    /* Tight, because the strip is a drawer in the bar rather than a second bar. The 7 and
       the 6 are the gap above and below the rule; together they are the whole distance
       between Home and the links. */
    margin-top: 7px;
    padding-top: 6px;
    /* Room for the scrollbar, so it does not sit on top of the link text. */
    padding-bottom: 2px;
    border-top-width: 1px;
    opacity: 1;
    visibility: visible;
    transform: none;
    transition:
      max-height 220ms var(--ease),
      margin-top 220ms var(--ease),
      padding-top 220ms var(--ease),
      padding-bottom 220ms var(--ease),
      border-top-width 220ms var(--ease),
      opacity 180ms var(--ease) 40ms,
      transform 200ms var(--ease),
      visibility 0s linear;
  }

  /*
   * The links are the whole width of this strip, so they must not be squeezed — and
   * they are tighter here than on desktop, because the three of them plus 12px of
   * padding each came to 23px more than a 390px phone has. Trimmed to 8px they fit
   * that width exactly, which is the one most people are holding; narrower phones
   * scroll the last one into view.
   *
   * 5px vertical rather than the 7px they get everywhere else, which makes each link
   * exactly 34px — the height of the button that reveals them, so the drawer is one
   * control tall. It is also the shortest this can go and still be a tap target: 34px
   * is what the toggle settled on for the same reason.
   */
  .nav__rest .nav__link {
    flex: 0 0 auto;
    padding: 5px 8px;
  }

  .topbar__inner {
    /*
     * No gap between rows at all — every pixel between the first row and the strip below
     * it is the strip's own margin, so it grows and shrinks with the fold.
     *
     * It was 9px, which was right while the strip was permanent. Folded, that 9px stayed
     * behind as a band of nothing between the first row and the bar's own bottom border,
     * which is half of what read as a double line.
     */
    row-gap: 0;
    /* Home reads as part of the brand at this width rather than as the first of a
       list it is no longer next to. */
    column-gap: 10px;
  }
}

@media (max-width: 560px) {
  .shell {
    --shell-pad: 16px;
    padding-left: var(--shell-pad);
    padding-right: var(--shell-pad);
  }

  .theatre__head,
  .auditorium__head,
  .complaint,
  .savebar {
    padding-left: 16px;
    padding-right: 16px;
  }

  /* A room that folds keeps the room its chevron needs. The rule above weighs what
     .auditorium__head--toggle weighs and is written later, so it took the 42px away and the
     chevron, which stays 22px in, sat on the room's first letter. */
  .auditorium__head--toggle {
    padding-left: 40px;
  }

  .person {
    padding-left: 16px;
    padding-right: 16px;
    row-gap: 8px;
  }

  .person__scope,
  .person__seen {
    flex: 1 1 100%;
    text-align: left;
  }

  .filters__summary {
    flex-wrap: wrap;
    padding-left: 12px;
    padding-right: 12px;
  }

  /* The buttons keep the first row; what is filtering the queue gets its own line
     rather than being truncated to an ellipsis on a narrow screen.
     Buttons stretch at this width, which would leave the bar itself — whose job
     is to open the filters — a sliver to tap. These stay their own size.
     Two classes deep on purpose: the generic `.btn { flex: 1 1 auto }` below
     shares this rule's specificity and would win on source order. */
  .filters__summary .btn {
    order: 1;
    flex: 0 0 auto;
  }

  .filters__active {
    order: 2;
    flex: 1 1 100%;
    white-space: normal;
  }

  .filters__body {
    padding-left: 12px;
    padding-right: 12px;
  }

  /* Still has to clear the chevron. The header wraps to three lines at this
     width, so the chevron sits against the cinema name rather than floating in
     the middle of the block. */
  .theatre__head--toggle {
    padding-left: 38px;
  }

  .theatre__head--toggle::before {
    left: 16px;
    top: 28px;
  }

  .complaint__when {
    margin-left: 0;
    width: 100%;
  }

  /* A report card's reference and time take a line of their own below the badges, left
     aligned under them — but ONE line, as a pair. The rule above is for the Location
     info rows, where `__when` holds a button that does want the full width; inside a
     group the time must not, or it would push the reference off the line. */
  .complaint__id {
    margin-left: 0;
    width: 100%;
  }

  .complaint__id .complaint__when {
    width: auto;
  }

  /*
   * The compliance grid stops being a grid.
   *
   * Four columns in a 400px box gives each about 80px, which is a pill and nothing else — no
   * date under it, and "Record the re-inspection" wrapped onto four lines. So each inspection
   * becomes a block: its name across the top, where it stands and its last visit side by side
   * under that, each with its own label, because a bare date with no column heading above it
   * does not say what it is. The button and the booking take the full width below.
   */
  .compliance {
    display: flex;
    flex-direction: column;
  }

  .compliance__head {
    display: none;
  }

  .compliance__row {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 8px;
    align-items: start;
  }

  .compliance__name,
  .compliance__act {
    grid-column: 1 / -1;
  }

  .compliance__act {
    justify-content: flex-start;
  }

  /* Sized to their words, against the phone rule below that stretches every button across its
     row: one full-width button per inspection is a wall of them, and these are invitations. */
  .compliance__act .btn,
  .compliance__booking .btn {
    flex: 0 0 auto;
  }

  .compliance__cell::before {
    content: attr(data-label);
    display: block;
    font-size: 0.68rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-faint);
    font-weight: 650;
    margin-bottom: 2px;
  }

  /* The well gives up most of its padding. What the reader needs on a narrow screen is
     the card's own inset; a tray showing 12px down each side of it costs 24px of text
     width to say something the card's border already says. Enough is left for the well
     to read as a surface behind the cards rather than as a line beside them. */
  .auditorium__body {
    padding: 7px;
    gap: 8px;
  }

  .theatre__count {
    margin-left: 0;
    width: 100%;
  }

  .btn {
    flex: 1 1 auto;
  }

  /*
   * The location takes a line of its own, so every alert has the same shape.
   *
   * Left to wrap naturally the row broke wherever the name happened to run out: a
   * cinema called Rex kept its age on the first line and pushed the button alone onto
   * the second, while a long one wrapped the name and put age and button together. Same
   * button, same place, but three different stacks down one panel. Forcing the break
   * makes it name / age-and-action / what / update every time.
   */
  .alarm__where {
    flex: 1 1 100%;
  }

  /* Still roomier than it was, but a phone has a fifth of the height to spend and the
     header is not what anybody scrolled to. */
  .page-head {
    padding-top: 30px;
    margin-bottom: 22px;
  }

  .page-head::after {
    margin-top: 10px;
  }
}

/* =========================================================================
   The showtime scheduler

   Rooms down the side, the clock across the top — the layout a TMS uses, which
   is not flattery: the two windows are open side by side in a projection booth
   and a page laid out the other way round would have to be translated rather
   than copied.

   EVERYTHING HERE IS POSITIONED BY MINUTE. `--from` and `--span` are set on the
   grid by js/schedule-grid.js, `--at` and `--for` on each pill, all in minutes;
   `--minute` is the one thing that turns those into pixels.

   Its value below is a STARTING POINT, not the answer: fitDay() measures the box
   and sets `--minute` so the whole day fits on the screen, because a day you have
   to scroll sideways through is the question this page exists to answer, asked
   again. The floor is in the module — under about 0.8px a minute the pills stop
   being readable, so a phone scrolls instead of shrinking further.
   ========================================================================= */
.sched__range {
  margin: 0;
  align-self: flex-end;
  flex: 1 1 200px;
  font-size: 0.86rem;
  color: var(--text-dim);
}

/* Whose clock the times are on — drawn only for a reader outside the building's zone, so it
   is a quiet line rather than a banner. See zoneNote in js/schedule-grid.js. */
.sched__zone {
  margin: 0;
  flex: 1 1 100%;
  font-size: 0.8rem;
  color: var(--text-faint);
}

.sched__zone[hidden] {
  display: none;
}

.sched__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin: 0 0 12px;
}

/*
 * The pickers and what can be done to the whole week, on one line: the cinema and the week on the
 * left, the week's tools across from them, wrapping under the pickers rather than squeezing them.
 */
.sched__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: 12px 16px;
  margin: 0 0 16px;
}

.sched__bar > .sched__toolbar {
  flex: 1 1 480px;
  margin: 0;
}

.sched__bar > .sched__actions {
  justify-content: flex-end;
  margin: 0;
}

/* Nothing in it to show: a phone that has not pressed Edit, on a cinema scheduled by hand. */
.sched__actions:not(:has(> :not([hidden]))) {
  display: none;
}

/* The far end of the day strip: film colours and the clock, and a phone's Edit beside them. */
.weeknav__end {
  display: flex;
  flex: 0 0 auto;
  align-items: center;
  gap: 8px;
  margin-left: auto;
}

/* Film colours and the clock, kept together so that a row too narrow for them and Edit moves
   both, not the clock alone; and able to stack themselves, on a screen that has room for neither
   beside the other. */
.weeknav__settings {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
}

/* The line under the title names the week on a computer and the day on a phone — paintHeadSub in
   js/showtimes.js writes both, and this picks. */
.sched__when--day {
  display: none;
}

@media (max-width: 700px) {
  .sched__when--week {
    display: none;
  }

  .sched__when--day {
    display: inline;
  }

}

/*
 * 12h / 24h — two ways of being right, so a segmented pair rather than a switch.
 *
 * A checkbox would make one of them the absence of the other, and neither is: the marquee outside
 * says 7:30pm and the TMS in the booth says 19:30. Both buttons are always visible with one pressed,
 * which is also what tells a reader the control exists at all — a toggle showing only the state it
 * is in has to be guessed at.
 *
 * Sized to sit in the schedule's toolbar beside the POS badge, so it reads as one row of small
 * controls rather than a thing of its own.
 */
.seg {
  display: inline-flex;
  padding: 2px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface-2);
}

.seg__opt {
  padding: 4px 12px;
  border: 0;
  border-radius: 999px;
  background: none;
  color: var(--text-dim);
  font: inherit;
  font-size: 0.76rem;
  font-weight: 600;
  cursor: pointer;
  /* The pressed pill slides rather than blinks, like every other state change in the app. */
  transition:
    background 160ms var(--ease),
    color 160ms var(--ease);
}

.seg__opt:hover {
  color: var(--text);
}

.seg__opt--on {
  background: var(--accent-fill);
  color: var(--accent-ink);
}

/*
 * A setting with a real off — the scheduler's Film colours — as a switch: the words, and a small
 * track whose knob says on or off. Beside the 12h / 24h pair and dressed like it, so the far end of
 * the day strip is one row of the board's settings rather than two kinds of control.
 *
 * The state is the button's aria-checked and nothing else, so what a screen reader is told and what
 * the eye sees are one attribute and cannot drift apart. Off is the quiet words and a hollow
 * track; on is the words at full strength and the track in the accent, with the knob across.
 */
.switch {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 5px 6px 5px 12px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--text-dim);
  font: inherit;
  font-size: 0.76rem;
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
  transition: color 160ms var(--ease);
}

.switch:hover,
.switch[aria-checked='true'] {
  color: var(--text);
}

.switch__track {
  position: relative;
  flex: none;
  width: 26px;
  height: 14px;
  border-radius: 999px;
  background: var(--border-strong);
  transition: background 160ms var(--ease);
}

.switch__track::after {
  content: '';
  position: absolute;
  top: 2px;
  left: 2px;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--text-dim);
  transition:
    transform 160ms var(--ease),
    background 160ms var(--ease);
}

.switch[aria-checked='true'] .switch__track {
  background: var(--accent-fill);
}

.switch[aria-checked='true'] .switch__track::after {
  background: var(--accent-ink);
  transform: translateX(12px);
}

/*
 * "POS Sync active · last sync 9 min ago" — the badge that stands where the Features button
 * does on a cinema whose schedule is written by its POS.
 *
 * A quiet chip rather than the full-width info notice this replaced: on a circuit that runs a
 * POS, synced is the ordinary state of every cinema, and a paragraph explaining the ordinary
 * every time you open the page is a paragraph that pushes the schedule down and gets skipped.
 * The dot is the one live thing about it — the same reading as the dashboard's pulse: something
 * out there is still talking to us.
 */
.pos-state {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  padding: 6px 12px;
  border: 1px solid color-mix(in oklab, var(--accent) 35%, var(--border));
  border-radius: 999px;
  background: color-mix(in oklab, var(--accent) 6%, var(--surface));
  font-size: 0.78rem;
  font-weight: 550;
  color: var(--text-dim);
  cursor: default;
}

.pos-state::before {
  content: '';
  width: 7px;
  height: 7px;
  flex: none;
  border-radius: 50%;
  background: var(--accent);
}

/* A sync that could not reach the till still says so, in the app's own warn colour rather than
   in a second sentence: the schedule on screen is real, it is just not this minute's. */
.pos-state--warn {
  border-color: color-mix(in oklab, var(--warn) 45%, var(--border));
  background: color-mix(in oklab, var(--warn) 8%, var(--surface));
  color: var(--warn);
}

.pos-state--warn::before {
  background: var(--warn);
}

/*
 * The box the day scrolls sideways in.
 *
 * A nineteen-hour day does not fit on a screen at any sensible scale, so something has to
 * scroll — and it is this box rather than the page. A page that scrolled sideways would take
 * the top bar and the heading with it, and the room names down the left are the labels that
 * must never leave: they are `position: sticky` against THIS scrollport.
 */
.sched__scroll {
  overflow-x: auto;
  overflow-y: hidden;
  background: var(--glass);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  /*
   * NO LEFT PADDING, and that is a fix rather than a style.
   *
   * A sticky element sticks to the scrollport's CONTENT edge, so 12px of left padding here was a
   * 12px strip that the day scrolled through and nothing could cover: a time label or the tail of a
   * pill slid into it and appeared to the left of the room names, sliced. On a phone, where the day
   * never fits and there is always something scrolled off, "08" sat against the word AUDITORIUM.
   *
   * Measured before and after by sampling elementFromPoint across the edge — the topmost thing in
   * the first ten pixels was `grid__hour`, and the corner only took over at twelve.
   *
   * The inset the padding was there for moves onto the sticky column's own padding, which travels
   * with it. Right padding stays: the sticky column is on the left, so nothing scrolls through the
   * far end, and it keeps the last hour of the day off the card's border.
   */
  padding: 12px 12px 12px 0;
  margin-bottom: 24px;
}

.grid {
  --minute: 1.9px;
  --lane: 58px;
  --room: 150px;
  /* The containing block for the "now" line, which crosses every lane and so cannot live in one. */
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 6px;
  /* min-content, so the flex children below keep their computed widths instead of being
     squeezed to fit the box — being wider than the box is the point. */
  min-width: min-content;
}

.grid__ruler {
  display: flex;
  align-items: flex-end;
}

.grid__corner,
.grid__room {
  /* Held against the left edge while the day scrolls under it. Opaque, or the pills would
     slide visibly beneath the names. */
  position: sticky;
  left: 0;
  z-index: 2;
  flex: 0 0 var(--room);
  width: var(--room);
  background: var(--surface);
  /*
   * A soft edge on the right, so a pill passing behind the names reads as going UNDER something
   * rather than being cut in half. The border alone was not enough: a title clipped mid-letter hard
   * against a hairline looks like a rendering fault, and on a phone — where the day never fits and
   * there is always a pill under the column — that is most of what somebody sees.
   */
  box-shadow: 8px 0 10px -8px var(--p-shadow);
}

.grid__corner {
  /* The inset the scroll box used to provide with padding, on the element that keeps it while
     scrolling. Matched to .grid__room below so the heading sits over the names. */
  padding-left: 12px;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-faint);
  font-weight: 600;
  padding-bottom: 4px;
}

.grid__hours,
.grid__track {
  position: relative;
  flex: 0 0 calc(var(--span) * var(--minute));
}

.grid__hours {
  height: 20px;
}

.grid__hour {
  position: absolute;
  left: calc((var(--at) - var(--from)) * var(--minute));
  transform: translateX(-50%);
  /* Doubled classes on the two below: they override this rule's own `transform`, and a modifier
     that only wins because of where it sits in the file is the bug test/stylesheet.test.js is for. */
  font-size: 0.72rem;
  font-variant-numeric: tabular-nums;
  color: var(--text-faint);
  white-space: nowrap;
}

/* The ends are flush rather than centred on their tick. Visible overflow counts towards a scroll
   container's width, so a half-label hanging off the right would put a scrollbar under a day that
   otherwise fits exactly. */
.grid__hour.grid__hour--first {
  transform: none;
}

.grid__hour.grid__hour--last {
  transform: translateX(-100%);
}

/* A label there is no room for at this scale — see thinHours in js/schedule-grid.js, which
   measures rather than guesses. Kept in the row rather than removed so a widened window can
   bring it back without a repaint. */
.grid__hour--off {
  display: none;
}

/* The hour the NOW badge is painted over — see hideHourUnderNow in js/schedule-grid.js. Its own
   class rather than the one above, because thinning and the now-line run on different clocks and
   would otherwise undo each other. */
.grid__hour--covered {
  display: none;
}

.grid__lane {
  display: flex;
  align-items: stretch;
}

.grid__lane--archived {
  opacity: 0.55;
}

/*
 * A screen that is DOWN — an open emergency case on the case monitor, surfaced where the
 * decision gets made. The whole timeline tints rather than only a badge appearing, because the
 * reader is scanning lanes, not reading words: red under the pills says "these shows have no
 * room" before a single name is read. The gridlines stay visible through the tint (they are a
 * gradient painted over this colour), so the shows being rescued can still be read against the
 * clock.
 */
.grid__lane--down .grid__track {
  background-color: var(--danger-soft);
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--danger) 40%, transparent);
}

.grid__lane--down .grid__number {
  color: var(--danger);
}

/* The word beside the name, for the reader the colour alone would leave guessing. */
.grid__downbadge {
  display: inline-block;
  margin-left: 6px;
  padding: 1px 7px;
  border-radius: 999px;
  font-size: 0.62rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  background: var(--danger);
  color: var(--p-base);
  vertical-align: 2px;
}

.grid__room {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 2px;
  /* 12px on the left rather than 2px: it replaces the scroll box's left padding, which had to go —
     see .sched__scroll above. */
  padding: 4px 12px;
  border-right: 1px solid var(--border-strong);
}

.grid__number {
  font-weight: 600;
  font-size: 0.88rem;
  line-height: 1.2;
}

/* A room's name holds its seat count on hover (see seatNote in js/schedule-grid.js). The cursor is
   the only thing that says so — a tooltip nobody knows to hover for is a tooltip nobody reads. */
.grid__number[title] {
  cursor: help;
}

.grid__facts {
  font-size: 0.72rem;
  color: var(--text-faint);
}

/* The keyboard's way onto the grid. Dragging a film out of the panel is the quick way and it
   is not the only way — see the note at the top of js/showtimes.js. */
.grid__add {
  align-self: flex-start;
  margin-top: 2px;
  font: inherit;
  font-size: 0.72rem;
  color: var(--text-dim);
  background: none;
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius-sm);
  padding: 1px 7px;
  cursor: pointer;
}

.grid__add:hover {
  color: var(--accent);
  border-color: var(--accent);
}

/*
 * One room's day.
 *
 * The hour and half-hour lines are a background rather than thirty-eight more elements: a
 * gradient re-scales with `--minute` for free, and nobody reads a gridline. The window always
 * starts on the hour — see windowFor in js/schedule-grid.js — so the first line of the pattern
 * lands on a labelled tick.
 */
.grid__track {
  height: var(--lane);
  border-radius: var(--radius-sm);
  background:
    repeating-linear-gradient(
      90deg,
      var(--border-strong) 0 1px,
      transparent 1px calc(30 * var(--minute)),
      var(--border) calc(30 * var(--minute)) calc(30 * var(--minute) + 1px),
      transparent calc(30 * var(--minute) + 1px) calc(60 * var(--minute))
    ),
    var(--surface-2);
}

.grid__empty {
  margin: 8px 0 0;
  color: var(--text-dim);
  font-size: 0.9rem;
}

/*
 * Where now is: one line down the whole grid, labelled.
 *
 * A child of the grid rather than one per lane, so it is a single element crossing every room —
 * which is what makes it readable as a time rather than as ten marks that happen to line up. It
 * sits in the grid's own coordinate space, so it scrolls with the day; `--room` is added because
 * that space starts at the left edge of the room column, not of the first track.
 *
 * Below the room names in the stack (they are z-index 2) so a scrolled grid slides it under the
 * labels rather than across them, and above the pills' background but not their text.
 */
.grid__now {
  position: absolute;
  top: 18px;
  bottom: 0;
  left: calc(var(--room) + (var(--at) - var(--from)) * var(--minute));
  width: 2px;
  z-index: 1;
  background: var(--danger);
  pointer-events: none;
}

/* The label, so a line on a grid of times is not something to guess at. */
.grid__now::before {
  content: 'now';
  position: absolute;
  top: -15px;
  left: -1px;
  padding: 0 5px;
  border-radius: 3px;
  background: var(--danger);
  color: var(--bg);
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1.5;
}

/* Near the right edge the label reads to the LEFT of its line — see markNow in
   js/schedule-grid.js: read rightward there, it pokes past the grid and mints a
   sideways scrollbar on a day that fits. */
.grid__now--flip::before {
  left: auto;
  right: -1px;
}

/*
 * A booking.
 *
 * The time is above the name because the time is what is being checked; the name is how you
 * know which line you are on. A pill can be narrower than its own label — a short film at this
 * scale is about 200px, a trailer reel far less — so the name truncates and the `title` carries
 * the whole thing.
 */
.showing {
  position: absolute;
  top: 4px;
  bottom: 4px;
  left: calc((var(--at) - var(--from)) * var(--minute));
  width: calc(var(--for) * var(--minute));
  min-width: 58px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 1px;
  overflow: hidden;
  text-align: left;
  padding: 4px 8px;
  border: 1px solid color-mix(in oklab, var(--accent) 45%, var(--border-strong));
  border-left: 3px solid var(--accent);
  border-radius: var(--radius-sm);
  background: var(--accent-soft);
  color: var(--text);
  cursor: grab;
  /* The gesture must not also scroll the day sideways on a touchscreen, and this is the
     modern way to say so — see js/showtimes.js, which does NOT preventDefault on pointerdown
     because the pill is also a button somebody clicks. */
  touch-action: none;
}

.showing:hover {
  border-color: var(--accent);
  box-shadow: var(--shadow);
}

/*
 * The editor's pill is a wrapper holding two buttons — the face, and the quick-remove × —
 * because a button cannot legally hold another. The board's pills and the drag ghost are
 * still single elements, so the layout above stays on .showing itself and these two sit
 * absolutely inside it.
 */
.showing__open {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 1px;
  min-width: 0;
  padding: 4px 8px;
  overflow: hidden;
  text-align: left;
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  border-radius: inherit;
  cursor: inherit;
}

.showing__x {
  position: absolute;
  top: 1px;
  right: 1px;
  z-index: 1;
  width: 18px;
  height: 18px;
  display: grid;
  place-items: center;
  padding: 0;
  font-size: 0.85rem;
  line-height: 1;
  color: var(--text-dim);
  background: color-mix(in oklab, var(--surface) 55%, transparent);
  border: 0;
  border-radius: 5px;
  cursor: pointer;
  opacity: 0.55;
}

.showing:hover .showing__x,
.showing__x:focus-visible {
  opacity: 1;
}

.showing__x:hover {
  color: var(--danger);
  background: var(--surface);
}

.showing__time {
  font-size: 0.74rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
  white-space: nowrap;
}

.showing__name {
  font-size: 0.8rem;
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* A film that finishes after midnight, marked on the edge it runs off. The grid already draws
   past 24:00 so the end time is visible; this is what says the date has changed. */
.showing.showing--overnight {
  border-right: 3px solid var(--warn);
}

/*
 * The occupancy gauge: the pill fills bottom-to-top with how sold the show is, `--fill` being
 * sold/(sold+remaining) — see gauge() in js/schedule-grid.js. A BACKGROUND-IMAGE rather than an
 * extra element, because a background can never sit on top of the times and the class composes
 * with every pill variant (flat, overnight, synced) without a stacking rule anywhere. The hard
 * stop in the gradient is the waterline.
 */
.showing--gauge {
  background-image: linear-gradient(
    to top,
    color-mix(in oklab, var(--accent) 30%, transparent) 0 calc(var(--fill) * 100%),
    transparent calc(var(--fill) * 100%)
  );
}

/* The read-only board's pills are divs, so nothing about them should suggest a control. */
/* The board's pills open the details box now, and so do the editor's while the POS owns its
   grid — in both places the hand that grabs would be a lie, and the pointer is the truth. */
.showing.showing--flat,
.grid--pos .showing {
  cursor: pointer;
}

/* Where a booking was, while it is being dragged. Pointer-transparent, so elementFromPoint
   finds the lane underneath rather than the thing being carried over it. */
.showing.showing--dragging {
  opacity: 0.45;
  border-style: dashed;
  box-shadow: none;
  pointer-events: none;
}

/* A film out of the panel, carried under the cursor. Fixed rather than in a lane: it has no
   place on the grid yet, which is what is being decided. */
.showing.showing--ghost {
  position: fixed;
  top: 0;
  bottom: auto;
  left: 0;
  width: auto;
  min-width: 132px;
  max-width: 240px;
  height: auto;
  z-index: 70;
  transform: translate(-50%, -50%);
  background: var(--surface);
  box-shadow: var(--shadow-lift);
  pointer-events: none;
}

/* Not over a lane, so there is nowhere for it to land. */
.showing.showing--ghost-off {
  opacity: 0.6;
  border-color: var(--border-strong);
}

/*
 * RECOMMENDATIONS, SKETCHED ONTO THE GRID — see the `advice` argument of paintDay in
 * js/schedule-grid.js and paintAdvice in js/showtimes.js.
 *
 * A slot that changes film is drawn as a SPLIT PILL: the showing as it stands in the top half,
 * struck through and dimmed, and the film the advice would put there in the bottom half, dashed,
 * because it does not exist. One slot, two films, read top to bottom as "this becomes that" —
 * which is the whole of what a move is. A showing that would move ROOMS is the same picture with
 * both halves saying where from and where to. A showing worth cutting with nothing to put in its
 * place keeps the whole pill, struck.
 *
 * The sketch never takes the pointer: the real pill underneath keeps its click and its drag,
 * and the ghost is a drawing over it.
 */
.showing.showing--cutting,
.showing.showing--moving {
  opacity: 0.55;
  border-style: dashed;
}

.showing.showing--cutting {
  border-left-color: var(--danger);
}

/* The name is struck, and only the name: the words the advice adds after it are not. The
   sketch wraps the name's text in its own span for exactly this — text-decoration reaches
   every in-flow child, so a strike on the name line would strike the advice too. */
.showing.showing--cutting .showing__name-text {
  text-decoration: line-through;
}

/* The top half, when something is drawn underneath it. Half a lane holds one line, and the line
   that matters here is the NAME with the advice after it — "Cascade Point · → 2 · The Roxy" —
   so the time goes: the ghost sits in the same slot, and the slot's time is on the ruler. */
.showing.showing--halved {
  bottom: 50%;
}

.showing.showing--halved .showing__time {
  display: none;
}

.showing.showing--advised {
  top: 50%;
  bottom: 3px;
  z-index: 2;
  justify-content: center;
  padding-top: 2px;
  padding-bottom: 2px;
  border: 1px dashed var(--accent);
  border-left-width: 3px;
  background: color-mix(in oklab, var(--accent) 16%, var(--surface));
  cursor: default;
  pointer-events: none;
}

/* A cut with nothing to put in its place keeps the whole pill — there is no bottom half. */
.showing.showing--advised .showing__name {
  font-size: 0.76rem;
}

/* The advice's words ride on the name line — "→ Cascade Point", "from Screen 1" — rather than
   in a third line a lane has no height for. On the struck top half the words are NOT struck. */
.showing__name[data-advice]::after {
  content: ' · ' attr(data-advice);
  font-weight: 500;
  font-size: 0.72rem;
  color: var(--text-dim);
}

/* The AI Scheduling Agent's button, on a line beside the Optimizer's rather than at the head of
   the band it opens — see #agentbar in showtimes.html for why it moved. */
.agentbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 12px;
  margin: 0 0 12px;
}

/* The band that says the sketch is on, and what the day's changes are. Kin to .planbar. */
.advicebar {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin: 0 0 12px;
}

.advicebar--on {
  padding: 10px 14px;
  background: var(--accent-soft);
  border: 1px solid var(--border-strong);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius);
}

.advicebar__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 16px;
}

.advicebar__text {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.advicebar__lines {
  margin: 0;
  padding: 0 0 0 18px;
  font-size: 0.82rem;
  color: var(--text);
}

.advicebar__lines li + li {
  margin-top: 2px;
}

.advicebar__lines .advicebar__quiet {
  color: var(--text-dim);
  list-style: none;
  margin-left: -18px;
}

/*
 * WHERE THE REST OF IT IS: one chip per day the drawn picture would change, and a way there.
 *
 * The band above argues about the day on screen; a plan argues about the week, and most of what
 * it proposes is therefore on a day the reader is not looking at. Reported from a phone, of two
 * plans that differed: "the different plans have different suggestions, having it visually show
 * it on the schedule too would be beneficial" — the overlay WAS following the chosen plan, but
 * from a day it left alone the two plans looked identical.
 *
 * Only the days with something on them: a row of seven chips where five say nought is a worse
 * index than a row of two that say where to look. The day strip above the grid carries a dot on
 * the same days; this is the same fact in words, with the number on it.
 */
.advicebar__jumps {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  font-size: 0.8rem;
}

.advicebar__jumpsaid {
  color: var(--text-dim);
}

.advicebar__jump {
  font: inherit;
  font-size: 0.8rem;
  padding: 2px 9px;
  border-radius: 999px;
  border: 1px solid var(--border-strong);
  background: transparent;
  color: var(--text);
  cursor: pointer;
  font-variant-numeric: tabular-nums;
}

/* The day already on screen is not a place to go, so it reads as a marker rather than a
   control — outlined in the accent, with the page's own ink. */
.advicebar__jump--here {
  border-color: var(--accent);
  color: var(--accent);
}

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

/*
 * The write-up under the band: the pager between the sketch and every plan written for the
 * week, the press, and the model's prose drawn as prose. Its own dashed rule, because above it
 * is what the rules argue and below it is what the assistant argued, and a reader should be
 * able to tell whose words they are reading. See paintWriteup in js/showtimes.js.
 */
.advicebar__writeup {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding-top: 8px;
  border-top: 1px dashed var(--border-strong);
}

/*
 * THE AGENT AT WORK, said twice over: the spark on its own button breathing, and the rule above
 * the block running with light.
 *
 * The press reads three or four tools and then writes a page of prose, which is tens of seconds
 * on a real week — and a page that does not move through a wait that long reads as broken rather
 * than as busy. The spark is the same mark, on the same cycle, as the AI Reviewer Agent's own box
 * (verify-spark, further up this file), so the two agents behave alike wherever a reader meets
 * them. The sweep is the second signal: the block being waited on says so itself, which the
 * button alone cannot do once somebody has scrolled to the prose.
 */
.advicebar__writeup--working {
  position: relative;
  border-top-color: transparent;
}

.advicebar__writeup--working::before {
  content: '';
  position: absolute;
  inset: 0 0 auto;
  height: 1px;
  background: linear-gradient(
    90deg,
    transparent 0%,
    color-mix(in oklab, var(--accent) 70%, transparent) 35%,
    var(--accent) 50%,
    color-mix(in oklab, var(--accent) 70%, transparent) 65%,
    transparent 100%
  );
  background-size: 45% 100%;
  background-repeat: no-repeat;
  animation: writeup-sweep 1.9s linear infinite;
}

@keyframes writeup-sweep {
  0% {
    background-position: -45% 0;
  }
  100% {
    background-position: 145% 0;
  }
}

/* The agent's own spark, breathing while it thinks — the reviewer's cycle, at a button's size. */
.agentrow__spark--working {
  animation: verify-spark 1.7s ease-in-out infinite;
}

/*
 * A button that cannot be pressed because it is BUSY is not the same as one that cannot be
 * pressed because there is nothing behind it, and the grey every other disabled control wears
 * says the second thing. While the agent is out, its button keeps the accent it had and only the
 * spark moves — the thing under the reader's finger is the thing that is working, and it should
 * look like it.
 */
.advicebar__writeup--working .agentrow__btn[disabled] {
  opacity: 1;
  background: color-mix(in oklab, var(--accent) 13%, var(--surface));
  color: var(--accent);
  border-color: color-mix(in oklab, var(--accent) 32%, transparent);
  cursor: progress;
}

/*
 * A reader who asked for no motion is told the same thing without it: the rule stays lit across
 * its whole width rather than running, and the spark holds still. The note beside them was always
 * the thing carrying the meaning.
 */
@media (prefers-reduced-motion: reduce) {
  .advicebar__writeup--working::before {
    animation: none;
    background: var(--accent);
    background-size: 100% 100%;
    opacity: 0.7;
  }

  .agentrow__spark--working {
    animation: none;
  }
}

.writeup__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 12px;
}

/*
 * TELLING THE AGENT WHAT YOU WANT — folded, because most presses want nothing said.
 *
 * "Show another" asks the same question with the last answer ruled out, which is a different
 * angle chosen at random. This is the other half, and it is the SECOND press that usually has an
 * opinion: by then the reader has read a plan and knows what they want different. So it stays a
 * summary until somebody has something to say, and the band keeps its height.
 */
.writeup__steerfold {
  margin-top: 10px;
}

/*
 * IT HAS TO LOOK LIKE SOMETHING YOU CAN PRESS. It was dim grey text with the default marker
 * suppressed by the inline-flex, so it read as a caption — "you wouldn't even know it's a button
 * unless you knew what you were looking for", which is a control nobody finds.
 *
 * A bordered chip with its own chevron, at full text colour. Still quiet enough to sit under a
 * plan without competing with it, but unmistakably a thing rather than a label about a thing.
 */
.writeup__steersum {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  width: fit-content;
  padding: 6px 12px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: var(--surface);
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--text);
  cursor: pointer;
  list-style: none;
}

.writeup__steersum::-webkit-details-marker {
  display: none;
}

/* The chevron, turned down when the box is open — the disclosure's own affordance, drawn rather
   than borrowed from the browser so it survives the list-style above. */
.writeup__steersum::after {
  content: '';
  width: 7px;
  height: 7px;
  border-right: 2px solid currentcolor;
  border-bottom: 2px solid currentcolor;
  transform: rotate(-45deg);
  margin-top: -3px;
  transition: transform 0.15s ease;
}

.writeup__steerfold[open] > .writeup__steersum::after {
  transform: rotate(45deg);
  margin-top: -5px;
}

.writeup__steersum:hover {
  border-color: var(--accent);
  color: var(--accent);
}

/* The mic on one side, the send button on the other — the box's own controls, on its own line. */
.writeup__steerrow {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 8px 10px;
  margin-top: 8px;
}

.writeup__steerrow > .writeup__steertools {
  margin: 0;
}

.writeup__steergo {
  margin-left: auto;
}

.writeup__steerhint {
  margin: 6px 0 0;
  color: var(--text-faint);
  font-size: 0.78rem;
}

.writeup__steer {
  margin-top: 8px;
}

.writeup__steerbox {
  width: 100%;
  resize: vertical;
  font: inherit;
  line-height: 1.45;
}

.writeup__steertools {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 10px;
  margin: 8px 0 0;
}

.writeup__steertools:empty {
  display: none;
}

/*
 * WHAT WAS ASKED FOR, kept with the plan it produced. Quoted rather than stated, because it is the
 * reader's own sentence coming back to them and a plain line would read as the app's words.
 */
.writeup__asked {
  margin: 10px 0 0;
  padding-left: 10px;
  border-left: 2px solid color-mix(in oklab, var(--accent) 40%, transparent);
  font-size: 0.85rem;
  color: var(--text-dim);
  font-style: italic;
}

.writeup__pager {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}

.writeup__chip {
  font: inherit;
  font-size: 0.8rem;
  padding: 3px 10px;
  border-radius: 999px;
  border: 1px solid var(--border-strong);
  background: transparent;
  color: var(--text-dim);
  cursor: pointer;
}

.writeup__chip[aria-pressed='true'] {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
}

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

.writeup__prose {
  max-width: 72ch;
  font-size: 0.9rem;
  line-height: 1.5;
}

.prose__h {
  margin: 12px 0 4px;
  font-size: 0.95rem;
  font-weight: 680;
  letter-spacing: -0.01em;
}

.prose__p {
  margin: 6px 0;
}

.prose__list {
  margin: 4px 0;
  padding-left: 18px;
}

.prose__scroll {
  overflow-x: auto;
}

.prose__table {
  border-collapse: collapse;
  font-size: 0.85rem;
  margin: 6px 0;
  font-variant-numeric: tabular-nums;
}

.prose__table th,
.prose__table td {
  padding: 4px 12px 4px 0;
  text-align: left;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}

.prose__table th {
  color: var(--text-dim);
  font-weight: 600;
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

/*
 * The list under the grid: NOT ON THE SCREEN.
 *
 * It was a card, then a folded one, and now it is print-only. The grid says the same thing, and a
 * day laid out on a timeline does not need writing out underneath as well.
 *
 * Paper is the exception and the reason this still exists: the print rules hide the timeline —
 * nine hours across A4 halftones into a smear — and show this instead, because a booth typing into
 * the TMS wants one line per showing in the order they happen. `display: none` here, `block` in the
 * print block below.
 */
.sched__list {
  display: none;
  margin-bottom: 24px;
}

/*
 * The ticket sales panel, under the grid. It exists only for the eyes the server granted —
 * paintSales in js/schedule-grid.js leaves it hidden for everybody else, and their payload
 * carries no numbers to draw regardless. Quiet on purpose: a card of three facts, not a
 * dashboard, because the grid above is still the page.
 */
.sales {
  margin-bottom: 24px;
  padding: 14px 16px;
  background: var(--glass);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
}

.sales__title {
  margin: 0 0 10px;
  font-size: 0.78rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-faint);
}

.sales__facts {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 36px;
}

.sales__fact {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.sales__label {
  font-size: 0.74rem;
  color: var(--text-dim);
}

.sales__figure {
  font-size: 0.95rem;
  font-variant-numeric: tabular-nums;
}

.sales__hint {
  font-size: 0.74rem;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}

/* ---------------------------------------------------------------- the Showtime Optimizer
 *
 * The way in and out, on the seam between what the week did and the week itself. A row rather
 * than a button on its own so the state of the plan can stand beside the control that reaches
 * it — "Saved, not pushed" is the thing somebody needs before they decide to press anything.
 */
.optbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 12px;
  margin: 0 0 12px;
}

.optbar__note {
  margin: 0;
  font-size: 0.78rem;
  color: var(--text-dim);
}

/* The band on a manager's board that says which week is under it. Above everything, because it
   changes what the rest of the page means — see paintPlan in js/schedule.js. */
.planbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 8px 16px;
  margin: 0 0 16px;
  padding: 10px 14px;
  background: var(--glass);
  border: 1px solid var(--border-strong);
  border-left: 3px solid var(--border-strong);
  border-radius: var(--radius);
}

/* Marked out only while the PLAN is the one showing. A booth copying times into a TMS is the
   reader this is for, and "this has not happened yet" is the one thing it must not have to
   work out. Back to the plain band once the running schedule is on screen. */
.planbar--plan {
  border-left-color: var(--accent);
  background: var(--accent-soft);
}

.planbar__text {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

/* ---------------------------------------------------------------- the schedule draft
 *
 * The drafter's week (draft.html): the board's grid under a planbar, then the why of it — what
 * the AI check said and changed, where each film's forecast comes from, how the scored weeks came
 * out, the rules, the mode. Built from the pieces the scheduler and the settings page already
 * use; what is here is only what those pieces do not say.
 */
.draft__acts {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

/* No draft to draw: a sentence where the grid would be, rather than an empty grid that looks
   like a week with nothing on. */
.draft__waiting {
  margin: 8px 0 16px;
  padding: 18px 20px;
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius);
  color: var(--text-dim);
  max-width: 72ch;
}

/* The sections after the grid, spaced as the cards on every other page are. */
.draft__part {
  margin-top: 16px;
}

/* A card's line of explanation, with room under it before the table or button it explains. */
.draft__part > .field__hint {
  margin: 0 0 12px;
}

/* What the check said beside where the forecast came from: side by side where there is room for
   both to be read, one after the other on anything narrower. */
.draft__why {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 16px;
  margin-top: 16px;
}

.draft__why > .draft__part {
  margin-top: 0;
}

@media (min-width: 1024px) {
  .draft__why {
    grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
    align-items: start;
  }
}

.draft__subhead {
  margin: 18px 0 8px;
  font-size: 0.92rem;
}

/* Each change the check made: what moved, the check's own reason, and what the forecast says it
   gains or costs — in that order, because the reason is the part the booker is weighing. */
.draft__changes {
  display: grid;
  gap: 8px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.draft__change {
  display: grid;
  gap: 3px;
  padding: 10px 12px;
  border-left: 3px solid var(--accent);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
}

/* Proposed and left out for breaking a rule: kept on the list, because what the check wanted to
   do and could not is itself worth knowing, but quieter than what it did. */
.draft__change--dropped {
  border-left-color: var(--border-strong);
}

.draft__changewhat {
  font-size: 0.86rem;
  font-weight: 600;
}

.draft__changewhy {
  font-size: 0.84rem;
  color: var(--text-dim);
}

.draft__impact {
  font-size: 0.78rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
}

/* A cost, not a warning: the check may keep a change the forecast prices below the solver's, and
   say why. The figure is information, so it is quiet rather than red. */
.draft__impact--down {
  color: var(--text-dim);
}

/*
 * The draft's tables scroll sideways inside their card on a narrow screen. Their own box rather
 * than .table__scroll, which stops scrolling under 560px because the programming lists turn into
 * stacked blocks there — a plain table given that box pushes the whole page wider than the phone.
 */
.draft__scroll {
  overflow-x: auto;
}

.draft__table th,
.draft__table td {
  vertical-align: top;
}

/* Two classes, to outrank `.table th`'s left alignment and weight without an !important. */
.draft__table .draft__num {
  text-align: right;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

.draft__table .draft__film {
  font-weight: 600;
}

.draft__table .draft__from {
  min-width: 18ch;
  color: var(--text-dim);
}

/* The cinema on screen, in the circuit table: the row the page is about. */
.draft__table .draft__row--on > * {
  background: var(--accent-soft);
}

.draft__rules {
  display: grid;
  margin: 0 0 14px;
  padding: 0;
  list-style: none;
}

.draft__rule {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 8px 0;
  border-bottom: 1px solid var(--border);
  font-size: 0.88rem;
}

.draft__rule--none {
  border-bottom: 0;
  color: var(--text-dim);
}

.draft__ruleform {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 10px 14px;
}

.draft__picks {
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}

.draft__checks {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 12px;
}

.draft__check {
  padding-bottom: 0;
  font-size: 0.84rem;
}

.draft__lookback {
  max-width: 72ch;
  margin: 14px 0 10px;
}

/* The four modes, as one segmented row: a choice between states, like 12h / 24h. */
.draft__modes {
  flex-wrap: wrap;
  margin-bottom: 10px;
}

.draft__modes .seg__opt {
  padding: 6px 16px;
  font-size: 0.84rem;
}

/* Automatic where the POS does not run the schedule: offered, and plainly not available. */
.draft__modes .seg__opt:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/*
 * A showing the AI check put where it is: a folded corner, the mark a page gets where somebody
 * has written on it. In the text's own colour, so it reads on every film's colour and in both
 * themes; and said in words in the pill's hint and its details box, for anybody the corner does
 * not reach.
 */
.showing.showing--changed::after {
  content: '';
  position: absolute;
  top: 0;
  right: 0;
  border-style: solid;
  border-width: 0 11px 11px 0;
  border-color: transparent var(--text) transparent transparent;
  pointer-events: none;
}

/* ------------------------------------------------------------------------------ the dock
 *
 * The two boxes that follow you down a week being built: the seat count, and the save bar under
 * it. One sticky container rather than two, because two elements each pinned at `bottom: 16px`
 * are two elements in the same sixteen pixels — they would overlap, and which one won would be
 * whichever the stacking order happened to favour.
 *
 * The order is the order the work needs. Somebody placing shows looks at what it did to the week
 * far more often than they press Save, so the number is on top and the buttons are nearest the
 * thumb.
 */
.dock {
  position: sticky;
  /* Clear of the very bottom so it reads as floating over the page, not welded on. */
  bottom: 16px;
  z-index: 20;
  margin-top: 16px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/*
 * While a pill is in hand the dock stands aside: transparent to the pointer, and faded so
 * the lane underneath is readable through it. A drop aimed at the bottom lanes used to land
 * ON the dock — elementFromPoint answered with the panel, the lane was never found, and the
 * drag bounced. The figures come straight back the moment the pill is let go, which is also
 * when they change.
 */
body.is-dragging .dock {
  pointer-events: none;
  opacity: 0.25;
}

/* The bar keeps its floating LOOK and gives up its own position: the dock is what sticks now,
   and a sticky box inside a sticky box pins itself against the wrong thing. */
.dock .savebar--sticky {
  position: static;
  margin-top: 0;
}

/*
 * The panel borrows the bar's treatment, since the two are now one object on screen — and is
 * TIGHTER than it was standing under the grid, at every width rather than only on a phone.
 *
 * Measured stacked at 1280x720 the dock came to 352px: half the window, floating over the
 * Copy-day and Features row so neither could be pressed. A floating box is not a panel that
 * happens to move — it is spending screen the schedule needs, and it has to cost as little of it
 * as the numbers allow.
 */
.dock__panel {
  margin-bottom: 0;
  padding: 10px 14px;
  border-color: var(--border-strong);
  border-radius: var(--radius-lg);
  background: linear-gradient(180deg, var(--surface-3), var(--surface));
  box-shadow: var(--shadow-lift);
}

/* The figures and the week strip share one line, scrolling rather than wrapping — see the note
   in paintMaxes. Two rows of this is a third of a laptop window spent on a summary. */
.maxes__row {
  display: flex;
  align-items: center;
  gap: 8px 22px;
  overflow-x: auto;
  scrollbar-width: thin;
}

.dock__panel .sales__facts {
  flex: 0 0 auto;
  flex-wrap: nowrap;
  gap: 22px;
}

/* Tighter than the same figures standing under the grid: this is a floating box, and every line
   of it is a line of schedule somebody cannot see. */
.dock__panel .sales__figure {
  font-size: 0.9rem;
  line-height: 1.2;
}

/* The per-day chips lose their shows count: two lines, not three. The day's number is what the
   strip is for, and the working out behind it is already in the two figures beside it. */
.dock .maxes__shows {
  display: none;
}

.dock .maxes__days {
  margin-top: 0;
}

.dock .maxes__day {
  width: 66px;
  padding: 4px 8px;
}

.maxes__gap {
  margin: 6px 0 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/*
 * The bar in the dock is the phone's bar at every width, which is what was asked for: reported as
 * "huge, and covers a lot of what you're working on" from a desktop.
 *
 * Its explaining sentence is clamped to one line rather than dropped, with the whole of it on the
 * hover — what Save and Save-and-push each do is worth having the first few times, and two rows
 * of it is two rows of schedule nobody can see. Under 700px it goes entirely; there is no room to
 * clamp it into.
 */
.dock .savebar {
  padding: 12px 14px;
  gap: 10px;
}

.dock .savebar .field__hint {
  display: block;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.dock .savebar .btn {
  padding: 8px 12px;
  font-size: 0.84rem;
}

/*
 * Room to scroll the last lane clear of what is floating over it.
 *
 * A sticky element still occupies its place in the flow, so without this the bottom of a
 * ten-auditorium day can never be reached: the dock sits over it and the page has nowhere further
 * to go. Only while something in the dock is showing — holding the page open for a bar that
 * appears on the first change and a panel that only exists on a plan would be worse than either.
 */
/*
 * A phone that has not pressed Edit is reading the day, not building it, so the seat count sits
 * under the day rather than floating over it. It floats to be watched while shows are placed, and
 * nothing is being placed: floating, it was a box over the last lanes of a day somebody had come
 * to read. `.grid--view` is canEdit's answer in js/showtimes.js.
 */
.shell:has(.grid--view) .dock {
  position: static;
}

.shell:has(.grid--view):has(#dock > :not([hidden])) {
  padding-bottom: 80px;
}

.shell:has(#dock > :not([hidden])) {
  /* Measured on every paint rather than guessed at — see sizeDock in js/showtimes.js. A constant
     would be right for one screen: this bar is four buttons on a desktop and two rows of them on a
     phone, and the panel above it grows a line when a room has no seat count. */
  padding-bottom: var(--dock-room, 200px);
}

/*
 * On a phone the dock has to earn its space, because it is floating over the thing it describes.
 *
 * Measured at 390px it came to 481 of 700 — most of the screen given over to two boxes about a
 * schedule you could no longer see. The sentence under the save bar's heading goes (the heading
 * still says what is unsaved, and the buttons say what they do), and everything else tightens.
 */
@media (max-width: 700px) {
  .dock__panel,
  .dock .savebar {
    padding: 10px 12px;
  }

  .dock__panel .sales__facts {
    gap: 18px;
  }

  .maxes__row {
    gap: 6px 14px;
  }

  .dock .maxes__days {
    gap: 4px;
  }

  .dock .maxes__day {
    width: 54px;
    padding: 4px 6px;
  }

  /*
   * The sentence under the bar's heading goes. The heading still says what is unsaved and the
   * buttons say what they do — and what it costs is three lines of a box floating over the
   * schedule it is about.
   *
   * The panel's own hints STAY, phone or not: on a week copied from one that is running they read
   * "99 sold so far", which is the number the whole rebuild turns on.
   */
  .dock .savebar .field__hint {
    display: none;
  }

  /* Smaller buttons, so the four of them are one row rather than two. */
  .dock .savebar .btn {
    padding: 7px 10px;
    font-size: 0.8rem;
  }
}

/* Every day of the planned week at once. A weekly total says nothing about WHERE the week is
   short, and where is the only actionable half of the question. */
.maxes__days,
.sales__days {
  display: flex;
  gap: 6px;
  margin-top: 12px;
  /* One row that scrolls rather than two that wrap: the dock is floating over the grid, and a
     panel that grows a second row on a phone takes the day it is describing off the screen. */
  overflow-x: auto;
  scrollbar-width: thin;
}

.maxes__day,
.sales__day {
  display: flex;
  flex-direction: column;
  gap: 1px;
  /* Fixed rather than minimum: the row scrolls, so a chip that grew with its number would leave
     the seven days at seven different widths and the strip impossible to read across. */
  flex: 0 0 auto;
  width: 74px;
  padding: 6px 10px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}

/* The day the grid above is showing. The panel and the grid must never leave a reader working
   out which day a number belongs to. */
.maxes__day--on,
.sales__day--on {
  border-color: var(--accent);
}

.maxes__dayname,
.sales__dayname {
  font-size: 0.7rem;
  color: var(--text-dim);
}

.maxes__figure,
.sales__dayfigure {
  font-size: 0.92rem;
  font-variant-numeric: tabular-nums;
}

.maxes__shows {
  font-size: 0.68rem;
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}

.schedule__film {
  font-weight: 600;
}

.schedule__at {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.schedule__runtime,
.schedule__next {
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}

.schedule__next {
  font-size: 0.78em;
}

/* ------------------------------------------------------------- the film panel
   Out from the right rather than above the grid: the list of every film the circuit holds is
   only wanted while something is being placed, and a film is DRAGGED from here onto a lane —
   which is why this is not a modal. There is no backdrop and the grid behind stays live. */
.library {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  width: min(400px, 92vw);
  /* Above the sticky top bar and its account menu, below the borrowed-view frame. */
  z-index: 45;
  display: flex;
  flex-direction: column;
  gap: 14px;
  /*
   * The insets, because this is a full-height overlay and on a phone the top and bottom of
   * the screen are not the top and bottom of the safe area.
   *
   * In the iOS home-screen app the page runs BEHIND the status bar — that is what
   * `apple-mobile-web-app-status-bar-style: black-translucent` buys, and it is why every
   * other fixed thing in this stylesheet already pads by `env(safe-area-inset-top)`. A panel
   * pinned to `top: 0` with a flat 18px put its header, and the only button that closes it,
   * underneath the clock. It could not be tapped and it could not be scrolled to, so the way
   * out of this panel was to force the app shut.
   */
  padding: calc(18px + env(safe-area-inset-top, 0px)) 18px
    calc(18px + env(safe-area-inset-bottom, 0px));
  /*
   * NOT the scroller any more — .library__scroll below is. The header has to stay on screen,
   * because it holds the way out.
   */
  overflow: hidden;
  background: var(--glass);
  border-left: 1px solid var(--border-strong);
  box-shadow: var(--shadow-lift);
  transform: translateX(100%);
  transition: transform 200ms var(--ease);
}

/*
 * The part that scrolls, and the part that keeps its gesture to itself.
 *
 * `overscroll-behavior: contain` is the whole of the second fix: without it a flick that
 * reaches the end of the features list is handed on to the page underneath, so on a phone
 * the schedule scrolled while the panel sat still — which reads as a panel that cannot
 * scroll at all. The same guard the modal bodies carry, for the same reason.
 *
 * `min-height: 0` because this is a flex child: without it a column flex item refuses to
 * shrink below its content, the box grows past the panel, and nothing scrolls anywhere.
 */
.library__scroll {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 14px;
  overflow-y: auto;
  overscroll-behavior: contain;
  /* Vertical panning only: the drag handles inside set their own touch-action. */
  touch-action: pan-y;
}

.library--open {
  transform: none;
}

/*
 * While the panel is out, the page shares the window with it instead of lying underneath:
 * the shell takes a right margin the panel's own width, the grid's scroll box narrows, and
 * fitDay redraws the whole day into what is left — nothing covered, nothing off screen.
 * Only where there is genuinely room to share; below it the panel overlays, as any sheet
 * does on a phone.
 */
@media (min-width: 1100px) {
  body.library-beside #main {
    margin-right: min(400px, 92vw);
  }
}

@media (prefers-reduced-motion: reduce) {
  .library {
    transition: none;
  }
}

.library__head {
  flex: none;
  display: flex;
  align-items: center;
  gap: 10px;
}

.library__title {
  margin: 0;
  font-size: 1.02rem;
  flex: 1 1 auto;
}

.library__close {
  font: inherit;
  font-size: 1.4rem;
  line-height: 1;
  width: 34px;
  height: 34px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  color: var(--text-dim);
  cursor: pointer;
}

.library__close:hover {
  color: var(--text);
  border-color: var(--border-strong);
}

.library__new {
  display: grid;
  gap: 10px;
  padding: 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
}

.library__new .field {
  margin: 0;
}

/* Search, with the count under it. Sticky, so scrolling a long library does not take away the
   box that shortens it. */
.library__find {
  position: sticky;
  /* Zero, not -18px: it sticks to .library__scroll now, and that box has no padding of its
     own to climb back over. */
  top: 0;
  z-index: 1;
  display: grid;
  gap: 4px;
  padding: 8px 0;
  background: var(--surface);
}

.library__count {
  margin: 0;
}

.library__list {
  display: grid;
  gap: 10px;
}

.feature {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-areas:
    'grip main'
    'grip facts'
    'grip actions';
  gap: 6px 10px;
  padding: 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  /*
   * Each film laid out only while it is on screen.
   *
   * The library is every film the circuit has booked — 846 on four years of twenty cinemas, seven
   * thousand elements, nine in ten of the page — and the panel is shut most of the time, off the
   * right-hand edge. Without this, every measurement the grid takes (fitting the day, the now
   * line, the dock, the day strip's edges) laid all of them out again: fifty milliseconds a time,
   * four or five times on every change of building, and on every step of a drag. `auto` in the
   * size keeps a row's real height once it has been seen, so the scrollbar settles rather than
   * jumps; 140px is a row as drawn, one line of buttons.
   */
  content-visibility: auto;
  contain-intrinsic-size: auto 140px;
}

.feature--retired {
  opacity: 0.72;
}

/* The grip, not the whole row: a row you can drag is a row you cannot select text in, and the
   name beside it is a text box. Hidden from assistive technology — the Place button next to it
   is the same journey without a gesture. */
.feature__grip {
  grid-area: grip;
  align-self: center;
  font-size: 1.1rem;
  line-height: 1;
  padding: 6px 2px;
  border: 0;
  background: none;
  color: var(--text-faint);
  cursor: grab;
  touch-action: none;
}

.feature__main {
  grid-area: main;
  display: flex;
  gap: 8px;
}

.feature__name {
  flex: 1 1 auto;
  min-width: 0;
}

.feature__runtime {
  flex: 0 0 104px;
  font-variant-numeric: tabular-nums;
}

.feature__facts {
  grid-area: facts;
  margin: 0;
}

.feature__actions {
  grid-area: actions;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

/* A dialog's second line: which cinema and which day it is about. The title says what is being
   done, and on a page where both change under you, saying where is not optional. */
.modal__sub {
  margin: 4px 0 0;
  font-size: 0.86rem;
  color: var(--text-dim);
}

@media (max-width: 700px) {
  /* A narrower name column, which is the part fitDay cannot decide: it gives the day more of a
     390px screen to be drawn in, and "Auditorium 4" still fits. The scale itself is measured. */
  .grid {
    --room: 112px;
  }
}

/* =========================================================================
   Printed placards

   These go on paper beside an auditorium door, so they are deliberately black
   on white regardless of the theme — a dark placard would waste toner and scan
   no better.

   ## Why this card is designed rather than merely laid out

   It was a bordered white box with a code in it, which is what a placard looks
   like when nobody has decided what it should look like. The version below is
   meant to be printed, trimmed and mounted AS IT COMES: an amber cap across the
   top, the circuit's own mark or their name, the code inside a viewfinder, and a
   foot that answers the question somebody standing in front of it is actually
   asking — what is this going to want from me.

   ## Why the colours are forced to print

   Browsers drop backgrounds when printing, which is the right default nearly
   everywhere and wrong for exactly this card. The amber cap, the amber pupil in
   the wordmark's reticle and the reticle's own masked ring are all drawn as
   backgrounds — at the default a placard would come off the printer as a white
   box with a hole where the mark should be. `print-color-adjust` says this one is
   the document rather than decoration around it. Everything else here is drawn
   with borders and type, which print without being asked.
   ========================================================================= */

.placards {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
  gap: 16px;
  /*
   * Every card the same height, so a sheet of them is a grid rather than a ragged
   * edge. `height: 100%` on the card cannot do this on its own — it equalises
   * within a row that is already tall enough, and "Auditorium 1" next to
   * "Concessions & bar" is precisely the case where it is not.
   *
   * Two rows to each slot, the card's and its caption's: every card row is 1fr, which in a grid
   * of no set height is the tallest card, and every caption row is as tall as its tallest caption.
   * One track per slot made the caption part of the card's height, so a long name wrapping under
   * one card made that card the short one.
   */
  grid-auto-rows: 1fr auto;
}

/*
 * One printable card plus its admin controls. The controls are a sibling rather
 * than a child so the card stays a clean white object that can be trimmed out,
 * and so `.no-print` can drop them without leaving a gap inside the border.
 */
.placard-slot {
  display: grid;
  grid-row: span 2;
  grid-template-rows: subgrid;
  row-gap: 10px;
  /* A <figure>, whose browser margin would push every card in from its grid cell. */
  margin: 0;
  break-inside: avoid;
}

/*
 * The strip under each card: the address the code points at, and the button that rotates it.
 *
 * Stacked rather than in a row, because the address wraps on a narrow column and a flex row
 * would put a two-line string beside a button and leave the button floating against nothing.
 * Dropped entirely when printing — see the `.no-print` block below — which is the whole reason
 * the address lives here rather than on the card.
 */
.placard__admin {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}

/*
 * The controls over the sheet, as drawn: the building, which pile, whose design and the bare print
 * in one row, with the two choices as pill tracks the height of the select beside them so the row
 * reads as one line of one kind of control.
 */
.placards__bar {
  align-items: flex-end;
}

.placards__bar .seg__opt {
  min-height: 34px;
}

/* The building's picker as wide as a building's name, not half the row: the choices beside it are
   what the row is for. */
.placards__bar .field:not(.field--check) {
  flex: 0 1 300px;
}

/* What will come out of the printer, in a sentence, under the controls that decide it. */
.placards__about {
  max-width: 72ch;
  margin: -8px 0 22px;
  color: var(--text-dim);
  font-size: 0.92rem;
  line-height: 1.5;
}

.placards__about:empty {
  display: none;
}

/*
 * Under each card: its place and, across from it, the kind of place, then the address and Rotate.
 * Left-aligned as one block under the card's left edge, the way a caption sits under a picture.
 */
.placard-slot__caption {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.placard-slot__line {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  min-width: 0;
}

.placard-slot__name {
  min-width: 0;
  font-weight: 650;
  overflow-wrap: anywhere;
}

.placard-slot__kind {
  flex: none;
  color: var(--text-dim);
  font-size: 0.82rem;
}

.placard-slot__caption .placard__admin {
  align-items: flex-start;
}

.placard-slot__caption .placard__url,
.placard-slot__caption .placard__design {
  text-align: left;
}

/* ------------------------------------------------------------------ seat tags
 *
 * A code for each seat, on a tag for the seat back or the armrest rather than a placard for a door:
 * the code on the left, and beside it the seat, big, with its room under it. The shape of a 64 by 38
 * millimetre label, on screen and on paper alike, so the page is still the print preview.
 *
 * Sized from its own width (container units), so the type is the same share of the tag on the page
 * as on paper, and a tag squeezed onto a phone keeps its proportions rather than wrapping.
 */
.seats {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(232px, 1fr));
  gap: 14px;
}

/*
 * The slot is the container, not the tag: an element's container units are measured against the
 * container AROUND it, never itself, so a tag that was its own container sized its padding and gap
 * against the page — four per cent of 1440 pixels of padding, and the seat squeezed to a column one
 * letter wide. The slot is exactly the tag's width, so every unit below is a share of the tag.
 */
.seat-slot {
  container-type: inline-size;
  display: grid;
  gap: 6px;
  /* No floor from what is inside it: the tag below takes the column's width, never sets it. */
  min-width: 0;
  margin: 0;
  break-inside: avoid;
}

.seat-tag {
  display: grid;
  grid-template-columns: 47% 1fr;
  align-items: center;
  gap: 5cqi;
  /*
   * The label's shape, held. An aspect ratio can carry the content's height across into a minimum
   * WIDTH: while the tag was its own container it came out 355px wide in a 275px column and the tags
   * overlapped. With the slot as the container the type scales with the column and never outgrows the
   * label, so `min-width: 0`, `min-height: 0` and the clip are a floor under that rather than what
   * holds it up — taking them out changes nothing today (fault-planted), and they stay so it cannot
   * come back.
   */
  aspect-ratio: 64 / 38;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
  padding: 3cqi 4cqi;
  border: 1px solid #11111b;
  border-radius: 10px;
  background: #ffffff;
  color: #11111b;
  -webkit-print-color-adjust: exact;
  print-color-adjust: exact;
}

.seat-tag__code {
  display: block;
  width: 100%;
  height: auto;
}

.seat-tag__words {
  display: flex;
  flex-direction: column;
  min-width: 0;
  line-height: 1.1;
}

.seat-tag__kicker {
  font-size: 4.2cqi;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: #4c4f69;
}

.seat-tag__seat {
  /* As big as the longest seat there can be, "ZZ99", can be and still keep to one line in its column.
     Four characters is the most: the seat box offers no longer name (js/seat-ranges.js). */
  font-size: 15cqi;
  font-weight: 800;
  letter-spacing: -0.02em;
  overflow-wrap: anywhere;
}

.seat-tag__room {
  margin-top: 1cqi;
  font-size: 4.8cqi;
  font-weight: 600;
}

.seat-tag__ask {
  margin-top: 3cqi;
  font-size: 3.9cqi;
  color: #4c4f69;
}

/* Rotate, under its tag, at the tag's left edge. */
.seat-slot__caption {
  display: flex;
}

/* Code and name only: the code and the seat, nothing to read around them. */
html[data-placard='bare'] .seat-tag {
  border-color: transparent;
}

html[data-placard='bare'] .seat-tag__kicker,
html[data-placard='bare'] .seat-tag__room,
html[data-placard='bare'] .seat-tag__ask {
  display: none;
}

/*
 * The seat box on Location info (js/locations.js): a line per block of rows, "Rows A to C" over
 * "Seats 1 to 11", with its ✕ and its count down the right. One grid for both halves of a line, so
 * the seat dropdowns sit under the row ones, and one layout at every width: with dropdowns as wide as
 * two characters and a chevron, a line fits a phone as it is.
 */
.seat-lines {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-top: 14px;
}

.seat-line {
  display: grid;
  grid-template-columns: auto auto minmax(0, 1fr);
  align-items: center;
  gap: 8px 12px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.seat-line__label,
.seat-line__to {
  color: var(--text-dim);
  font-size: 0.88rem;
}

.seat-line__range {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

/* Two characters and the chevron: the shared select rule's padding is for a field as wide as its form. */
.seat-line__pick {
  width: 3.9rem;
  padding-left: 11px;
  padding-right: 26px;
  background-position: right 14px center, right 9px center;
}

.seat-line__remove,
.seat-line__count {
  justify-self: end;
}

.seat-line__count {
  color: var(--text-dim);
  font-size: 0.88rem;
  font-variant-numeric: tabular-nums;
  text-align: right;
}

/* The ✕, as a card's on Home: quiet until it is pointed at, and then the colour of taking away. */
.seat-line__remove {
  display: grid;
  place-items: center;
  width: 36px;
  height: 36px;
  padding: 0;
  border: 1px solid transparent;
  border-radius: 8px;
  background: none;
  font: inherit;
  font-size: 0.85rem;
  line-height: 1;
  color: var(--text-faint);
  cursor: pointer;
}

.seat-line__remove:hover,
.seat-line__remove:focus-visible {
  background: color-mix(in oklab, var(--warn) 12%, var(--surface));
  border-color: color-mix(in oklab, var(--warn) 45%, var(--border));
  color: var(--warn);
}

.seat-lines__more {
  margin-top: 10px;
}

/*
 * Equipment in a screen (js/locations.js, editKit): what is in now and what was before, a block
 * per unit — what it is in bold and which one, then its serial and its days, then the reports it
 * answers for — with what can be done to it underneath. Outlined like a seat line, not lifted: they
 * are entries in a list inside a box, not cards of their own.
 */
.kit__hint {
  margin: 0 0 14px;
}

/* What the last save did, over the list it changed. */
.kit__said {
  margin: 0 0 14px;
  padding: 10px 12px;
  border-radius: var(--radius);
  background: var(--ok-soft);
  color: var(--ok);
  font-size: 0.88rem;
}

.kit__heading {
  margin: 18px 0 8px;
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.kit__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.kit__unit {
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

/* Out of the screen: still there to read, and plainly not what is in it now. */
.kit__unit--past {
  border-style: dashed;
}

.kit__unit--past .kit__what {
  color: var(--text-dim);
}

.kit__what {
  margin: 0;
  overflow-wrap: anywhere;
}

.kit__facts,
.kit__issues {
  margin: 4px 0 0;
  font-size: 0.84rem;
  color: var(--text-dim);
  overflow-wrap: anywhere;
}

.kit__issues {
  font-variant-numeric: tabular-nums;
}

.kit__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 10px;
}

/* Make and model side by side where there is room for both, one over the other on a phone. */
.kit__pair {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 0 16px;
}

.kit__wipe {
  margin-top: 6px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
}

/*
 * Equipment changed, on Mark resolved (js/progress.js): what was swapped to fix the case, recorded
 * with the closure. Its fields open under the tick box, set in and ruled down their left in the
 * border's own grey, so they read as the box's questions rather than more of the closure's. Unticked,
 * they are put away and nothing in them is sent.
 *
 * A fieldset (js/progress.js says why), so the browser's own frame and inset are taken off — and its
 * min-width, which a fieldset takes from the widest thing in it. Defence, not a fix for anything seen:
 * every field here fills the width it is given and every line wraps, so in Chromium nothing in it is
 * wider than a phone, however long the model name. But an engine that sizes a control from its own
 * content (WebKit's date field, see input[type='date']) would widen the whole set past the edge.
 */
.kitchange__parts {
  min-width: 0;
  margin: 14px 0 0;
  padding: 0 0 0 14px;
  border: 0;
  border-left: 2px solid var(--border);
}

.kitchange__parts > .field:last-child {
  margin-bottom: 0;
}

/*
 * What is in a screen now, under its row on Location info: "Projector Christie CP4230 · Cinema server
 * Dolby IMS3000". The kind in the row's ink, which one in the dimmer one, dotted apart.
 */
.auditorium__kit {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 0;
  margin: 8px 0 0;
  font-size: 0.84rem;
  color: var(--text-dim);
}

.auditorium__unit + .auditorium__unit::before {
  content: '·';
  margin: 0 8px;
  color: var(--text-faint);
}

.auditorium__unit strong {
  font-weight: 600;
  color: var(--text);
}

.placard {
  position: relative;
  display: flex;
  flex-direction: column;
  /* Fills its row of the sheet, so the foot of every card on a sheet lands on the same line. */
  align-self: stretch;
  overflow: hidden;
  background: #ffffff;
  color: #11111b;
  border: 1.5px solid #11111b;
  border-radius: 14px;
  padding: 22px 18px 16px;
  text-align: center;
  break-inside: avoid;
  -webkit-print-color-adjust: exact;
  print-color-adjust: exact;
}

/*
 * The cap: the one band of colour on the card, and most of what makes it look made
 * rather than generated. A strip of amber across the top of a white card is what a
 * door badge or a hotel key card does, and it is five printed points of ink.
 *
 * A pseudo-element rather than a thicker `border-top`, because a border of a
 * different width on one side leaves a mitred wedge in each top corner once the
 * card has a radius. This is clipped by `overflow: hidden` instead, so it follows
 * the curve.
 */
.placard::before {
  content: '';
  position: absolute;
  inset: 0 0 auto;
  height: 5px;
  background: var(--brand-pupil);
}

/*
 * The circuit's own mark, when they have uploaded one — the same trade the guest
 * page makes. Bounded rather than sized: a logo is whatever shape that company's
 * logo is, and the card should accommodate it rather than stretch it.
 */
.placard__logo {
  display: block;
  max-height: 34px;
  max-width: 72%;
  width: auto;
  height: auto;
  object-fit: contain;
  margin: 0 auto 8px;
}

.placard__theatre {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  font-weight: 650;
  color: #6c6f85;
}

.placard__aud {
  font-size: 2.15rem;
  font-weight: 750;
  letter-spacing: -0.03em;
  margin: 3px 0 0;
  line-height: 1.05;
}

/*
 * The code, inside a viewfinder.
 *
 * Four corner brackets rather than a box: it is the shape a phone camera puts on
 * screen when it is hunting for something, so it reads as "point it here" without
 * spending a word on it, and it rhymes with the reticle now sitting in the middle
 * of the code — four ticks, four corners. That rhyme is what makes the card and
 * the code look like one designed object instead of a picture pasted onto paper.
 *
 * Borders rather than a background image, because a border is the one way of
 * drawing a line that no printer will decide is decoration — and these brackets
 * are the instruction. Four corners need four boxes, so the wrapper carries two
 * and an empty span carries the other two, which is how the wordmark's reticle is
 * drawn as well.
 */
/*
 * The padding is deliberately small, and it is the gap between the brackets and the code.
 *
 * The code brings its OWN four modules of quiet zone — the specification's, and the thing that
 * makes it scannable — so any padding here is a second margin stacked on the first. At 13px the
 * brackets floated in the middle of nowhere and read as a box the code happened to be inside
 * rather than a viewfinder on it.
 */
.placard__code {
  position: relative;
  /*
   * `fit-content` so the brackets hug the code rather than the column, and `max-width: 100%` so
   * a column narrower than the code shrinks it instead of letting it hang out over the edge.
   * Without the cap the bare sheet — which packs more cards to a row on purpose — clipped the
   * right-hand code off every row.
   */
  width: fit-content;
  max-width: 100%;
  margin: 14px auto 14px;
  padding: 5px;
}

.placard__code img {
  width: 100%;
  max-width: 210px;
  height: auto;
  display: block;
}

.placard__frame {
  position: absolute;
  inset: 0;
}

.placard__code::before,
.placard__code::after,
.placard__frame::before,
.placard__frame::after {
  content: '';
  position: absolute;
  width: 17px;
  height: 17px;
  border: 2.5px solid #11111b;
}

.placard__code::before {
  top: 0;
  left: 0;
  border-right: 0;
  border-bottom: 0;
  border-top-left-radius: 5px;
}

.placard__code::after {
  top: 0;
  right: 0;
  border-left: 0;
  border-bottom: 0;
  border-top-right-radius: 5px;
}

.placard__frame::before {
  bottom: 0;
  left: 0;
  border-right: 0;
  border-top: 0;
  border-bottom-left-radius: 5px;
}

.placard__frame::after {
  bottom: 0;
  right: 0;
  border-left: 0;
  border-top: 0;
  border-bottom-right-radius: 5px;
}

.placard__cta {
  font-size: 1.05rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  margin: 0 0 5px;
}

.placard__sub {
  font-size: 0.78rem;
  color: #5c5f77;
  margin: 0;
}

/*
 * The foot, pushed to the bottom so a sheet of cards lines up along it.
 *
 * A border rather than a tinted strip, for the same reason as the brackets: this
 * has to survive a printer's opinion about backgrounds.
 */
.placard__foot {
  margin-top: auto;
  padding-top: 12px;
  border-top: 1px solid #dcdce4;
}

/*
 * What scanning costs them.
 *
 * This is the line that decides whether somebody standing in front of the card
 * actually takes their phone out. The unasked question is what it is going to want
 * — an app, an account, an email address — and the answer is none of it. It is the
 * same promise the form makes above its send button, moved to where the decision
 * is really taken, which is on the wall rather than on the page.
 */
.placard__promise {
  font-size: 0.7rem;
  font-weight: 600;
  color: #4c4f69;
  margin: 8px 0 0;
}

/*
 * And whose software it is. Ours, at the foot, never conditional on the circuit's
 * logo — their brand goes at the top and this stays, exactly as under the guest
 * form's send button.
 */
.placard__by {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
  margin: 5px 0 0;
  font-size: 0.6rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  /* Quiet, but not so quiet it halftones away: at #8c8fa1 this was 3.2:1 on the card, which is
     under the readable floor before a printer has had its say. */
  color: #6c6f85;
}

/*
 * Fixed colours, not the themed ones the mark carries everywhere else.
 *
 * `.wordmark` takes `var(--text)` and `.wordmark__cine` takes `var(--text-dim)`,
 * which are right on every page and wrong on this one: the card is a white sheet
 * whatever theme the app is wearing, so on a dark theme the mark would be drawn in
 * near-white on white and simply not be there. The same trap `.placard__url`
 * below fell into once already.
 */
.placard__by .wordmark {
  font-size: 0.63rem;
  letter-spacing: 0.1em;
  text-transform: none;
  color: #4c4f69;
}

.placard__by .wordmark__cine {
  color: #6c6f85;
}

/* ------------------------------------------------------------------ the bare print
 *
 * The same cards with everything but the identifier and the code taken off, for an operator
 * dropping these into a sign of their own. Wanted for TONER as much as for looks: the amber cap
 * is the most expensive square centimetre on the sheet, and printing a full placard in order to
 * cut the middle out of it wastes the rest of it too.
 *
 * A stamp on <html> rather than a second markup path — see BARE_KEY in public/js/qr.js. There is
 * one drawing of a placard, and the page is its own print preview, so what is ticked here is what
 * comes out of the printer.
 *
 * What survives is deliberately just the two things: the place, so whoever is assembling the
 * sheet knows which code is which, and the code. The cinema's name goes with the rest — the page
 * prints one cinema at a time, and the sign this is going into will be carrying their own name
 * already.
 */
html[data-placard='bare'] .placard {
  border: 0;
  border-radius: 0;
  padding: 4px 4px 10px;
}

html[data-placard='bare'] .placard::before,
html[data-placard='bare'] .placard__code::before,
html[data-placard='bare'] .placard__code::after,
html[data-placard='bare'] .placard__frame::before,
html[data-placard='bare'] .placard__frame::after {
  content: none;
}

html[data-placard='bare'] .placard__logo,
html[data-placard='bare'] .placard__theatre,
html[data-placard='bare'] .placard__cta,
html[data-placard='bare'] .placard__sub,
html[data-placard='bare'] .placard__foot {
  display: none;
}

/* The name shrinks with the card: at 2.15rem it was the placard's headline, and here it is a
   label on a code. Still the biggest thing on the card, because it is one of only two. */
html[data-placard='bare'] .placard__aud {
  font-size: 1.15rem;
  font-weight: 700;
  margin: 0;
}

html[data-placard='bare'] .placard__code {
  margin: 6px auto 0;
  padding: 0;
}

/*
 * More of them to a sheet, which is the other half of not wasting paper — and no narrower than
 * the code itself wants, because a column that squeezes it is a column that prints an unscannable
 * one. 210px of code, plus the padding either side.
 */
html[data-placard='bare'] .placards {
  grid-template-columns: repeat(auto-fill, minmax(222px, 1fr));
  gap: 10px;
}

/*
 * The address, now on the page rather than on the card.
 *
 * Themed tokens rather than the card's own fixed greys: the card is a white sheet whatever the
 * app's theme is, because it is printed, and these colours were chosen against that white. Read
 * on the dark page they were nearly invisible.
 */
.placard__url {
  font-family: var(--mono);
  font-size: 0.66rem;
  color: var(--text-faint);
  text-align: center;
  max-width: 260px;
  overflow-wrap: anywhere;
}

/* Which of the circuit's own designs a placard prints on, under it with the address. */
.placard__design {
  margin: 0;
  max-width: 260px;
  font-size: 0.72rem;
  line-height: 1.4;
  color: var(--text-dim);
  text-align: center;
}

/*
 * A PLACARD ON THE CIRCUIT'S OWN DESIGN: their picture, with this placard's code and name on it.
 *
 * Drawn by js/placard-art.js, which places the code and the name in percentages of the page, so
 * this element is the same placard at any width — the column's on screen, the paper's in print
 * (see .own-page in the print block). The name is sized in shares of the page's width through
 * container units for the same reason, which is what the container type is for.
 *
 * White behind the picture, because a design with transparency in it is printed on white paper
 * and should be previewed on it, whatever the app's theme is.
 */
.placard-own {
  position: relative;
  container-type: inline-size;
  width: 100%;
  overflow: hidden;
  background: #fff;
  color: #11111b;
  border-radius: 4px;
  box-shadow: 0 0 0 1px var(--border), 0 10px 26px -14px rgba(0, 0, 0, 0.55);
  -webkit-print-color-adjust: exact;
  print-color-adjust: exact;
}

.placard-own__art {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Square on paper whatever the page's shape: its width is set, its height follows. */
.placard-own__code {
  position: absolute;
  aspect-ratio: 1;
}

.placard-own__code img {
  display: block;
  width: 100%;
  height: 100%;
}

/*
 * The place's name, centred on its point and kept to one line: a name too long for its room is
 * set smaller to fit (--name-fit, see fitName), never broken or cut. Tracked capitals in the
 * edition's own display face, the way a sign is lettered; the left padding balances the tracking
 * after the last letter, so the word sits on its centre rather than a hair left of it.
 */
.placard-own__name {
  position: absolute;
  margin: 0;
  padding-left: 0.12em;
  transform: translate(-50%, -50%);
  white-space: nowrap;
  line-height: 1;
  font-family: 'Archivo', var(--font);
  font-stretch: 112%;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  font-size: calc(var(--name-size, 0.045) * var(--name-fit, 1) * 100cqw);
}

html[data-edition='standalone'] .placard-own__name {
  font-family: 'Manrope', var(--font);
  font-stretch: normal;
}

/*
 * Code and name only, on a design: the picture fades rather than vanishing, so the preview still
 * shows where the code and the name will land on the card that is already printed. On paper it is
 * gone altogether — see the print block.
 */
html[data-placard='bare'] .placard-own__art {
  opacity: 0.16;
}

/* The paper copies of the placards above, never on screen. See drawOwnPages in js/qr.js. */
.own-pages {
  display: none;
}

/*
 * PLACARD DESIGNS, in Settings: one row per design — the design itself, small, with a real code on
 * it; what it is for; and the three things to do with it. See js/placard-designs.js.
 */
.designs {
  display: grid;
  gap: 10px;
  margin-top: 14px;
}

.designs__empty {
  margin: 0;
  font-size: 0.9rem;
  color: var(--text-dim);
}

.designs__foot {
  display: flex;
  margin-top: 14px;
}

.designs__said {
  margin-top: 12px;
}

.design {
  display: grid;
  grid-template-columns: 76px minmax(0, 1fr) auto;
  align-items: center;
  gap: 14px;
  padding: 12px;
  border-radius: var(--radius);
  background: var(--surface-2);
}

.design__thumb {
  width: 76px;
}

.design__body {
  display: grid;
  gap: 2px;
  min-width: 0;
}

.design__title {
  margin: 0;
  font-weight: 650;
}

.design__where,
.design__meta {
  margin: 0;
  font-size: 0.85rem;
  color: var(--text-dim);
}

/* Why a design is not printed on just now — said where it is kept, not found out at the printer. */
.design__idle {
  margin: 4px 0 0;
  font-size: 0.85rem;
  color: var(--warn);
}

.design__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 6px;
}

@media (max-width: 640px) {
  .design {
    grid-template-columns: 64px minmax(0, 1fr);
  }

  .design__thumb {
    width: 64px;
  }

  .design__actions {
    grid-column: 1 / -1;
    justify-content: flex-start;
  }
}

/*
 * THE EDITOR: the design on the left, as it prints, and what can be changed about it on the right;
 * one column on a phone, the design first.
 */
.modal--design {
  width: min(980px, calc(100vw - 32px));
  max-height: min(92vh, 920px);
  max-height: min(92dvh, 920px);
}

.designer {
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(280px, 1fr);
  align-items: start;
  gap: 22px;
}

@media (max-width: 760px) {
  .designer {
    grid-template-columns: minmax(0, 1fr);
  }
}

.designer .field {
  margin-bottom: 0;
}

.designer__stage {
  display: grid;
  justify-items: center;
  gap: 10px;
  padding: 22px 18px 14px;
  border-radius: var(--radius);
  background: var(--surface-2);
}

.designer__empty {
  display: grid;
  place-items: center;
  width: 100%;
  min-height: 300px;
  padding: 20px;
  border: 1.5px dashed var(--border-strong);
  border-radius: var(--radius);
  color: var(--text-dim);
  text-align: center;
}

.designer__empty p {
  margin: 0;
}

.designer__hint {
  margin: 0;
  text-align: center;
}

.designer__panel,
.designer__for,
.designer__placement {
  display: grid;
  gap: 14px;
  min-width: 0;
}

.designer__target {
  margin: 0;
  color: var(--text-dim);
}

.designer__file {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
}

.designer__file-name {
  font-size: 0.85rem;
  color: var(--text-dim);
  overflow-wrap: anywhere;
}

.designer__line {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 10px;
}

.designer__line label {
  font-weight: 600;
  font-size: 0.9rem;
  color: var(--p-subtext1);
}

.designer__out {
  font-weight: 650;
  font-variant-numeric: tabular-nums;
}

.designer__line + input[type='range'] {
  width: 100%;
  margin-top: 6px;
  accent-color: var(--accent-fill);
}

.designer .field > input[type='range'] {
  width: 100%;
  accent-color: var(--accent-fill);
}

.designer__swatches {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
}

.designer__swatch {
  width: 34px;
  height: 34px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--swatch, #11111b);
  box-shadow: inset 0 0 0 1px rgba(127, 127, 127, 0.5);
  cursor: pointer;
}

.designer__swatch--on {
  box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px var(--accent);
}

.designer__swatches input[type='color'] {
  width: 38px;
  height: 34px;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}

/*
 * What the placement comes to, in words, each with a dot: the server's own checks, so a red one is a
 * Save the server would refuse, and the button waits until it is put right.
 */
.designer__checks {
  display: grid;
  gap: 6px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.designer__check {
  position: relative;
  padding-left: 20px;
  font-size: 0.86rem;
  line-height: 1.4;
  color: var(--text-dim);
}

.designer__check::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0.38em;
  width: 10px;
  height: 10px;
  border-radius: 50%;
}

.designer__check--ok::before {
  background: var(--ok);
}

.designer__check--warn::before {
  background: var(--warn);
}

.designer__check--bad {
  color: var(--danger);
}

.designer__check--bad::before {
  background: var(--danger);
}

.designer__said {
  margin: 0 auto 0 0;
  align-self: center;
}

/*
 * The code and the name, while they can be moved: an outline round the code, a handle at each
 * corner to size it, its width above it, and the name outlined when it can be dragged too.
 *
 * Unclipped while editing, so the width can sit above a code at the very top of the page. The
 * buttons over the code and the name are what take a drag and the keyboard; the handles are for a
 * pointer alone, and the size slider and plus and minus do their job from the keyboard.
 */
.placard-own--editing {
  overflow: visible;
  touch-action: none;
  user-select: none;
}

.placard-own--editing .placard-own__code {
  outline: 2px solid var(--accent-fill);
  outline-offset: 2px;
}

.placard-own__grab {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: grab;
}

.placard-own__grab:active {
  cursor: grabbing;
}

.placard-own__grab:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 4px;
}

.placard-own__handle {
  position: absolute;
  width: 14px;
  height: 14px;
  border: 2px solid var(--accent-fill);
  border-radius: 3px;
  background: #fff;
  transform: translate(-50%, -50%);
  touch-action: none;
}

.placard-own__handle--nw {
  left: 0;
  top: 0;
  cursor: nwse-resize;
}

.placard-own__handle--ne {
  left: 100%;
  top: 0;
  cursor: nesw-resize;
}

.placard-own__handle--sw {
  left: 0;
  top: 100%;
  cursor: nesw-resize;
}

.placard-own__handle--se {
  left: 100%;
  top: 100%;
  cursor: nwse-resize;
}

.placard-own__badge {
  position: absolute;
  left: 50%;
  bottom: calc(100% + 9px);
  padding: 3px 9px;
  border-radius: 999px;
  background: var(--accent-fill);
  color: var(--accent-ink);
  font: 700 0.72rem/1.2 var(--font);
  letter-spacing: 0;
  text-transform: none;
  white-space: nowrap;
  transform: translateX(-50%);
  pointer-events: none;
}

.placard-own__name--movable {
  outline: 1.5px dashed var(--accent-fill);
  outline-offset: 5px;
}

@media print {
  .topbar,
  /* The dock is the phone's chrome, and paper is not a phone. In the one block, because
     the placard pages' print rules are the last word on print and there is exactly one. */
  .dockbar,
  .dockbar__scrim,
  .dockbar__menu,
  .toolbar,
  .filters,
  .page-head p,
  .notice,
  .no-print,
  /* A borrowed view is a mode somebody is in, not part of the document they are
     printing — and the frame in particular would print as a box around every page. */
  .borrowed,
  .watched,
  html[data-borrowed]::after,
  /*
   * The schedule prints as the LIST, not as the timeline.
   *
   * Nineteen hours across A4 halftones into a smear, and the list is the half somebody in a
   * booth types from anyway — one line per showing, in the order they happen. The day tabs and
   * the panel go with it: both are ways of choosing what to look at, and paper has already
   * chosen.
   */
  .tabs,
  .weeknav,
  .page-head__actions,
  .sched__scroll,
  .sched__actions,
  .savebar,
  /* The printout is the booth's copy of the schedule. The takings are not part of it — a
     grant that walked out of the office on a sheet of A4 would not be much of a grant. */
  .sales,
  .library {
    display: none !important;
  }

  /* The half that IS printed. Hidden on screen — see .sched__list above. */
  .sched__list {
    display: block;
    border: 0;
    padding: 0;
  }

  /* Both, because the ground is on the root and the canvas is what a printer would otherwise fill
     with a page-sized block of Crust. */
  html,
  body {
    background: #fff;
    color: #11111b;
  }

  /* The TINT goes with it. It used to be part of the root's background, so `background: #fff`
     above replaced it in one go; it is a fixed layer of its own now, which the whitening cannot
     reach — left alone it would print as a grey wash over the top of every page, and the printout
     is a booth's working copy. */
  html::before {
    display: none;
  }

  .page-head h1 {
    color: #11111b;
  }

  /* The placards page gets printed. A tinted tile beside the heading is toner spent on
     decoration, and the heading says what the page is without it. Same for the rule: a
     colour that fades to nothing on a screen halftones into a grey smear on paper. */
  .page-head__icon,
  .page-head::after {
    display: none;
  }

  .shell {
    --shell-pad: 0px;
    max-width: none;
    padding: 0;
  }

  .placards {
    grid-template-columns: repeat(2, 1fr);
    gap: 12px;
  }

  /* The slot collapses to just the card once its controls are hidden. */
  .placard-slot,
  .placard {
    page-break-inside: avoid;
    gap: 0;
  }

  /*
   * Seat tags three across whatever width the paper and the printer's margins leave, edge to edge, to
   * be cut apart along the hairlines — one cut between two tags rather than two around a gap. A fixed
   * 64 millimetres was too wide: three of them are wider than A4 inside a browser's usual margins, and
   * the third column came off the paper. About 63 by 37 millimetres on A4, seven rows to a sheet.
   *
   * No heading over them either: the sheet is tags and nothing else, so every page holds the same
   * number, and each tag names its own room.
   */
  .seats {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 0;
  }

  .seat-tag {
    border: 0.2mm solid #b8b8c4;
    border-radius: 0;
  }

  html[data-qr-pile='seats'] .page-head {
    display: none;
  }

  html[data-placard='bare'] .seat-tag {
    border-color: transparent;
  }

  /*
   * PLACARDS ON THE CIRCUIT'S OWN DESIGN, one to a page at the design's own size.
   *
   * Printed from the copies in #own-pages, never from the grid: the grid lays our cards two across
   * to be trimmed from a sheet, and a design is a page of its own. So the grid's copies go; and when
   * every placard on the pile is on a design, the page the grid sits on goes too, or the printout
   * would open on a sheet with only a heading on it.
   *
   * Each copy is its paper's size exactly, on a page of that size with no margin, from a named page
   * per paper and way round — so "Save as PDF" makes a file a print shop can print as it is, and a
   * card that is already printed goes back through the printer with the code landing where it was
   * placed. Code and name only takes the picture off and leaves the code and the name where they
   * are, for exactly that.
   */
  .own-pages {
    display: block;
  }

  .placard-slot--own,
  html[data-own-placards='all'] .shell {
    display: none !important;
  }

  .own-page {
    break-after: page;
    page-break-after: always;
  }

  .own-page:last-child {
    break-after: auto;
    page-break-after: auto;
  }

  .own-page .placard-own {
    width: var(--paper-w);
    height: var(--paper-h);
    border-radius: 0;
    box-shadow: none;
  }

  html[data-placard='bare'] .placard-own__art {
    visibility: hidden;
  }

  .own-page[data-page='a4-portrait'] { page: own-a4-portrait; }
  .own-page[data-page='a4-landscape'] { page: own-a4-landscape; }
  .own-page[data-page='a5-portrait'] { page: own-a5-portrait; }
  .own-page[data-page='a5-landscape'] { page: own-a5-landscape; }
  .own-page[data-page='a6-portrait'] { page: own-a6-portrait; }
  .own-page[data-page='a6-landscape'] { page: own-a6-landscape; }
  .own-page[data-page='letter-portrait'] { page: own-letter-portrait; }
  .own-page[data-page='letter-landscape'] { page: own-letter-landscape; }
  .own-page[data-page='half-letter-portrait'] { page: own-half-letter-portrait; }
  .own-page[data-page='half-letter-landscape'] { page: own-half-letter-landscape; }
  .own-page[data-page='5x7-portrait'] { page: own-5x7-portrait; }
  .own-page[data-page='5x7-landscape'] { page: own-5x7-landscape; }
  .own-page[data-page='4x6-portrait'] { page: own-4x6-portrait; }
  .own-page[data-page='4x6-landscape'] { page: own-4x6-landscape; }

  @page own-a4-portrait { size: 210mm 297mm; margin: 0; }
  @page own-a4-landscape { size: 297mm 210mm; margin: 0; }
  @page own-a5-portrait { size: 148mm 210mm; margin: 0; }
  @page own-a5-landscape { size: 210mm 148mm; margin: 0; }
  @page own-a6-portrait { size: 105mm 148mm; margin: 0; }
  @page own-a6-landscape { size: 148mm 105mm; margin: 0; }
  @page own-letter-portrait { size: 215.9mm 279.4mm; margin: 0; }
  @page own-letter-landscape { size: 279.4mm 215.9mm; margin: 0; }
  @page own-half-letter-portrait { size: 139.7mm 215.9mm; margin: 0; }
  @page own-half-letter-landscape { size: 215.9mm 139.7mm; margin: 0; }
  @page own-5x7-portrait { size: 127mm 177.8mm; margin: 0; }
  @page own-5x7-landscape { size: 177.8mm 127mm; margin: 0; }
  @page own-4x6-portrait { size: 101.6mm 152.4mm; margin: 0; }
  @page own-4x6-landscape { size: 152.4mm 101.6mm; margin: 0; }

  /*
   * On paper everything is present: a card still waiting to be scrolled to is a card
   * somebody printed, and a page of blanks is not a printout. Inside this block rather
   * than in one of its own — the stylesheet has exactly one print block by design, and
   * two tests hold it to that, counting the words that open one.
   */
  [data-caseview] .complaint--report.reveal {
    opacity: 1;
    transform: none;
  }
}

/* ------------------------------------------------------------- your password
   A narrow column on purpose. These inputs hold a dozen or so characters, and a
   field the width of the card reads as a text area rather than a password box.
   The button sits under them rather than beside: on a phone the row would wrap
   anyway, and a submit that moves depending on width is a submit people miss. */
.password-form {
  display: grid;
  gap: 16px;
  max-width: 380px;
}

.password-form .btn {
  justify-self: start;
}

/* ================================================================== sign-in
   The front door, and the only page with no top bar: everywhere else that bar
   carries a nav and an account menu, and here there is neither.

   One column, centred, with the mark doing the work a header would otherwise do.
   100svh rather than vh, so a phone's collapsing address bar cannot push the card
   under the fold and then leave a gap when it retracts. `place-items: center` with
   `align-content` so a short viewport scrolls instead of clipping the logo. */
.signin {
  display: grid;
  place-items: center;
  align-content: center;
  min-height: 100svh;
  padding: 40px 20px 56px;
  box-sizing: border-box;
}

.signin__panel {
  width: 100%;
  max-width: 400px;
  display: grid;
  gap: 26px;
}

/*
 * The claim page, wider than the sign-in cards it shares its dress with.
 *
 * Those are two fields and a button. This is the first screen of a product somebody has just
 * paid for: a heading, the plan, five fields and the address being made — and at 400px the
 * address wrapped to three lines and the whole card read like a login box that had grown.
 *
 * A MEDIA QUERY rather than a bigger max-width, because max-width alone would change nothing
 * on a phone (the screen is already narrower) and everything about how this reads at 1280.
 * Below the breakpoint it is the panel every other card is, which is what a phone wants.
 */
@media (min-width: 560px) {
  .signin__panel--claim {
    max-width: 520px;
  }

  .signin__panel--claim .signin__card {
    padding: 32px 34px 34px;
  }
}

.signin__brand {
  display: grid;
  justify-items: center;
  gap: 10px;
  text-align: center;
}

/*
 * The mark at display size.
 *
 * Everything inside the lockup is sized in em — including the reticle and its pupil —
 * so one font-size here scales the whole thing, and the markup stays character for
 * character identical to the copy in every top bar. clamp() rather than a media query:
 * the step from a 320px phone to a desktop is smooth, and there is no width at which
 * the wordmark wraps or overflows.
 *
 * There was a soft accent bloom behind this. It read as a smudge rather than as depth,
 * and the mark holds the page on its own — the reticle's amber pupil is the only colour
 * it needs. Its removal also took the only thing on either page that was wider than the
 * panel, which is what used to push a 320px phone into scrolling sideways.
 */
.signin__brand .wordmark {
  font-size: clamp(1.7rem, 8.5vw, 2.5rem);
  letter-spacing: 0.075em;
}

/* The apex picker: one button per circuit the proven account belongs to, stacked, each as
   pressable as the submit it replaces. Only ever seen by somebody whose password verified on
   more than one circuit — see public/js/sign-in.js. */
.signin__choice {
  display: block;
  width: 100%;
  margin: 0 0 8px;
}

/*
 * The way back to the circuit this device used last, above the form on the apex.
 *
 * The apex exists so nobody has to know their subdomain, and it does that job by asking for a
 * password first — which is right for somebody arriving for the first time and is a long way
 * round for the same person on the same phone every morning. So the second visit leads with
 * where they went, and the form stays underneath for everybody else.
 *
 * A rule under it rather than a card around it: this sits above the sign-in form and must read
 * as the top of that flow, not as a competing panel with its own edges.
 */
.signin__known {
  display: grid;
  gap: 10px;
  margin: 0 0 20px;
  padding: 0 0 20px;
  border-bottom: 1px solid var(--border);
}

/* The circuit's own name is the label, so it wraps rather than being clipped — some of these
   are long, and a name cut in half is the one word somebody needed to recognise. */
.signin__known .btn {
  white-space: normal;
}

.signin__known .linkish {
  justify-self: center;
  font-size: 0.9rem;
}

/*
 * The apex pages' forms — the sign-in and the claim page.
 *
 * The same grid-and-18px rhythm as .signin__group, and for a discovered reason rather than
 * taste: these forms hold their button DIRECTLY, where login.html's button sits inside a
 * group, so without this the button lands flush against the last field and the forgot
 * disclosure's -8px pull (meant to tuck it under a button 18px away) overlaps the button
 * itself — which is exactly how it shipped, and how it looked on a phone.
 */
.signin__form {
  display: grid;
  gap: 18px;
}

/* The apex's Forgot password? is a disclosure, not a control that goes somewhere: resets
   live on each circuit's own page, and this opens to the sentence that says so. The summary
   borrows .linkish so it reads like the circuits' own escape hatch, minus the marker. */
.signin__where summary.linkish {
  display: inline-block;
  list-style: none;
}

.signin__where summary.linkish::-webkit-details-marker {
  display: none;
}

.signin__where summary.linkish::marker {
  content: '';
}

.signin__where .field__hint {
  margin-top: 8px;
  text-align: left;
}

/* ------------------------------------------------------------- console: what is broken

   The attention strip. Warm rather than red: these are sales to chase this morning, not an
   outage — and a console that cries emergency at a spam-foldered email is one whose warnings
   get ignored. See renderAttention in public/js/console.js. The badge it puts on the rail is
   .rail__count, which lives with the rest of the rail further down. */

.attention {
  border-color: var(--warn);
  margin-bottom: 16px;
}

.attn__head {
  display: flex;
  align-items: baseline;
  gap: 12px;
  flex-wrap: wrap;
  margin: 0 0 4px;
}

.attn__head .field__hint {
  margin: 0;
  font-weight: 400;
}

/* One row: what kind, who and why, when, and the single button that ends it. */
.attn {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 8px 14px;
  padding: 12px 0;
  border-top: 1px solid var(--border);
}

/* A fixed column, so the names line up: five rows whose text starts at five different
   places reads as a list nobody has looked at. */
.attn__tag {
  flex: none;
  width: 8.5rem;
  padding: 2px 9px;
  border-radius: 999px;
  background: var(--surface-3);
  font-size: 0.76rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  text-align: center;
}

/* The three that cost a customer something already — a licence they paid for and did not
   get, a sale that vanished, a link that never arrived — marked apart from the merely slow. */
.attn--suspended .attn__tag,
.attn--licence .attn__tag,
.attn--checkout .attn__tag,
.attn--email .attn__tag {
  background: var(--warn-soft);
  color: var(--warn);
}

/* ------------------------------------------------------------- the compliance register

   One row per obligation: where it stands, what it is for, which regimes ask for it, and the
   one button that records an answer. Same skeleton as .attn above — a fixed first column so
   twenty rows read as a list rather than as twenty different left margins. */

.oblig {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: start;
  gap: 8px 14px;
  padding: 14px 0;
  border-top: 1px solid var(--border);
}

.oblig__state {
  flex: none;
  width: 7rem;
  padding: 3px 9px;
  border-radius: 999px;
  background: var(--surface-3);
  font-size: 0.76rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  text-align: center;
}

.oblig__state--ok { background: var(--ok-soft); color: var(--ok); }
.oblig__state--warn { background: var(--warn-soft); color: var(--warn); }
.oblig__state--bad { background: var(--danger-soft); color: var(--danger); }
.oblig__state--quiet { background: var(--surface-3); color: var(--p-subtext0); }

.oblig__what {
  display: grid;
  gap: 4px;
  min-width: 0;
}

.oblig__what .field__hint { margin: 0; }

/* What the CHECK found, as opposed to what the obligation is — the one line that changes
   between two readings of this page, so it is the one that reads as data rather than prose. */
.oblig__detail {
  font-size: 0.86rem;
  color: var(--text);
  overflow-wrap: anywhere;
}

.oblig__tags {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  margin-top: 2px;
}

.oblig__tag {
  padding: 1px 7px;
  border: 1px solid var(--border);
  border-radius: 999px;
  font-size: 0.72rem;
  color: var(--p-subtext0);
}

.oblig__evidence { font-size: 0.86rem; }

.oblig__do { display: flex; align-items: center; }

.compliance__actions { margin-top: 14px; }

@media (max-width: 640px) {
  /* The state chip keeps its own line rather than squeezing the title into two words. */
  .oblig { grid-template-columns: minmax(0, 1fr); }
  .oblig__state { width: max-content; }
}

/* A circuit that is not answering outranks all of them: nobody there can sign in and no
   guest can report, right now, which is not the same class of problem as a licence that has
   not landed yet. The one row on this page that should read as red. */
.attn--down .attn__tag {
  background: var(--danger-soft);
  color: var(--danger);
}

.attn__what {
  display: grid;
  gap: 2px;
  min-width: 0;
}

.attn__what .field__hint {
  margin: 0;
  overflow-wrap: anywhere;
}

.attn__when {
  margin: 0;
  white-space: nowrap;
}

.attn__do {
  display: flex;
  justify-content: flex-end;
}

@media (max-width: 720px) {
  .attn {
    grid-template-columns: 1fr auto;
  }

  .attn__what {
    grid-column: 1 / -1;
    order: 3;
  }

  .attn__do {
    grid-column: 1 / -1;
    order: 4;
    justify-content: flex-start;
  }
}

/* The counted numbering form: one question, one dropdown, sized like an answer rather
   than stretched like a form row. See openNumbering in public/js/starter.js. */
.starter__countRow {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 4px 0 8px;
}

.starter__countLabel {
  font-weight: 600;
}

.starter__countPick {
  width: auto;
  min-width: 96px;
}

/* ------------------------------------------------------------------- the claim page
   Where a licence becomes a circuit: the buyer's one form, in the sign-in's own card.
   See public/claim.html and src/routes/claim.js. */

/* What the link buys, said before anything is asked. The form's grid gap spaces it. */
/*
 * The claim card: the first screen of a product somebody has just paid for.
 *
 * The heading and the plan are CENTRED and everything below them is not, deliberately. A
 * centred masthead over a left-aligned form is how a welcome reads; centring the fields too
 * would make the labels wander and turn a form into a poster.
 */
.claim__title {
  margin: 0 0 6px;
  text-align: center;
}

.claim__plan {
  margin: 0 0 4px;
  text-align: center;
  color: var(--text-dim);
  /* The one line saying what was bought, so it carries a little more weight than a hint. */
  font-size: 0.95rem;
  letter-spacing: 0.01em;
}

/*
 * The address falling out of the name, under the field that makes it.
 *
 * A consequence rather than a control, so it is dimmed and small — and the Edit beside it is a
 * link rather than a button, because pressing it opens a field rather than doing anything.
 */
.claim__address {
  display: grid;
  gap: 3px;
  margin: 7px 0 0;
  font-size: 0.85rem;
}

/* The instruction — brighter than the line under it, because this is the address a whole
   team is meant to remember and the other one is a convenience. */
.claim__address-main {
  color: var(--text);
}

/*
 * The direct line, and Edit trailing it INSIDE the same flow.
 *
 * Not a flex row with the button as its own item: at 390px the address wraps to two lines and
 * a flex item would sit against the top one, leaving "Edit" pointing at nothing. As inline
 * text it follows the last word wherever that lands.
 *
 * `overflow-wrap` because a hostname has no spaces to break at — "grandpalacecinemas.
 * cinescope-app.com" is one word to a browser, and without this it pushes the card open on a
 * phone rather than wrapping inside it.
 */
.claim__address-direct {
  color: var(--text-dim);
  overflow-wrap: anywhere;
}

.claim__edit {
  margin-left: 6px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--accent);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  text-decoration: underline;
}

.claim__edit:hover,
.claim__edit:focus-visible {
  text-decoration: none;
}

/* The address: the typed label and the fixed suffix, reading as one address. */
.claim__domain {
  display: flex;
  align-items: center;
  gap: 8px;
}

.claim__domain input {
  flex: 1;
  min-width: 0;
}

.claim__suffix {
  color: var(--text-dim);
  font-family: var(--mono, ui-monospace, monospace);
  font-size: 0.92rem;
  white-space: nowrap;
}

/* Whose account this becomes — the check against a forwarded link, under the button. */
.claim__whose {
  margin: 0;
  text-align: center;
}

/* ------------------------------------------------------------- console: provisioning
   The mint form, the one-moment link box, and the book of claims. */

.provision {
  display: grid;
  gap: 4px;
  max-width: 560px;
}

/* Edition beside the location count: one decision, two dials. */
.provision__plan {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0 16px;
}

.provision > .btn {
  justify-self: start;
  margin-top: 4px;
}

/* The plaintext link's one moment on this side: boxed to copy, never stored. */
.provision__result {
  margin-top: 14px;
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface-3);
}

.provision__url {
  margin: 0 0 10px;
}

.provision__url code {
  display: block;
  overflow-wrap: anywhere;
  font-size: 0.9rem;
}

.provision__actions {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
}

/* One claim: who, the plan, the state, the levers. */
.claimrow {
  display: grid;
  grid-template-columns: minmax(160px, 1.2fr) minmax(160px, 1fr) auto auto;
  align-items: center;
  gap: 8px 16px;
  padding: 12px 0;
  border-top: 1px solid var(--border);
}

.claimrow:first-child {
  border-top: 0;
  padding-top: 4px;
}

.claimrow__who,
.claimrow__plan {
  display: grid;
  gap: 2px;
  min-width: 0;
}

.claimrow__who .field__hint,
.claimrow__plan .field__hint {
  margin: 0;
  overflow-wrap: anywhere;
}

.claimrow__actions {
  display: flex;
  gap: 8px;
  justify-content: flex-end;
}

@media (max-width: 720px) {
  .provision__plan {
    grid-template-columns: 1fr;
  }

  .claimrow {
    grid-template-columns: 1fr auto;
  }

  .claimrow__plan {
    grid-column: 1 / -1;
    order: 3;
  }

  .claimrow__actions {
    grid-column: 1 / -1;
    order: 4;
    justify-content: flex-start;
  }
}

/* ------------------------------------------------------------- console: invoiced licences

   One row per circuit — who, until when, the standing, the two buttons — on the claim row's
   skeleton, so the book of sales and the book of payments read as one kind of list. The panel
   under a row holds the payment form or the history, one at a time, at the row's full width.
   See the invoiced licences section of public/js/console.js.

   ONE grid for the whole list, and every row a subgrid of it. The first version gave each row a
   grid of its own, and the two `auto` columns — the badge, the buttons — then sized to that row's
   own badge and that row's own "Payments (2)", so the dates started somewhere different on every
   line: six rows reading as six unrelated sentences. Shared columns size to the widest row, and
   the dates line up down the page. */

.liclist {
  display: grid;
  grid-template-columns: minmax(160px, 1fr) minmax(200px, 1.4fr) max-content max-content;
  column-gap: 16px;
  margin-top: 14px;
}

/* The skeleton and the "nobody here yet" line are not rows; they take the list's whole width. */
.liclist > * {
  grid-column: 1 / -1;
}

.licrow {
  display: grid;
  grid-template-columns: subgrid;
  align-items: center;
  gap: 8px 16px;
  padding: 12px 0;
  border-top: 1px solid var(--border);
}

/* The standing, one width down the column, like the attention strip's tags. */
.licrow > .badge {
  justify-content: center;
}

.licrow:first-child {
  border-top: 0;
  padding-top: 4px;
}

.licrow__who,
.licrow__term {
  display: grid;
  gap: 2px;
  min-width: 0;
}

.licrow__who .field__hint,
.licrow__term .field__hint {
  margin: 0;
  overflow-wrap: anywhere;
}

.licrow__actions {
  display: flex;
  gap: 8px;
  justify-content: flex-end;
}

.licrow__panel {
  grid-column: 1 / -1;
  padding: 14px 16px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface-2, var(--surface));
}

.licrow__history {
  display: grid;
  gap: 12px;
  justify-items: start;
}

/* The payment form: six fields, two rows of three on a desktop — what was paid, then what it
   covers — and one column on a phone. A fixed three rather than auto-fit, which left an amount
   too narrow to show "19,250.00" whole and the term's length alone on a row of its own. */
.licpay {
  display: grid;
  gap: 10px;
}

.licpay__grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0 16px;
}

/* What pressing the button will set, said before it is pressed. */
.licpay__result {
  margin: 0;
  font-weight: 600;
  color: var(--text);
}

.licpay__actions {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
}

/* The book: every payment on the record, newest first. Scrolls in its own box on a phone
   rather than pushing the page sideways. */
.licbook {
  max-width: 100%;
  overflow-x: auto;
}

.licbook__table {
  font-variant-numeric: tabular-nums;
}

.licbook__table td,
.licbook__table th {
  white-space: nowrap;
}

/* Who recorded it wraps instead: a full address and a time is the one cell long enough to push
   the rest of the book off the side of the panel. */
.licbook__table .licbook__who {
  white-space: normal;
  min-width: 12rem;
}

/* An undone payment stays on the record, struck through: a correction, not a gap. */
.licbook__row--undone td:not(:last-child) {
  text-decoration: line-through;
  color: var(--text-faint);
}

/* A date that never reached its circuit, and a customer already in grace or read-only, wear the
   warm tag the claim strip's worst rows do. A renewal merely coming due keeps the plain one: it
   is a date on the calendar, not something going wrong. */
.attn--unsynced .attn__tag,
.attn--lapsed .attn__tag,
.attn--grace .attn__tag {
  background: var(--warn-soft);
  color: var(--warn);
}

@media (max-width: 720px) {
  .liclist {
    grid-template-columns: minmax(0, 1fr) max-content;
  }

  .licpay__grid {
    grid-template-columns: minmax(0, 1fr);
  }

  .licrow__term {
    grid-column: 1 / -1;
    order: 3;
  }

  .licrow__actions {
    grid-column: 1 / -1;
    order: 4;
    justify-content: flex-start;
  }

  .licrow__panel {
    order: 5;
  }
}

/* A touch more room than a card in a page full of cards: this one is the page. */
.signin__card {
  position: relative;
  padding: 26px 24px 28px;
  display: grid;
  gap: 18px;
  overflow: hidden;
}

/* A hairline of accent along the top edge. Inside the card's own rounded corners,
   which is what `overflow: hidden` above is for — a straight line laid over a 16px
   radius pokes out at both ends. */
.signin__card::before {
  content: '';
  position: absolute;
  inset: 0 0 auto;
  height: 2px;
  background: linear-gradient(
    90deg,
    transparent,
    color-mix(in oklab, var(--accent) 70%, transparent),
    transparent
  );
}

.signin__title {
  margin: 0;
  font-size: 1.12rem;
  font-weight: 600;
  letter-spacing: 0.01em;
}

/* The gap above is doing the spacing, so the fields lose their own bottom margin —
   otherwise the rhythm is two different sizes stacked. */
.signin__card .field {
  margin: 0;
}

/*
 * The password fields and their button, which are one box now rather than three rows.
 *
 * They were direct children of the card, spaced by its own grid gap; they are wrapped so the
 * page can hide them when this circuit does not take a password — see signInMethods in
 * src/auth.js. A wrapper is one grid item, so it has to carry the same gap inside itself or
 * the email, the password and the button close up against each other.
 */
.signin__group {
  display: grid;
  gap: 18px;
}

/*
 * Face ID, under the password button on a device that can use it.
 *
 * Its own grid so the divider and the button sit tighter to each other than the card's own 18px
 * rhythm — the pair is one offer, not two rows in the stack. Pulled up against the button above it
 * for the same reason the forgot-password line is: it belongs to that control.
 */
.signin__passkey {
  display: grid;
  gap: 10px;
  margin: -4px 0 0;
}

/*
 * "or" — a rule with a word sitting in it.
 *
 * The word is what makes the button under it a choice rather than a second step: a bare line
 * between two controls leaves a reader working out whether the one below is a fallback, the next
 * thing to do, or something else. The line is drawn with two flex-grown pseudo-elements, so it
 * fills whatever the text does not and needs no width known in advance.
 */
.signin__or {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 0;
  color: var(--text-dim);
  font-size: 0.85rem;
}

.signin__or::before,
.signin__or::after {
  content: '';
  flex: 1 1 auto;
  height: 1px;
  background: var(--border);
}

/*
 * A quieter line under the button: "Forgot password?", and the way back from it.
 *
 * Centred and small because it is an escape hatch rather than a step — somebody signing in
 * normally should read past it without deciding anything. Negative top margin pulls it against
 * the button it belongs to, out of the card's own 18px rhythm, so it reads as attached to that
 * control instead of as a third thing in the stack.
 */
.signin__alt {
  margin: -8px 0 0;
  text-align: center;
  font-size: 0.9rem;
}

/* The status-page line under the card: present, and not competing with the form. */
.signin__status {
  margin: 18px 0 0;
  text-align: center;
}

/*
 * A button that reads as a link.
 *
 * A <button> rather than an <a href>, because it goes nowhere: it swaps what this card is
 * asking. Anything that looked like a link and did not navigate would be a lie to anybody
 * middle-clicking it, and an <a> with no href is invisible to a keyboard. So the element is
 * honest and the appearance is borrowed.
 */
.linkish {
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}

.linkish:hover {
  color: var(--text);
}

/* Every other way in, one button per row and tighter than the fields above: these are
   alternatives to each other, not steps in a form. */
.signin__methods {
  display: grid;
  gap: 10px;
}

/*
 * Empty on every circuit that takes only a password, which is all of them today.
 *
 * An empty grid item still takes the card's 18px gap, which would leave a hole under the
 * Sign in button that nobody could explain. Same reason `.person__state:empty` is hidden.
 */
.signin__methods:empty {
  display: none;
}

/*
 * There were two blocks of prose here — a lede inside the card and a footnote under it,
 * explaining who the page is for and who to ask for a password. Both are gone with the
 * paragraph they styled: a sign-in screen is read by people who already know why they
 * are on it, and the wordmark above the card says whose app it is.
 */

/* ---------------------------------------------------------- a borrowed view
 *
 * A dev looking at somebody else's dashboard, from View dashboard on Active users.
 *
 * Two pieces doing one job, and the second is the important one. The band carries the
 * detail and scrolls away like any other content; the frame is fixed, so forty reports
 * down a manager's queue the page is still visibly not yours. The failure this is built
 * against is quiet rather than loud — a manager's dashboard and a dev's own look broadly
 * alike, and by this point the nav and the account menu have legitimately become the other
 * person's — so the risk is not confusion in the moment but forgetting an hour later which
 * cinema's reports you were reading.
 *
 * Amber rather than red, matching `.alarm--due`: nothing is wrong, and a red page would say
 * something is. It is the same "you are in a mode" colour as a screen-sharing indicator,
 * which is exactly the relationship.
 */
.borrowed {
  background: color-mix(in oklab, var(--warn) 16%, var(--surface));
  border-bottom: 1px solid var(--warn);
  /* No stacking of its own, so the sticky top bar passes over it on the way up rather
     than under it. The frame is what keeps saying this after the band has gone. */
}

.borrowed__inner {
  max-width: 1180px;
  margin: 0 auto;
  padding: 9px 20px;
  display: flex;
  align-items: center;
  gap: 10px 14px;
  flex-wrap: wrap;
  font-size: 0.86rem;
}

.borrowed__tag {
  text-transform: uppercase;
  letter-spacing: 0.07em;
  font-size: 0.68rem;
  font-weight: 700;
  color: var(--warn);
}

.borrowed__who {
  color: var(--text);
  /* The one part that must survive a narrow screen intact, so it may take the width and
     push the rest onto a second line rather than being truncated itself. */
  min-width: 0;
}

.borrowed__note {
  color: var(--text-faint);
}

/* Pushed to the end of the row, so the way out is always in the same place whether the
   band is one line or three. */
.borrowed__out {
  margin-left: auto;
  font-weight: 600;
  color: var(--warn);
  white-space: nowrap;
}

/*
 * The frame. Fixed, so it does not scroll; `pointer-events: none`, so it is decoration
 * over a page somebody is using rather than a sheet of glass in front of it.
 *
 * On `html` rather than `body` because body is the element the band was prepended to, and
 * a pseudo-element of the same box would sit inside the scroll rather than over it.
 */
html[data-borrowed]::after {
  content: '';
  position: fixed;
  inset: 0;
  border: 3px solid var(--warn);
  pointer-events: none;
  z-index: 60;
}

/* Neither of these is printed — both are hidden in the app's one print block, above. */





/* The logo preview in Settings: whatever shape the company's logo is, on a neutral tile so a
   white mark on a white background is still visible as a picture rather than as nothing. */
.logo-preview {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 72px;
  padding: 12px;
  margin: 10px 0;
  border: 1px dashed var(--border);
  border-radius: 10px;
  background: var(--surface-2, var(--surface));
}

.logo-preview img {
  max-height: 48px;
  max-width: 100%;
  width: auto;
  object-fit: contain;
}

.logo-preview__none {
  font-size: 0.84rem;
  color: var(--text-faint);
}

/*
 * The picker and the Remove button, for both brand uploads.
 *
 * Two selectors rather than one shared class on both controls. They want identical spacing and
 * they are NOT the same control, and giving them one name is what made the logo's own browser
 * test match two buttons — see the note over qrMarkControl in public/js/settings.js. Sharing
 * belongs here, where it costs nothing; in the markup it costs every selector its meaning.
 */
.logo-actions,
.qrmark__actions {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}

/* What the upload said, under the control it is about rather than in the page banner. */
.logo-notice,
.qrmark__said {
  margin-top: 10px;
}

/*
 * The QR mark's preview, which is a whole code rather than the picture that was uploaded.
 *
 * On white, always, and not on --surface: the code is drawn with its own white quiet zone
 * painted into it, so on a dark theme a card-coloured plate behind it would put a white square
 * in the middle of a dark box and make the preview look like a mistake. White edge to edge is
 * also what the placard is, which is the thing being previewed.
 *
 * 168px is small on purpose. A code the width of the card would flatter a mark that will be
 * printed at a fifth of an inch; this is close to how big it actually lands on the card.
 */
.qrmark {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
  margin: 10px 0;
}

.qrmark__code {
  width: 168px;
  height: 168px;
  flex: none;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: #fff;
}

/*
 * The two lines beside it. NOT .field__hint, deliberately — a hint is a description and is
 * hidden until somebody turns Page descriptions on, and neither of these is a description:
 * one stops a bad print run and the other stops a false conclusion about the setting.
 * See the note where they are built in public/js/settings.js.
 */
.qrmark__note {
  flex: 1 1 15rem;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.qrmark__note p {
  margin: 0;
  font-size: 0.84rem;
  line-height: 1.45;
  color: var(--text-dim);
}

/*
 * Which state the control is in, said in the same voice as the logo's "No logo —" line.
 *
 * Qualified with the parent because `.qrmark__note p` above is a class AND an element and would
 * otherwise win the colour — the quiet kind of collision where the rule is present, correct and
 * simply never applies.
 */
.qrmark__note .qrmark__none {
  color: var(--text-faint);
}



/* ---------------------------------------------------------------------- units
 *
 * A hotel's rooms or a facility's storage units, in Location info. A wrapping row of small
 * chips rather than a list of rows: two hundred rooms as full-width rows is a page nobody can
 * scroll past, and the only things worth showing per unit are its name and the way to remove it.
 */
/*
 * A BLOCK PER ROOM, and the things inside it indented under it.
 *
 * The rows were one wrapping run of pills, which is right while every place is a room and wrong
 * the moment one of them holds nine others: a wrapping row cannot indent, so a restroom's
 * urinals would flow on and read as nine more restrooms. Each room is a block now, its fixtures
 * on their own row beneath it behind a hairline, which says "inside this one" without a word.
 *
 * Twenty in from each side, like everything else in these folded sections — the auditorium rows
 * (`.complaint`, 16px 20px), the empty line, the holidays form, the summary's own right edge. This
 * list alone had none, so every room pill and the Add restrooms button under them sat hard against
 * the section's left edge. Found by a sweep that measured every button in the app against the box
 * it sits in; nothing else it flagged was a fault.
 */
.units {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 12px 20px 4px;
}

/* One room and everything in it. */
.unitfamily {
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-width: 0;
}

/* The row of fixtures, set in from the room and hung off a rule that points at it. */
.unitkids {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin-left: 14px;
  padding-left: 12px;
  border-left: 1px solid var(--border);
}

/* Quieter than the room it is in: the room is the thing being divided, and nine equally loud
   pills under it would bury it. */
.unit--inside {
  font-size: 0.8rem;
  padding: 4px 6px 4px 10px;
  background: transparent;
}

.unit__inside-add {
  padding: 2px 8px;
}

/*
 * The equipment box (js/locations.js): a row per kind of thing, what it is and how many, the
 * shape the first-run flow uses when it wants a count. One grid for the headings and every row,
 * so "How many" sits over the dropdowns at every width rather than roughly near them.
 */
.equip__head,
.equip__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 88px;
  gap: 8px;
  align-items: center;
}

.equip__head {
  margin-top: 14px;
}

/* The legend's own bottom margin is for a label over one field; here the rows sit close. */
.equip__head .field__legend {
  margin-bottom: 0;
}

.equip__rows {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-top: 6px;
}

.equip__name,
.equip__count {
  width: 100%;
  min-width: 0;
}

.equip__more {
  margin-top: 10px;
}

.unit {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 6px 8px 6px 12px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface-2, var(--surface));
  font-size: 0.86rem;
}

/* Switched off: still listed, because a room somebody turned off is a room they may turn back
   on, and a list that hides them is a list that cannot explain the count. */
.unit--off {
  opacity: 0.55;
  border-style: dashed;
}

.unit__name {
  font-weight: 600;
  color: var(--text);
}

.unit__meta {
  color: var(--text-faint);
  font-size: 0.78rem;
}

.unit__remove {
  padding: 2px 8px;
}

/* The same inset as the list above it, and the same 16px under it as an auditorium row, so the
   button does not sit on the section's bottom edge. */
.unit__add {
  padding: 8px 20px 16px;
}

/* -------------------------------------------------------------------- the licence
 *
 * How many locations are in use against how many were paid for, at the foot of the list it is
 * counting. Adding one used to be a developer's job because it crossed the licence meter and
 * the meter was an honour system; the meter is a number now, so the button that crosses it can
 * sit here, and the button that RAISES it takes its place at the ceiling.
 */
.licence {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px 14px;
  margin: 18px 0 6px;
  padding: 14px 16px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--surface-2, var(--surface));
}

.licence__count {
  font-weight: 650;
  color: var(--text);
}

.licence__note {
  flex: 1 1 auto;
  font-size: 0.86rem;
  color: var(--text-muted, var(--text));
}

/* The last button on the row, whichever it is: Add while there is room, Upgrade once there
   is not. `margin-left: auto` on the row's end rather than on a specific control, so the two
   states line up in the same place instead of jumping. */
.licence .btn {
  margin-left: auto;
  flex: 0 0 auto;
}

/*
 * A way out to a page that explains, rather than a button that does something. Plain and
 * quiet on purpose: it sits on the same row as "Ask for more", and two things that looked
 * equally pressable would make the reading matter compete with the paying one.
 *
 * `flex-basis: 100%` puts it on its own line beneath the sentence it belongs to, so the row
 * above stays the count-note-button shape every other licence row has.
 */
.licence__link {
  flex: 0 0 100%;
  font-size: 0.86rem;
  color: var(--accent);
  text-decoration: none;
}

.licence__link:hover,
.licence__link:focus-visible {
  color: var(--accent-hover);
  text-decoration: underline;
}

/* At the ceiling the row says so with an edge rather than a colour, which would read as an
   error — being fully subscribed is not a fault. */
.licence--full {
  border-color: var(--accent);
}

/* An invoiced licence in its last thirty days, or past them: amber, like the band a page wears
   once it has ended, and an edge rather than a fill for the reason the full row above is — a
   renewal coming due is a date, not a fault. See termRow in settings.js. */
.licence--ending {
  border-color: color-mix(in oklab, var(--warn) 70%, transparent);
}

@media (max-width: 560px) {
  .licence .btn {
    margin-left: 0;
    flex: 1 1 100%;
  }
}

/* ------------------------------------------------------------- quick-start presets
 *
 * The standalone licence's first question, riding the layout box's tile styles — same
 * furniture, same feel, one new piece: the detail block that says what a selected preset
 * actually sets up, so "Set up my circuit" is pressed on facts rather than on a label.
 */
/*
 * A GRID, overriding the layout box's single column.
 *
 * `.preset__choices` is a flex column, which is right for three choices and wrong for seven:
 * stacked, the tiles filled the whole scroll area and pushed the detail block — the thing that
 * justifies pressing the button — off the bottom behind the actions bar. Found by looking at a
 * screenshot, because every assertion about it passed: the element was in the DOM, Playwright
 * called it visible, and a person could not read it.
 *
 * `display: grid` is restated rather than assumed, since the rule it overrides sets `flex`.
 */
.starter__choices {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 10px;
}

/* A name, centred, with nothing under it — so the tiles are a row of choices to scan rather
   than seven paragraphs to read. What each one contains is the detail block's job. */
.starter__tile {
  align-items: center;
  text-align: center;
  padding: 14px 12px;
}

.starter__tile.is-selected {
  border-color: var(--accent);
  outline: 2px solid var(--accent);
  outline-offset: -1px;
}

/*
 * Wider than a form dialog, because this one is a CHOICE between seven things shown side by
 * side. The class was named by the markup before it existed here — the dialog asked for
 * `modal--wide` and got the default 660px, which is the other half of why the tiles ran long.
 */
.modal.modal--wide {
  width: min(820px, calc(100vw - 32px));
}

.starter__detail {
  /* Room to breathe on both sides: the tiles above and the buttons below were both close
     enough to read as one crowded block rather than three steps. */
  margin: 14px 0 18px;
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface-2, var(--surface));
}

/* The business the detail block is describing, so the box always says what is about to be
   applied even when the tile itself has scrolled away. */
.starter__chosen {
  margin: 0 0 8px;
  font-weight: 650;
  color: var(--text);
}

.starter__line {
  margin: 0 0 6px;
  font-size: 0.84rem;
  color: var(--text-muted, var(--text));
}

.starter__line:last-child {
  margin-bottom: 0;
}

.starter__line strong {
  color: var(--text);
}

/* --------------------------------------------- the two steps, and moving between them
 *
 * The tiles and the numbering form are the same box at the same size, so without a direction
 * of travel pressing Back is indistinguishable from pressing forward: the content simply
 * becomes different content. The slide is what says which way you went, and it is the only
 * thing that does — this is the first minute somebody spends in the app.
 *
 * Both directions are 220ms, matching STEP_MS in public/js/starter.js, which is what times the
 * class coming back off. Entry-length rather than exit-length in both directions because
 * neither of these is an exit: Back is a step in a form somebody is still filling in, not a
 * decision they have finished with.
 *
 * Everything here is zeroed by the sheet's blanket prefers-reduced-motion rule.
 */
.starter__step.is-forward {
  animation: starter-step-in 220ms var(--ease-arrive) both;
}

.starter__step.is-back {
  animation: starter-step-back 220ms var(--ease-arrive) both;
}

@keyframes starter-step-in {
  from {
    opacity: 0;
    transform: translateX(18px);
  }
}

@keyframes starter-step-back {
  from {
    opacity: 0;
    transform: translateX(-18px);
  }
}

/* The detail block redrawing under the tiles. Small on purpose — it sits a few pixels below
   the thing just clicked, and anything larger reads as the box itself having moved. */
.starter__detail.is-fresh {
  animation: starter-detail-in 180ms var(--ease-arrive) both;
}

@keyframes starter-detail-in {
  from {
    opacity: 0;
    transform: translateY(-4px);
  }
}

/* ------------------------------------------------------ the welcome, the plan, the finish
 *
 * Three screens that are not forms. The two at the front say what this is and what the next few
 * minutes hold; the one at the end says it worked. They share a shape — the mark, a line of
 * type, and a button — because they are the same kind of moment at either end of the same job.
 *
 * Centred and given a floor to stand on, so the box does not change height between the
 * invitation and the form that follows it.
 */
.starter__hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  /* Tall enough that the box does not jump between the invitation and the form after it, and
     no taller: the first version left a hand's width of nothing under the welcome line. */
  min-height: 300px;
  padding: 8px 0 4px;
}

/*
 * The mark, in the top quarter.
 *
 * `flex-basis: 25%` of the hero's height rather than a margin, so it holds that share of the
 * box at every size — which is the point of asking for a quarter rather than a gap.
 */
.starter__crown {
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 25%;
  min-height: 76px;
}

/* Four times the top bar's, which is em-based throughout, so the ring and its pupil scale
   with the letters rather than needing their own sizes here. */
.starter__mark {
  font-size: calc(2.1rem * var(--mark-scale));
}

/*
 * The words, centred in what the mark leaves rather than tucked under it.
 *
 * The first version put them directly below the logo with the rest of the box empty beneath,
 * so they read as a caption on the mark instead of as the thing the screen is saying. Taking
 * the remaining height and centring inside it gives them the separation and puts them on the
 * eye line — which is the whole difference between a welcome screen and a header.
 */
.starter__say {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 10px;
  padding: 18px 0 8px;
}

.starter__welcome {
  margin: 6px 0 0;
  font-size: 1.35rem;
  font-weight: 650;
  line-height: 1.25;
  color: var(--text);
}

.starter__lede {
  max-width: 46ch;
  margin: 0 auto;
}

/* ------------------------------------------------------------------- what happens next */

.starter__plan {
  list-style: none;
  margin: 22px 0 4px;
  padding: 0;
  /* Left-aligned inside a centred column: three ticks down a ragged left edge read as three
     unrelated notes rather than as a list of three things. */
  text-align: left;
  width: min(340px, 100%);
}

.starter__planItem {
  display: grid;
  grid-template-columns: 26px 1fr;
  align-items: baseline;
  gap: 2px 10px;
  padding: 10px 0;
  border-bottom: 1px solid var(--border);
}

.starter__planItem:last-child {
  border-bottom: 0;
}

.starter__planTick {
  grid-row: span 2;
  align-self: center;
  justify-self: center;
  width: 22px;
  height: 22px;
  display: grid;
  place-items: center;
  font-size: 0.78rem;
  border-radius: 50%;
  color: var(--accent);
  border: 1px solid var(--accent);
}

/*
 * The same list on a step that is not a hero.
 *
 * `.starter__plan` is capped at 340px and left-aligned inside a CENTRED column, which is right
 * for the three two-word lines on the welcome screens and wrong everywhere else: the AI features
 * and the next steps carry a sentence each, and at 340px on the left of a 1080px box they read
 * as a narrow column somebody forgot to finish. Same rhythm, the width it is actually given.
 */
.starter__plan--full {
  width: 100%;
  margin: 14px 0 4px;
}

/* Tighter per row than the welcome screens' three, because these lists run to six — and six
   rows breathing like three is a list whose last item is below the fold on a laptop. The last
   item on the cinema walk is Connect POS Sync, which is exactly the one somebody would not
   think to go looking for. */
.starter__plan--full .starter__planItem {
  padding: 7px 0;
}

/*
 * The AI features, two to a row where there is room: nine of them, under three headings, read as
 * a glance in two columns and as a scroll in one. No rule between rows — in a grid the last row
 * may hold one item or two, and a rule under only one of them reads as a mistake.
 */
.starter__plan--pair {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: 28px;
  margin-top: 6px;
}

.starter__plan--pair .starter__planItem {
  border-bottom: 0;
  padding: 6px 0;
  /* Packed to the top: a row's two items are as tall as the longer one, and the shorter one's
     note drifted down into the gap left between its name and its sentence. */
  align-content: start;
}

/* A heading over each group, closer to its list than the issue types' "Staff only" is to its. */
.starter__group--ai {
  margin-top: 14px;
}

/* What makes one feature unlike the rest: AI triage's own setting, a guide's own tick. */
.starter__planTag {
  margin-left: 8px;
  padding: 1px 7px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  font-size: 0.72rem;
  font-weight: 500;
  color: var(--text-dim);
  white-space: nowrap;
}

@media (max-width: 560px) {
  .starter__plan--pair {
    grid-template-columns: minmax(0, 1fr);
  }
}

.starter__planName {
  font-weight: 650;
  color: var(--text);
}

.starter__planNote {
  grid-column: 2;
  font-size: 0.84rem;
  color: var(--text-muted, var(--text));
}

/* The setup box's finish: its two ways on, centred, with nothing else on the bar. */
.modal__foot--solo {
  justify-content: center;
}

.btn--lg {
  padding: 13px 34px;
  font-size: 1.02rem;
}

@media (max-width: 560px) {
  .starter__hero {
    min-height: 210px;
  }

  .starter__mark {
    font-size: calc(1.7rem * var(--mark-scale));
  }

  .starter__welcome {
    font-size: 1.16rem;
  }

  /* Full width, because a centred button with room either side of it on a phone reads as
     something optional. */
  .modal__foot--solo .btn {
    flex: 1 1 100%;
  }
}

/* ------------------------------------------------- the editable places and issue types
 *
 * Two screens of the same row: a name somebody can retype, and a way to take it out. They are
 * the point of the flow rather than decoration on it — by the time the last button is pressed
 * the owner has read their own vocabulary and changed a line of it, which is the difference
 * between an app that was configured for them and one they know is theirs.
 */
.starter__rows {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin: 10px 0 0;
}

.edit {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  /* No box of its own. The input inside already draws one, and a bordered row around a
     bordered field is two rectangles saying the same thing — which is what a column of eleven
     of them looked like. */
  padding: 0;
}

/* The name takes the room. Everything beside it is a fixed-width answer about the name. */
.edit__label {
  flex: 1 1 160px;
  min-width: 0;
}

.edit__placard {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: 0 0 auto;
  font-size: 0.82rem;
  color: var(--text-muted, var(--text));
  white-space: nowrap;
  cursor: pointer;
}

.edit__tick {
  margin: 0;
}

/* Why this row is about to be asked about floor by floor, said where the row is rather than
   two screens later where the question turns up. */
.edit__note {
  flex: 0 0 auto;
  font-size: 0.76rem;
  color: var(--text-muted, var(--text));
  white-space: nowrap;
}

/*
 * The quiet way out of a row.
 *
 * A bordered "Remove" repeated down eleven rows was the loudest thing on a screen whose whole
 * subject is the names beside it — a wall of buttons with the content squeezed between them.
 * This is a glyph that colours on hover and carries its full sentence as an accessible name,
 * so the list reads as a list and the removals are still exactly where they were.
 */
.edit__drop {
  flex: 0 0 auto;
  width: 30px;
  height: 30px;
  padding: 0;
  font: inherit;
  font-size: 1.15rem;
  line-height: 1;
  color: var(--text-muted, var(--text));
  background: none;
  border: 1px solid transparent;
  border-radius: var(--radius-sm, 8px);
  cursor: pointer;
}

.edit__drop:hover,
.edit__drop:focus-visible {
  color: var(--danger);
  border-color: var(--border);
}

/* The heading between the guest list and the staff list. */
.starter__group {
  margin: 20px 0 0;
  font-size: 0.84rem;
  color: var(--text-muted, var(--text));
}

.starter__group strong {
  color: var(--text);
}

/*
 * The one tickbox on this flow, and the shape is doing the reading for somebody.
 *
 * The box is pinned to the FIRST LINE rather than centred against the whole paragraph, because
 * the paragraph is three lines on a phone and a control floating beside its middle reads as
 * belonging to nothing. `align-items: start` plus a nudge down to sit on the text's baseline.
 */
.starter__check {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: start;
  gap: 10px;
  margin-top: 8px;
  font-size: 0.84rem;
  line-height: 1.5;
  color: var(--text-muted, var(--text));
  cursor: pointer;
}

.starter__check input {
  margin-top: 3px;
}

/*
 * The AI switch, given a box of its own.
 *
 * Every other tickbox in this flow is one option among a screenful; this one is the screen's
 * whole question, and the three features above it are the case for leaving it alone. Boxed and
 * tinted, it stops reading as a footnote under a list and starts reading as the thing being
 * decided — which is what it is, and the only decision here somebody could regret.
 */
/*
 * The screens step: one cinema per row, its name against a dropdown.
 *
 * A grid rather than a flex row, so every dropdown starts at the same x whatever the buildings
 * are called — "Alphaville 12" and "Riverside" beside each other with ragged selects would read
 * as a list of unrelated controls rather than as one question asked nine times.
 */
.starter__screens {
  display: grid;
  gap: 8px;
  margin: 14px 0 4px;
}

.starter__screenRow {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 12px;
  padding: 9px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-2, var(--surface));
}

.starter__screenName {
  font-weight: 600;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
}

@media (max-width: 560px) {
  /* Stacked, because a long cinema name and a dropdown do not share 320px. */
  .starter__screenRow {
    grid-template-columns: minmax(0, 1fr);
    gap: 6px;
  }
}

.starter__check--ai {
  margin-top: 16px;
  padding: 12px 14px;
  border: 1px solid color-mix(in oklab, var(--accent) 30%, transparent);
  border-radius: var(--radius-sm);
  background: color-mix(in oklab, var(--accent) 7%, var(--surface));
  color: var(--text);
}

/* The consequence, under the box that has it — sized like the hints it sits among rather than
   like the decision above it. */
.starter__aiNote {
  margin-top: 12px;
}

/* What the next screens hold, under the detail block that describes this one. */
.starter__next {
  margin-top: 10px;
  padding-top: 8px;
  border-top: 1px solid var(--border);
  color: var(--text-muted, var(--text));
}

/* Back is hidden on the first screen and "set up later" on every screen but it. The footer
   keeps its right-hand alignment because `modal__foot` pushes its last child over; an empty
   first slot changes nothing. */
.starter__foot [hidden] {
  display: none;
}

/*
 * The one way past a screen, and quieter than the way through it.
 *
 * It sits beside the primary rather than opposite it, because it is a variation on continuing
 * — the flow goes to the same next screen either way — rather than the opposite of it, which
 * is what Back is and where Back sits.
 */
.starter__later {
  margin-left: auto;
  font-size: 0.86rem;
  /* Quieter than Back, which is quieter than Continue. Three buttons on one bar have to rank
     themselves, and a bordered "set up later" beside a filled "Continue" read as two offers of
     equal weight — which is not what this screen means. */
  border-color: transparent;
  background: none;
  color: var(--text-muted, var(--text));
}

.starter__later:hover,
.starter__later:focus-visible {
  color: var(--text);
  border-color: var(--border);
}

/* What the licence leaves room for, under the Add button it governs. */
.starter__allowance {
  margin: 4px 0 0;
}

/*
 * The footer wraps, which is what lets a phone put the third button on its own line.
 *
 * Without this the `flex: 1 1 100%` below had nothing to wrap ONTO: the row is a plain flex
 * line, so a basis of 100% is simply shrunk back to fit and the three buttons stay jammed
 * together — which is exactly what a phone showed.
 */
.starter__foot {
  flex-wrap: wrap;
}

@media (max-width: 560px) {
  /*
   * On a phone: Back and Continue share the first line, and the sentence-long way past goes
   * underneath, quieter and smaller.
   *
   * Three buttons do not fit one phone line, and the one that should give way is the one least
   * likely to be pressed. Continue takes the rest of the line and sits on the RIGHT of Back,
   * which is where a primary belongs — it was on the left, before the wrap worked, because an
   * auto margin on the third button had shoved it there.
   */
  .starter__foot {
    gap: 10px;
  }

  .starter__foot .btn--cta {
    order: 2;
    flex: 1 1 auto;
  }

  .starter__later {
    order: 3;
    flex: 1 1 100%;
    margin-left: 0;
    text-align: center;
    /* Smaller than the two above it: it is an aside about this screen, not one of its two
       answers, and at the same size it read as a third equal choice. */
    font-size: 0.8rem;
    padding: 8px 10px;
  }
}

.edit.is-new {
  animation: floor-in 190ms var(--ease-arrive) both;
}

.edit.is-going {
  animation: floor-out 150ms var(--ease-out) both;
  pointer-events: none;
}

@media (max-width: 560px) {
  /* The name on its own line, and the answers about it underneath — the same rearrangement
     the floor rows make, and for the same reason: this row wraps at this width regardless,
     and wrapping mid-row leaves a tick box marooned beside a Remove button. */
  .edit__label {
    flex: 1 1 100%;
  }

  /*
   * Except where the row IS just a name. A location has no ticks, so stacking left the × alone
   * on a line under the field, which reads as a control belonging to nothing. Beside the name
   * is where it belongs when there is nothing else competing for the line.
   */
  .edit--plain .edit__label {
    flex: 1 1 0;
    min-width: 0;
  }

  .edit__drop {
    margin-left: auto;
  }

  /*
   * Which name the tick is about, settled by spacing.
   *
   * Stacked, every gap was the same size, so a row's "QR placard" sat exactly as close to the
   * name BELOW it as to its own — and read as belonging to the wrong one. Found in a
   * screenshot; nothing about it is visible at desktop width, where the row is a row.
   *
   * A row is now tight inside and loose outside, which is the only thing that says what
   * belongs to what when there is no line to follow.
   */
  .edit {
    row-gap: 2px;
  }

  .starter__rows {
    gap: 16px;
  }

  /* And indented under the name, so the second line reads as subordinate rather than as the
     start of the next thing. */
  .edit__placard {
    padding-left: 12px;
  }
}

/* ------------------------------------------------------- numbering, a floor at a time */

.starter__whereRow {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 14px 0 10px;
}

.starter__whereLabel {
  font-size: 0.84rem;
  color: var(--text-muted, var(--text));
}

.starter__whereOne {
  font-weight: 600;
}

/* Why there is a picker at all, beside the picker. A chain's second hotel is not numbered like
   its first, and nothing else on the screen says the two sets are separate. */
.starter__whereNote {
  flex: 1 1 100%;
  font-size: 0.82rem;
  color: var(--text-muted, var(--text));
}

.starter__floors {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/*
 * One floor: its name, two numbers, what that comes to, and a way to drop it.
 *
 * Wraps rather than scrolls, because on a phone this is four controls in a 320px column and
 * the alternative to wrapping is a row nobody can reach the end of.
 */
.floor {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface-2, var(--surface));
}

.floor__name {
  font-weight: 650;
  /* Fixed, so the inputs on every row start at the same x — a ragged left edge down a
     column of ten floors reads as ten unrelated controls. Bottom-aligned with them, since
     the captions above the boxes would otherwise leave it floating. */
  flex: 0 0 74px;
  align-self: flex-end;
  padding-bottom: 9px;
}

.floor__drop {
  align-self: flex-end;
}

/* A captioned box. The caption is what makes two fields read as "first" and "last" rather
   than as one range somebody has to know the syntax for. */
.floor__field {
  display: flex;
  flex-direction: column;
  gap: 3px;
  flex: 0 1 120px;
  min-width: 96px;
}

.floor__cap {
  font-size: 0.74rem;
  letter-spacing: 0.01em;
  color: var(--text-muted, var(--text));
}

.floor__num {
  width: 100%;
}

.floor__count {
  flex: 1 1 auto;
  font-size: 0.84rem;
  color: var(--text-muted, var(--text));
  /* The captions raise the boxes, so a count on the row's centre line sits above the text it
     is about. Nudged down to sit with the inputs instead. */
  align-self: flex-end;
  padding-bottom: 9px;
}

.floor__count.is-bad,
.starter__total.is-bad {
  color: var(--danger);
  font-weight: 600;
}

.floor__drop {
  margin-left: auto;
}

/* A floor arriving under the one above it, and leaving without the rows below it jumping up
   into the gap. Both are short: this is a row in a form, not a page. */
.floor.is-new {
  animation: floor-in 190ms var(--ease-arrive) both;
}

.floor.is-going {
  animation: floor-out 150ms var(--ease-out) both;
  pointer-events: none;
}

@keyframes floor-in {
  from {
    opacity: 0;
    transform: translateY(-6px);
  }
}

@keyframes floor-out {
  to {
    opacity: 0;
    transform: translateX(14px);
  }
}

/*
 * On a phone, a floor is a small block rather than a row.
 *
 * The row wraps at this width whatever it is told, and wrapping put "First room" beside the
 * floor's name and "Last room" underneath it against the left edge — two boxes that are a pair,
 * drawn as though they were not, with the name stranded between them. Found in a screenshot;
 * every assertion about this step passed at both widths.
 *
 * So the name takes the whole first line and the two boxes share the second, which is the
 * shape they have on a wide screen and the shape they mean.
 */
@media (max-width: 560px) {
  .floor__name {
    flex: 1 1 100%;
    align-self: flex-start;
    padding-bottom: 0;
  }

  .floor__field {
    /* Equal halves of one line. `min-width: 0` because a flex item will not shrink below its
       content otherwise, and an input's default size is wider than half a phone. */
    flex: 1 1 0;
    min-width: 0;
  }

  .floor__count {
    flex: 1 1 100%;
    align-self: flex-start;
    padding-bottom: 0;
  }

  /* Sized to its label, not to the line it landed on: a full-width "Remove" is the loudest
     control in a block whose subject is two numbers. */
  .floor__drop {
    margin-left: 0;
    flex: 0 0 auto;
    align-self: flex-start;
    width: auto;
  }
}

.starter__floorAdd {
  padding: 10px 0 2px;
}

.starter__total {
  margin: 6px 0 0;
  font-weight: 650;
}

/* ------------------------------------------------ the circuit's own mark, or ours
 *
 * Two marks ship in the guest page and exactly one is drawn. The server stamps `data-logo` on
 * <html> before first paint — see themeScript in src/app.js — so this is settled by the time the
 * page renders and a guest never sees one mark replaced by the other.
 *
 * Written as a pair of rules keyed off the stamp rather than a `hidden` attribute set by script,
 * for that reason alone: JS runs after paint, and a flash of somebody else's brand on the one
 * page the public sees is the thing this feature exists to remove.
 */
.brand-logo {
  display: none;
}

html[data-logo] .guest-hero .brand-logo {
  display: block;
  /* Bounded, not sized: a logo is whatever shape the company's logo is, and the page should
     accommodate it rather than stretch it. Height is what keeps the header consistent between
     a wide wordmark and a square badge. */
  max-height: 44px;
  max-width: min(260px, 70vw);
  width: auto;
  height: auto;
  object-fit: contain;
  margin-bottom: 4px;
}

html[data-logo] .guest-hero .wordmark {
  display: none;
}

/*
 * The credit, under the send button and under the anonymity note.
 *
 * It used to be a footer at the foot of the document — below the confirmation panel, which on a
 * form this short put it off the end of the page where nobody read it. Here it is out of the way
 * of the thing a guest came to do and still on screen while they do it.
 *
 * Quieter than the note above it: that note is a promise about their data and this is a byline.
 * Never conditional on the logo — a circuit's own mark goes at the top and this stays.
 */
.guest-send__by {
  margin: 6px 0 0;
  text-align: center;
  font-size: 0.72rem;
  letter-spacing: 0.02em;
  color: var(--text-faint);
}

.guest-send__by strong {
  font-weight: 600;
  color: var(--text-muted, var(--text-faint));
}


/* ------------------------------------------------- somebody else is in this circuit
 *
 * The band that says the deployment's support account is signed in right now.
 *
 * Deliberately the same shape and the same amber as `.borrowed` above, because they are the
 * same KIND of statement — "you are in a mode you did not choose to be reminded of" — and two
 * different-looking bands stacked on one page would read as two unrelated warnings rather than
 * as one page describing its situation.
 *
 * No fixed frame, though, and that is the difference. A borrowed view needs the frame because
 * the risk is forgetting whose dashboard you are reading and acting on it. Nothing here changes
 * what the reader is looking at or what their clicks do; it is information, not a mode, so it
 * scrolls away like the rest of the page.
 */
.watched {
  background: color-mix(in oklab, var(--warn) 12%, var(--surface));
  border-bottom: 1px solid color-mix(in oklab, var(--warn) 55%, transparent);
  /*
   * On the veil's own layer, and after it in the page, so it paints OVER it — and still under
   * the bar (20) once it scrolls up that far.
   *
   * On a phone the top veil (`body::before`, 19) is a fixed 150px fade to the page's ground, and
   * it sits over whatever is at the top of the page at rest. For content that is the point: it
   * dissolves on its way under the bar. For these bands it was a bug nobody had looked for — the
   * band IS the top of the page, so its tag and first line sat in the veil's solid part, and the
   * sentence a manager most needed ("the app becomes read-only on Tuesday") was mist. A
   * screenshot of the grace band on a phone found it; the read-only band had it all along.
   */
  position: relative;
  z-index: 19;
}

/* The suspended band, a shade firmer than the support one it sits beside: this is a state the
   reader has to do something about, where support being present is a thing they agreed to. Still
   amber rather than red — nothing is broken and nothing is lost. See markSuspended in util.js.

   The grace band wears the same shade: an invoiced licence that has ended and not yet locked is
   the same state a day early, and the reader has the same thing to do about it. See markGrace. */
.watched--suspended,
.watched--grace {
  background: color-mix(in oklab, var(--warn) 20%, var(--surface));
  border-bottom-color: color-mix(in oklab, var(--warn) 75%, transparent);
}

.watched__inner {
  max-width: 1180px;
  margin: 0 auto;
  padding: 9px 20px;
  display: flex;
  align-items: center;
  gap: 8px 14px;
  flex-wrap: wrap;
  font-size: 0.86rem;
}

.watched__tag {
  text-transform: uppercase;
  letter-spacing: 0.07em;
  font-size: 0.68rem;
  font-weight: 700;
  color: var(--warn);
  white-space: nowrap;
}

.watched__what {
  color: var(--text);
  /* Takes the width on a narrow screen and pushes the link to its own line, rather than
     being truncated — the sentence is the whole point of the band. */
  min-width: 0;
}

/* Pushed to the end, in the same place the borrowed band keeps its way out. */
.watched__out {
  margin-left: auto;
  font-weight: 600;
  color: var(--warn);
  white-space: nowrap;
}


/* ----------------------------------------------------------------- issue types
 *
 * One row per type in Settings: its name as an editable field, what it is called in the
 * database, what has been filed under it, and the buttons that apply to it.
 *
 * A row rather than a table because the actions belong to the row and a table cell full of
 * buttons wraps badly on a phone — this is a list somebody edits, not a grid they read across.
 */
.types {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin: 14px 0 18px;
}

.type {
  display: grid;
  /* Name and buttons on one line while there is room, stacked when there is not. The facts
     line always spans, so it reads as a caption under the name rather than a third column. */
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 4px 12px;
  padding: 12px 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
}

.type__main {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  min-width: 0;
}

.type__name {
  flex: 1 1 200px;
  min-width: 0;
}

/* The notice window on an inspection row: a label, a narrow number, and "mo" so the unit is
   on screen rather than in a tooltip. Narrow because the value is 1–11 — a full-width number
   box would imply a bigger answer than the server will take. */
.type__months {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 0.84rem;
  color: var(--text-dim);
  white-space: nowrap;
}

.type__months-input {
  width: 72px;
}

/* How often it comes round: sized to its longest phrase ("Every 6 months (default)") and no wider,
   so the row keeps the name as its widest part. */
.type__every {
  flex: 0 0 auto;
  width: auto;
  max-width: 100%;
}

/* Booked ahead: a checkbox with its words, the whole of it clickable, as every checkbox here is. */
.type__bookable {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 0.84rem;
  color: var(--text-dim);
  white-space: nowrap;
  cursor: pointer;
}

/* The required-here checkbox on a location's view of the inspections — the name is a label
   for the box, so the whole name is clickable, like every other checkbox on this page. */
.type__required {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  flex: 1 1 200px;
  min-width: 0;
  cursor: pointer;
}

/* The permanent key, quiet and monospaced. On screen because it is what a report actually
   stores and what the API takes — and because it explains why renaming is safe. */
.type__key {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.78rem;
  color: var(--text-dim);
}

.type__facts {
  grid-column: 1 / -1;
  margin: 0;
}

/*
 * Which issue types count towards an inspection: a fold across the foot of the row, shut by default,
 * its summary saying what is chosen so a shut fold still answers the question. The choices wrap as a
 * row of checkboxes — ten issue types in a column would be taller than the rest of the list.
 */
.type__counts {
  grid-column: 1 / -1;
  font-size: 0.84rem;
}

.type__counts-summary {
  cursor: pointer;
  color: var(--text-dim);
}

.type__counts-list {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 16px;
  padding-top: 6px;
}

.type__counts-choice {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
}

.type__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  align-items: start;
}

/* --------------------------------------------- what kind of business one building is
 *
 * Sits in the row of things you DO to a location, beside Print QR placards, so it has to keep
 * that row's rhythm: inline, one line, the same height as the buttons either side of it. Left
 * to itself the label wrapped above a full-width select and the row stopped reading as a row.
 */
.preset-pick {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

/* What it is now, before the control that changes it — a statement, not a button. */
.preset-pick__now {
  font-size: 0.86rem;
  font-weight: 600;
  color: var(--text-dim);
  white-space: nowrap;
}

.preset-pick__select {
  /* Wide enough for the longest preset name and no wider; the row has other work to do. */
  max-width: 190px;
  padding: 6px 10px;
  font-size: 0.86rem;
}

/* ------------------------------------------- what a placard asks, inside the place itself
 *
 * SPANS BOTH COLUMNS, like the facts line above it, and that is load-bearing rather than
 * cosmetic: .type is a two-column grid — name on the left, buttons on the right — and a
 * fourth child left to auto-place lands back in the name's column, which squeezes the name
 * field into a strip and stacks "QR code" underneath it. Which is exactly what the first
 * draft of this did.
 */
.place__ask {
  grid-column: 1 / -1;
}

/* A place with no code asks nothing, and an empty block would still take a grid row's gap. */
.place__ask:empty {
  display: none;
}

/* Set in from the row's own edge, so the pills read as belonging to the place above them
   rather than as a second row of controls at the same level. */
.place__ask .setting__place {
  margin-top: 8px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
}

/* Retired: still legible, visibly not in play. Dimmed rather than hidden, because the row is
   how somebody restores it — and its name is still what old reports read by. */
.type--retired {
  background: var(--surface);
  border-style: dashed;
}

.type--retired .type__name {
  color: var(--text-dim);
}

@media (max-width: 560px) {
  .type {
    grid-template-columns: minmax(0, 1fr);
  }
}

/*
 * A type with an unsaved edit on it.
 *
 * The accent down the left edge, which is what the app uses elsewhere to mean "this row is not
 * like its neighbours" — and a caption on the row says what changed in words, because a colour
 * alone cannot distinguish a rename from a retirement, and this page may well be read by
 * somebody who cannot tell the two hues apart.
 */
.type--dirty {
  border-color: color-mix(in oklab, var(--accent) 45%, var(--border));
  box-shadow: inset 3px 0 0 0 var(--accent);
}

/*
 * Show more, under a list the server has only sent one page of.
 *
 * Centred and quiet: it is the end of the list rather than an action anybody came for, and it
 * carries the honest count above it — see public/js/paging.js for why that sentence exists.
 */
.more {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  padding: 28px 16px 8px;
  text-align: center;
}

.more__count {
  margin: 0;
  color: var(--text-dim);
  font-size: 0.9rem;
}

/* ------------------------------------------------------- the strength meter
 * Under every field where a password is chosen. The rule it draws is shared with the
 * server (public/js/password-strength.js), so the bar and the refusal cannot disagree.
 *
 * Hidden until something is typed: an empty box is not a weak password, and a red bar
 * on a field nobody has touched reads as a form that is already wrong.
 */
.strength {
  margin-top: 8px;
}

/* Four blocks rather than one growing bar. A percentage of a password means nothing;
   four segments are countable, and they are the same four rules listed underneath. */
.strength__bar {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 4px;
}

.strength__seg {
  height: 6px;
  border-radius: 999px;
  background: var(--surface-2);
  transition: background-color 180ms var(--ease);
}

/* Colour AND how many are lit move together, so the meter still reads for somebody who
   cannot tell red from green — and the words beside it say the same thing again, which is
   what actually carries it. */
.strength[data-score='1'] .strength__seg.is-on {
  background: var(--danger);
}

.strength[data-score='2'] .strength__seg.is-on {
  background: var(--warn);
}

.strength[data-score='3'] .strength__seg.is-on,
.strength[data-score='4'] .strength__seg.is-on {
  background: var(--ok);
}

.strength__says {
  margin: 8px 0 0;
  font-size: 0.82rem;
  color: var(--text-dim);
}

.strength__label {
  font-weight: 650;
  color: var(--text);
}

/* The rules, ticking as they are met. */
.strength__rules {
  list-style: none;
  margin: 8px 0 0;
  padding: 0;
  display: grid;
  gap: 4px;
}

.strength__rule {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 0.8rem;
  color: var(--text-faint);
  transition: color 180ms var(--ease);
}

.strength__rule.is-met {
  color: var(--text-dim);
}

/* An empty ring until the rule is met, then a filled tick. A shape change as well as a
   colour change, for the same reason the segments count as well as colour. */
.strength__mark {
  flex: none;
  width: 13px;
  height: 13px;
  border-radius: 50%;
  border: 1.5px solid var(--border-strong);
  position: relative;
  transition: background-color 180ms var(--ease), border-color 180ms var(--ease);
}

.strength__rule.is-met .strength__mark {
  background: var(--ok);
  border-color: var(--ok);
}

.strength__rule.is-met .strength__mark::after {
  content: '';
  position: absolute;
  left: 3.5px;
  top: 1px;
  width: 3px;
  height: 6px;
  border-right: 1.5px solid var(--bg);
  border-bottom: 1.5px solid var(--bg);
  transform: rotate(45deg);
}

/* Somebody who asked for no motion gets the state and none of the movement. */
@media (prefers-reduced-motion: reduce) {
  .strength__seg,
  .strength__rule,
  .strength__mark {
    transition: none;
  }
}

/* ------------------------------------------------- where you are signed in
 * Settings > Where you are signed in, every role. Rows rather than cards, like the log
 * below — but each row here carries an action, so it needs the room the log does not.
 *
 * The list is short by construction: a handful of browsers, and only the ones still live.
 */
.sessions {
  list-style: none;
  margin: 14px 0;
  padding: 0;
}

.session {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 12px;
  padding: 12px 0;
  border-bottom: 1px solid var(--border);
}

.session:last-child {
  border-bottom: 0;
}

/* The one you are reading this on, marked with an edge as well as the badge. Scanning for
   "which of these is me" is the first thing anybody does here. */
.session--current {
  box-shadow: inset 3px 0 0 var(--ok, var(--accent));
  padding-left: 12px;
}

.session__top {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  flex: 1 1 100%;
}

.session__what {
  font-weight: 650;
  color: var(--text);
}

.session__when {
  color: var(--text-faint);
  font-size: 0.82rem;
  margin-left: auto;
}

.session__facts {
  margin: 0;
  flex: 1 1 auto;
  font-size: 0.84rem;
  color: var(--text-dim);
  overflow-wrap: anywhere;
}

/* ------------------------------------------------------------- the sign-in log
 * Settings > Sign-in log, dev only. A run of rows rather than cards: it is read by
 * scanning down it for a pattern, and a stack of bordered boxes makes that harder.
 */
.audit__filters {
  /* The shared filter row brings its own padding for a bordered panel; here it sits under a
     paragraph of prose and needs the gap above instead. */
  padding: 0 0 12px;
  margin-top: 14px;
}

.audit__row {
  padding: 10px 0;
  border-bottom: 1px solid var(--border);
}

.audit__row:last-child {
  border-bottom: 0;
}

/* A refusal is the thing worth spotting while scrolling, so it gets an edge as well as
   the badge — the badge says it and the edge is what makes a run of them visible. */
.audit__row--refused {
  box-shadow: inset 3px 0 0 var(--danger);
  padding-left: 12px;
}

.audit__top {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

.audit__action {
  font-weight: 650;
  color: var(--text);
}

.audit__when {
  color: var(--text-faint);
  font-size: 0.82rem;
  margin-left: auto;
}

.audit__who,
.audit__detail {
  margin: 2px 0 0;
  font-size: 0.84rem;
  color: var(--text-dim);
  overflow-wrap: anywhere;
}

.audit__detail {
  color: var(--text-faint);
}

/* The count line and the download sit on one row, so the button is beside what it
   would give you rather than under a list it has to be scrolled past. */
.audit__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  margin-bottom: 4px;
}

.audit__head .field__hint {
  margin: 0;
}

/* --------------------------------------------------- two-step verification */
.totp__qr {
  display: block;
  /* On its own white ground, always. A QR code inverted by a dark theme is a QR code no
     camera will read, and the failure looks like a broken phone rather than a broken page. */
  background: #fff;
  padding: 8px;
  border-radius: var(--radius-sm);
  margin: 12px 0;
}

.totp__secret {
  font-family: var(--mono);
  letter-spacing: 0.08em;
  overflow-wrap: anywhere;
}

/* Read off a screen by somebody having a bad day, so: monospaced, spaced out, and
   selectable in one gesture. */
.totp__codes {
  font-family: var(--mono);
  font-size: 0.95rem;
  line-height: 1.9;
  letter-spacing: 0.06em;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 12px 16px;
  margin: 12px 0;
  user-select: all;
}

/* ------------------------------------------------- withheld from the support account
 * The server took the text out — see src/privacy.js — and this is what stands where it was.
 *
 * A blur rather than a gap. An empty line reads as a report filed by somebody who typed
 * nothing, which is a bug worth chasing; a blurred bar reads as a decision somebody made.
 * There is nothing behind it to reveal: these characters are a placeholder the page invented,
 * not the guest's words with a filter over them.
 */
.complaint__body--hidden {
  user-select: none;
}

.redacted {
  display: inline-block;
  max-width: 42ch;
  border-radius: 3px;
  background: var(--surface-2, var(--surface));
  color: var(--text-faint);
  /* Enough to be unreadable at any zoom, and still obviously a line of text rather than a box. */
  filter: blur(4px);
  /* Not selectable and not read out: it is a placeholder, and copying it would paste a sentence
     nobody wrote. The paragraph carries an aria-label so a screen reader says "Hidden" instead. */
  pointer-events: none;
}

/* ----------------------------------------- the charts, withheld from the support account
 * Drawn instead of the real ones, not over them. The figures arrive zeroed from the server
 * — see FIGURE_KEYS in src/privacy.js — so there is nothing here to un-blur: these bars are
 * invented by the page at fixed heights and claim nothing about anybody's business.
 */
.chart--hidden__bars {
  display: flex;
  align-items: flex-end;
  gap: 10px;
  height: 120px;
  margin-top: 16px;
  filter: blur(5px);
  opacity: 0.55;
  pointer-events: none;
}

.chart--hidden__bar {
  flex: 1 1 0;
  border-radius: 4px 4px 0 0;
  background: var(--accent, var(--text-dim));
}

/* A download the support account may not open. Still visible, so the page reads the same for
   everybody and the reason is one click away rather than a missing button somebody hunts for. */
.btn--blocked {
  opacity: 0.55;
  cursor: not-allowed;
}

/* ============================================================= the deployment console
   One card per circuit, each a row of counts. The classes are `con*` rather than reusing the
   overview's stat tiles wholesale, because a circuit card packs thirteen small figures where a
   stat tile carries one hero number — different density, different type scale. */

.concard {
  margin-bottom: 16px;
}

/* An unreachable circuit is dimmed but present: on a fleet page, "this one did not answer" is
   information, and a card that vanished would read as a circuit that never existed. */
.concard--down {
  opacity: 0.75;
}

.conpanel__head {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px 12px;
  margin-bottom: 14px;
}

.concard__name {
  margin: 0;
  font-size: 1.05rem;
}

/* The durable name beside the human label, quiet and monospaced — it is what the routing and
   the registry key on, and what a support conversation needs spelled exactly. */
.concard__slug {
  font-family: var(--mono);
  font-size: 0.78rem;
  color: var(--text-faint);
}

.concard__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(128px, 1fr));
  gap: 10px 14px;
  margin-bottom: 12px;
}

.confact {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
}

.confact__value {
  font-size: 1.15rem;
  font-weight: 650;
  font-variant-numeric: tabular-nums;
}

.confact__label {
  font-size: 0.74rem;
  color: var(--text-dim);
  letter-spacing: 0.02em;
}

/*
 * A figure that is also the way to the list behind it — bugs open, AI gaps open — opening the
 * console's own section on that circuit. Drawn as the figures beside it, with a hover and a focus
 * ring that say it can be pressed, and none of a link's underline.
 */
.confact--link {
  color: inherit;
  text-decoration: none;
  cursor: pointer;
}

.confact--link:hover {
  border-color: var(--accent);
}

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

.concard__pulse {
  margin: 0;
}

/* The card's second line under the pulse — how far in the product goes — sits a little below it. */
.concard__pulse + .concard__pulse {
  margin-top: 4px;
}

/*
 * The marketing numbers (js/console.js, src/marketing.js): one figure a tile, its number first, what
 * it is and over when under it, and what it rests on last — the line that lets it be quoted. A
 * figure that is mostly one customer says so in the warning's colours, in words, never colour alone.
 */
.mknums {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
  gap: 12px;
  margin: 14px 0 0;
  padding: 0;
  list-style: none;
}

.mknums__item {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 14px 16px;
  border-radius: var(--radius);
  background: var(--surface-2);
}

.mknums__value {
  font-size: 1.6rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
}

.mknums__label {
  font-weight: 600;
  font-size: 0.9rem;
}

.mknums__basis {
  color: var(--text-dim);
  font-size: 0.8rem;
  overflow-wrap: anywhere;
}

.mknums__mostly {
  align-self: flex-start;
  margin-top: 4px;
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--warn-soft);
  color: var(--warn);
  font-size: 0.75rem;
  font-weight: 650;
}

.mknums__actions {
  display: flex;
  justify-content: flex-end;
  margin-top: 14px;
}

/*
 * The card prices: a row of three fields and a button, which wraps to a column on a phone. Not
 * .prices, which is the ticket types' price list in Settings and sits later in this file — its
 * `margin: 0` and grid would win here.
 */
.cardprices {
  margin-top: 16px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
}

.cardprices__fields {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 10px 14px;
}

.cardprices__fields .field {
  flex: 1 1 160px;
  margin: 0;
}

/* The fleet table's last cell: the leave-out button and End, side by side and never squeezed. */
.fleet__actions {
  white-space: nowrap;
}

.fleet__actions .btn + .btn {
  margin-left: 6px;
}

/* The adoption line: which features this customer uses, as badges beside a quiet label. */
.concard__features {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin: 0 0 10px;
}

/* --------------------------------------------------------------------- the section rail
   A vertical menu beside the content, in the manner of the admin dashboards the operator
   asked the console to feel like: a handful of sections, the active one a filled pill.
   Anchors, so the stylesheet keys the pill on aria-current — the state IS the style, set
   by one place.

   Shared by the deployment console and by Settings, which is why it is `rail` and not
   `conrail` as it was when only one page had one. Both pages have the same problem — more
   sections than fit on a screen anybody can scan — and a second, near-identical rail
   would have been exactly the sort of accretion Settings was reorganised to undo. */

.railed {
  display: grid;
  grid-template-columns: 200px minmax(0, 1fr);
  gap: 20px;
  align-items: start;
}

/*
 * Exactly two children, and the second holds everything.
 *
 * Not a style so much as a load-bearing rule: the rail is sticky, and a sticky element only
 * travels as far as its own containing block. Let the sections be grid items in their own
 * right and the rail's box is one row tall — it scrolls away with the first card, which on a
 * page you scroll is a menu that is never there when you want it.
 */
.railed__body {
  /* Long tables and wide code inside a grid item overflow the column without this. */
  min-width: 0;
}

.rail {
  /* Below the sticky topbar, so scrolling a long fleet keeps the menu in reach. */
  position: sticky;
  top: 76px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 10px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
}

.rail__item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 12px;
  border-radius: 9px;
  font-size: 0.92rem;
  font-weight: 600;
  color: var(--text-dim);
  text-decoration: none;
  transition:
    background-color 140ms var(--ease),
    color 140ms var(--ease);
}

.rail__item:hover {
  background: var(--surface-3);
  color: var(--text);
}

.rail__item[aria-current='true'] {
  background: var(--accent-fill);
  color: var(--accent-ink);
}

.rail__icon {
  flex: none;
  width: 17px;
  height: 17px;
}

/* What is waiting behind a section you are not looking at — sales to chase on the console,
   unsaved edits on Settings. Warm rather than red in both: neither is an outage, and a badge
   that cries emergency at a half-typed field is one that gets ignored when it matters. */
.rail__count {
  margin-left: auto;
  min-width: 20px;
  padding: 1px 7px;
  border-radius: 999px;
  background: var(--warn-soft);
  color: var(--warn);
  font-size: 0.78rem;
  font-weight: 700;
  text-align: center;
}

.rail__foot {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-top: 10px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
}

.rail__note {
  margin: 0;
  font-size: 0.76rem;
  line-height: 1.45;
  color: var(--text-faint);
  overflow-wrap: anywhere;
}

/* The rail folds to a horizontal strip where there is no room for a column — the same
   breakpoint family the rest of the shell uses for one-column layouts. */
@media (max-width: 860px) {
  .railed {
    grid-template-columns: minmax(0, 1fr);
  }

  .rail {
    position: static;
    flex-direction: row;
    align-items: center;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
  }

  .rail__foot {
    flex-direction: row;
    align-items: center;
    margin: 0 0 0 auto;
    padding: 0 0 0 12px;
    border-top: 0;
  }

  /* The observed line is detail, and a scrolling strip has no room for a sentence; the
     overview's own header still says what was read. */
  .rail__note {
    display: none;
  }
}

/* -------------------------------------------------------------- the headline figures
   The deployment as a row of stat cards: a tinted icon chip, the number, the label, and an
   honest delta where the payload carries the window before. The chip tint comes from the
   same soft tokens every badge uses, so the row recolours with all seven themes. */

.kpis {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 12px;
  margin-bottom: 20px;
}

/* The console's business figures carry longer labels ("Customers lost · 12 months"), and a label
   never wraps: three to a row at a laptop's width rather than four that spill out of their tiles. */
.kpis--wide {
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}

.kpi {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 14px 16px;
  background: var(--glass);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
}

.kpi__chip {
  flex: none;
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border-radius: 10px;
}

.kpi__chip svg {
  width: 20px;
  height: 20px;
}

.kpi__chip--accent {
  background: var(--accent-soft);
  color: var(--accent);
}

.kpi__chip--info {
  background: var(--info-soft);
  color: var(--info);
}

.kpi__chip--ok {
  background: var(--ok-soft);
  color: var(--ok);
}

.kpi__chip--warn {
  background: var(--warn-soft);
  color: var(--warn);
}

.kpi__chip--danger {
  background: var(--danger-soft);
  color: var(--danger);
}

.kpi__body {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.kpi__value {
  font-size: 1.55rem;
  font-weight: 780;
  letter-spacing: -0.035em;
  line-height: 1.15;
  font-variant-numeric: tabular-nums;
}

.kpi__label {
  font-size: 0.76rem;
  color: var(--text-dim);
  letter-spacing: 0.02em;
  white-space: nowrap;
}

.kpi__delta {
  align-self: flex-start;
  margin-left: auto;
  padding: 3px 8px;
  border-radius: 999px;
  font-size: 0.72rem;
  font-weight: 650;
  white-space: nowrap;
}

.kpi__delta--up {
  background: var(--ok-soft);
  color: var(--ok);
}

.kpi__delta--down {
  background: var(--danger-soft);
  color: var(--danger);
}

.kpi__delta--flat {
  background: var(--surface-3);
  color: var(--text-faint);
}

/* ------------------------------------------------------------------- the fleet table
   Every circuit as one sortable row. The headers are real buttons — sorting is keyboard
   work like any other — and the sorted column says so with aria-sort, which is also all
   the arrow is: the state, made visible. */

.fleet__scroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

.fleet__table {
  width: 100%;
  min-width: 720px;
}

.fleet__sort {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-weight: 650;
  color: inherit;
  cursor: pointer;
  white-space: nowrap;
}

.fleet__sort:hover {
  color: var(--accent);
}

.fleet__arrow {
  /* Reserved even when empty, so the header does not shift as sorting moves between columns. */
  display: inline-block;
  min-width: 1ch;
  color: var(--accent);
}

.fleet__num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}

th.fleet__num .fleet__sort {
  justify-content: flex-end;
  width: 100%;
}

.fleet__name strong {
  display: block;
}

.fleet__slug {
  font-family: var(--mono);
  font-size: 0.74rem;
  color: var(--text-faint);
}

.fleet__hosts {
  display: block;
  font-size: 0.78rem;
}

.fleet__hosts a {
  color: var(--accent);
  text-decoration: none;
}

.fleet__hosts a:hover {
  text-decoration: underline;
}

.fleet__hosts a + a::before {
  content: ' · ';
  color: var(--text-faint);
}

.fleet__row--down {
  opacity: 0.65;
}

/* ================================================================== the assistant
   A conversation, drawn the way chat is drawn everywhere: the asker on the right in the
   accent, the answers on the left on a card surface. The scroll region is the messages,
   not the page, so the composer never leaves reach. */

.chat {
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
  overflow: hidden;
}

.chat__scroll {
  overflow-y: auto;
  max-height: min(58vh, 640px);
  min-height: 260px;
  padding: 18px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

.chat__msg {
  max-width: 82%;
  padding: 10px 14px;
  border-radius: 14px;
}

.chat__msg--user {
  align-self: flex-end;
  background: var(--accent-fill);
  color: var(--accent-ink);
  border-bottom-right-radius: 5px;
}

.chat__msg--assistant {
  align-self: flex-start;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-bottom-left-radius: 5px;
}

.chat__text {
  margin: 0;
  /* The model answers in prose with deliberate line breaks; keep them, wrap the rest. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  font-size: 0.94rem;
  line-height: 1.55;
}

/* The "Looking into it…" placeholder breathes so a long think reads as alive. */
.chat__msg--thinking .chat__text {
  color: var(--text-dim);
  animation: chat-breathe 1.6s ease-in-out infinite;
}

@keyframes chat-breathe {
  50% {
    opacity: 0.45;
  }
}

/* What the answer looked at — quiet provenance under the bubble's text. */
.chat__intro {
  margin: auto 0;
}

/*
 * "Continues below", when an answer runs past the bottom of the transcript — which it now does on
 * purpose, because the transcript stops following the words down once the question is at the top
 * (see keepInView in js/assistant-widget.js). No height of its own and stuck to the bottom of what
 * is visible, so it coming and going never moves a word; the pill is drawn just above that line.
 */
.chat__more {
  position: sticky;
  bottom: 10px;
  z-index: 1;
  align-self: center;
  height: 0;
  /* The transcript's gap, which one more item — even an empty one — would otherwise add. */
  margin-top: -12px;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  cursor: pointer;
}

.chat__more-pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  transform: translateY(-100%);
  padding: 5px 10px 5px 12px;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  background: var(--surface);
  color: var(--text-dim);
  font-size: 0.78rem;
  font-weight: 650;
  white-space: nowrap;
  box-shadow: var(--shadow-lift);
}

.chat__more:hover .chat__more-pill {
  color: var(--text);
  border-color: var(--accent-line);
}

.chat__composer {
  display: flex;
  gap: 10px;
  align-items: flex-end;
  padding: 12px 14px;
  border-top: 1px solid var(--border);
  background: var(--surface-2);
}

.chat__composer textarea {
  flex: 1 1 auto;
  resize: none;
  min-height: 44px;
}

.chat__foot {
  margin: 0;
  padding: 8px 16px 12px;
  background: var(--surface-2);
}

/* ------------------------------------------------------------ a change, to confirm
   The card under an answer that drafted a change (src/assistant-actions.js, drawn by
   js/assistant-card.js): the case it is about, the words that will be saved exactly as they
   will be saved, and what saving them will do — above the one button that does it. It is the
   only thing in the transcript that acts, so it is a card on the page's own surface rather than
   more of the bubble, and its state is a pill in the corner that reads without reading.

   The bubble that carries one takes the transcript's whole width: a list of facts squeezed into a
   bubble's 82% wraps every value, and the reader is about to decide something from them. */

.chat__msg--card {
  max-width: 100%;
  align-self: stretch;
}

/*
 * What's new (js/whats-new.js): a short list in a box, each item a heading, a sentence or two, and a
 * button where it has a page. Narrower than the app's form boxes, because it is read rather than
 * filled in, and a line of prose wider than this is harder to read than it is to scroll.
 */
.newsbox {
  max-width: 560px;
}

.newsbox__list {
  display: grid;
  gap: 18px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.newsbox__item {
  display: grid;
  gap: 6px;
  justify-items: start;
}

.newsbox__title {
  margin: 0;
  font-size: 1rem;
  font-weight: 650;
}

.newsbox__text {
  margin: 0;
  color: var(--text-dim);
  font-size: 0.92rem;
  line-height: 1.5;
}

.newsbox__go {
  margin-top: 4px;
}

/*
 * "This didn't answer it", at the foot of every answer (js/assistant-widget.js). Quiet, because it
 * is under every answer and most answers did; set in by its own padding so its words line up with
 * the answer's. Pressed, the bubble takes the transcript's width while the box is open, as it does
 * for a card, so the box is not squeezed into the width of a one-line answer.
 */
.chat__msg--asking {
  max-width: 100%;
  align-self: stretch;
}

.chat__miss-foot {
  margin-top: 6px;
}

.chat__miss {
  margin-left: -12px;
  color: var(--text-dim);
}

.chat__missed {
  margin: 4px 0 0;
  font-size: 0.82rem;
  color: var(--text-dim);
}

.chat__miss-box {
  display: grid;
  gap: 8px;
  margin-top: 6px;
}

.chat__miss-label {
  font-size: 0.88rem;
  font-weight: 600;
}

/* A few lines rather than the 148px every other text box has, and a ground apart from the bubble's. */
.chat__miss-words {
  min-height: 5em;
  background-color: var(--surface);
}

.chat__miss-hint {
  margin: 0;
  font-size: 0.8rem;
  color: var(--text-dim);
}

.chat__miss-problem {
  margin: 0;
  font-size: 0.82rem;
  color: var(--danger);
}

.chat__miss-buttons {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 8px;
}

.aicard {
  display: grid;
  gap: 12px;
  margin-top: 12px;
  padding: 14px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface);
}

.aicard:focus {
  outline: none;
}

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

/* Waiting on the reader: the one state that asks for something, so the one with a coloured edge. */
.aicard--pending {
  border-color: color-mix(in oklab, var(--accent) 55%, var(--border-strong));
}

.aicard--sending {
  cursor: progress;
}

/* The pill keeps the corner and the title wraps beside it: "Mark CQ-7HF2-K93M resolved" is longer
   than a phone's card is wide, and a pill wrapped under it read as the title's second line. */
.aicard__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 10px;
}

.aicard__title {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 0.95rem;
  font-weight: 700;
  text-wrap: balance;
}

/* A case reference, which a line break would turn into two strings nobody can read back. */
.aicard__nobreak {
  white-space: nowrap;
}

/*
 * Save as Note or Resolved: the model's guess pressed, the other one press away. The app's own
 * segmented pair (.seg) beside its label, the two hugging the card's left edge rather than
 * stretched across the card's grid, where the track would run on past its buttons.
 */
.aicard__kind {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 10px;
  justify-self: start;
}

.aicard__state {
  flex: none;
  padding: 2px 9px;
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--accent);
  font-size: 0.74rem;
  font-weight: 650;
  white-space: nowrap;
}

.aicard--sending .aicard__state,
.aicard--cancelled .aicard__state {
  background: var(--surface-3);
  color: var(--text-dim);
}

.aicard--done .aicard__state {
  background: var(--ok-soft);
  color: var(--ok);
}

.aicard--failed .aicard__state {
  background: var(--danger-soft);
  color: var(--danger);
}

.aicard--unknown .aicard__state {
  background: var(--warn-soft);
  color: var(--warn);
}

/* A declined card is history: still readable, no longer asking to be read first. */
.aicard--cancelled > :not(.aicard__head, .aicard__outcome) {
  opacity: 0.65;
}

/* Label and value, two columns that stay two columns at a phone's width — the labels are short. */
.aicard__facts {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  gap: 4px 14px;
  margin: 0;
  font-size: 0.86rem;
  line-height: 1.45;
}

.aicard__fact {
  display: contents;
}

.aicard__fact dt {
  color: var(--text-dim);
}

.aicard__fact dd {
  margin: 0;
  overflow-wrap: anywhere;
}

.aicard__label {
  margin: 0;
  color: var(--text-dim);
  font-size: 0.78rem;
  font-weight: 650;
}

.aicard__words {
  display: grid;
  gap: 5px;
  margin: 0;
}

/* The words that will be saved, set apart as a quotation: exactly this, and nothing else. */
.aicard__words blockquote {
  margin: 0;
  padding: 9px 12px;
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  font-size: 0.9rem;
  line-height: 1.5;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* The line a guest will read is public, and tinted so it cannot be mistaken for the team's note —
   with a hairline of the same hue, because on the dark ground the tint alone sat within a shade of
   the note above it. Inset, so the edge takes no room and the two quotes stay the same width. */
.aicard__words--guest blockquote {
  background: var(--info-soft);
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--info) 40%, transparent);
}

.aicard__open {
  display: grid;
  gap: 6px;
  padding: 10px 12px;
  border-radius: var(--radius-sm);
  background: var(--warn-soft);
}

.aicard__open ul {
  display: grid;
  gap: 4px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.aicard__open li {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 8px;
  font-size: 0.86rem;
}

.aicard__ref {
  font-variant-numeric: tabular-nums;
}

.aicard__effects {
  display: grid;
  gap: 3px;
  margin: -6px 0 0;
  padding-left: 18px;
  font-size: 0.86rem;
  line-height: 1.45;
}

.aicard__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.aicard__outcome {
  margin: 0;
  font-size: 0.88rem;
  line-height: 1.45;
}

.aicard--failed .aicard__outcome {
  color: var(--danger);
}

/* ------------------------------------------------------------ a guide it used
   Under an answer that came from one of the circuit's own guides (src/guides.js, drawn by
   js/assistant-guides.js): the guide's name, then its video and other links, its PDF and the guide
   itself as buttons. The answer is plain text and none of it is ever a link, so this is where the
   video a manager was promised is pressed. A card on the page's own surface like the change card
   above, without the coloured edge: it asks for nothing. */

.aiguides {
  display: grid;
  gap: 10px;
  margin-top: 12px;
}

.aiguide {
  display: grid;
  gap: 6px;
  padding: 12px 14px 14px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface);
}

.aiguide__kind {
  margin: 0;
  color: var(--text-dim);
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.aiguide__title {
  color: var(--text);
  font-size: 0.95rem;
  font-weight: 700;
  text-decoration: none;
  text-wrap: balance;
}

.aiguide__title:hover {
  text-decoration: underline;
}

.aiguide__note {
  margin: 0;
  color: var(--text-dim);
  font-size: 0.85rem;
}

.aiguide__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 4px;
}

/* A long label ("Christie CP2220 lamp change, the maker's own video") wraps inside its button
   rather than running the button off the side of a phone's card. */
.aiguide__button {
  max-width: 100%;
  white-space: normal;
  text-align: left;
}

/* ------------------------------------------------------------ the past cases it named
   Under an answer that told how a fault was fixed before (past_fixes in src/assistant-tools.js,
   drawn by js/assistant-cases.js): one card of the same make as a guide's, a row per case — its
   reference, where it was and when it was fixed, then the button that opens it. Each row reads on
   one line where it fits and the button drops under the words on a phone. */

.aicases {
  margin-top: 12px;
}

.aicase {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 6px 12px;
  padding-top: 8px;
  border-top: 1px solid var(--border);
}

/* The first row sits straight under "Past cases", with no rule between them. */
.aiguide__kind + .aicase {
  padding-top: 0;
  border-top: 0;
}

.aicase__what {
  display: grid;
  gap: 2px;
  min-width: 0;
}

.aicase__reference {
  color: var(--text);
  font-size: 0.95rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.aicase__where {
  color: var(--text-dim);
  font-size: 0.85rem;
}

/* ------------------------------------------------------- the corner launcher and panel
   The assistant lives in the bottom-right of every staff page: a round launcher, and a
   panel that opens over the page rather than instead of it — asking a question should
   never mean losing the screen the question is about. The inner parts are the chat
   classes above; this block is only the floating chrome. */

.aihub__launcher {
  position: fixed;
  right: 18px;
  bottom: 18px;
  z-index: 25; /* over the page (topbar is 20), under any open <dialog>'s top layer */
  display: grid;
  place-items: center;
  width: 52px;
  height: 52px;
  border: 1px solid var(--border-strong);
  /*
   * A rounded SQUARE of the same glass as the dock, not a filled circle: the reference
   * button is a dark tile whose identity lives in the ring inside it, and matching the
   * dock's corner radius is what makes the two read as one family on the phone.
   */
  border-radius: 18px;
  background: color-mix(in srgb, var(--surface-3) 70%, transparent);
  -webkit-backdrop-filter: blur(18px);
  backdrop-filter: blur(18px);
  color: var(--text);
  cursor: pointer;
  /* The slight glow: the accent breathing off the tile, under the ordinary lift. */
  box-shadow:
    var(--shadow-lift),
    var(--rim),
    0 0 16px color-mix(in srgb, var(--accent-fill) 40%, transparent);
  transition: transform 140ms var(--ease);
}

/*
 * The gradient RING around the logo — where the launcher's colour lives now, the way the
 * reference wears its identity as a ring around the W. Drawn as a 2px annulus: a gradient
 * fill masked so only the border of the circle survives. The spark sits inside it.
 */
.aihub__launcher::before {
  content: '';
  position: absolute;
  inset: 8px;
  border-radius: 50%;
  padding: 2px;
  background: linear-gradient(
    135deg,
    color-mix(in oklab, var(--accent-fill) 70%, white),
    var(--accent-fill) 45%,
    color-mix(in oklab, var(--accent-fill) 55%, var(--p-pink))
  );
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  pointer-events: none;
}

/*
 * The static copy the staff pages ship (see the dock's own comment in their markup):
 * hidden until the pre-paint stamp says this circuit has an assistant at all, so the
 * tile is on the first frame of every page turn instead of popping in when the widget
 * module lands. The widget takes the --static class off at adopt time, which is what
 * keeps a borrowed ?as= view — never stamped — showing the launcher its script built.
 */
html:not([data-assistant]) .aihub__launcher--static {
  display: none;
}

/* Guarded like every hover in this app that a tap must not stick: iOS holds :hover
   after a press, and an unguarded lift left the tile floating 2px high on a phone. */
@media (hover: hover) {
  .aihub__launcher:hover {
    transform: translateY(-2px);
  }
}

.aihub__panel {
  position: fixed;
  right: 18px;
  bottom: 82px;
  z-index: 25;
  display: flex;
  flex-direction: column;
  /* Closed state: just under its open position and slightly small, so opening reads as the
     panel coming UP out of the launcher rather than appearing on top of the page. The
     transform origin is the corner it grows from. */
  transform: translateY(12px) scale(0.98);
  transform-origin: 100% 100%;
  opacity: 0;
  transition:
    transform 200ms var(--ease),
    opacity 160ms var(--ease);
  width: min(390px, calc(100vw - 36px));
  /* A desktop window has no on-screen keyboard to make room for, so this is the window and
     nothing more. The phone surface below is where the keyboard is dealt with. */
  height: min(600px, calc(100dvh - 120px));
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lift);
  overflow: hidden;
}

.aihub__panel--open {
  transform: translateY(0) scale(1);
  opacity: 1;
}

/* The launcher's spark turns as the panel opens — one movement, two parts of it. */
.aihub__launcher svg {
  transition: transform 200ms var(--ease);
}

.aihub__launcher[aria-expanded='true'] svg {
  transform: rotate(90deg);
}

.aihub__head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 14px;
  border-bottom: 1px solid var(--border);
  background: var(--surface-2);
}

.aihub__title {
  margin: 0;
  font-size: 0.98rem;
  /* One line. The close button gives up nothing, so without this the heading wrapped to
     "AI / Assistant" on a narrow phone and dragged the bar's height with it. */
  white-space: nowrap;
  /* The mark and the name as one unit, so they align on the same centre line and travel
     together. Flex rather than an inline SVG, which would sit on the text baseline and leave
     the star looking dropped. */
  display: flex;
  align-items: center;
  gap: 7px;
  min-width: 0;
}

/* The star keeps its size however narrow the bar gets — a flex item shrinks by default, and a
   squashed four-point spark reads as a smudge. The same declaration .spark carries. */
.aihub__title-spark {
  flex: none;
  color: var(--accent);
}

/*
 * The way out, hard against the right edge of the bar.
 *
 * `margin-left: auto` rather than a spacer element, and that is a repair rather than a tidy-up.
 * The header used to push the button across with a <span class="topbar__spacer">, borrowed from
 * the top nav — where a phone-width rule turns that class into `flex: 0 0 100%; height: 0` so
 * the nav can fold onto two lines. This bar has no second line to fold onto, so below that
 * breakpoint the spacer collapsed, stopped pushing, and the X came to rest against the heading
 * in the middle of the bar with empty space to its right.
 *
 * `flex: none` because the auto margin only works while the button keeps its width: as a
 * shrinkable item next to a nowrap heading it would be squeezed to nothing on a narrow phone.
 */
.aihub__close {
  display: grid;
  place-items: center;
  flex: none;
  margin-left: auto;
  width: 30px;
  height: 30px;
  border: 0;
  border-radius: 8px;
  background: none;
  color: var(--text-dim);
  cursor: pointer;
}

.aihub__close:hover {
  background: var(--surface-3);
  color: var(--text);
}

/* The scroll region grows to fill the panel; the page-sized cap above does not apply.
   `min-height: 0` is load-bearing rather than tidy: a flex item's default minimum is its
   content, so without it the transcript refuses to shrink, the panel overflows its own height,
   and the region has nothing left to scroll — which hands the gesture to the page behind. */
.aihub__scroll {
  flex: 1 1 auto;
  max-height: none;
  min-height: 0;
  /* A scroll that reaches the last message stops there, instead of continuing into the page. */
  overscroll-behavior: contain;
}

.aihub__error {
  margin: 0;
  padding: 8px 14px;
  font-size: 0.82rem;
  color: var(--danger);
  background: var(--danger-soft);
}

.aihub__composer textarea {
  min-height: 40px;
  max-height: 120px;
}

/*
 * The mic beside Ask, as its mark alone — see the note on THE MIC in js/assistant-widget.js. The
 * word is kept for a screen reader and taken off the screen: the composer is one narrow line, and
 * the width a word would take is the width the question is typed into. Square, the height of Ask.
 */
.aihub__composer .dictate {
  width: 44px;
  padding-inline: 0;
}

.aihub__composer .dictate__word {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

/* Send, as its mark alone too: the paper plane in the same square as the mic, so the two sit as a
   pair at the end of the line. Named Ask for a screen reader (js/assistant-widget.js). Never
   shrunk: the box beside it takes all the room there is, and with no word to hold it open the
   square was squeezed to 32px — the mic is kept whole the same way (.dictate). */
.aihub__send {
  flex: none;
  width: 44px;
  padding-inline: 0;
}

/* ---------------------------------------------------- the backup window picker
   One row per timeframe: the radio, the name, and the measured size beside it. An option
   over the restore limit stays visible but disabled — "too big" is information, and hiding
   it would make Most available look arbitrary. */

.backup-windows {
  display: grid;
  gap: 6px;
  margin: 10px 0 14px;
}

.backup-window {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 8px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
  cursor: pointer;
}

.backup-window:has(input:checked) {
  border-color: var(--accent);
}

.backup-window__label {
  font-weight: 650;
  font-size: 0.9rem;
  white-space: nowrap;
}

.backup-window__detail {
  font-size: 0.8rem;
  color: var(--text-dim);
}

.backup-window--over {
  opacity: 0.55;
  cursor: not-allowed;
}

/* ----------------------------------------------------------- a restore under way
   The one card on Settings while a backup is written back a step at a time (src/restore.js): what
   is being restored, how far it has got as a bar and a sentence, and the two things that can be done
   about it. Everything else on the page is hidden meanwhile — every request behind it answers 503
   until the restore finishes or is undone, so a card showing anything else would be a card of
   failures. The bar borrows the token bar's track rather than inventing a second bar language. */
.restore-run {
  display: grid;
  gap: 14px;
}

/* The grid's gap is the spacing here. A card's first heading brings 16px of its own and a hint 6px
   more, which stacked on the gap left the heading floating above the rest; hence the specificity. */
.card.restore-run > .restore-run__title,
.restore-run > .field__hint {
  margin: 0;
}

.restore-run__title {
  font-size: 1.1rem;
}

.restore-run__track {
  height: 14px;
  border-radius: 7px;
  overflow: hidden;
  background: var(--surface);
  box-shadow: 0 0 0 1px var(--border) inset;
}

.restore-run__fill {
  display: block;
  height: 100%;
  /* Set from the restore's own count of its work, in the script — through the CSSOM, since the CSP
     refuses a style attribute. */
  width: 0;
  background: var(--accent-fill);
  transition: width 320ms var(--ease);
}

.restore-run__said {
  margin: 0;
  font-weight: 650;
  /* The count grows every step; figures that kept their width stop the sentence twitching. */
  font-variant-numeric: tabular-nums;
}

.restore-run__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}

/* ---------------------------------------------------- deleting reports for good
   The reports a delete would take, one row each, ticked. A tick means "this goes", so an
   unticked row fades: it is the one being kept, and it should read as set aside rather than
   as selected. The list scrolls inside itself, because a site's test reports run to fifty or
   more and the confirmation under the list has to stay within reach. See settings.js. */

/* A date is ten characters: sized to them, not to the card, which on a desktop drew a box as
   wide as the page for a date and pushed Find reports to the far edge. */
.purge-date {
  flex: 0 1 13rem;
}

/* What was found, under the date: clear of the row above it, whether it is the list or the
   one line saying what was deleted. */
.purge-found {
  margin-top: 16px;
}

.purge-list {
  display: grid;
  gap: 2px;
  max-height: 22rem;
  overflow-y: auto;
  margin: 8px 0 14px;
  padding: 4px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
}

.purge-list__row {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 6px 8px;
  border-radius: var(--radius);
  cursor: pointer;
}

.purge-list__row input {
  margin-top: 3px;
  flex: none;
}

.purge-list__row:has(input:not(:checked)) {
  opacity: 0.5;
}

.purge-list__text {
  display: grid;
  gap: 2px;
  min-width: 0;
}

.purge-list__ref {
  font-weight: 650;
  font-size: 0.9rem;
  font-variant-numeric: tabular-nums;
}

.purge-list__detail {
  font-size: 0.8rem;
  color: var(--text-dim);
}

.aihub__foot {
  margin: 0;
  padding: 6px 14px 10px;
  background: var(--surface-2);
  font-size: 0.72rem;
}

/* ================================================================= the phone surface
   A DIFFERENT THING from the corner panel above, not a narrow version of it.

   The two want opposite geometry. The corner panel floats over a page that carries on working:
   it is small, it is anchored to a corner, and the page behind it still scrolls. A phone has no
   room for any of that, and it has a keyboard that takes half the screen — so on a phone the
   assistant is a card across the screen that holds the page still while it is open.

   NOT THE WHOLE SCREEN, AND NOT THE DOCK. It was the whole screen, and the operator found an
   entire page arriving "quite jarring"; then a sheet to the bottom edge, and they wanted it
   smaller, the dock still showing and the whole chat rounded. So it is a card, rounded all
   round, the dock's width: from a gap below the top of what can be seen (--sheet-gap) to 10px
   above the dock and its button (--sheet-floor), with the page dimmed around it (.aihub__scrim)
   and the dock in front of the dim. A press on the dim closes it, and so does the button, which
   is the close control while the chat is open. The gap is at least the status bar and 12px in
   the installed app, and otherwise a seventh of the visible height, so it shrinks with the
   keyboard instead of squeezing the chat.

   Trying to serve both from one set of rules is what produced a chat that floated in the middle
   of the screen with the page showing underneath it, could not be scrolled, and jumped about as
   the keyboard came and went. The class is put on by the widget from the same media query, so
   the JS and the stylesheet cannot disagree about which surface is in use.

   THE GEOMETRY IS THE VISIBLE STRIP AND NOTHING ELSE. --vtop is where the visible part of the
   window starts and --vh is how tall it is, both straight from window.visualViewport (see
   viewport.js). Because a `position: fixed` element is placed against the LAYOUT viewport —
   which on iOS neither shrinks for the keyboard nor moves when the visual viewport is scrolled —
   those two properties are the only way to cover exactly what a person can see. No vh, no
   percentage, and above all no innerHeight: the earlier version subtracted innerHeight from the
   visible height to guess a keyboard inset, and iOS changes innerHeight when it collapses its
   toolbars, so the guess outlived the keyboard. */
@media (max-width: 560px) {
  .aihub--phone .aihub__panel {
    --sheet-gap: max(
      calc(12px + env(safe-area-inset-top, 0px)),
      calc(var(--vh, 100dvh) * 0.14)
    );
    /*
     * Where it stops, 10px short: the top of the dock and its button while they are on screen
     * (--vfloor, measured by the widget off the two themselves). While a question is being typed,
     * the keyboard's top — the bottom of what can be seen, which the widget writes as --vfloor
     * without measuring anything, with the dock put away (see the end of this block). Or else the
     * bottom of what can be seen, clear of the home indicator. dvh is the fallback for a browser
     * with no visualViewport at all, which is also a browser with no on-screen keyboard to measure.
     */
    --sheet-floor: var(--vfloor, calc(var(--vtop, 0px) + var(--vh, 100dvh) - env(safe-area-inset-bottom, 0px)));

    inset-inline: var(--dock-inset, 14px);
    top: calc(var(--vtop, 0px) + var(--sheet-gap));
    height: calc(var(--sheet-floor) - 10px - var(--vtop, 0px) - var(--sheet-gap));
    bottom: auto;
    width: auto;
    max-height: none;
    border: 0;
    border-radius: 22px;
    /* No movement of its own: it rises out of the button below its corner and sinks back into it
       with the corner panel's (the base rule's), which a card this size carries — the sheet it
       was slid the whole height of the screen instead. */
  }

  /* The transcript is the ONLY thing that scrolls, and it has to be genuinely scrollable for
     the gesture to land here rather than on the page. */
  .aihub--phone .aihub__scroll {
    padding: 14px;
    -webkit-overflow-scrolling: touch;
    touch-action: pan-y;
  }

  /* Bigger targets, and clear of the home indicator. env() collapses to 0 while the keyboard is
     up, which is exactly when the indicator is not there either. */

  /* The header clears the status bar by where the sheet starts (--sheet-gap above), not by a
     padding of its own. It used to pad itself, back when the sheet covered the whole window:
     installed on a home screen the page starts under the clock, and a header up there put the
     close button where a tap belongs to iOS — a chat that could only be left by force-quitting. */
  .aihub--phone .aihub__head {
    padding: 14px 16px;
  }

  .aihub--phone .aihub__close {
    width: 40px;
    height: 40px;
  }

  .aihub--phone .aihub__composer {
    padding: 12px 14px;
  }

  /* The card stops above the dock, so it has no home indicator to clear. */
  .aihub--phone .aihub__foot {
    padding: 6px 14px 12px;
  }

  /* The page around the card, dimmed as the page behind every box in the app is (.modal::backdrop's
     colour), and pressable: the widget closes the chat on a press here. Over the page and the top
     bar, under the card and the dock (below). No blur: one over the whole page is redrawn on every
     frame the card moves, which is the stall the boxes' own backdrop switches its blur off to avoid. */
  .aihub--phone .aihub__scrim:not([hidden]) {
    position: fixed;
    inset: 0;
    z-index: 24;
    display: block;
    background: rgba(10, 10, 16, 0.66);
    opacity: 0;
    transition: opacity 240ms var(--ease);
  }

  /* With the rule above's :not([hidden]), and after it: that counts as a class, and a plain
     `.aihub__scrim--on` lost to it and left the strip lit — a scrim at opacity 0. */
  .aihub--phone .aihub__scrim--on:not([hidden]) {
    opacity: 1;
  }

  /*
   * THE DOCK IN FRONT OF THE DIM. The operator wants it still showing with the chat open, and
   * working: a tab goes to its page, and the conversation goes with it (the widget reopens it
   * there). The button beside it already stands at the chat's height (25), over the dim (24). The
   * More menu opens over the chat, with its own scrim between, so a press anywhere else puts the
   * menu away and leaves the chat. aihub-locked is on the page only while the phone's chat is open.
   */
  html.aihub-locked .dockbar {
    z-index: 26;
  }

  html.aihub-locked .dockbar__scrim {
    z-index: 27;
  }

  html.aihub-locked .dockbar__menu {
    z-index: 28;
  }

  /*
   * WHILE A QUESTION IS BEING TYPED, THE DOCK AND THE BUTTON STAND DOWN. The keyboard was meant to
   * cover them. In the installed app on the operator's iPhone it did not: both rode up with the
   * keys and floated over them, over the foot of a card that now runs to the keys. The card's
   * header has its own close, and a tap that takes the focus back brings them home. The widget
   * puts aihub-typing on the page only on a phone, while a field in the card has the focus (see
   * WHILE THE KEYS ARE UP in js/assistant-widget.js).
   */
  html.aihub-locked.aihub-typing .dockbar,
  html.aihub-locked.aihub-typing .aihub--phone .aihub__launcher {
    visibility: hidden;
  }
}

/* Nothing to dim anywhere else: the corner panel leaves the page lit and working. */
.aihub__scrim {
  display: none;
}

/*
 * NOTHING HERE HOLDS THE PAGE, and that is deliberate.
 *
 * The phone's card held it from the stylesheet twice. First with overflow:hidden on the document,
 * which iOS scrolls straight through; then with the body pinned by position:fixed over it, which
 * held — and in the installed iOS app a document that cannot scroll is laid out short at the foot
 * by the status bar's height. On the operator's phone the dock, the assistant's button and the dim
 * all rose 62pt when the chat opened, over a strip of bare ground. The widget holds the page by
 * refusing the gestures that would move it instead (lockPage in js/assistant-widget.js), and
 * aihub-locked is on <html> only to stand the dock in front of the dim (above). A rule here that
 * stops the document brings the band back, and a browser test fails on it.
 */

/* The composer's own scroll, once a question outgrows the box, stops at its end rather than handing
   the rest of the drag to the page — as the transcript's does (.aihub__scroll). */
.aihub--phone .aihub__composer textarea {
  overscroll-behavior: contain;
}

/* ============================================================ accessibility
 *
 * How one reader needs the app DRAWN, chosen per account in Settings → Accessibility and
 * applied to <html> before the first paint (see themeScript in src/app.js). Last in this
 * file on purpose: every rule here is somebody overriding a decision the stylesheet made
 * above, and the cascade should not need a specificity fight to let them.
 *
 * The registry in src/db.js says where each option comes from in WCAG. The rule these all
 * follow: a switch that changes only a stored value would be worse than no switch at all,
 * because it tells somebody the help is there when it is not.
 */

/* ------------------------------------------------- the option rows in Settings
 *
 * One row of buttons per accessibility option, with the current one marked. A dropdown was
 * the alternative and it is the wrong control here: what is being chosen is how the app is
 * drawn, the change lands on the page the instant it is pressed, and a list you have to open
 * hides the alternatives from somebody trying two to see which they can read.
 */
.choices {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.choice {
  padding: 7px 13px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface-2);
  font: inherit;
  font-size: 0.85rem;
  font-weight: 550;
  color: var(--text-dim);
  cursor: pointer;
  transition:
    background-color 140ms var(--ease),
    border-color 140ms var(--ease),
    color 140ms var(--ease);
}

.choice:hover {
  border-color: color-mix(in oklab, var(--accent) 45%, var(--border));
  color: var(--text);
}

/* The chosen one is filled rather than only outlined: `aria-pressed` tells a screen reader
   which it is, and a border alone is not enough of a difference for everybody else — which
   would be an odd thing to get wrong in this particular section. */
.choice--on {
  border-color: var(--accent);
  background: var(--accent-fill);
  color: var(--accent-ink);
}

/* ---------------------------------------------------------- skip to content
 *
 * WCAG 2.4.1. Every page repeats the same top bar — brand, up to five nav links, the
 * account menu — so a keyboard or switch user met a dozen stops before the page's own
 * content on every single navigation. There was no way past it; now the first Tab of any
 * page offers one.
 *
 * CLIPPED, not moved above the page — and that correction came from the home-screen app.
 *
 * It was `transform: translateY(-200%)`, which parks the link in the space above the
 * document. That space is not nothing on iOS: rubber-band past the top and the browser
 * shows you what is up there, so the link appeared on every overscroll — constantly, in a
 * standalone app where the page fills the screen and there is no chrome to hide behind.
 * Hidden by being scrolled out of view is only hidden while nobody scrolls the other way.
 *
 * So it is clipped to nothing where it stands, which is the same technique as
 * `.visually-hidden` above and for the same reason: `display: none` cannot be focused, and
 * a skip link nobody can focus is not a skip link. On focus it becomes a real box, below
 * the status bar on a phone that has one.
 */
.skip {
  position: absolute;
  z-index: 100;
  top: 0;
  left: 0;
  width: 1px;
  height: 1px;
  padding: 0;
  /*
   * No negative margin, unlike .visually-hidden above. That -1px is there to keep a clipped
   * element from affecting the layout around it, which an absolutely positioned one cannot do
   * anyway — and it put this link one pixel above the top of the window, which is one pixel of
   * exactly the thing that made it visible on an overscroll. Zero.
   */
  margin: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
  background: var(--surface);
  color: var(--text);
  font-size: 0.9rem;
  font-weight: 600;
  text-decoration: none;
}

.skip:focus {
  /* Fixed rather than absolute, so it lands in the corner of the WINDOW wherever the reader
     had scrolled to — a link that appears eight pixels from the top of the document is a link
     nobody sees when they tab into a page halfway down it. */
  position: fixed;
  /* Clear of the clock on a phone running this from its home screen, where the page starts at
     the physical top of the screen. Zero everywhere else. */
  top: calc(8px + env(safe-area-inset-top, 0px));
  left: 8px;
  width: auto;
  height: auto;
  margin: 0;
  padding: 10px 16px;
  overflow: visible;
  clip: auto;
  clip-path: none;
  border: 1px solid var(--accent);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-lift);
}

/*
 * And where the link lands. `#main` carries `tabindex="-1"` ONLY so this link has something
 * to move focus to — the two appear on exactly the same fourteen pages and on no others.
 *
 * The global :focus-visible ring cannot draw that box, and the way it fails is the bug this
 * fixes. Measured, on the review page:
 *
 *   393px   main spans 0..393 — the whole viewport — and starts at y=63, under a 63px sticky
 *           bar. With `outline-offset: 2px` the left edge draws at -2, the right at 395 and
 *           the top behind the bar, so THREE OF THE FOUR EDGES ARE OFF SCREEN. What is left
 *           is one lavender line lying across the page at 88% height, attached to nothing —
 *           reported, reasonably, as "a weird permanent line on the bottom of the screen".
 *   1280px  sides make it, top still does not: a three-sided box.
 *
 * So the ring is off by default and drawn INSIDE the box when the skip link is what sent the
 * reader here. Inset, all four edges are on screen at every width and it reads as what it is.
 *
 * Two things this deliberately does NOT do. It does not lean on :focus-visible, which matches
 * a programmatic focus here anyway (measured — so it cannot tell the skip link from anything
 * else). And it does not use `#main:target`, which looks free until you notice settings.js and
 * console.js already read the hash as their section name.
 *
 * The class is added by the link and removed when main blurs, so a focus that arrives any
 * OTHER way paints nothing — which is the Safari case behind the report, where closing a
 * dialog leaves focus on the landmark and Chromium hands it back to the button instead.
 */
#main:focus {
  outline: none;
}

#main.is-skip-target:focus {
  outline: 2px solid var(--focus);
  outline-offset: -2px;
}

/* ------------------------------------------------------------------ motion
 *
 * WCAG 2.2.2 and 2.3.3. The device preference is honoured throughout this file already;
 * this is the account's own switch, for the reader whose machine says nothing on their
 * behalf — a shared booth PC, a browser that never asked.
 *
 * Stronger than shortening: everything stops. `!important` because the rules being
 * overridden are component rules with class specificity, and 1ms rather than 0 because a
 * zero-duration animation never fires its `animationend`, which some panels wait for
 * before they finish opening. Smooth scrolling goes too — a page that glides to a heading
 * is motion nobody asked for.
 */
:root[data-a11y-motion='reduce'],
:root[data-a11y-motion='reduce'] *,
:root[data-a11y-motion='reduce'] *::before,
:root[data-a11y-motion='reduce'] *::after {
  animation-duration: 1ms !important;
  animation-iteration-count: 1 !important;
  transition-duration: 1ms !important;
  /*
   * The ROOT is in this list, and it has to be: `scroll-behavior: smooth` is declared on
   * <html> itself, so a rule that reached only its descendants left every in-page jump gliding
   * for somebody who had just asked for no movement. A browser test caught it, which is the
   * argument for testing what the page does rather than what the stylesheet says.
   */
  scroll-behavior: auto !important;
}

/* The dashboard's pulsing orb is the one thing here that moves for its own sake — it says
   "this number is live". Held still and left visible, exactly as the device preference
   already does it: the reading is the point, the pulse was the decoration. */
:root[data-a11y-motion='reduce'] .plot__orb-halo {
  animation: none !important;
  opacity: 0.3;
}

/* ------------------------------------------------------- colour vision
 *
 * WCAG 1.4.1. Status in this app always carries its words — a warning says what is wrong,
 * a resolved case says resolved — and these palettes are the other half of that promise.
 *
 * The palette SLOTS are overridden rather than the semantic tokens, which is what makes
 * this one short rule per mode instead of one per theme: every theme derives --ok, --warn
 * and --danger (and their soft fills, by color-mix) from these three, so replacing the
 * slots recolours all nine themes and anything added later.
 *
 * Colours are Okabe–Ito, the palette designed to stay distinct for every common type of
 * colour blindness, and they are chosen for LIGHTNESS separation as well as hue — two hues
 * a reader cannot tell apart are still two shades they can.
 *
 * !important, which this file keeps for the reader's own settings (reduced motion above is the
 * other): a colour-vision choice has to beat every palette, and Studio's staff palettes set these
 * slots in blocks up to three attributes heavy. Weight would be a race against the next palette;
 * this is the reader's setting outranking the page, which is what it is.
 */

/* Red–green (deuteranopia, protanopia): the good/bad pair becomes blue and vermillion,
   with caution a much lighter yellow so the three differ by lightness as well. */
:root[data-a11y-colour='red-green'] {
  --p-green: #0072b2 !important;
  --p-yellow: #f0e442 !important;
  --p-red: #d55e00 !important;
}

/* Blue–yellow (tritanopia): blue and yellow are the confusable pair here, so good goes to
   a bluish green, caution to orange, and bad to a red that sits darker than both. */
:root[data-a11y-colour='blue-yellow'] {
  --p-green: #009e73 !important;
  --p-yellow: #e69f00 !important;
  --p-red: #d62728 !important;
  --p-blue: #56b4e9 !important;
}

/* --------------------------------------------------------------- text size
 *
 * WCAG 1.4.4. Browser zoom does this too and does it better, because it scales layout with
 * the text; this is for the reader on a machine whose zoom will not stay put — a shared
 * office desktop, a kiosk browser that resets. The root is the dial and every component
 * sizes in rem (see body above), so the whole app grows together.
 */
:root[data-a11y-text='large'] {
  font-size: 112.5%;
}

:root[data-a11y-text='larger'] {
  font-size: 125%;
}

/* --------------------------------------------------------------- contrast
 *
 * WCAG 1.4.3 and 1.4.11. The themes are built to pass, and passing is not the same as
 * readable in a bright lobby or a projector-lit booth. The dim and faint text tokens move
 * up a step each — which is where nearly all of this app's low contrast lives, in notes,
 * hints and table captions — and the hairline borders become the strong ones, so a card
 * has an edge rather than an implication.
 */
:root[data-a11y-contrast='high'] {
  --text-dim: var(--text);
  --text-faint: var(--p-subtext0);
  --border: var(--p-surface1);
  --border-strong: var(--p-overlay1);
  /* Glass is the other half of a low-contrast surface: whatever is behind it shows
     through and takes the contrast with it. Solidified at the token, so every panel
     that drinks from it goes opaque in one move. */
  --glass: var(--surface);
  --glass-strong: var(--surface);
}

/* Translucency and blur are the other half of a low-contrast surface: whatever is behind
   them shows through and takes the contrast with it. */
:root[data-a11y-contrast='high'] .topbar,
:root[data-a11y-contrast='high'] .modal,
:root[data-a11y-contrast='high'] .aihub__panel {
  backdrop-filter: none;
  background-color: var(--surface);
}

/* ------------------------------------------------------------------- links
 *
 * WCAG 1.4.1, in its most literal reading: a link told apart from its paragraph only by
 * colour is not told apart for everybody. Not on by default because most links in this app
 * are not in prose — they are nav items, buttons and whole cards, where an underline is
 * noise — so this underlines the ones that ARE in text and leaves the furniture alone.
 */
:root[data-a11y-links='underline'] a:not(.btn):not(.nav__link):not(.brand):not(.stat):not(.skip) {
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* --------------------------------------------------------------------- focus
 *
 * WCAG 2.4.7 is already met for keyboard users — the app rings :focus-visible throughout.
 * This is for the reader who needs to see where they are after a pointer press too, which
 * :focus-visible deliberately does not show. Same ring, wider net.
 */
:root[data-a11y-focus='always'] :focus {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* --------------------------------------------------------------- the offline page
 *
 * public/offline.html, which the service worker answers a navigation with when there is no
 * network. Its own few rules rather than inline styles, because the app's CSP allows no inline
 * style — and because a page that only ever appears when something has gone wrong should look
 * like the app rather than like an unstyled document, which is what a reader reads as broken.
 */
.offline {
  margin-top: 12vh;
  text-align: center;
}

.offline__mark {
  border-radius: 18px;
}

.offline__title {
  margin: 18px 0 6px;
  font-size: 1.3rem;
}

.offline p {
  margin: 0 auto 14px;
  max-width: 34rem;
  color: var(--text-dim);
}

/* ====================================================== installed on a home screen
 *
 * The app run from an iPhone's home screen, which is the same pages with Safari's chrome
 * taken away — no address bar at the top, no toolbar at the bottom, and therefore nothing
 * between this page and the hardware. Everything here is about that gap.
 *
 * Scoped to `display-mode: standalone` rather than applied everywhere, because in a browser
 * tab the chrome already occupies these areas and padding for them would be a strip of empty
 * page under the address bar. `minimal-ui` is in the list for the same reason: it is what iOS
 * reports for a home-screen app in some versions, and being wrong about which one costs the
 * reader their top bar under the clock.
 */
@media (display-mode: standalone), (display-mode: minimal-ui), (display-mode: fullscreen) {
  /*
   * THE TOP BAR CLEARS THE STATUS BAR, AND NOTHING MORE.
   *
   * With `apple-mobile-web-app-status-bar-style: black-translucent` the page starts at the
   * physical top of the screen — under the clock and the battery — which is what allows a dark
   * app to keep white status text instead of Apple's black-on-white. The cost is that the page
   * owns the inset, and this is where it pays it: the sticky bar keeps its own 12px and gains the
   * status bar's height above it, so the bar's colour runs all the way up behind the clock.
   *
   * iOS 26 and later draw a blur of their own down from the status bar in an installed web app,
   * Liquid Glass's scroll edge effect, about 40 points deep, over the whole web view; nothing on the
   * page turns it off. For one release every header pinned to the top padded by that depth as well
   * — this bar, the phone's search box, the assistant's card, the film panel and the skip link — so
   * that nothing read started inside it. The operator had it all moved back up: the bar "looks too
   * far down on the screen which is more off putting than the slight blur", and the band it left
   * was dead space the content can have. So each clears the status bar by its own few pixels and no
   * more, with the top of what it holds in the edge of the blur, and test/ios-edge-blur.test.js
   * holds them there.
   */
  .topbar__inner {
    padding-top: calc(12px + env(safe-area-inset-top, 0px));
  }

  /* Landscape on a notched phone insets the sides as well, and a nav flush to the edge there
     is a nav with its first link under the rounded corner. */
  .topbar__inner,
  .shell {
    padding-left: max(var(--shell-pad, 20px), env(safe-area-inset-left, 0px));
    padding-right: max(var(--shell-pad, 20px), env(safe-area-inset-right, 0px));
  }

  /*
   * And the bottom, for the home indicator — the bar that replaced the button. The three
   * elements that sit against the bottom edge already handle it (the guest form's send bar, the
   * assistant's footer, the phone sheet), so this is the page's own end: without it the last
   * card of a long page stops underneath the indicator and reads as cut off.
   *
   * On the shell rather than on body, and ADDED to the 80px the shell already ends with — a bare
   * inset here would replace that and pull the last card up against the edge of the screen.
   * Body is also the wrong element for a second reason: it deliberately declares nothing about
   * its own box (the layers under the page sit at a negative z-index behind it), and a test holds
   * it to that.
   */
  .shell {
    padding-bottom: calc(80px + env(safe-area-inset-bottom, 0px));
  }

  /*
   * The save bar floats at the foot of the window while a schedule is being built, so it is
   * the one element that has to clear the indicator by itself rather than relying on the
   * page's end.
   */
  .savebar--sticky {
    bottom: calc(16px + env(safe-area-inset-bottom, 0px));
  }
}

/* ================================================================== the lock
 *
 * The cover over a locked session. What is protecting the data is the server refusing every API
 * answer with a 423 (src/lock.js); this is what stops the last render being legible over
 * somebody's shoulder while it is locked, and it is where the way back in lives.
 *
 * Fixed and above everything, including the top bar and the assistant's sheet, because a lock
 * with something on top of it is not a lock.
 */
.lockscreen {
  position: fixed;
  inset: 0;
  /*
   * And out over the scrollbar gutter, which `inset: 0` does not reach.
   *
   * A fixed box is placed against the layout viewport, and on a wide screen that viewport now
   * stops short of the window by the width of the reserved gutter (see `scrollbar-gutter` on
   * html). Left at `inset: 0` this cover ended ten pixels early and the page showed through
   * beside it — on the one panel in the app whose whole job is that nothing shows through.
   * `100vw` is measured against the window rather than the viewport, which is exactly the
   * difference; a fixed element is out of flow, so it cannot put the page into a sideways
   * scroll by being that wide.
   */
  width: 100vw;
  z-index: 100;
  display: grid;
  place-items: center;
  padding: 24px;
  /* The blur is on the cover rather than on the page, so nothing under it has to be moved into a
     stacking context or filtered — a `filter` on the page would break the sticky top bar and the
     dialogs, both of which position against the viewport. */
  backdrop-filter: blur(22px) saturate(60%) brightness(0.55);
  -webkit-backdrop-filter: blur(22px) saturate(60%) brightness(0.55);
  /* And a ground colour for the browsers that have no backdrop filter, where the blur would
     otherwise be nothing at all and the page would sit there readable. */
  background: color-mix(in oklab, var(--p-crust) 82%, transparent);
}

.lockscreen__card {
  max-width: 26rem;
  padding: 26px 24px calc(22px + env(safe-area-inset-bottom, 0px));
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
  background: var(--glass);
  box-shadow: var(--shadow-lift);
  text-align: center;
}

.lockscreen__mark {
  border-radius: 16px;
}

.lockscreen__title {
  margin: 14px 0 8px;
  font-size: 1.2rem;
}

.lockscreen__note {
  margin: 0 0 18px;
  color: var(--text-dim);
}

.lockscreen__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  justify-content: center;
}

/* A dismissed prompt is not a fault, so this is a note rather than the app's error red. */
.lockscreen__trouble {
  margin: 16px 0 0;
  font-size: 0.85rem;
  color: var(--warn);
}

/*
 * While locked, the page behind does not scroll.
 *
 * Not for tidiness: scrolling a page you cannot read is how somebody discovers that the cover is
 * one element and the content is still there. It also stops a phone rubber-banding the cover away
 * from the top of the screen.
 */
.is-locked,
.is-locked body {
  overflow: hidden;
}

/* -------------------------------------------------------- the devices list
 * Settings → Face ID unlock. One row per device that can open this account.
 */
.devices {
  list-style: none;
  margin: 14px 0;
  padding: 0;
  display: grid;
  gap: 8px;
}

.device {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
}

.device__name {
  font-weight: 600;
}

.device__when {
  flex: 1 1 auto;
  font-size: 0.8rem;
  color: var(--text-dim);
}

/* ============================================================ what the POS says about itself
 *
 * The ticket price list, the POS version, and the raw concession list — folded under each cinema's
 * row in Settings → POS Scheduling. Reference rather than configuration: nothing in here is
 * editable, which is why it is a <details> and not another set of boxes.
 */
.pos-extra {
  margin: 10px 0 0;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
}

.pos-extra__summary {
  padding: 8px 12px;
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--text-dim);
  cursor: pointer;
}

.pos-extra__summary:hover {
  color: var(--text);
}

.pos-extra__body {
  display: grid;
  gap: 10px;
  padding: 0 12px 12px;
}

.pos-extra__probe {
  display: grid;
  gap: 8px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
}

/*
 * A grid item's min-width is its CONTENT by default, and the content here includes .pos-raw —
 * a third party's XML on one unbroken line. Without this, that line SIZES the track: the probe
 * panels measured 1272px inside an 840px card, the card clipped them, and the pre's own
 * overflow scrollbar never appeared because the pre was never actually too small. Zero hands
 * the width back to the card and the scrolling back to the pre, which is the design.
 */
.pos-extra__body > *,
.pos-extra__probe > * {
  min-width: 0;
}

/* One row per ticket type: the name, then the price, then what tax is inside it. */
.prices {
  display: grid;
  gap: 4px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.prices__row {
  display: flex;
  align-items: baseline;
  gap: 10px;
  font-size: 0.84rem;
}

.prices__name {
  flex: 1 1 auto;
}

/* Tabular figures so a column of prices lines up on the decimal point rather than on the glyph
   widths, which is the difference between a price list and a list of prices. */
.prices__price {
  font-weight: 650;
  font-variant-numeric: tabular-nums;
}

.prices__tax {
  color: var(--text-dim);
  font-size: 0.78rem;
}

/*
 * A third party's XML, shown as it arrived.
 *
 * Scrolls in both directions and wraps nothing: this is being read to work out a file's shape, and
 * re-wrapped XML hides exactly the nesting somebody is looking for. Capped in height so a long file
 * does not push the rest of the page out of reach.
 */
.pos-raw {
  max-height: 22rem;
  margin: 0;
  padding: 10px 12px;
  overflow: auto;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: 0.74rem;
  line-height: 1.5;
  white-space: pre;
  tab-size: 2;
}

/* The request half of a probe panel, folded — the answer is the headline, the packet is for
   whoever needs to quote it to RTS support. */
.pos-sent {
  margin: 4px 0;
}

.pos-sent summary {
  cursor: pointer;
  color: var(--text-dim);
  font-size: 0.78rem;
}

.pos-sent .pos-raw {
  margin-top: 4px;
  max-height: 8rem;
  white-space: pre-wrap;
  word-break: break-all;
}

/* The per-place issue-type rows on the settings page. */
.setting__place {
  padding: 0.6rem 0;
  border-top: 1px solid var(--line, rgba(128, 128, 128, 0.2));
}
.setting__place:first-child {
  border-top: 0;
  padding-top: 0;
}
.setting__place-head {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  margin-bottom: 0.35rem;
}
.setting__place-note {
  font-size: 0.85em;
  opacity: 0.7;
}

/*
 * The honeypot on the guest form: gone, without `display: none`.
 *
 * A field that is `display: none` or `hidden` is the one a well-written bot skips, because
 * that is the obvious tell. This is off the canvas instead — as far as the page is
 * concerned it has a position and a size, and only a human's eyes are spared it.
 */
.hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* A translated report: the English a reviewer acts on, the guest's own words behind it. */
.complaint__translated {
  margin: 0;
}
.complaint__original > summary {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  cursor: pointer;
  margin-top: 0.35rem;
  list-style: none;
}
.complaint__original-hint {
  font-size: 0.85em;
  opacity: 0.7;
}
.complaint__body-original {
  margin: 0.35rem 0 0;
  padding-left: 0.6rem;
  border-left: 2px solid var(--line, rgba(128, 128, 128, 0.3));
  opacity: 0.85;
}

/*
 * The guest form's language picker.
 *
 * Given room on both sides rather than just above. It sits between the heading and the place
 * chip, and at half a rem it read as crowding the chip — on a phone the two ran together into
 * one busy block where the chip is the thing worth reading. The picker is furniture; the chip
 * tells somebody we know where they are.
 */
.guest-lang {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0.85rem 0 1rem;
}
.guest-lang__label {
  font-size: 0.9em;
  opacity: 0.8;
}

/* ------------------------------------------------------------ welcome tour
 *
 * The first-run walkthrough: a veil over the page, one element left lit, and a card
 * beside it saying what the thing is. Mounted by js/walkthrough.js on the pages of a
 * new account's own top bar, once per account ever.
 *
 * The dimming is not a backdrop element: it is the spotlight's own box-shadow, spread
 * further than any screen (200vmax). One element therefore owns the geometry — move the
 * hole and the darkness moves with it, and the glide between steps is one transition on
 * one node instead of four rects kept in agreement.
 *
 * z-index 80: over everything the pages stack (the topbar's 20, the assistant's 25, the
 * skip link's spotlighted moments), under nothing it needs to defer to — the tour never
 * runs while a <dialog> is open, and the top layer would beat it anyway.
 */
/*
 * Also the click shield. Transparent — the hole's shadow does the dimming — but this is
 * the element every stray click lands on, which is what makes the tour narration rather
 * than an obstacle course: the page underneath cannot be half-used mid-tour. The page
 * can still SCROLL (a shield blocks clicks, not wheels), and the spotlight follows;
 * only pressing things waits for the tour.
 */
.tour {
  position: fixed;
  inset: 0;
  z-index: 80;
}

.tour__hole {
  position: fixed;
  border-radius: 12px;
  pointer-events: none;
  /* Transparent until the tour says ready, so the dim fades in rather than banging on. */
  box-shadow: 0 0 0 200vmax rgba(9, 11, 19, 0);
  /* A thin ring in the app's own focus colour, tying "lit" to what focus already means. */
  outline: 1.5px solid color-mix(in oklab, var(--focus) 75%, transparent);
  outline-offset: 2px;
}

.tour--ready .tour__hole {
  box-shadow: 0 0 0 200vmax rgba(9, 11, 19, 0.62);
  transition:
    top 0.34s cubic-bezier(0.3, 0.8, 0.3, 1),
    left 0.34s cubic-bezier(0.3, 0.8, 0.3, 1),
    width 0.34s cubic-bezier(0.3, 0.8, 0.3, 1),
    height 0.34s cubic-bezier(0.3, 0.8, 0.3, 1),
    box-shadow 0.4s ease;
}

/*
 * While the page is scrolling under the tour, the spotlight is repositioned every frame
 * to stay glued to its element — and the glide transition above would turn that into a
 * spotlight chasing its target from behind. Pinned for the duration of the scroll,
 * glide again the moment it settles. js/walkthrough.js toggles this.
 */
.tour--pinned .tour__hole {
  transition: box-shadow 0.4s ease;
}

/* The farewell has nothing to point at: the spotlight closes to nothing, the dim stays. */
.tour__hole--none {
  width: 0;
  height: 0;
  top: 50vh;
  left: 50vw;
  outline: none;
}

.tour__card {
  position: fixed;
  z-index: 1;
  width: min(430px, calc(100vw - 24px));
  padding: 16px 20px 14px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lift);
  opacity: 0;
  transform: translateY(10px);
}

/* Re-armed per step by the module (class off, reflow, class on), so each step enters. */
.tour__card--in {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 0.3s ease, transform 0.3s cubic-bezier(0.2, 0.8, 0.3, 1);
}

/* Focused programmatically each step so the reader's screen reader is told where they
   are. tabindex="-1" — nobody can Tab onto it — so a ring would mark nothing anyone did.
   Same reasoning as dialog:focus above. */
.tour__card:focus {
  outline: none;
}

/* The farewell card stands alone, centre stage. The module clears its inline position
   so these win. */
.tour__card--bare {
  inset: 0;
  margin: auto;
  height: fit-content;
}

.tour__kicker {
  margin: 0 0 6px;
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent);
}

.tour__title {
  margin: 0 0 6px;
  font-size: 1.06rem;
  line-height: 1.3;
}

.tour__text {
  margin: 0 0 14px;
  font-size: 0.92rem;
  line-height: 1.5;
  color: var(--text-dim);
}

/* A question's answers, where a step would have Continue — the layout box's own choices (see
   .preset__choices), so the question looks the same whether the tour asks it or the box does. */
.tour__choices {
  margin: 0 0 14px;
}

/* An answer the server refused, said on the card, which stays put until one is taken. */
.tour__problem {
  margin: 0 0 12px;
  font-size: 0.88rem;
  color: var(--danger);
}

.tour__row {
  display: flex;
  align-items: center;
  gap: 10px;
}

.tour__count {
  margin-left: auto;
  font-size: 0.8rem;
  font-variant-numeric: tabular-nums;
  color: var(--text-dim);
}

.tour__next {
  min-width: 40%;
}

/*
 * Phones: the card becomes a bottom sheet — full width, above the home indicator — and
 * the module stops positioning it per-target (it clears top/left, so these rules hold).
 * The spotlight scrolls its element into the top half instead, where the sheet is not.
 */
@media (max-width: 700px) {
  .tour__card {
    top: auto;
    left: 10px;
    right: 10px;
    bottom: calc(10px + env(safe-area-inset-bottom, 0px));
    width: auto;
  }

  .tour__card--bare {
    inset: auto 10px calc(10px + env(safe-area-inset-bottom, 0px));
    margin: 0;
  }

  .tour__next {
    min-width: 0;
    flex: 1;
  }

  /* Back at its own width, against the phone's blanket `.btn { flex: 1 1 auto }`. Grown, it took
     half the row, and the last card of a page had "Next: Case monitor →" running out of its
     button and past the card's edge, with the count jammed against it. */
  .tour__back {
    flex: none;
  }
}

/*
 * The owner's email on the claim page: a field that is a FACT rather than a question.
 *
 * Readonly and dimmed so it does not invite typing, but a real input so it can be selected,
 * copied, and read out in the order the form is read. It was a hint under the button, which
 * is where a page puts something nobody needs — and this is the one line that catches a claim
 * link somebody forwarded before a circuit is created under the wrong account.
 */
.claim__owner {
  color: var(--text-dim);
  cursor: default;
}

.claim__owner:focus {
  outline: none;
}

/* ------------------------------------------------- the brand colour control (Settings)
 *
 * A colour well, its hex, and the two buttons that fill it. One row that wraps, because on a
 * phone "Match logo" and "Clear" belong under the colour rather than squeezed beside it.
 */
.brandpick__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
}

/*
 * The native picker, dressed to look like the app.
 *
 * Browsers draw `input[type=color]` as a bevelled button with the swatch inset a few pixels;
 * stripping the chrome and zeroing the swatch's own padding is what turns it into a plain well
 * of colour. Both vendor spellings, because neither alone covers Chromium and Firefox.
 */
input.brandpick__well {
  flex: none;
  width: 46px;
  height: 34px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: none;
  cursor: pointer;
}

.brandpick__well::-webkit-color-swatch-wrapper {
  padding: 2px;
}

.brandpick__well::-webkit-color-swatch,
.brandpick__well::-moz-color-swatch {
  border: 0;
  border-radius: 4px;
}

/* Monospace and tabular, because a hex is read character by character when somebody is
   checking it against a brand guide. */
input.brandpick__hex {
  /* Nine characters PLUS the shared rule's padding: the app is border-box everywhere, so a
     bare `9ch` is nine characters minus 26px of padding and "#c8102e" loses its last glyph. */
  width: calc(9ch + 26px);
  flex: none;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  text-transform: lowercase;
}

.brandpick__said {
  margin: 8px 0 0;
}

/* A row of named options — two or three buttons, not a dropdown. */
.choicerow {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

/* ------------------------------------------------- what is on in this room (guest form)
 *
 * The cinema's own schedule, stated in the header beside where the guest is standing.
 *
 * It began as a tinted card inside the form, in the space the "Film" and "Showtime" boxes used
 * to hold — which put it after the description box, and on a phone that is below the fold: the
 * one line the feature exists to show was the one line nobody saw until they scrolled down to
 * send. It is context, so it now sits with the other context, above the first question.
 *
 * No box, no tint, no icon. The chip beside it is the thing that has to be read; this is the
 * caption on it, and two boxed things side by side are two claims competing.
 */
.guest-hero__place {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  /* The whole of the responsive behaviour, and no breakpoint to guess at: the film rides
     beside the chip while both fit on the line and takes the next row when they do not. */
  gap: 7px 12px;
}

.onnow {
  margin: 0;
  /*
   * Nothing here shrinks to stay on the line. A title that does not fit takes the whole line
   * down to the next row rather than being squeezed into a column two words wide — which is
   * what a flex item does when its min-content is left alone.
   *
   * break-word rather than anywhere, deliberately: `anywhere` would make the min-content one
   * character wide, the block would always fit, and it would never wrap down at all.
   */
  max-width: 100%;
  overflow-wrap: break-word;
  font-size: 0.86rem;
  line-height: 1.4;
  color: var(--text-dim);
}

.onnow__label {
  font-size: 0.82em;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  opacity: 0.75;
}

.onnow__film {
  /* The part of the line that is the answer, and the only part at full strength: the label
     and the clock either side of it are there to be glanced at, not read. */
  color: var(--text);
  font-weight: 650;
}

.onnow__time {
  opacity: 0.85;
}

/* An escape hatch, sized and weighted like one: the guest whose report is about this film —
   nearly all of them — should read past it without pausing. */
.onnow__not {
  margin: 6px 0 0;
  padding: 0;
  font-size: 0.9em;
  opacity: 0.85;
}

/* ------------------------------------------------- the format choice, and the way out of it
 *
 * The detailed report's dialog offers two formats and one way to another report. Both used to
 * be things squeezed in beside something else — the formats were a second button in the foot,
 * which on a phone made three side by side and clipped Cancel off the edge; the other report was
 * an underlined phrase in a sentence, the only thing in the dialog shaped like nothing else in
 * the app.
 *
 * Both are now what the dialog already had a shape for: a full-width bordered control. The
 * format rides on .rangepick, the same control the period above it uses, and needs only the two
 * lines below because an option now carries a caption as well as a name.
 */
.rangepick__name {
  display: block;
}

/* What the option is FOR, under its name. Dimmer and lighter, so the name is still the thing
   being read and the caption is there for the reader who wants it. */
.rangepick__why {
  display: block;
  margin-top: 2px;
  font-size: 0.78rem;
  font-weight: 450;
  color: var(--text-dim);
  /* The chosen option tints its own text through `color`; the caption keeps its own so the
     tint marks the choice rather than shouting the sentence under it. */
  opacity: 0.85;
}

/*
 * A row that leaves: a name, a caption, and a chevron.
 *
 * Borrowed from .rangepick__option deliberately — same border, same radius, same padding — so it
 * sits in the column of controls above it rather than looking bolted on. What separates it is
 * the chevron: these choose, this one goes somewhere, and the arrow is the whole of that
 * distinction.
 */

/*
 * And a hairline above the field that holds one, because the chevron alone is not enough.
 *
 * Measured on a 390px phone: 6px between the two Format options, 14px before the jump row. At
 * that spacing three identical bordered rows read as one list of three, and the list is headed
 * "Format" — so "Closed cases" reads as a third thing to download. The rule is the same one
 * .modal__foot uses, and it means the same thing: what follows is a different region, not the
 * next item.
 *
 * No margin-top here on purpose. The space above the rule is the field's own margin, whatever
 * the surrounding block sets it to — `#report-dialog .field` carries an id and would win that
 * declaration anyway, and a property that loses everywhere it is used is worse than absent.
 */
.field--apart {
  padding-top: 16px;
  border-top: 1px solid var(--border);
}

.jumprow {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  border: 1px solid var(--border);
  background: var(--surface-2);
  border-radius: 10px;
  padding: 10px 12px;
  font: inherit;
  color: var(--text-dim);
  text-align: left;
  cursor: pointer;
  transition:
    background-color 140ms var(--ease),
    border-color 140ms var(--ease),
    color 140ms var(--ease);
}

.jumprow:hover {
  background: var(--surface-3);
  border-color: color-mix(in oklab, var(--accent) 35%, var(--border));
  color: var(--text);
}

.jumprow__said {
  display: block;
  /* Takes the free space so the chevron is pushed to the far edge rather than sitting against
     the words, which is what makes the row read as a row. */
  flex: 1 1 auto;
  min-width: 0;
}

.jumprow__name {
  display: block;
  font-size: 0.88rem;
  font-weight: 550;
  color: var(--text);
}

.jumprow__note {
  display: block;
  margin-top: 2px;
  font-size: 0.78rem;
  color: var(--text-dim);
}

/* Sized in its own right rather than inheriting the caption's size: it is the affordance, and a
   chevron the size of small print does not read as one. */
.jumprow__arrow {
  flex: 0 0 auto;
  font-size: 1.25rem;
  line-height: 1;
  color: var(--text-dim);
}

.jumprow:hover .jumprow__arrow {
  color: var(--accent);
}

/* ------------------------------------------------------------ install page
   Four rules for /install.html, which is otherwise built entirely out of the app's own
   cards, hints and buttons. What it needed that nothing else has: numbered steps that read
   as instructions rather than as prose, and a row that holds the switch beside its proof. */
.install__step {
  margin: 0 0 10px;
}

/* The aside on a step — which button, where — a shade quieter than the instruction itself,
   so the thing to DO stays the thing you read first. */
.install__detail {
  color: var(--text-faint);
}

.install__done {
  margin: 0;
  color: var(--ok);
  font-weight: 600;
}

.install__row {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-bottom: 12px;
}

/*
 * A row of buttons under the sentence that explains it gets room from that sentence: the install
 * page's step 2 ("Turn them on in Settings…", then Open notification settings) and the iPhone line in
 * Settings (then How to install it). Neither paragraph brings a bottom margin, so without this the
 * button sits on the sentence's last line.
 *
 * Not after a hint inside Settings, which hides hints until page help is on: there the row above it
 * already brings its 12px, and a hidden hint in between would double the gap.
 */
.field__hint + .install__row,
.setting__state + .install__row {
  margin-top: 12px;
}

.setting .field__hint + .install__row {
  margin-top: 0;
}

@media (max-width: 560px) {
  /* One tap target per line at phone width: these two buttons are "turn it on" and "prove it
     worked", and a wrapped pair with one of them hanging is exactly the arrangement the
     operator asked this app not to have. */
  .install__row .btn {
    flex: 1 1 100%;
  }
}

/* ================================================================== the dock

   Phone navigation as a bar under the thumb, drawn on the FIRST PAINT: the dock is static
   markup on every staff page (see the block each page carries), and which items show is
   decided by the same pre-paint stamps the top bar's links use — data-roles, data-grants and
   data-edition, written by theme.js before any script runs. js/dockbar.js only wires it:
   Home's true href, the current tab, the More menu. That is what stops the dock vanishing on
   every navigation and reappearing when /api/me answered, which is how the first version
   behaved and exactly what an app pinned to a home screen must not do.

   Desktop never sees any of it, and the media query is scoped to SCREEN: this section sits
   after the file's one print block, so an unqualified match would out-cascade print's
   display:none on narrow paper and put the phone's chrome onto a placard. */

.dockbar,
.dockbar__scrim,
.dockbar__menu {
  display: none;
}

@media screen and (max-width: 700px) {
  /* The strip, its fold button and Home all live inside .nav; the dock replaces the lot —
     from the first paint, since both the dock and this rule key on markup, not on script. */
  html:has(.dockbar) .nav {
    display: none;
  }

  /*
   * The bar itself steps aside on the way down and returns on the way up — the class is
   * toggled by wireBarFade in js/dockbar.js from scroll DIRECTION. Sticky layout is kept
   * (no reflow, only paint), and the pointer cannot press what the eye cannot see.
   */
  /*
   * THE CHROME RIDES THE SCROLL ITSELF. One number — `--chrome-away`, written per frame
   * by wireBarFade in js/dockbar.js: 0 at rest, 1 fully away, accumulating scroll the
   * way Safari's toolbar does — and everything below derives from it through calc().
   * The bar's fade and the dock and tile's shrink therefore move WITH the finger, both
   * directions, mid-gesture; nothing here flips a state and then animates on its own
   * clock. That was the operator's third note on this chrome: the quick timed version
   * read as the bar popping between sizes.
   *
   * ONE scale (6% at full away) and ONE bottom anchor for dock and tile both, per the
   * note before that: unequal scales dropped the tile's top out of level with the
   * dock's and the pair stopped reading as one bar.
   */
  html:has(.dockbar) .topbar {
    opacity: calc(1 - var(--chrome-away, 0));
    transform: translateY(calc(var(--chrome-away, 0) * -24px));
  }

  /* The one thing a number cannot carry: a bar that is visually gone must not swallow
     taps. Toggled by the same wirer, at nearly-gone. */
  html:has(.dockbar) .topbar--away {
    pointer-events: none;
  }

  /* Room at the page's end, so the last card scrolls clear of the bar. */
  html:has(.dockbar) body {
    padding-bottom: calc(79px + env(safe-area-inset-bottom, 0px));
  }

  /*
   * The two sticky bottom bars — the settings savebar and the schedule's tray — used to
   * float 16px off the window's edge, which is now inside the dock. Lifted to float the
   * same 16px above it instead, so saving and navigating never fight for the same pixels.
   */
  html:has(.dockbar) .savebar--sticky,
  html:has(.dockbar) .dock {
    bottom: calc(75px + env(safe-area-inset-bottom, 0px));
  }

  /*
   * The assistant's orb joins the dock's line: same height, same baseline, a matched 10px
   * gap on each side — one bar, two shapes. Its panel is unaffected; on a phone that is a
   * card that stops above this line (see .aihub--phone), with the dock kept in front of the
   * dim around it.
   */
  html:has(.dockbar) .aihub__launcher {
    width: 52px;
    height: 52px;
    right: 10px;
    bottom: calc(7px + env(safe-area-inset-bottom, 0px));
    /* The same scrubbed shrink as the dock, one scale off one bottom anchor — and no
       transition: the base rule's 140ms is a desktop hover snap, and here the value
       rides the scroll per frame (the settle animates on html instead). */
    transition: none;
    transform-origin: 50% 100%;
    transform: scale(calc(1 - 0.06 * var(--chrome-away, 0)));
  }

  /*
   * THE VEIL: the page dims as it slides under the dock, so the bar reads as floating over
   * the content rather than pasted on it. A tint mixed from the theme's own ground, ramped in
   * by a mask — dark pages dim, light pages mist. On the body's own pseudo so no page carries
   * an element for it, inert to the pointer, and UNDER the dock and its menu in the stack.
   */
  /*
   * And the same veil at the TOP, mirrored — for the moments the bar has stepped aside:
   * content scrolling up dims on its way under the clock rather than running into it at full
   * strength. BEHIND the bar in the stack (19 to its 20), and faded in only as the bar fades
   * out, so the two never have to agree about which is in front — see the opacity below.
   */
  html:has(.dockbar) body::before {
    content: '';
    position: fixed;
    inset-inline: 0;
    top: 0;
    /*
     * Tall, and ending in NOTHING LEFT: frame-stepping the reference recording, content is
     * fully swallowed by the ground colour before it reaches the bar, and the ramp spans a
     * quarter of the screen — a long gentle fade-to-background, not a short blur. So the
     * gradient holds solid var(--bg) through the bar's zone, then lets go over ~150px. The
     * opacity ramp is the dish; the light blur this once carried was seasoning, and is gone —
     * see below.
     */
    /* Shorter than it once was, on purpose: this ramp was sized for a bar that never left,
       and now the bar steps aside on the way down — so the fade only needs to swallow
       content at the screen's own edge, not reserve a bar's worth of depth below it. */
    height: 150px;
    z-index: 19;
    pointer-events: none;
    /*
     * NO BLUR, like the floor below — and for a reason the floor did not have.
     *
     * This veil used to soften as well as shade, and on the operator's iPhone, installed to the
     * home screen, the softening landed ON THE BAR: the wordmark and the account button were
     * blurred at their tops and sharp at their bottoms, which is this veil's mask drawn over
     * them. By the stacking the bar is above this layer (20 to its 19), and Chromium keeps it
     * there; WebKit did not. A blur that can reach the one thing on the page that must never be
     * soft is not worth keeping as seasoning.
     */
    /*
     * And THERE ONLY WHILE THE BAR IS AWAY, because the shade did the same thing the blur did.
     *
     * The operator's next screenshots settled which way WebKit gets it wrong: the pull-to-refresh
     * disc — at 30, above everything — came out washed pale and soft where it crossed this layer.
     * So on an iPhone this veil is drawn over whatever it covers, whatever the stacking says, and
     * taking the blur off still leaves its colour on the bar's top half.
     *
     * Nothing needs it while the bar is there: the bar's own glass runs up under the clock and is
     * what content slides beneath. It exists for the bar being AWAY — content passing under the
     * clock with nothing behind it. So it rides the same number the bar does, the other way:
     * invisible at rest, arriving as the bar leaves, gone again as it returns. The bar and the
     * veil are never both fully there, so the order they are drawn in stops mattering.
     */
    opacity: var(--chrome-away, 0);
    -webkit-mask-image: linear-gradient(to bottom, black 25%, transparent 80%);
    mask-image: linear-gradient(to bottom, black 25%, transparent 80%);
    background: linear-gradient(
      to bottom,
      var(--bg) 0%,
      var(--bg) 15%,
      color-mix(in srgb, var(--bg) 50%, transparent) 48%,
      transparent 95%
    );
  }

  html:has(.dockbar) body::after {
    content: '';
    position: fixed;
    inset-inline: 0;
    bottom: 0;
    height: 160px;
    z-index: 21;
    pointer-events: none;
    /* Shade only — no backdrop blur. This veil once carried one, and the operator's
       phone showed why it was wrong: the action buttons scrolling through the bottom of
       the screen read as SMEARED, not shaded, because unlike the top veil this one sits
       over content a reader is actively aiming for. The dock's own glass keeps the blur;
       the floor under it is colour alone. */
    /*
     * The stops are cut around the DOCK'S OWN GEOMETRY, not spread evenly, and that is
     * the second lesson from the operator's phone. The first deepening put its darkest
     * paint at the viewport's bottom edge — which the dock covers — so the strength went
     * where nobody could see it and the visible band above the bar barely moved. The bar
     * tops out 59px up (7px inset + 52px of bar); the 37% stop below is that edge, still
     * carrying most of the floor's weight, so the shadow is at full strength exactly
     * where content and dock meet and the separation holds AT ALL TIMES — this is a
     * fixed pseudo, there whether or not anything scrolls. Above that it thins over
     * ninety-odd pixels to nothing. No mask: it was feathering the blur, and the gradient
     * fades itself.
     *
     * In the THEME'S veil rather than in black, which is the third lesson. Translucent
     * black was meant to darken a light page the way a shadow would; on the operator's
     * phone it read as a sooty band along the bottom of a daylight page. So each palette
     * names what it fades into — black for a dark one, white for a light one (--p-veil) —
     * and a light page mists into white under its dock the way a dark one shades into
     * black, with the same stops.
     */
    background: linear-gradient(
      to top,
      color-mix(in srgb, var(--p-veil) 80%, transparent) 0%,
      color-mix(in srgb, var(--p-veil) 62%, transparent) 37%,
      color-mix(in srgb, var(--p-veil) 26%, transparent) 70%,
      transparent 100%
    );
  }

  .dockbar {
    position: fixed;
    left: 10px;
    right: 10px;
    /* 7px — the midpoint of two notes: "lower than 10" and then "not so close as 4,
       the iPhone's corners curve". The safe-area inset still rides on top, so a real
       home-indicator phone lifts the whole line clear of the curve regardless. */
    bottom: calc(7px + env(safe-area-inset-bottom, 0px));
    /* Over the page, its sticky bars (20) and the veil (21); under the assistant (25). With the
       chat open on a phone it rises over the chat's dim instead (see .aihub--phone). */
    z-index: 22;
    display: flex;
    justify-content: space-around;
    align-items: center;
    /* 52px, down from 58 — the same trim as the bottom inset above: a slimmer bar,
       nearer the floor, ceding a little more of the screen to the content. */
    height: 52px;
    /* The ends breathe a little more than they did (4px), which draws the items a
       touch closer together — the operator asked for "ever so slightly", and a
       shrink-wrapped pill turned out to be far more than that. */
    padding: 0 12px;
    /* The scrubbed shrink, anchored to the bar's own bottom edge so it never opens a
       gap against the floor. NO transition: the value changes per frame with the
       scroll, and the settle animates the property itself on html instead. */
    transform-origin: 50% 100%;
    transform: scale(calc(1 - 0.06 * var(--chrome-away, 0)));
    /* Glass, not paint: enough of the theme's surface to read on, little enough that the
       page glows through — the blur underneath does the legibility work. Clipped, so the
       active tab's bloom below rises from the pill's own edge instead of leaking under it. */
    background: color-mix(in srgb, var(--surface-3) 58%, transparent);
    -webkit-backdrop-filter: blur(18px);
    backdrop-filter: blur(18px);
    border: 1px solid var(--border-strong);
    /* A rounded rectangle, not a capsule: the reference bar's corners turn at about a third
       of its height, which is what keeps it reading as a surface rather than a button. */
    border-radius: 18px;
    box-shadow: var(--shadow-lift), var(--rim);
    overflow: hidden;
  }

  /* With an orb to share the line with, the bar stops short of it: 10px edge + 52px orb
     + 10px gap. Stamped pre-paint by theme.js, so the pill never draws wide and shrinks. */
  html[data-assistant] .dockbar {
    right: 72px;
  }

  /*
   * WHO SEES WHICH ITEM — the same three-layer gate the top bar's links pass, verbatim in
   * spirit (see .nav__link[data-role] above): hidden until the pre-paint role stamp claims
   * one, grants narrowing roles, the edition trimming the schedule pages, and the data-nav
   * flag as the JS-reconciled fallback for accounts the stamps cannot describe.
   */
  .dockbar__item[data-role] {
    display: none;
  }

  html[data-roles~='admin'] .dockbar__item[data-role~='admin'],
  html[data-roles~='gm'] .dockbar__item[data-role~='gm'],
  html[data-roles~='maintenance'] .dockbar__item[data-role~='maintenance'],
  html[data-nav='ready'] .dockbar__item[data-role] {
    display: flex;
  }

  html:not([data-grants~='review']) .dockbar__item[data-grant='review'] {
    display: none !important;
  }

  html[data-grants~='review'] .dockbar__item[data-nogrant='review'] {
    display: none !important;
  }

  html[data-edition='standalone'] .dockbar__item[data-edition='cinema'] {
    display: none !important;
  }

  /*
   * THE OPTIONAL TABS, which belong to nobody by default.
   *
   * The bar seats four and a button, so a page cannot simply be added to it — the fifth would
   * push More off. The pages worth adding are the ones some people live in and others never
   * open, so they ship in the markup and are drawn only for an account whose own dock names
   * them. That answer is a preference rather than a rung, so it arrives as its own pre-paint
   * stamp (see the dock stamp in src/app.js) and the default here is no.
   *
   * Without this the tab was drawn on every first paint by the role rules above and taken away
   * again when wireDock answered — a fifth tab that appeared and vanished on every navigation.
   * `!important` because the rule that draws it is a stamp plus two attributes and would
   * otherwise win on specificity, which is the same reason the two rules above carry it.
   */
  .dockbar__item[data-optional] {
    display: none !important;
  }

  html[data-dock~='/market.html'] .dockbar__item[data-optional][href='/market.html'] {
    display: flex !important;
  }

  /* An engineer's two pages both earn tabs, leaving nothing for More to hold — and a
     button over an empty menu is worse than no button. Pre-paint, like everything above;
     the JS removes it too, for the accounts the stamps cannot see. */
  html[data-roles~='maintenance'] .dockbar__more {
    display: none;
  }

  .dockbar__item {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    min-width: 52px;
    padding: 2px 5px;
    border: 0;
    background: none;
    color: var(--text-dim);
    text-decoration: none;
    font: inherit;
    font-size: 10.5px;
    font-weight: 600;
    letter-spacing: 0.01em;
    cursor: pointer;
    /* No system flash on tap either — the item's own ink change is the whole feedback. */
    -webkit-tap-highlight-color: transparent;
  }

  /*
   * The dock refuses the page's link hover. Items are <a>, and the global `a:hover`
   * paints the accent — which iOS asserts on TAP and leaves asserted, so pressing a tab
   * flashed it blue and left it blue until the next touch (the operator saw exactly
   * that). On this bar brightness is the signal and ink is the only colour: resting
   * items stay dim under a finger, and the lit one stays at full ink.
   */
  .dockbar__item:hover {
    color: var(--text-dim);
  }

  .dockbar__item--on:hover {
    color: var(--text);
  }

  .dockbar__item svg {
    transition: transform 160ms var(--ease);
  }

  /*
   * The lit tab: the icon steps up and goes to full ink — var(--text), which is the
   * prominent white on the dark theme and stays legible on the light one — with a soft
   * bloom rising from the pill's bottom edge beneath it. Ink rather than accent, because
   * on a glass bar the accent read as one more tinted thing; brightness is the signal.
   */
  .dockbar__item--on {
    color: var(--text);
  }

  .dockbar__item--on svg {
    color: var(--text);
    transform: scale(1.18);
  }

  /*
   * WHOOP's light, MEASURED this time rather than eyeballed: sampling their pill's glass
   * top-to-bottom, the inactive zones darken toward the bottom edge while the active tab's
   * zone brightens toward it — light rising from underneath, broad and edgeless, strongest
   * at the rim and gone by icon height. Built the same way here: a wide soft ellipse whose
   * CENTRE sits below the pill's bottom edge, clipped by the pill's own overflow — so what
   * shows is only the upper slice of something big and gentle, never the outline of a shape.
   */
  .dockbar__item--on::after {
    content: '';
    position: absolute;
    left: 50%;
    bottom: -36px;
    transform: translateX(-50%);
    width: 100px;
    height: 62px;
    z-index: -1;
    border-radius: 50%;
    background: radial-gradient(
      closest-side,
      color-mix(in srgb, var(--text) 15%, transparent),
      transparent
    );
    pointer-events: none;
  }

  /*
   * THE TABLET'S DOCK IS BIGGER, like everything else on that page: read at arm's length
   * and tapped with a thumb while holding something else. The rest of the bar is the app's
   * and left exactly alone — the glass, the bloom under the lit tab, the scrubbed shrink.
   *
   * Three tabs and no More button, so nothing is ever pushed off it.
   */
  .dockbar--floor {
    height: 60px;
  }

  .dockbar--floor .dockbar__item {
    min-width: 64px;
    font-size: 11.5px;
  }

  /*
   * THE CHROME ARRIVES WITH THE UNLOCK, and so must the space it reserves.
   *
   * Every rule in this block keys on :has(.dockbar), which is true from the first paint —
   * but this page's dock is hidden until somebody has typed four digits. Without this the
   * PIN screen padded its foot for a bar nobody could see, and cast both of the bar's veils
   * over a screen with nothing to scroll under them.
   */
  body.shopfloor:not(.shopfloor--open) {
    padding-bottom: 0;
  }

  body.shopfloor:not(.shopfloor--open)::before,
  body.shopfloor:not(.shopfloor--open)::after {
    content: none;
  }

  .dockbar__scrim {
    position: fixed;
    inset: 0;
    z-index: 21;
    display: block;
    background: rgba(0, 0, 0, 0.35);
  }

  .dockbar__menu {
    position: fixed;
    right: 10px;
    bottom: calc(69px + env(safe-area-inset-bottom, 0px));
    z-index: 23;
    display: flex;
    flex-direction: column;
    min-width: 210px;
    padding: 6px;
    background: var(--glass-strong);
    -webkit-backdrop-filter: blur(14px);
    backdrop-filter: blur(14px);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-lift);
  }

  /* The classes above set display, which would otherwise outrank the hidden attribute the
     JS toggles — and a menu that ignores [hidden] is open forever. */
  .dockbar__scrim[hidden],
  .dockbar__menu[hidden] {
    display: none;
  }

  .dockbar__menu-link {
    display: flex;
    align-items: center;
    gap: 11px;
    padding: 11px 12px;
    border-radius: 11px;
    color: var(--text);
    text-decoration: none;
    font-size: 14.5px;
    font-weight: 600;
  }

  .dockbar__menu-link svg {
    color: var(--text-dim);
    flex: 0 0 auto;
  }

  .dockbar__menu-link:active {
    background: color-mix(in srgb, var(--text) 7%, transparent);
  }

  .dockbar__menu-link[aria-current='page'] {
    color: var(--accent);
  }

  /* A page in beta keeps its label in the dock's menu too — see .nav__link[data-beta]. */
  .dockbar__menu-link[data-beta]::after {
    content: 'Beta';
    margin-left: auto;
    font-size: 0.62rem;
    font-weight: 650;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    padding: 1px 6px;
    border-radius: 5px;
    background: var(--info-soft);
    color: var(--info);
    border: 1px solid color-mix(in oklab, var(--info) 35%, transparent);
  }
}

/*
 * The scrub's number, registered so .chrome-settle can ANIMATE it: when scrolling goes
 * quiet mid-way the wirer sets the nearer end and this transition carries every calc()
 * that reads the property there smoothly. See wireBarFade in js/dockbar.js.
 */
@property --chrome-away {
  syntax: '<number>';
  inherits: true;
  initial-value: 0;
}

html.chrome-settle {
  transition: --chrome-away 320ms var(--ease);
}

/*
 * Premium pass: first-arrival choreography: the page's boxes rise into place,
 * top to bottom, ONCE per visit. The `arriving` class is put on <html> as the page's
 * modules boot and taken off shortly after load (see util.js), so anything drawn in
 * that window — the first data paint — performs its entrance, and anything drawn later
 * (the 20-second poll, a range switch, a tab change) simply appears where it belongs:
 * data changing must never bounce furniture. Backwards fill holds each box invisible
 * through its own delay, which is what makes the stagger read as one curtain rising
 * rather than boxes popping in a row.
 */
@keyframes arrive {
  from {
    opacity: 0;
    transform: translateY(var(--arrive-rise, 14px)) scale(var(--arrive-scale, 1));
  }
}

html.arriving :is(.stat, .card, .kpi, .empty, [data-caseview] .complaint--report) {
  animation: arrive 480ms var(--ease) backwards;
}

html.arriving :is(.stat, .card, .kpi, .empty, [data-caseview] .complaint--report):nth-child(2) {
  animation-delay: 45ms;
}

html.arriving :is(.stat, .card, .kpi, .empty, [data-caseview] .complaint--report):nth-child(3) {
  animation-delay: 90ms;
}

html.arriving :is(.stat, .card, .kpi, .empty, [data-caseview] .complaint--report):nth-child(4) {
  animation-delay: 135ms;
}

html.arriving :is(.stat, .card, .kpi, .empty, [data-caseview] .complaint--report):nth-child(5) {
  animation-delay: 180ms;
}

html.arriving :is(.stat, .card, .kpi, .empty, [data-caseview] .complaint--report):nth-child(6) {
  animation-delay: 225ms;
}

html.arriving :is(.stat, .card, .kpi, .empty, [data-caseview] .complaint--report):nth-child(n + 7) {
  animation-delay: 260ms;
}

@media (prefers-reduced-motion: reduce) {
  html.arriving :is(.stat, .card, .kpi, .empty, [data-caseview] .complaint--report) {
    animation: none;
  }
}

/*
 * THE BACK-OFFICE TABLET JOINS THE CHOREOGRAPHY.
 *
 * Putting the dock on floor.html is what makes it a "staff page" to util.js, which is where
 * the window is opened and closed — so its rows needed only a selector. Same keyframe and
 * the same 45ms stagger as the dashboard's cards, because the point of giving that screen
 * the app's chrome is that it IS the app and should not move differently from it.
 *
 * On a tab press the window is re-armed by replayArrival() and opened inside the render by
 * arrivalPaint(): the documented path for a deliberate change of subject, which a tab is
 * and a twenty-second poll is not.
 */
html.arriving .shopfloor__rise {
  animation: arrive 480ms var(--ease) backwards;
}

html.arriving .shopfloor__rise:nth-child(2) {
  animation-delay: 45ms;
}

html.arriving .shopfloor__rise:nth-child(3) {
  animation-delay: 90ms;
}

html.arriving .shopfloor__rise:nth-child(4) {
  animation-delay: 135ms;
}

html.arriving .shopfloor__rise:nth-child(5) {
  animation-delay: 180ms;
}

html.arriving .shopfloor__rise:nth-child(6) {
  animation-delay: 225ms;
}

html.arriving .shopfloor__rise:nth-child(n + 7) {
  animation-delay: 260ms;
}

@media (prefers-reduced-motion: reduce) {
  html.arriving .shopfloor__rise {
    animation: none;
  }
}

/* Premium pass: the press, felt everywhere — the same dip the stat tiles take. */
.btn:active:not(:disabled) {
  transform: scale(0.97);
}

/* ------------------------------------------------- pull down to refresh
 *
 * The installed app's copy of the gesture every browser tab already has — built and
 * armed by js/pull-refresh.js, and ONLY when the app runs standalone on a touch device,
 * which is exactly where the native gesture is missing. The disc is the house glass at
 * the house radius, and it comes out from UNDER the top bar: behind the bar in the stack,
 * in front of the page, so a pull draws it down out of the bar's lower edge and it spins
 * just below it. A pull only begins at the top of the page, where the bar is always home.
 */

/* The whole reason the gesture pays for itself: with a pull standing in for the button,
   Refresh stops spending a controls row on every phone screen. Gated on the data flag
   the module stamps only after every arming check has passed — in a browser tab, or on
   a mouse, the button stands exactly as before. */
html[data-ptr] #refresh {
  display: none;
}

.ptr {
  /*
   * ABSOLUTE, not fixed, and that is the operator's phone talking — twice.
   *
   * iOS rubber-bands the whole page during an overscroll and carries fixed elements along
   * with it, so a fixed disc rode the bounce instead of appearing in it. Parked in the
   * DOCUMENT it moves with the page and with the bar, which is what the second report was
   * about: it used to be parked ABOVE the document, in the space the rubber band reveals —
   * and installed to the home screen that space is the status bar, above the top bar, under
   * the clock. So `top` is now set by the module at the start of each pull, tucked just
   * behind the bar's lower edge, and the pull draws it out below the bar.
   *
   * 19: under the bar (20), and after the top veil (also 19, but first in the page), so it
   * is drawn in front of the veil and the page and behind the bar it comes out of.
   */
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  display: flex;
  justify-content: center;
  z-index: 19;
  pointer-events: none;
  /* Parked invisible above the document edge; the module reveals it for the length of a
     pull — which is also what keeps it out of print. */
  visibility: hidden;
  transition: transform 220ms var(--ease);
}

.ptr--out {
  visibility: visible;
}

/* While a finger holds it the disc is the finger's, not the transition's. */
.ptr--dragging {
  transition: none;
}

.ptr__disc {
  display: flex;
  align-items: center;
  justify-content: center;
  /* Sized up from 44 after the operator could barely find it mid-pull. */
  width: 48px;
  height: 48px;
  border-radius: 50%;
  background: var(--glass-strong);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  border: 1px solid var(--border-strong);
  box-shadow: var(--shadow-lift);
  color: var(--text-dim);
}

/* Past the threshold the release will act, and the accent says so — the same ink every
   pressed control in the app uses for "this is live". */
.ptr--armed .ptr__disc {
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 45%, var(--border-strong));
}

.ptr--busy .ptr__disc {
  animation: ptr-spin 800ms linear infinite;
}

@keyframes ptr-spin {
  to {
    transform: rotate(360deg);
  }
}

@media (prefers-reduced-motion: reduce) {
  .dockbar__item svg,
  .dockbar,
  .aihub__launcher,
  .topbar,
  .ptr {
    transition: none;
  }

  /* The settle snaps, and the wirer skips the scrub itself under this preference —
     the chrome changes state without performing motion. */
  html.chrome-settle {
    transition: none;
  }

  /* The pull is acknowledged by the disc appearing and the accent arming — not by spin. */
  .ptr--busy .ptr__disc {
    animation: none;
  }
}

/* ==================================================== Motion pass
 *
 * The round the operator chose from recordings, whole: pages that hand over instead of
 * cutting, popups that grow from the press that asked for them, a queue that says
 * goodbye to finished work and hello to new, a pull that resolves into a tick, menus
 * that arrive from their triggers — and skeletons rebuilt to the measured size of what
 * replaces them, so every page's first paint lands IN PLACE. Each movement is scoped,
 * runs once per cause, and dies under reduced motion — the house rules.
 */

/* 3 · The card that was acted on takes a bow: a small lift and fade, then the list
 *     closes the gap it leaves. Driven by bowOut() in review.js. */
.complaint--bow {
  overflow: hidden;
  transition:
    opacity 190ms var(--ease),
    transform 190ms var(--ease),
    height 300ms var(--ease) 130ms,
    margin 300ms var(--ease) 130ms,
    padding 300ms var(--ease) 130ms;
}
.complaint--bowed {
  opacity: 0;
  transform: translateY(-6px) scale(0.985);
  margin-top: 0 !important;
  margin-bottom: 0 !important;
  padding-top: 0 !important;
  padding-bottom: 0 !important;
}

/* 4 · The chart tooltip steps in rather than teleporting. Replayed only when it moves
 *     to a different mark — see tooltips() in charts.js. */
.chart__tip--in {
  animation: tip-in 130ms var(--ease);
}
@keyframes tip-in {
  from {
    opacity: 0;
    transform: translateY(3px);
  }
}

/* 5 · A report that arrived while the page was open says so once, when the reader asks
 *     to see it: a short rise, and the severity rail breathes a single glow. */
.complaint--fresh {
  animation:
    fresh-in 380ms var(--ease) backwards,
    fresh-glow 1.5s var(--ease) 380ms backwards;
}
@keyframes fresh-in {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
}
@keyframes fresh-glow {
  0% {
    box-shadow: -10px 0 26px -10px var(--card-edge, var(--border-strong));
  }
  100% {
    box-shadow: -10px 0 26px -10px transparent;
  }
}

/* 6 · The photo viewer ZOOMS from the press instead of rising like a form — same
 *     mechanism as every dialog (dialogs.js sets the origin from the pointer), with a
 *     deeper starting scale that reads as "the photograph grew from its thumbnail". */
dialog.viewer.is-arming {
  opacity: 0;
  transform: scale(0.8);
}
dialog.viewer.is-opening {
  animation-name: viewer-in;
}
@keyframes viewer-in {
  from {
    opacity: 0;
    transform: scale(0.8);
  }
}

/* 7 · The pull-to-refresh disc closes with a tick: spin ends, a check draws itself,
 *     then the disc leaves. States driven by pull-refresh.js. */
.ptr__check {
  position: absolute;
  inset: 0;
  margin: auto;
  opacity: 0;
}
.ptr__check path {
  stroke-dasharray: 22;
  stroke-dashoffset: 22;
}
.ptr--done .ptr__arrow {
  opacity: 0;
  transform: scale(0.6);
  transition:
    opacity 110ms var(--ease),
    transform 110ms var(--ease);
}
.ptr--done .ptr__disc {
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 45%, var(--border-strong));
}
.ptr--done .ptr__check {
  opacity: 1;
  transition: opacity 80ms var(--ease);
}
.ptr--done .ptr__check path {
  stroke-dashoffset: 0;
  transition: stroke-dashoffset 240ms var(--ease) 70ms;
}

/* 8 · Menus arrive from their trigger, and dock icons take the press. */
.account__menu--in {
  transform-origin: top right;
  animation: menu-down 150ms var(--ease);
}
.dockbar__menu--in {
  transform-origin: 50% 100%;
  animation: menu-up 160ms var(--ease);
}
@keyframes menu-down {
  from {
    opacity: 0;
    transform: scale(0.94);
  }
}
@keyframes menu-up {
  from {
    opacity: 0;
    transform: translateY(6px) scale(0.94);
  }
}
.dockbar__item {
  transition: transform 260ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
.dockbar__item:active {
  transform: scale(0.88);
  transition-duration: 90ms;
}

@media (prefers-reduced-motion: reduce) {
  .complaint--bow,
  .complaint--bowed,
  .dockbar__item {
    transition: none;
  }
  .complaint--fresh,
  .chart__tip--in,
  .account__menu--in,
  .dockbar__menu--in,
  dialog.viewer.is-opening {
    animation: none;
  }
  .ptr--done .ptr__check path {
    transition: none;
    stroke-dashoffset: 0;
  }
}

/* 9 · Motion pass: skeletons that hold the page's real silhouette.
 *
 * The queue's ghosts were three short cards and the overview had none at all, so the
 * moment data landed the page GREW — content did not appear in place, the layout
 * unrolled top to bottom, and the entrance choreography rode on top of the reflow.
 * These ghosts are built to the measured size of what replaces them (tiles ~120px in
 * the tile grid's own columns, charts ~340px, queue cards ~270px), so the silhouette
 * is stable from the first paint and the real cards materialise where a ghost stood.
 */
.skelwrap {
  display: contents;
}
.skelcard--stat {
  height: 120px;
  align-content: center;
}
.skel--stat-value {
  height: 26px;
  width: 56%;
}
.skelcard--chart {
  height: 340px;
  grid-template-rows: auto 1fr;
}
.skel--plot {
  height: auto;
  border-radius: var(--radius-lg);
}
.skel--band {
  height: 54px;
  border-radius: var(--radius-lg);
}
.skelcard--case {
  height: 258px;
  grid-template-rows: auto auto auto 1fr;
}
.skel--act {
  height: 44px;
  align-self: end;
  border-radius: 12px;
}

/* 10 · Motion pass: the queue's arrival, made unmissable.
 *
 * A case card is ~280px of near-black on near-black: the shared 14px rise reads on the
 * overview's small tiles and vanishes on these. So the queue's cards travel further,
 * grow slightly into place, take longer, and are spaced twice as far apart — a
 * procession you can watch, not a shimmer you can miss. Values only; the animation,
 * its window, and the reduced-motion kill all stay the shared ones.
 */
html.arriving [data-caseview] .complaint--report {
  --arrive-rise: 26px;
  --arrive-scale: 0.965;
  animation-duration: 620ms;
  animation-delay: 130ms;
}
html.arriving [data-caseview] .complaint--report:nth-child(2) {
  animation-delay: 220ms;
}
html.arriving [data-caseview] .complaint--report:nth-child(3) {
  animation-delay: 310ms;
}
html.arriving [data-caseview] .complaint--report:nth-child(4) {
  animation-delay: 400ms;
}
html.arriving [data-caseview] .complaint--report:nth-child(5) {
  animation-delay: 490ms;
}
html.arriving [data-caseview] .complaint--report:nth-child(n + 6) {
  animation-delay: 560ms;
}

/* The LIST view's rows are forty-pixel lines, not cards: the full 26px rise read as
   the table falling apart. They keep the procession at a line's own scale. */
html.arriving [data-caseview] .complaint__row {
  --arrive-rise: 12px;
  --arrive-scale: 1;
  animation-duration: 420ms;
}

/* The furniture leads the procession: the cinema band first, its room row next, the
   cards after — four or five moving parts per screen instead of two. */
html.arriving [data-caseview] :is(.theatre__head, .auditorium__head) {
  --arrive-rise: 18px;
  animation: arrive 520ms var(--ease) backwards;
}
html.arriving [data-caseview] .auditorium__head {
  animation-delay: 70ms;
}
@media (prefers-reduced-motion: reduce) {
  html.arriving [data-caseview] :is(.theatre__head, .auditorium__head) {
    animation: none;
  }
}

/* 11 · Motion pass: the scheduler and the accounts page join the procession.
 *
 * The week grid arrives one LANE at a time — the ruler first, then each auditorium's
 * row with its pills — and the accounts page arrives one person at a time. Same window,
 * same keyframe, same kill switch; only the cast is new.
 */
html.arriving :is(.grid__ruler, .grid__lane, .person) {
  --arrive-rise: 20px;
  animation: arrive 560ms var(--ease) backwards;
}
html.arriving :is(.grid__lane, .person):nth-child(2) {
  animation-delay: 80ms;
}
html.arriving :is(.grid__lane, .person):nth-child(3) {
  animation-delay: 160ms;
}
html.arriving :is(.grid__lane, .person):nth-child(4) {
  animation-delay: 240ms;
}
html.arriving :is(.grid__lane, .person):nth-child(5) {
  animation-delay: 320ms;
}
html.arriving :is(.grid__lane, .person):nth-child(n + 6) {
  animation-delay: 400ms;
}
@media (prefers-reduced-motion: reduce) {
  html.arriving :is(.grid__ruler, .grid__lane, .person) {
    animation: none;
  }
}

/* 12 · Motion pass: the cards a reader has not reached yet.
 *
 * The arrival window greets what is on screen; a queue is forty cards long and a phone
 * holds two, so the rest waited armed and arrive as they are scrolled to (js/reveal.js).
 * `.reveal` is the armed state and is only ever ON a card the observer is holding —
 * every way out of that module takes it off again, so a card cannot be left invisible.
 */
[data-caseview] .complaint--report.reveal {
  opacity: 0;
  /* TRANSLATE ONLY, deliberately: a scale moves every child sideways by a fraction of a
     percent, and the list view's whole promise is that each row's columns sit at the
     same x as the row above (test/browser/case-view.browser.js measures exactly that). */
  transform: translateY(16px);
}

/* Arriving IS the class coming off: the properties are transitioned rather than
   animated, so a card that is revealed mid-scroll moves from wherever it actually is
   rather than restarting from a keyframe's first frame. */
[data-caseview] .complaint--report:not(.complaint--bow) {
  transition:
    opacity 420ms var(--ease),
    transform 420ms var(--ease);
}

@media (prefers-reduced-motion: reduce) {
  [data-caseview] .complaint--report:not(.complaint--bow) {
    transition: none;
  }
  [data-caseview] .complaint--report.reveal {
    opacity: 1;
    transform: none;
  }
}

/* 13 · Motion pass: the location pages arrive a building at a time.
 *
 * Location info and My cinemas draw one .theatre section per building — the estate, not a
 * queue — and they were the last work pages where everything simply appeared. Scoped by
 * `#results:not([data-caseview])`, which is exactly what tells these pages apart from the
 * case monitor: the queue puts its own cinema groups under that attribute, and those are
 * deliberately NOT the unit that animates there (one full-screen block fading dark on dark
 * reads as nothing — see the cast above, which moves the cards instead).
 */
html.arriving #results:not([data-caseview]) > .theatre {
  --arrive-rise: 20px;
  animation: arrive 560ms var(--ease) backwards;
}
html.arriving #results:not([data-caseview]) > .theatre:nth-child(2) {
  animation-delay: 80ms;
}
html.arriving #results:not([data-caseview]) > .theatre:nth-child(3) {
  animation-delay: 160ms;
}
html.arriving #results:not([data-caseview]) > .theatre:nth-child(4) {
  animation-delay: 240ms;
}
html.arriving #results:not([data-caseview]) > .theatre:nth-child(n + 5) {
  animation-delay: 300ms;
}

@media (prefers-reduced-motion: reduce) {
  html.arriving #results:not([data-caseview]) > .theatre {
    animation: none;
  }
}

/* ==================================================== PROTOTYPE · programming page
 *
 * Five lists that read the till: films under and over their building's usual, runs short
 * of screenings and runs wasting them, and shows still to play that are filling fast.
 * Marked as a prototype because it is being shown before it is agreed; the marker goes
 * when it does. Everything here leans on tokens the app already has — no new colours.
 *
 * Each list is a .section — the settings page's disclosure, heading and count on the
 * summary — so the page folds to a table of contents. The rules for the fold itself live
 * with the settings page; only what the lists add is here.
 */
.panel__head {
  display: grid;
  gap: 4px;
  margin-bottom: 14px;
}

/*
 * ------------------------------------------------------- the programming page's rail
 *
 * A column down the left naming every list with its count, grouped by what a reader is there
 * to do, and beside it one thing at a time: the overview, or the list chosen. Sticky, so the
 * table of contents is there however long the list beside it runs. See js/market.js.
 */
.market {
  display: grid;
  grid-template-columns: 236px minmax(0, 1fr);
  gap: 20px;
  align-items: start;
}

.market__rail {
  position: sticky;
  top: 16px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 12px 10px 14px;
  background: var(--glass);
  box-shadow: var(--rim);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-lg);
}

/*
 * THE CASE MONITOR'S RAIL, which is this rail with one difference.
 *
 * A manager gets no rail — see review.js — so `#case-rail` is in the DOM and empty. An empty
 * grid column would still take its 236px and leave the queue inset for no reason, so an empty
 * rail takes no space and the grid collapses to the one column the page has always been.
 *
 * :empty rather than a class, because the condition IS emptiness: nothing has to remember to
 * add a class in the branch that decided not to draw it.
 */
.market__rail:empty {
  display: none;
}

.market:has(> .market__rail:empty) {
  grid-template-columns: minmax(0, 1fr);
}

/*
 * A page's own overview, the panel a rail's first entry opens. Its tiles and charts are the
 * app's own `.stats` and `.charts`; this is only the stack and the line over them.
 *
 * Named for what it is rather than for the first page to have one — the case monitor and
 * Location info draw the same panel, and a third would too.
 */
.glance {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.glance__line {
  margin: 0;
  font-size: 0.92rem;
  color: var(--text-dim);
}

/*
 * A tile whose number is something to act on. The label rather than a note, because these
 * tiles are a count and a name with nothing else on them — see drawGlance in js/users.js.
 */
.stat__label--warn {
  color: var(--warn);
}

/* Circuit-wide actions that belong to the overview rather than to any one location. */
.glance__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.market__railgroup {
  display: flex;
  flex-direction: column;
  gap: 1px;
}

.market__railhead {
  padding: 4px 10px 6px;
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.market__railitem {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 7px 10px;
  border-radius: 9px;
  text-decoration: none;
  font-size: 0.86rem;
  color: var(--text-dim);
  transition:
    background-color 120ms var(--ease),
    color 120ms var(--ease);
  /*
   * WRITTEN FOR AN <a>, AND A <button> WEARS IT TOO.
   *
   * Every rail built by js/listpage.js is anchors, so this rule never had to say what a
   * button's own chrome should do. The case monitor's rail is buttons — it switches a view
   * rather than going to an address — and without the six lines below every entry arrived
   * in the browser's default button dress: a grey border, a grey fill and the UA font, so
   * the whole rail read as a column of pressed keys.
   *
   * The reset lives on the shared class rather than at that one call site, because the next
   * rail built out of buttons would have hit exactly the same thing.
   *
   * NO `width: 100%` here, which the first cut had. A rail group is a column flex container,
   * so its children already stretch across it — the property bought nothing on a desktop and
   * broke the phone outright, where the media query below turns the rail into a horizontal
   * scroller of chips and 100% meant each chip was as wide as the whole rail. They stacked
   * over one another with their counts printed across their labels.
   */
  appearance: none;
  border: 0;
  background: none;
  font-family: inherit;
  text-align: left;
}

/* A rail entry that is a button is pressable; one that is a link already says so. */
button.market__railitem {
  cursor: pointer;
}

.market__railitem:hover {
  background: var(--surface-3);
  color: var(--text);
}

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

/* The one showing: lit in the accent, and said so to a screen reader by aria-current. */
.market__railitem--on {
  background: color-mix(in oklab, var(--accent) 14%, transparent);
  color: var(--text);
  font-weight: 650;
}

.market__railname {
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The count, as a pill — warm for a thing to do, the accent for a run still to play, faint
   for nothing listed, so the rail says at a glance where the work is. */
.market__railcount {
  flex: none;
  padding: 2px 7px;
  border-radius: 999px;
  background: var(--surface-3);
  font-size: 0.72rem;
  font-weight: 640;
  font-variant-numeric: tabular-nums;
  color: var(--text-dim);
}

.market__railcount--warn {
  color: var(--p-peach, var(--warn));
}

.market__railcount--go {
  color: var(--accent);
}

.market__railcount--faint {
  color: var(--text-faint);
}

.market__railnote {
  margin: 0;
  padding: 10px 10px 0;
  border-top: 1px solid var(--border);
  font-size: 0.72rem;
  line-height: 1.4;
  color: var(--text-faint);
}

.market__main {
  min-width: 0;
}

/* A list beside the strip starts flush under it; the card-to-card gap is for stacked cards,
   and these are shown one at a time. */
.market__main > .panel {
  margin-top: 0;
}

/* Why the advice tiles and lists are not on this window — one line above the tiles that are
   left, with the way to the week beside it. See adviceFor in src/market.js. */
.market__advice {
  margin: 0 0 10px;
  font-size: 0.8rem;
  line-height: 1.4;
  color: var(--text-faint);
}

.market__advice a {
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 2px;
}

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

/*
 * THE RAIL'S LOCATIONS — the sentiment page's: every building as an entry, the lit one the
 * building the page is on, with its code beside its name. A desktop-only group: on a phone the
 * rail is a strip of chips and twenty buildings would be twenty more of them, so the picker in
 * the range bar carries the choice there instead — and on a desktop the picker steps aside for
 * the rail, so the same choice is never offered twice (`rangebar__where--railed`).
 */
.market__railcode {
  flex: none;
  font-size: 0.68rem;
  letter-spacing: 0.04em;
  color: var(--text-faint);
}

@media (max-width: 860px) {
  .market__railgroup--places {
    display: none;
  }
}

@media (min-width: 861px) {
  .rangebar__where--railed {
    display: none;
  }
}

/* The one line above the sentiment page's tiles: what the numbers are, and what they are not.
   The staffing page's line is the same shape: the rule its crowd figures obey. */
.sentiment__line,
.rota__line {
  margin: 0 0 10px;
  font-size: 0.8rem;
  line-height: 1.4;
  color: var(--text-faint);
}

/*
 * How the overview is figured, as one quiet line that opens rather than a paragraph over the
 * tiles — the app's own quiet-disclosure shape, a muted line and a turning arrow, as the earlier
 * notes on a case have it (.progress__earlier). Opened, the paragraph it was sits under it.
 */
.rota__how {
  margin: 0 0 10px;
}

.rota__how > summary {
  cursor: pointer;
  list-style: none;
  width: fit-content;
  font-size: 0.8rem;
  color: var(--text-dim);
}

.rota__how > summary::-webkit-details-marker {
  display: none;
}

.rota__how > summary::before {
  content: '▸\00a0';
}

.rota__how[open] > summary::before {
  content: '▾\00a0';
}

.rota__how > .rota__line {
  margin: 6px 0 0;
}

/* A link in the line — not a button there, which keeps the button's own colours (see below). */
.sentiment__line a:not(.btn),
.rota__line a:not(.btn) {
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 2px;
}

.sentiment__line a:not(.btn):hover,
.rota__line a:not(.btn):hover {
  color: var(--accent-hover);
}

/* The staffing page's agent sits on the overview between the tiles and the charts, and needs
   the row gap its neighbours get from the grid they are in. */
.market__glance > .staffagent {
  margin-bottom: 16px;
}

/* Both placards off is a problem, not a note. */
.sentiment__line--warn {
  color: var(--p-peach, var(--warn));
}

/* The way to the placard switches, beside the warning that they are off: a control, on the line's
   own baseline rather than hanging below it. */
.sentiment__line--warn > .btn {
  margin-left: 6px;
  vertical-align: middle;
}

/* The same door under the empty overview's sentence — or, for a reader who cannot use it, who can —
   clear of the words above it. */
.empty > .btn,
.empty > .sentiment__who {
  margin-top: 18px;
}

.empty > .sentiment__who {
  display: block;
}

/* A list long enough to want a box to find a row in and a way to reorder it — every film. */
.panel__tools {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 4px;
}

/* Widths of their own: the shared control rule makes every input and select as wide as its
   row, which here would put the two on two lines with nothing beside either. */
.panel__find {
  flex: 1 1 200px;
  width: auto;
  min-width: 0;
  max-width: 340px;
}

.panel__order {
  flex: 0 1 auto;
  width: auto;
  min-width: 190px;
  /* Room for the arrow the select paints at its right edge. */
  padding-right: 34px;
}

/*
 * CONTRACT TERMS — the booker's form above the list of terms (js/terms-panel.js). A clause is one
 * line: what it holds on the left, the last day it holds on the right, so a column of "until"
 * dates reads down the form. On a phone the two halves stack.
 */
.terms {
  display: grid;
  gap: 12px;
  padding: 14px 16px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface-2);
}

.terms .field {
  margin-bottom: 0;
}

/* Which film and where; then the two booking days. Two abreast, one under the other on a phone. */
.terms__which {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: 12px;
}

.terms__clause {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 8px 16px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
}

.terms__what,
.terms__when {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}

.terms__what {
  flex: 1 1 320px;
}

.terms__what > label,
.terms__when > label {
  font-weight: 600;
  font-size: 0.9rem;
  color: var(--p-subtext1);
}

/* The global control rule makes every input as wide as its row; a count is not. Scoped to the form
   so the rule outranks `input[type='number']`, which a lone class does not. */
.terms .terms__count {
  width: 6.5em;
  flex: 0 0 auto;
}

/*
 * A date box that looks like one (dateBox in js/terms-panel.js). The operator, on an iPhone: "It
 * should be a date box, not just a little empty box". iOS draws an empty date field as nothing,
 * and the until boxes, sized to their content, shrank to blank pills. So a date has a width of its
 * own, never its content's; the app's calendar at its left on every device; and while it is empty
 * and not being typed in, mm/dd/yyyy over it — a desktop's own mm/dd/yyyy hidden under that, where
 * it would otherwise print beside it. The browser's picker button is kept but made invisible and
 * stretched over the whole box, so a press anywhere on it opens the calendar, as on a phone.
 */
.terms__date {
  position: relative;
  display: block;
  min-width: 11em;
}

.terms__when > .terms__date {
  flex: 1 1 11em;
}

.terms__date > .glyph {
  --glyph: 18px;
  position: absolute;
  top: 50%;
  left: 14px;
  z-index: 1;
  transform: translateY(-50%);
  color: var(--text-dim);
  pointer-events: none;
}

.terms .terms__date > input {
  position: relative;
  width: 100%;
  padding-left: 42px;
}

.terms .terms__date > input[type='date']::-webkit-calendar-picker-indicator {
  position: absolute;
  inset: 0;
  width: auto;
  height: auto;
  margin: 0;
  padding: 0;
  opacity: 0;
  cursor: pointer;
}

/* iOS centres the day it draws; the calendar is on the left, so the day is too. */
.terms__date > input::-webkit-date-and-time-value {
  text-align: left;
}

.terms__date[data-empty]:not(:focus-within) > input {
  color: transparent;
}

.terms__date[data-empty]:not(:focus-within)::after {
  content: attr(data-hint);
  position: absolute;
  inset: 0 14px 0 42px;
  display: flex;
  align-items: center;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  color: var(--text-faint);
  pointer-events: none;
}

.terms__room {
  width: auto;
  flex: 0 1 auto;
  min-width: 0;
  padding-right: 34px;
}

.terms__check {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

.terms__rooms {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 14px;
  flex-basis: 100%;
}

.terms__roomitem {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-weight: 400;
}

.terms__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

/* What guests at a cinema are shown today: the films, ranked, each with its score and count. */
.sentiment__shown {
  margin: 0;
  padding-left: 1.2em;
  display: grid;
  gap: 3px;
}

.sentiment__shown-film {
  font-weight: 600;
}

/* The overview: seven tiles across the column, then the charts two abreast. The tiles are
   narrower than the dashboard's so all seven fit one row beside the rail; their labels wrap. */
.market__glance .stats {
  grid-template-columns: repeat(auto-fit, minmax(104px, 1fr));
  gap: 10px;
  margin-bottom: 16px;
}

.market__glance .charts {
  margin-bottom: 0;
}

/* A list's top line: the heading and its count, over the body — the same line the rail reads. */
.panel__top {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 10px;
  padding: 16px 22px;
  border-bottom: 1px solid var(--border);
}

/* The way from a chart to the list behind it — the board's own link, on any card. */
.chart__link {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  margin-top: 10px;
  color: var(--text-dim);
  font-size: 0.84rem;
  text-decoration: none;
}

.chart__link:hover {
  color: var(--accent);
}

/* The rail folds to a strip of chips where there is no room for a column — the same
   breakpoint the rest of the shell uses for one-column layouts. */
@media (max-width: 860px) {
  .market {
    grid-template-columns: minmax(0, 1fr);
    gap: 14px;
  }

  .market__rail {
    position: static;
    flex-direction: row;
    align-items: center;
    gap: 6px;
    padding: 8px;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
  }

  .market__rail::-webkit-scrollbar {
    display: none;
  }

  .market__railgroup {
    flex-direction: row;
    gap: 6px;
  }

  .market__railhead,
  .market__railnote {
    display: none;
  }

  .market__railitem {
    flex: none;
    white-space: nowrap;
    padding: 6px 10px;
    border: 1px solid var(--border);
    border-radius: 999px;
    font-size: 0.8rem;
  }

  .market__railitem--on {
    border-color: color-mix(in oklab, var(--accent) 45%, var(--border));
  }
}

.panel__note {
  margin: 0;
  max-width: 68ch;
  color: var(--text-dim);
  font-size: 0.86rem;
  line-height: 1.45;
}

/* A table is the right shape for a ranked list of pairs, and the wrong shape for a phone:
   it scrolls in its own box rather than pushing the page sideways. */
.table__scroll {
  overflow-x: auto;
  margin: 0 -4px;
  padding: 0 4px;
}

.market__table {
  width: 100%;
  min-width: 620px;
}

.market__film {
  font-weight: 650;
}

/* The same film as the row above, in a list grouped by film (byFilm in js/market.js): its name in a
   quieter ink, so the eye reads one film at several buildings rather than a new title on each row.
   Only on a wide screen — on a phone each row is its own block, led by the name, and a faint
   heading there would read as a row that is disabled. */
@media (min-width: 561px) {
  .market__film--again {
    color: var(--text-faint);
    font-weight: 500;
  }
}

.market__code {
  display: block;
  color: var(--text-faint);
  font-size: 0.78rem;
}

.market__num {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.market__num--warn {
  color: var(--p-peach, var(--text));
}

.market__num--go {
  color: var(--accent);
}

/* A run's presale dates (presaleDates in js/market.js): two dates and a dash, which may break at the
   dash where the column is narrow. Each date is held together by its own no-break spaces. */
.market__num--dates {
  white-space: normal;
}

/* A run with nothing left to give: already down to its last screening a day. Said in the
   page's quietest ink, because "no move" is an answer and not an alarm. */
.market__num--none {
  color: var(--text-faint);
}

/*
 * The one cell on the programming page that holds a control: a room swap's "Draft into next
 * week's plan". Right-aligned and never wrapped, so a table of findings does not grow a ragged
 * left edge of buttons — and `white-space: nowrap` because the label is a sentence and a button
 * broken across two lines reads as two.
 *
 * On a phone the table becomes stacked blocks (see the data-label rule above), where a right
 * edge means nothing: the button goes full width there, like every other action in the app.
 */
.market__act {
  text-align: right;
  white-space: nowrap;
}

@media (max-width: 560px) {
  .market__act .btn {
    width: 100%;
  }
}

/* A row of actions under a setting — the digest's "email me one" beside its "preview it". */
.setting__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 6px;
}

/* A figure and, under it, what it is a share of — "4 shows" over "40% of this cinema's
   screenings". One element on purpose: on a phone the cell is a flex row of label and
   value, and two loose spans would sit side by side as two values. */
.market__stack {
  display: grid;
  gap: 1px;
}

.market__sub {
  color: var(--text-faint);
  font-size: 0.78rem;
  font-weight: 400;
  white-space: normal;
}

/* The count of what a list was chosen from, quieter than the sentence saying what it measures. */
.panel__note--faint {
  color: var(--text-faint);
  font-size: 0.8rem;
}

/*
 * AI Scheduling Agent insights (js/hours-insights.js): a cinema to a block, with its name, the
 * changes its paragraph was written about, and the paragraph. Ruled apart rather than boxed,
 * because they are one reading of one list and not cards to compare.
 */
.insights {
  display: grid;
  gap: 18px;
  margin-top: 14px;
}

.insights__cinema {
  display: grid;
  gap: 6px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
}

.insights__cinema:first-child {
  padding-top: 0;
  border-top: 0;
}

.insights__name {
  margin: 0;
  font-size: 1rem;
  font-weight: 650;
}

.insights__code {
  margin-left: 8px;
  color: var(--text-faint);
  font-size: 0.78rem;
  font-weight: 400;
}

.insights__changes {
  display: grid;
  gap: 2px;
  margin: 0;
  padding: 0;
  list-style: none;
  color: var(--text-dim);
  font-size: 0.84rem;
  font-variant-numeric: tabular-nums;
}

.insights__said {
  margin: 4px 0 0;
  max-width: 68ch;
  line-height: 1.5;
}

/* Whether the advice on this panel has been worth taking — see advice-outcomes.js. Set at full
   text colour against a tinted rule, because it is the one line here a booker should read
   BEFORE the list rather than after it, and because for the first month it is the line that
   says "there is not enough of a record to trust this yet". */
.panel__worked {
  margin: 0;
  max-width: 68ch;
  padding: 8px 0 8px 12px;
  border-left: 3px solid var(--accent);
  color: var(--text);
  font-size: 0.86rem;
  line-height: 1.5;
}

/* The index, as a track with PAR marked on it: the eye ranks the rows without reading a
   single number, and the line says where "doing what this building normally does" is. */
.idx {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 132px;
}

.idx__track {
  position: relative;
  flex: 1;
  height: 8px;
  border-radius: 999px;
  background: color-mix(in oklab, var(--text) 10%, var(--surface-2));
  overflow: hidden;
}

.idx__fill {
  height: 100%;
  border-radius: 999px;
  background: var(--accent);
}

.idx__fill--cold {
  background: color-mix(in oklab, var(--p-peach, var(--accent)) 78%, var(--accent));
}

/* Par sits at 1.00, which is halfway along a track that runs to 2.00. */
.idx__par {
  position: absolute;
  inset: 0 auto 0 50%;
  width: 2px;
  background: color-mix(in oklab, var(--text) 42%, transparent);
}

.idx__value {
  font-variant-numeric: tabular-nums;
  font-weight: 650;
  font-size: 0.85rem;
}

/* The coverage strip, above the lists it qualifies. */
.cover {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  padding: 12px 14px;
  margin-bottom: 16px;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
  color: var(--text-dim);
  font-size: 0.86rem;
  line-height: 1.5;
}

.cover__dot {
  width: 8px;
  height: 8px;
  margin-top: 6px;
  border-radius: 999px;
  flex: none;
  background: var(--accent);
}

/*
 * The two states of the strip, which are a colour apart and nothing else — the sentence
 * beside the dot does the telling. Green for a circuit whose POS feeds are all current,
 * amber for one where a location's numbers are understated and every list below it has to
 * be read as a question.
 */
.cover--ok .cover__dot {
  background: var(--ok);
}

.cover--quiet .cover__dot {
  background: var(--p-peach, var(--accent));
}

.cover__lead {
  margin: 0 0 4px;
}

/* The holiday line: the one caveat in the strip that is about the calendar, not the feed. */
.cover__lead--holiday {
  color: var(--text);
  font-weight: 560;
}

.cover__list {
  margin: 0;
  padding-left: 18px;
}

.market__quiet {
  display: inline-block;
  margin-top: 3px;
  padding: 1px 6px;
  border-radius: 999px;
  border: 1px solid color-mix(in oklab, var(--p-peach, var(--accent)) 45%, transparent);
  color: var(--p-peach, var(--text-dim));
  font-size: 0.72rem;
}

/*
 * WHY a film ranks where it does — the two facts that turn the rank lists from a puzzle into an
 * instruction, the screen time and then the fullness. The words say both and the colour says what
 * they come to, whether people came, because that is what a booker scans the column for (whyCell in
 * js/market.js): green where they did, grey, ring and all, where they stayed away, and the quiet ink
 * for a lead that is only the schedule's. It was two hues for the lever, the schedule's and the
 * audience's, until the words named the lever on every chip and the colour only said it again. Kept
 * the shape of the badge above so the two read as one family of marks rather than two systems.
 */
.market__why {
  display: inline-block;
  padding: 1px 7px;
  border-radius: 999px;
  border: 1px solid currentcolor;
  font-size: 0.72rem;
  white-space: nowrap;
}

/* Mixed toward the text, as the hues on the releases' marks are, so it holds its contrast on a light
   card as on a dark one. */
.market__why--came {
  color: color-mix(in oklab, var(--ok) 70%, var(--text));
}

.market__why--booked {
  color: var(--text-dim);
}

/* The ring softened as well as the words: a light theme's faint ink is close to its dim one, and the
   ring is what tells the two apart there. Mixed from the ink rather than a border token, which on
   some themes is a hairline the pill all but loses. */
.market__why--away {
  border-color: color-mix(in oklab, var(--text-faint) 45%, transparent);
  color: var(--text-faint);
}

/* The reason over the two figures it was read from (whyCell in js/market.js). The pill keeps its own
   width: the stack is a grid, and a grid item stretches unless told otherwise. */
.market__reason .market__why {
  justify-self: start;
}

.market__figure {
  white-space: nowrap;
}

/* A film holding up from one week of its run to the next (js/drop-words.js): the accent, because it
   is the good news on a list a booker can act on. Its own width under the name, not the column's —
   the stack it sits in is a grid, and a grid item stretches unless told otherwise. */
.market__why--holding {
  justify-self: start;
  color: var(--accent);
}

/* An upcoming release whose date moved (releasesPanel in js/market.js): the warm ink, because a
   booker planned the week around the date it had. Its own width, for the same reason as above. */
.market__why--moved {
  justify-self: start;
  color: var(--p-peach, var(--text));
}

/*
 * WHAT IS KNOWN OF AN UPCOMING RELEASE (releaseTags in js/market.js): the marks under its name, in a
 * row that wraps. A TENTPOLE is the one filled mark on the list, because it is the one a week's
 * screens are planned around and the eye should find it first; the rest are outlined like every
 * other reason on the page. How it opens in the quiet ink, because most films open wide and the mark
 * is there to be read when it does not; premium formats and streaming in a hue each, mixed toward the
 * text so they hold their contrast on a light card as on a dark one.
 */
.market__tags {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin-block: 3px 2px;
}

.market__why--tentpole {
  border-color: transparent;
  background: var(--accent-fill);
  color: var(--accent-ink);
  font-weight: 650;
}

.market__why--kind {
  color: var(--text-dim);
}

.market__why--format {
  color: color-mix(in oklab, var(--p-yellow, var(--text)) 70%, var(--text));
}

.market__why--stream {
  color: color-mix(in oklab, var(--p-teal, var(--text)) 70%, var(--text));
}

/* A line of what the film is, under its marks: kept to the column, never the cell's nowrap. */
.market__facts {
  max-width: 52ch;
}

/* A week's readers and the four weeks drawn beside them, on one line. */
.market__views {
  display: inline-flex;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
  font-weight: 650;
}

/* How the week compares with the one before: the accent for more readers, the warm ink for fewer. */
.market__sub--up {
  color: var(--accent);
}

.market__sub--down {
  color: var(--p-peach, var(--text-dim));
}

.sparkline {
  flex: none;
  overflow: visible;
}

.sparkline__area {
  fill: color-mix(in oklab, var(--accent) 16%, transparent);
  stroke: none;
}

.sparkline__line {
  fill: none;
  stroke: var(--accent);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.sparkline__end {
  fill: var(--accent);
  stroke: var(--surface);
  stroke-width: 1;
}

/*
 * An upcoming release's readers on Home's card (interestLine in js/release-marks.js): the four weeks
 * drawn small, then the week's figure in the row's quiet size, and which way it is going in the
 * accent for more readers and the warm ink for fewer, as the programming page's Interest column says
 * it. The line holds the drawing at its own width and lets the words wrap beside it.
 */
.interest {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 3px;
  color: var(--text-faint);
  font-size: 0.79rem;
  font-variant-numeric: tabular-nums;
}

.interest__words {
  min-width: 0;
}

.interest__change--up {
  color: var(--accent);
}

.interest__change--down {
  color: var(--p-peach, var(--text-dim));
}

/*
 * INTEREST VS TICKETS (interestPanel in js/market.js): every film as a dot and the picked one day by
 * day, side by side over the list where there is the room for both, one over the other below that.
 * Inside the list's own card, so the two are not cards of their own — no border, no glass, no shadow,
 * only each one's head and plot.
 */
.market__vs {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 24px 32px;
  margin: 6px 0 20px;
}

@media (min-width: 1180px) {
  .market__vs {
    grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
  }
}

.market__vs .chart {
  background: none;
  border: 0;
  box-shadow: none;
  padding: 0;
}

/* The film the right-hand side draws, picked by name as well as by its dot or its row. */
.market__vs-pick {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 12px;
  margin-bottom: 12px;
}

.market__vs-label {
  color: var(--text-dim);
  font-size: 0.74rem;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
}

.market__vs-pick .panel__order {
  min-width: 0;
  max-width: 100%;
}

/*
 * RISING INTEREST (risingPanel in js/market.js): the one film drawn week by week above the list, at the
 * width nine weeks read at rather than the panel's — a column a week across a whole wide screen is a
 * bar chart with nothing in it — and its chart part of the panel, not a card on a card.
 */
.market__rise {
  max-width: 760px;
  margin: 6px 0 20px;
}

.market__rise .chart {
  background: none;
  border: 0;
  box-shadow: none;
  padding: 0;
}

/* A film's latest week, under its name: rising filled in the accent, holding ringed in it (above), a
   one-day spike and as usual in the quiet inks, the spike's ring broken because its rise is not whole. */
.market__why--rising {
  border-color: transparent;
  background: var(--accent-fill);
  color: var(--accent-ink);
  font-weight: 650;
}

.market__why--spike {
  border-style: dashed;
  color: var(--text-dim);
}

.market__why--usual {
  color: var(--text-faint);
}

/* A format fuller than the standard print, a film with longer legs than usual, a town's lean
   (formatsPanel, legsPanel and ratingsPanel in js/market.js): the green the share lists give people
   coming, mixed toward the text so it holds its contrast on a light card as on a dark one. */
.market__why--up {
  color: color-mix(in oklab, var(--ok) 70%, var(--text));
}

/* Emptier than the standard print, shorter legs than usual: the warm ink the page gives a run to watch. */
.market__why--down {
  color: color-mix(in oklab, var(--p-peach, var(--text)) 80%, var(--text));
}

/* A cell that is an answer rather than a figure — "Not compared", "No lean": the quiet ink, at the
   weight of running text rather than of the figures around it. */
.market__table td.market__num--faint {
  color: var(--text-faint);
  font-weight: 500;
}

/* The circuit's own row over its cinemas' (ratingsPanel): the figures every other row is read
   against, so set apart by weight and a firmer rule under it. */
.market__circuit > td {
  font-weight: 600;
  border-bottom: 2px solid var(--border-strong);
}

/* A row draws its film day by day when pressed, and the one drawn is lit. */
.market__pick {
  cursor: pointer;
}

.market__pick:hover > td {
  background: color-mix(in oklab, var(--accent) 5%, transparent);
}

.market__pick--on > td {
  background: color-mix(in oklab, var(--accent) 10%, transparent);
}

/* A film against what its readers suggest: the accent for far more, the warm ink for far less. */
.market__against {
  min-width: 10em;
  white-space: normal;
}

.market__against--up {
  color: var(--accent);
}

.market__against--down {
  color: var(--p-peach, var(--text-dim));
}

/* What an axis is, in words: over the up axis's top tick, under the across axis's ticks. */
.plot__axis {
  fill: var(--text-dim);
  font-size: 10.5px;
  font-weight: 600;
  font-family: var(--font);
}

/* The line through the dots — what a film's readers suggest — dashed, like every line to be measured against. */
.plot__fit {
  fill: none;
  stroke: var(--text-faint);
  stroke-width: 1.5;
  stroke-dasharray: 5 4;
}

/* A dot a film, ringed in the card's own surface so two that touch still read as two. */
.plot__dotted {
  fill: var(--accent);
  stroke: var(--surface);
  stroke-width: 2;
  transition: fill 140ms var(--ease);
}

.plot__pin:hover .plot__dotted,
.plot__pin:focus-within .plot__dotted {
  fill: var(--text);
}

/*
 * A film that opened before the sales records began: a ring with the card's own surface in it — there,
 * and not one of the films the line is drawn through. Its ring takes the ink on hover as a dot's fill does.
 */
.plot__dotted--hollow,
.plot__pin:hover .plot__dotted--hollow,
.plot__pin:focus-within .plot__dotted--hollow {
  fill: var(--surface);
  stroke: var(--accent);
  stroke-width: 1.75;
}

.plot__pin:hover .plot__dotted--hollow,
.plot__pin:focus-within .plot__dotted--hollow {
  stroke: var(--text);
}

/* The film drawn day by day, ringed. */
.plot__ring {
  fill: none;
  stroke: var(--accent);
  stroke-width: 2;
}

.plot__pin .plot__hit {
  cursor: pointer;
}

.plot__pin:focus-within .plot__hit {
  stroke: var(--focus);
  stroke-width: 2;
}

.plot__name {
  fill: var(--text-dim);
  font-size: 10.5px;
  font-family: var(--font);
}

.plot__name--picked {
  fill: var(--text);
  font-weight: 600;
}

/* The day a film opened: a rule through both of its plots, and its words at the top. */
.plot__rule {
  stroke: var(--text-faint);
  stroke-width: 1;
  stroke-dasharray: 2 3;
}

.plot__rulelabel {
  fill: var(--text-dim);
  font-size: 10px;
  font-family: var(--font);
}

/* The keys: a dot, the dashed line, a ringed dot; and the day by day's line and column in its readout. */
.plot__swatch--pin,
.plot__swatch--picked {
  width: 9px;
  height: 9px;
  border: 0;
  border-radius: 50%;
  background: var(--accent);
}

.plot__swatch--picked {
  box-shadow: 0 0 0 2px var(--surface), 0 0 0 3.5px var(--accent);
}

.plot__swatch--hollow {
  box-sizing: border-box;
  width: 9px;
  height: 9px;
  border: 1.75px solid var(--accent);
  border-radius: 50%;
}

.plot__swatch--fit {
  border-top-color: var(--text-faint);
}

.plot__swatch--upper {
  border-top-color: var(--accent);
}

.plot__swatch--lower {
  width: 10px;
  height: 10px;
  border: 0;
  border-radius: 3px;
  background: var(--accent-fill);
}

/*
 * Over the list (releaseTools in js/market.js): the filters on the left, each a pill with its count,
 * and the order on the right in the app's segmented pair. One row that wraps; on a phone the order
 * drops under the filters rather than squeezing them.
 */
.market__tools {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 10px 16px;
  margin-top: 6px;
}

.market__filters {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}

.market__filter {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 11px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--text-dim);
  font: inherit;
  font-size: 0.78rem;
  font-weight: 600;
  cursor: pointer;
  transition:
    background 160ms var(--ease),
    color 160ms var(--ease),
    border-color 160ms var(--ease);
}

.market__filter:hover {
  color: var(--text);
}

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

.market__filter--on {
  border-color: transparent;
  background: var(--accent-fill);
  color: var(--accent-ink);
}

.market__filter-count {
  font-variant-numeric: tabular-nums;
  font-weight: 500;
  opacity: 0.78;
}

@media (max-width: 560px) {
  .market__filter {
    padding-block: 6px;
  }
}

/* What else opens within a week of a presale run (openingLine in js/market.js): the competition for
   the screens a booker is about to add, under the name in the sub-line's size and the page's blue,
   so it reads as context and not as one more figure. */
.market__opening {
  color: var(--p-blue, var(--accent));
  font-size: 0.78rem;
  font-weight: 400;
  white-space: normal;
}

/* "Show 3 more" on that line (openingLine in js/market.js): a plain button in the line's own blue,
   told from the names by its weight and an underline under the pointer. It was an outlined pill,
   which read as a tag; the operator asked for a simpler button. Taller on a phone, where it has to be
   a target a thumb can find. */
.market__expand {
  display: inline-block;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  font-weight: 700;
  line-height: 1.4;
  white-space: nowrap;
  cursor: pointer;
}

.market__expand:hover {
  text-decoration: underline;
}

.market__expand:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}

@media (max-width: 560px) {
  .market__expand {
    padding-block: 4px;
  }
}

/* A release's article on Wikipedia: the film's name, in its own ink, underlined only on a hover or
   a focus, as the page's other names are. */
.market__link {
  color: inherit;
  text-decoration: none;
}

.market__link:hover,
.market__link:focus-visible {
  text-decoration: underline;
}

/* Who a release is from, at the head of its credits line (fromWords in js/market.js): in the
   stronger of the two quiet inks, and a studio that opens wide in a heavier weight, because on each
   day the list puts those first and the eye should see why. */
.market__studio {
  color: var(--text-dim);
}

.market__studio--major {
  font-weight: 600;
}

/* The releases' rows run to four or five lines in the film's cell; every figure sits at the top,
   level with the film's name, rather than floating at the middle of a tall row. */
[data-panel='releases'] .market__table td {
  vertical-align: top;
}

/*
 * The per-cinema table that leads the circuit view: one row a building, its name a link
 * into that building's own view of the page. The counts are figures, not sentences, so the
 * columns are narrow and the cinema takes the width.
 */
.market__site {
  font-weight: 650;
}

.market__site a {
  color: inherit;
  text-decoration: none;
  border-bottom: 1px solid color-mix(in oklab, var(--accent) 40%, transparent);
}

.market__site a:hover {
  color: var(--accent);
  border-bottom-color: var(--accent);
}

.market__none {
  color: var(--text-faint);
}

/* The one figure on a row that is a count of something, drawn so the eye finds it. */
.market__count {
  font-weight: 650;
}

.market__ok {
  color: var(--text-faint);
  font-size: 0.8rem;
}

/*
 * The rest of a long list, behind a button. A circuit of twenty sites can put a hundred
 * rows on one list, and the first twenty are the ones that matter — the sort is by size —
 * so the page shows those and says how many more there are, rather than either scrolling
 * the reader through all of them or quietly cutting the list short.
 */
.market__more {
  display: block;
  width: 100%;
  margin-top: 8px;
  padding: 9px 12px;
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius-sm);
  background: none;
  font: inherit;
  font-size: 0.84rem;
  font-weight: 550;
  color: var(--text-dim);
  cursor: pointer;
}

.market__more:hover {
  color: var(--accent);
  border-color: var(--accent);
}

/* A card that lists rather than plots — the programming answers on the overview. */
.chart__plot--board {
  padding: 0;
}

.board {
  display: grid;
  gap: 2px;
}

.board__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 14px;
  padding: 9px 2px;
  border-bottom: 1px solid var(--border);
}

.board__row:last-child {
  border-bottom: 0;
}

/* A row with a line under it (an upcoming release's readers, on Home's card — see board() in
   js/charts.js): the film and its verdict share the first line as on every other row, and the line
   under them takes the row's whole width, so it keeps to one line however long the verdict is. */
.board__row--after {
  flex-wrap: wrap;
  row-gap: 0;
}

.board__after {
  flex: 0 0 100%;
  min-width: 0;
}

/*
 * The film and what to do about it share the row, and the film has first claim on it. The
 * verdict's note ran on one line however long it was — "take the 7:00pm off A Quiet
 * Inventory" — and on a card a quarter of the page wide that took two thirds of the row, so
 * the film came out as "Cascade P…". The verdict now takes at most half, its note wrapping
 * inside it, and a title too long for the rest runs to a second line rather than losing its
 * end: the film is what the row is about.
 */
.board__what {
  display: grid;
  gap: 1px;
  flex: 1 1 0;
  min-width: 0;
}

.board__title {
  font-weight: 620;
  overflow-wrap: break-word;
}

.board__where {
  color: var(--text-faint);
  font-size: 0.79rem;
}

.board__verdict {
  display: grid;
  justify-items: end;
  gap: 1px;
  flex: 0 1 auto;
  max-width: 50%;
  text-align: right;
}

.board__value {
  font-variant-numeric: tabular-nums;
  font-weight: 680;
}

.board__value--go {
  color: var(--accent);
}

.board__value--warn {
  color: var(--p-peach, var(--text));
}

.board__note {
  color: var(--text-faint);
  font-size: 0.78rem;
}

.board__more {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  margin-top: 10px;
  color: var(--text-dim);
  font-size: 0.84rem;
  text-decoration: none;
}

.board__more:hover {
  color: var(--accent);
}

/* Behind the button on Home's list cards, where "Show the numbers" was (js/card-more.js): an
   entry at a time — a film, a room or a cinema, the line that places it the way a row's does, and
   its showtimes, days, cinemas or releases as a short list: the lead in the text's ink, the figures
   after it in the rows' quiet one. Times and percentages in figures that line up. */
.board__detail {
  display: grid;
  gap: 14px;
  padding-top: 10px;
}

/*
 * HELD TO THE TABLE'S HEIGHT where cards sit side by side. The grid stretches every card in a row
 * to the tallest (see .charts — one size of card), so a list four films long opened in one card
 * stood its neighbour's button at the foot of six hundred pixels of nothing. The table this
 * replaced scrolled in 260px of its own (.chart__scroll) for exactly that reason, and so does
 * this. On a phone the cards are one column, nothing stands beside them, and the list reads at
 * its length with no box inside the page to scroll.
 */
@media (min-width: 760px) {
  .board__detail {
    max-height: 260px;
    overflow-y: auto;
    padding-right: 6px;
  }
}

.board__entry {
  display: grid;
  gap: 6px;
}

.board__list {
  display: grid;
  gap: 4px;
  margin: 0;
  /* Room for the bullets inside the box: where the list scrolls (below), a marker hanging past
     its edge is cut to a sliver. */
  padding: 0 0 0 20px;
  font-size: 0.86rem;
  font-variant-numeric: tabular-nums;
}

/* While a card's button is fetching what it holds, and if that fails, the line saying so with
   the way to ask again — a plain button in the text's ink, as the presale line's is. */
.chart__loading {
  margin: 10px 0 0;
  color: var(--text-faint);
  font-size: 0.84rem;
}

.chart__retry {
  display: inline-block;
  padding: 4px 0;
  border: 0;
  background: none;
  color: var(--text);
  font: inherit;
  font-weight: 700;
  cursor: pointer;
}

.chart__retry:hover {
  text-decoration: underline;
}

.chart__retry:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}

/* The programming page's own arrival: the lists rise like every other card, and their rows
   follow one after another — a ranked list reads better arriving in its order. */
html.arriving .market__table tbody tr {
  --arrive-rise: 10px;
  animation: arrive 420ms var(--ease) backwards;
}
html.arriving .market__table tbody tr:nth-child(2) {
  animation-delay: 60ms;
}
html.arriving .market__table tbody tr:nth-child(3) {
  animation-delay: 120ms;
}
html.arriving .market__table tbody tr:nth-child(4) {
  animation-delay: 180ms;
}
html.arriving .market__table tbody tr:nth-child(n + 5) {
  animation-delay: 230ms;
}

/* And the board cards' rows on the overview, on the same footing. */
html.arriving .board__row {
  --arrive-rise: 10px;
  animation: arrive 400ms var(--ease) backwards;
}
html.arriving .board__row:nth-child(2) {
  animation-delay: 55ms;
}
html.arriving .board__row:nth-child(3) {
  animation-delay: 110ms;
}
html.arriving .board__row:nth-child(4) {
  animation-delay: 165ms;
}
html.arriving .board__row:nth-child(n + 5) {
  animation-delay: 210ms;
}

@media (prefers-reduced-motion: reduce) {
  html.arriving .market__table tbody tr,
  html.arriving .board__row {
    animation: none;
  }
}

/*
 * The programming lists on a phone: stacked blocks, not a table dragged sideways.
 *
 * Six columns will not fit 390px and never will, and the scroll box that let them try was
 * the compromise: a reader saw the film and the cinema, and everything the page exists to
 * say — the index, the move, the pace — lived off the right-hand edge behind a drag most
 * people never make. So the table stops being a table here. Each row becomes a block with
 * the film as its heading and every figure printed against the name of its own column,
 * which is the one thing a dropped heading row has to be replaced by.
 */
@media (max-width: 560px) {
  .table__scroll {
    overflow-x: visible;
    margin: 0;
    padding: 0;
  }

  .market__table {
    min-width: 0;
    display: block;
  }

  /* Kept for screen readers, taken off the layout: the labels below say it in place. */
  .market__table thead {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .market__table tbody,
  .market__table tr,
  .market__table td {
    display: block;
  }

  .market__table tr {
    padding: 12px 0;
    border-bottom: 1px solid var(--border);
  }

  .market__table tr:last-child {
    border-bottom: 0;
  }

  .market__table td {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
    padding: 3px 0;
    border: 0;
  }

  /* The column's name, printed where the head used to be. */
  .market__table td::before {
    content: attr(data-label);
    flex: none;
    color: var(--text-faint);
    font-size: 0.78rem;
    letter-spacing: 0.02em;
    text-transform: uppercase;
  }

  /* The film leads the block as its heading, so the first line is what it is about — and on
     the per-cinema table it is the cinema that leads. */
  .market__table td.market__film,
  .market__table td.market__site {
    display: block;
    padding-bottom: 6px;
    font-size: 1.02rem;
    font-weight: 700;
  }

  .market__table td.market__film::before,
  .market__table td.market__site::before {
    content: none;
  }

  /* The cinema sits under it as a subtitle rather than as another labelled figure. By class,
     not by column position: the column is dropped altogether once one cinema is chosen, and
     a positional rule would then have restyled whichever figure moved into second place. */
  .market__table td.market__where::before {
    content: none;
  }

  .market__table td.market__where {
    display: block;
    margin-top: -4px;
    padding-bottom: 6px;
    color: var(--text-dim);
  }

  .market__table td.market__where .market__code,
  .market__table td.market__site .market__code {
    display: inline;
    margin-left: 6px;
  }

  /* A count of nothing is a dash on a wide screen and no line at all on a phone: twenty
     cinemas at six labelled lines each is a scroll, and most of those lines would read "—". */
  .market__table td.market__none {
    display: none;
  }

  /* The index bar keeps its track — it is the fastest read on the card. */
  .idx {
    min-width: 0;
    flex: 1;
    max-width: 60%;
  }

  /* The figure and its share stack against the right edge, under the label they answer. */
  .market__stack {
    text-align: right;
  }

  /* Not the block's heading, which answers no label: a film's name reads from the left. */
  .market__table td.market__film .market__stack {
    text-align: left;
  }

  /* A reason answers its label like every other figure, so it stands against the right edge too. */
  .market__reason .market__why {
    justify-self: end;
  }

  /* The releases open each block with the film, like every list here, though on a wide screen
     the day it opens is the first column: the list is a calendar there, and a set of films here. */
  [data-panel='releases'] .market__table tr {
    display: flex;
    flex-direction: column;
  }

  [data-panel='releases'] .market__table td.market__film {
    order: -1;
  }
}

/*
 * THE WEATHER on the programming page (js/market.js, weatherPanels). A day's sky is a small line
 * drawing in the text's own colour beside its words. On the circuit's list each cinema's week is a
 * strip of seven such days, and a day whose weather has clearly moved sales there before is tinted
 * the way its figure is — the accent for more, the warm ink for fewer. The tint repeats what the
 * row's last column says in words; it never says it alone.
 */
.wx__line {
  display: inline-flex;
  align-items: center;
  gap: 10px;
}

.wx__sky {
  display: inline-grid;
  place-items: center;
  flex: none;
  width: 22px;
  height: 22px;
  color: var(--text-dim);
}

.wx__sky svg {
  width: 22px;
  height: 22px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.wx__strip {
  display: grid;
  grid-template-columns: repeat(7, minmax(38px, 1fr));
  gap: 4px;
  max-width: 340px;
}

.wx__day {
  display: grid;
  justify-items: center;
  gap: 2px;
  padding: 5px 2px 6px;
  border-radius: 9px;
  font-variant-numeric: tabular-nums;
}

.wx__day--up {
  background: color-mix(in srgb, var(--accent) 14%, transparent);
}

.wx__day--down {
  background: color-mix(in srgb, var(--p-peach, var(--text)) 16%, transparent);
}

.wx__day .wx__sky,
.wx__day .wx__sky svg {
  width: 18px;
  height: 18px;
}

.wx__dow {
  color: var(--text-faint);
  font-size: 0.72rem;
  letter-spacing: 0.02em;
}

.wx__high {
  font-size: 0.82rem;
  font-weight: 600;
}

/* A day named in "Days to watch", in the ink its figure would take. */
.wx__mark--up {
  color: var(--accent);
}

.wx__mark--down {
  color: var(--p-peach, var(--text));
}

@media (max-width: 560px) {
  /* The week, and the days named under it, need the whole width of the block: their labels sit
     over them rather than beside, and a named day wraps rather than running off the card. */
  .market__table td.wx__strip-cell,
  .market__table td.wx__watch-cell {
    display: block;
    white-space: normal;
  }

  .market__table td.wx__strip-cell::before,
  .market__table td.wx__watch-cell::before {
    display: block;
    margin-bottom: 6px;
  }

  .market__table td.wx__watch-cell .market__stack {
    gap: 4px;
    text-align: left;
  }

  .wx__strip {
    max-width: none;
  }
}

/*
 * Location info's "Weather town" (js/locations.js): the box and its Find button on one line, the
 * towns found under them as a row of buttons.
 */
.weather-town__find {
  display: flex;
  gap: 8px;
}

.weather-town__find input {
  flex: 1;
  min-width: 0;
}

.weather-town__results {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 8px;
}

/*
 * A row of its own in the edit form. The form wraps its fields, so Save and Cancel flowed in beside
 * the towns found and read as one more town to press. The box and Find keep a field's width.
 */
.theatre__head--editing .weather-town {
  flex-basis: 100%;
  max-width: none;
}

.theatre__head--editing .weather-town__find {
  max-width: 480px;
}

/* A location whose weather is not being read: its own line under the address, not a chip in the row. */
.theatre__weather {
  flex-basis: 100%;
  margin: 0;
}

/* Settings → Weather: each cinema whose weather cannot be read, with what to do about it. */
.weather-trouble {
  display: grid;
  gap: 8px;
  margin: 8px 0 0;
  padding: 0;
  list-style: none;
}

/* The dock picker: the places this account can keep, and the order they are kept in. */
.dockpick {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.dockpick__item {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 7px 12px;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--text-dim);
  font: inherit;
  font-size: 0.87rem;
  cursor: pointer;
}

.dockpick__item:disabled {
  opacity: 0.45;
  cursor: default;
}

.dockpick__item--on {
  border-color: color-mix(in srgb, var(--accent) 55%, var(--border-strong));
  background: color-mix(in srgb, var(--accent) 14%, var(--surface-2));
  color: var(--text);
}

/* The number is the order the dock will hold them in, which is the order they were
   chosen — the one piece of state a row of chips cannot show any other way. */
.dockpick__rank {
  display: none;
  min-width: 17px;
  height: 17px;
  border-radius: 999px;
  background: var(--accent);
  color: var(--on-accent, #0b0b10);
  font-size: 0.72rem;
  font-weight: 700;
  line-height: 17px;
  text-align: center;
}

.dockpick__item--on .dockpick__rank {
  display: inline-block;
}

/* ======================================================================
   Strips that scroll sideways say so at their edges.

   The case tabs, the settings rail and the scheduler's day strip scroll rather than wrap on a
   phone, with the scrollbar hidden — and a strip cut clean at the edge looked finished: the
   fifth tab and the fourth shelf were there, and nothing said so. js/util.js (watchOverflow)
   marks which side has more, and the mask fades that edge into the page. A strip that fits
   carries neither class and no fade, which is every desktop.
   ====================================================================== */
.is-more-right {
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 36px), transparent);
  mask-image: linear-gradient(to right, #000 calc(100% - 36px), transparent);
}

.is-more-left {
  -webkit-mask-image: linear-gradient(to right, transparent, #000 36px);
  mask-image: linear-gradient(to right, transparent, #000 36px);
}

.is-more-left.is-more-right {
  -webkit-mask-image: linear-gradient(to right, transparent, #000 36px, #000 calc(100% - 36px), transparent);
  mask-image: linear-gradient(to right, transparent, #000 36px, #000 calc(100% - 36px), transparent);
}

/* ------------------------------------------------ the programming page's method, folded */

/*
 * "How this is judged", one line above each list, with the paragraph behind it. The method is
 * the same on every visit and a booker reads it once; after that it was four lines between
 * every heading and its numbers, and ten screens of them on a phone.
 */
.panel__how {
  margin: 0 0 6px;
}

.panel__how-summary {
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
  user-select: none;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--text-dim);
}

.panel__how-summary::-webkit-details-marker {
  display: none;
}

.panel__how-summary::before {
  content: '';
  width: 6px;
  height: 6px;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(-45deg);
  transition: transform 160ms var(--ease);
}

.panel__how[open] > .panel__how-summary::before {
  transform: rotate(45deg);
}

.panel__how-summary:hover {
  color: var(--text);
}

.panel__how-summary:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
  border-radius: 4px;
}

.panel__how > .panel__note {
  margin-top: 8px;
}

/* --------------------------------------------- the scheduler as a view, on a phone */

/*
 * Pills that scroll with the page. `.showing` takes the touch for the drag; on a cinema the
 * POS owns, and on a phone that has not pressed Edit, there is no drag to take it for — and a
 * pill that swallowed the swipe was the reason the page could only be scrolled from the gaps
 * between rooms. See canEdit in js/showtimes.js.
 */
.grid--view .showing,
.grid--pos .showing {
  touch-action: auto;
  cursor: pointer;
}

/* Phone-only: the Edit button. */
.sched__edit {
  display: none;
}

@media (max-width: 700px) {
  .sched__edit {
    display: inline-flex;
  }

  .sched__edit--on {
    border-color: var(--accent);
    color: var(--accent);
  }

  /*
   * The case monitor's toolbar as one row: "Filters" rather than "Advanced filters", and the
   * two buttons pushed to the right, so Refresh stops sitting alone on a second line.
   */
  .filters__label-long {
    display: none;
  }

  /* What is left reads "filters"; the capital comes back with this. */
  .filters__label {
    text-transform: capitalize;
  }

  .filters--advanced > .filters__summary {
    flex-wrap: wrap;
  }

  .filters--advanced > .filters__summary > #raise {
    margin-left: auto;
  }
}

/* ============================================================ the consent screen

   The connector's one page for a person: drawn by a route rather than being a file in public/,
   because it has to name the assistant that is asking. It borrows nothing from the app shell —
   no top bar, no dock — since it is reached from outside the app and the reader has one
   question to answer. See src/consent.js. */

.consent {
  display: grid;
  place-items: center;
  min-height: 100vh;
  padding: 24px 16px;
  background: var(--bg);
}

.consent__card {
  width: 100%;
  max-width: 30rem;
  display: flex;
  flex-direction: column;
  gap: 18px;
  padding: 28px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--surface);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06), 0 18px 40px -28px rgba(0, 0, 0, 0.5);
}

.consent__title {
  margin: 0;
  font-size: 1.35rem;
  font-weight: 650;
  line-height: 1.3;
  text-wrap: balance;
}

/* The stranger's text, which is the one thing on this page nobody here wrote. Given weight
   rather than colour: it has to be READ, and colour would make it look like a link. */
.consent__client {
  font-weight: 750;
}

.consent__who,
.consent__note {
  margin: 0;
  color: var(--text-dim);
  font-size: 0.92rem;
  line-height: 1.55;
}

.consent__list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 9px;
  font-size: 0.94rem;
  line-height: 1.5;
}

.consent__list li {
  padding-left: 2px;
}

/* The way to add a connector again (reconnectPage), which is a sequence, so it is numbered. */
.consent__steps {
  margin: 0;
  padding-left: 1.3rem;
  display: flex;
  flex-direction: column;
  gap: 12px;
  font-size: 0.94rem;
  line-height: 1.5;
}

.consent__steps .connect-how__row {
  margin-top: 8px;
}

.consent__yes,
.consent__no {
  font-weight: 650;
}

.consent__yes {
  color: var(--accent);
}

/* The line that makes it safe to say yes, so it is the one that gets the warm colour. */
.consent__no {
  color: var(--p-peach, var(--text));
}

.consent__actions {
  display: flex;
  gap: 10px;
  margin: 2px 0 0;
}

.consent__button {
  flex: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 11px 16px;
  border-radius: 9px;
  border: 1px solid transparent;
  font: inherit;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
}

.consent__button--go {
  background: var(--accent);
  color: var(--accent-ink);
}

.consent__button--quiet {
  background: transparent;
  border-color: var(--border);
  color: var(--text);
}

.consent__button:disabled {
  opacity: 0.55;
  cursor: default;
}

.consent__error {
  margin: 0;
  font-size: 0.9rem;
  color: var(--p-peach, var(--text));
}

/* ------------------------------------------------------------------ the connector's address */

/*
 * The address an assistant is pointed at, beside the button that copies it.
 *
 * A read-only <input> rather than a <code> block, because the job here is "get this into the
 * clipboard or into a text box somewhere else" and an input is the one element every browser
 * already lets you select, drag and copy out of with no help from us. It wraps on a phone,
 * where the button below the field beats a field too narrow to read.
 */
.connect-how {
  display: grid;
  gap: 0.5rem;
  margin-bottom: 1rem;
}

.connect-how__row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  align-items: center;
}

.connect-how__row .input {
  flex: 1 1 18rem;
  min-width: 0;
  font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: 0.85rem;
}

/* ------------------------------------------------------------------ the film rating card */

/*
 * GOLD, and deliberately the gold everybody already knows.
 *
 * A five-star row is one of the few controls a phone needs no instructions for, and that is
 * worth more here than a palette that matches the rest of the app: the guest has seconds and no
 * patience, and a rating in the circuit's own accent colour would be a rating they have to stop
 * and interpret. Its own variable rather than a literal, so a theme that genuinely cannot carry
 * it has somewhere to say so.
 */
.rate,
.verdict,
.rated {
  --star-gold: #F2B01E;
  --star-warm: #FFD166;
  --star-off: color-mix(in srgb, var(--text) 20%, transparent);
}

/*
 * CENTRED IN WHAT IS LEFT of the screen rather than stacked at the top.
 *
 * The first draft sat under the header with two thirds of a phone empty beneath it, which reads
 * as a page that failed to finish loading. min-height is the viewport less the header, so the
 * card holds the middle on a tall phone and simply flows on a short one.
 */
.rate,
.verdict,
.rate-nope {
  display: grid;
  justify-items: center;
  align-content: center;
  min-height: min(60vh, 32rem);
  padding: 2rem 1.25rem 3rem;
  text-align: center;
}

/* --------------------------------------------------------- the arrival */

/*
 * One movement, once, and it is the whole animation budget for the page.
 *
 * The card rises as it appears and its parts follow a beat apart, so the eye is walked down to
 * the stars rather than dropped in front of a finished screen. Everything is at its resting
 * state by ~600ms — this is a rating card in a corridor, not a title sequence, and the second
 * viewing of anything clever here is worse than the first.
 */
@keyframes rate-rise {
  from { opacity: 0; transform: translateY(0.75rem); }
  to   { opacity: 1; transform: none; }
}

.rate > *,
.verdict > * {
  animation: rate-rise 420ms var(--ease-out, cubic-bezier(0.2, 0.8, 0.3, 1)) backwards;
}
.rate > :nth-child(1), .verdict > :nth-child(1) { animation-delay: 40ms; }
.rate > :nth-child(2), .verdict > :nth-child(2) { animation-delay: 90ms; }
.rate > :nth-child(3), .verdict > :nth-child(3) { animation-delay: 140ms; }
.rate > :nth-child(4), .verdict > :nth-child(4) { animation-delay: 190ms; }
.rate > :nth-child(5), .verdict > :nth-child(5) { animation-delay: 240ms; }
.rate > :nth-child(6), .verdict > :nth-child(6) { animation-delay: 290ms; }
.rate > :nth-child(7), .verdict > :nth-child(7) { animation-delay: 340ms; }

/* --------------------------------------------------------- the words */

.rate__label {
  margin: 0;
  font-size: 0.78rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-muted, var(--text));
}

/* The subject of the card, set big: the film on /film.html, the cinema on /venue.html. Two
   selectors rather than one shared name, because a class called rate__film on a page rating a
   building is the kind of thing somebody later "corrects" by changing the CSS. */
.rate__film,
.rate__venue {
  margin: 0.25rem 0 0;
  font-size: clamp(1.5rem, 7vw, 2.25rem);
  line-height: 1.1;
  text-wrap: balance;
}

.rate__showtime {
  margin: 0.35rem 0 0;
  font-size: 0.9rem;
  color: var(--text-muted, var(--text));
}

/* The question, given room and weight — it is what the stars are for. */
.rate__ask {
  margin: 2rem 0 0;
  font-size: 1.15rem;
  font-weight: 600;
  text-wrap: balance;
}

/* --------------------------------------------------------- the stars */

.stars {
  display: flex;
  gap: 0.15rem;
  padding: 0.75rem 0 0;
}

/*
 * Big targets. 44px is the smallest a thumb reliably hits, and this is used one-handed, in a
 * corridor, by somebody walking — so the glyph is larger than that and the button larger again.
 */
.star {
  appearance: none;
  background: none;
  border: 0;
  padding: 0.35rem;
  cursor: pointer;
  line-height: 0;
  border-radius: 10px;
  color: var(--star-off);
  transition: color 180ms ease, transform 180ms var(--ease-out, ease);
}

.star__glyph {
  width: 3rem;
  height: 3rem;
  fill: currentColor;
  /* A hairline of the same gold under the outline state, so an unfilled star still reads as a
     star rather than as a grey smudge on a dark theme. */
  stroke: color-mix(in srgb, var(--star-gold) 40%, transparent);
  stroke-width: 0.75;
  transition: filter 220ms ease;
}

.star--on {
  color: var(--star-gold);
}

/*
 * THE CASCADE. Choosing four lights four, and they light left to right a beat apart rather than
 * all at once — which is the difference between a control that responds and one that simply is
 * in a new state. The delay is per-star and set inline-free by nth-child, so it works whichever
 * star was pressed.
 */
.star--on:nth-child(1) { transition-delay: 0ms; }
.star--on:nth-child(2) { transition-delay: 45ms; }
.star--on:nth-child(3) { transition-delay: 90ms; }
.star--on:nth-child(4) { transition-delay: 135ms; }
.star--on:nth-child(5) { transition-delay: 180ms; }

/* The one that was actually pressed gets the pop, so a tap has a target and not just a result. */
@keyframes star-pop {
  0%   { transform: scale(1); }
  45%  { transform: scale(1.28); }
  100% { transform: scale(1); }
}
.star--hit { animation: star-pop 340ms var(--ease-out, cubic-bezier(0.2, 0.8, 0.3, 1)); }
.star--on .star__glyph { filter: drop-shadow(0 0 6px color-mix(in srgb, var(--star-warm) 45%, transparent)); }

.star:active { transform: scale(0.92); }
.star:focus-visible { outline: 2px solid var(--focus, var(--accent)); outline-offset: 3px; }

/* The word for the rating, swapped in rather than replaced, so it reads as an answer. */
@keyframes hint-in {
  from { opacity: 0; transform: translateY(0.25rem); }
  to   { opacity: 1; transform: none; }
}

.rate__hint {
  margin: 0.9rem 0 0;
  min-height: 1.5rem;
  font-size: 1.05rem;
  font-weight: 600;
  color: var(--text-muted, var(--text));
}
.rate__hint--said {
  color: var(--star-gold);
  animation: hint-in 260ms var(--ease-out, ease);
}

.rate__send {
  margin-top: 1.5rem;
  min-width: 11rem;
  transition: opacity 220ms ease, transform 220ms var(--ease-out, ease);
}
.rate__send[disabled] { opacity: 0.45; transform: scale(0.97); }

.rate__quiet {
  margin: 1.25rem 0 0;
  font-size: 0.8rem;
  color: var(--text-muted, var(--text));
  opacity: 0.75;
}

/* --------------------------------------------------------- after the tap */

/* text-wrap: balance because this is a sentence now rather than a word, and a phone breaks it
   over two lines — balanced, those two lines are even; unbalanced, the second is one word. */
.verdict__title { margin: 0; font-size: 1.6rem; line-height: 1.2; text-wrap: balance; }
.verdict__film,
.verdict__venue {
  margin: 0.3rem 0 1.75rem;
  color: var(--text-muted, var(--text));
  letter-spacing: 0.04em;
}

.verdict__pair {
  display: flex;
  gap: 2.75rem;
  align-items: flex-start;
  justify-content: center;
  flex-wrap: wrap;
}

.verdict__side { display: grid; gap: 0.2rem; }

.verdict__cap {
  margin: 0;
  font-size: 0.72rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-muted, var(--text));
}

.verdict__score {
  margin: 0;
  font-size: 1.9rem;
  font-weight: 700;
  color: var(--star-gold);
  font-variant-numeric: tabular-nums;
  line-height: 1.2;
}

.verdict__count { margin: 0; font-size: 0.78rem; color: var(--text-muted, var(--text)); }
.verdict__note { margin: 1.75rem 0 0; font-size: 1rem; text-wrap: balance; }

.rate-nope__text { margin: 0 auto; max-width: 28rem; color: var(--text-muted, var(--text)); }

/*
 * THE PAGE A RATINGS EMAIL'S LINKS OPEN (rated-email.html): one sentence and one button, then what
 * became of it. As narrow as the rating card, because it is opened from an email on a phone.
 */
.rated-link {
  display: grid;
  gap: 1rem;
  width: 100%;
  max-width: 26rem;
  margin: 0 auto;
  padding: 2rem 1.25rem 3rem;
  text-align: center;
}

.rated-link:focus {
  outline: none;
}

.rated-link__text {
  margin: 0;
  font-size: 1.02rem;
  line-height: 1.55;
  text-wrap: balance;
}

/* The answer, once the button has done its work: what is true now, set a little heavier than the question. */
.rated-link__text--done {
  font-weight: 600;
}

/* The other answer to the yearly question, quieter than the button above it. */
.rated-link__other {
  justify-self: center;
  font-size: 0.92rem;
  color: var(--text-muted, var(--text));
}

/* ------------------------------------------- what people here are rating highly */

/*
 * THEIR NEXT FILM, on the thank-you screen: a short list under the comparison, quieter than
 * the score above it and louder than the note — the one thing on the screen a guest can act on.
 * The same rows the public page draws (see rated.html), so the two cannot say one fact in two
 * shapes.
 */
.also {
  display: grid;
  gap: 0.6rem;
  justify-items: center;
  width: 100%;
  max-width: 24rem;
  margin-top: 1.75rem;
}

.also__cap {
  margin: 0;
  font-size: 0.72rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-muted, var(--text));
}

/* The one press on the thank-you screen that goes somewhere: a button, full width like Send above it. */
.also__more {
  width: 100%;
  margin-top: 0.35rem;
  text-decoration: none;
}

/*
 * COMMUNITY RATINGS BY EMAIL, under the films it is about (see film.html and src/rated-email.js):
 * the report confirmation's own box — the field, the button beside it, the line under it — as wide
 * as the list above it, and set left to right, because a sentence about what will happen to an
 * address is read rather than glanced at.
 */
.rated-email {
  width: 100%;
  max-width: 24rem;
  margin-top: 1.75rem;
  text-align: left;
}

/* Level with the field, as on the report's confirmation: the button's own rule puts space above it. */
.rated-email .thanks__email-row .btn {
  flex: 0 0 auto;
  margin-top: 0;
}

/* What became of the address, where the box was: no wider than the box. */
.verdict > .thanks__email-done {
  max-width: 24rem;
}

/* A problem, said inside the box under the field: the box's own gap is the space above it. */
.rated-email > .thanks__email-done {
  margin: 0;
}

.rated-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.5rem;
  width: 100%;
}

/* Film and score on one line, the count under them: what the score rests on, never hidden behind it. */
.rated-list__film {
  display: grid;
  grid-template-columns: 1fr auto;
  column-gap: 0.75rem;
  row-gap: 0.1rem;
  align-items: baseline;
  padding: 0.6rem 0.85rem;
  border-radius: var(--radius);
  background: color-mix(in oklab, var(--text) 6%, transparent);
  text-align: left;
}

/*
 * RANKED: the place, large and faint, down the left — best first is the whole message of the
 * list, and a numeral says it without a sentence. A CSS counter, so the same rows rank
 * themselves on the thank-you screen and on the page, and nothing in the script knows.
 */
.rated-list--ranked {
  counter-reset: rated;
}

.rated-list--ranked .rated-list__film {
  grid-template-columns: auto 1fr auto;
  counter-increment: rated;
}

.rated-list--ranked .rated-list__film::before {
  content: counter(rated);
  grid-row: 1 / 3;
  align-self: center;
  min-width: 1.4rem;
  font-size: 1.35rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: color-mix(in oklab, var(--text) 38%, transparent);
}

.rated-list--ranked .rated-list__count {
  grid-column: 2 / -1;
}

.rated-list__name {
  font-weight: 600;
  overflow-wrap: anywhere;
}

.rated-list__score {
  font-weight: 700;
  color: var(--star-gold);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.rated-list__count {
  grid-column: 1 / -1;
  font-size: 0.78rem;
  color: var(--text-muted, var(--text));
}

/* The public page: the list at reading width, one small line of rules above it, the method under it. */
.rated {
  display: grid;
  justify-items: center;
  align-content: start;
  gap: 0.9rem;
  padding: 1.5rem 1.25rem 3rem;
}

.rated .rated-list {
  max-width: 28rem;
}

.rated__eyebrow {
  margin: 0;
  font-size: 0.72rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  text-align: center;
  color: var(--text-muted, var(--text));
}

.rated__foot {
  margin: 0.4rem 0 0;
  max-width: 28rem;
  text-align: center;
  font-size: 0.82rem;
  color: var(--text-faint, var(--text-muted, var(--text)));
}

/*
 * ON A DESKTOP the page is a link on the cinema's own site, not a placard at a door, and the
 * operator asked for it to look designed at that width too. The phone column floating in a
 * wide dark field did not. So past a laptop's width: the band grows with a bigger heading, the
 * list widens to reading width, the top film becomes a featured card across the whole width
 * and the rest sit in two columns under it — with an odd last one stretched across, so the
 * grid never ends on a lone half-width card. Nothing in the script knows any of this.
 */
@media (min-width: 900px) {
  .guest-hero--rated {
    padding: 44px 20px 40px;
  }

  .guest-hero--rated .guest-hero__inner {
    max-width: 60rem;
  }

  .guest-hero--rated h1 {
    margin: 16px 0 14px;
    font-size: 2.4rem;
  }

  .guest-hero--rated .guest-hero__where {
    font-size: 1rem;
    padding: 8px 16px 8px 12px;
  }

  .rated {
    gap: 1.25rem;
    padding: 2.25rem 2rem 4rem;
  }

  .rated .rated-list {
    max-width: 60rem;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.9rem;
  }

  .rated .rated-list__film {
    padding: 1.1rem 1.3rem;
    border: 1px solid var(--border);
  }

  .rated .rated-list__name {
    font-size: 1.12rem;
  }

  .rated .rated-list__score {
    font-size: 1.25rem;
  }

  .rated .rated-list--ranked .rated-list__film::before {
    font-size: 1.6rem;
    min-width: 1.8rem;
  }

  /* The featured one. */
  .rated .rated-list__film:first-child {
    grid-column: 1 / -1;
    padding: 1.6rem 1.8rem;
    column-gap: 1.1rem;
  }

  .rated .rated-list__film:first-child .rated-list__name {
    font-size: 1.7rem;
    letter-spacing: -0.01em;
  }

  .rated .rated-list__film:first-child .rated-list__score {
    font-size: 2rem;
  }

  .rated .rated-list__film:first-child .rated-list__count {
    font-size: 0.92rem;
  }

  .rated .rated-list--ranked .rated-list__film:first-child::before {
    font-size: 2.6rem;
    min-width: 2.6rem;
  }

  /* An odd last card takes the whole row rather than sitting alone on the left. */
  .rated .rated-list__film:last-child:nth-child(even) {
    grid-column: 1 / -1;
  }

  .rated__eyebrow {
    font-size: 0.8rem;
  }

  .rated__foot {
    font-size: 0.9rem;
  }
}

/*
 * Everything above is decoration on a page whose whole job is one tap, so all of it goes when
 * somebody has asked for less movement — and what is left is the page at its resting state,
 * which is the state every one of these animations ends in anyway.
 */
@media (prefers-reduced-motion: reduce) {
  .rate > *,
  .verdict > *,
  .star,
  .star--hit,
  .rate__hint--said,
  .rate__send {
    animation: none;
    transition: none;
  }
}

/* ------------------------------------------------------------------ the recommend row */

/*
 * Nought to ten, as eleven targets.
 *
 * A slider was the obvious alternative and is worse in every way that matters on a phone held
 * one-handed in a corridor: it needs a drag rather than a tap, it needs aiming, and it leaves
 * the reader unsure which number they landed on. Eleven buttons wrap onto two rows on a narrow
 * screen, each is one tap, and the one chosen is unmistakable.
 */
.nps {
  display: grid;
  justify-items: center;
  gap: 0.6rem;
  width: 100%;
  max-width: 26rem;
  margin-top: 2rem;
}

/* A separating rule when it follows the stars, so the card reads as two questions and not one
   long one. Drawn on the block rather than between them, so a card asking only this has none. */
.asked:not([hidden]) + .nps {
  padding-top: 1.75rem;
  border-top: 1px solid var(--border, rgba(128, 128, 128, 0.25));
}

.rate__ask--second { margin-top: 0; }

/* And when the recommend row is the ONLY question — the visit card — the block's own top margin
   is the whole of the gap. Without this the ask carries both and sits four rems below the name it
   is asking about, which reads as two unrelated halves of a page rather than one question. */
.nps > .rate__ask:first-child { margin-top: 0; }

/*
 * ELEVEN ACROSS, ONE ROW, whatever the screen.
 *
 * They share the width rather than each claiming a minimum: `flex: 1 1 0` with `min-width: 0`
 * makes every button exactly one eleventh of what is there, so the scale reads as a scale — a
 * line from "not at all" to "extremely" — instead of as seven buttons and then four more, which
 * is what wrapping made of it.
 *
 * THE COST, because it is real: eleven across a narrow phone is about 30px each, under the 44px
 * a thumb reliably hits, and on an eleven-point scale a mis-tap can cross the band between a
 * passive and a promoter. Two mitigations rather than a shrug — they stay a full 44px TALL, so
 * only the horizontal axis is tight, and the gap is kept visible so the boundaries are legible
 * rather than a single grey bar to aim into.
 */
.nps__row {
  display: flex;
  flex-wrap: nowrap;
  justify-content: center;
  gap: 0.25rem;
  width: 100%;
}

.nps__n {
  appearance: none;
  flex: 1 1 0;
  min-width: 0;
  min-height: 2.75rem;
  padding: 0 0.1rem;
  border-radius: 8px;
  border: 1px solid var(--border, rgba(128, 128, 128, 0.3));
  background: transparent;
  color: var(--text);
  font: 600 0.95rem/1 var(--font, inherit);
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  transition: background 160ms ease, border-color 160ms ease, color 160ms ease, transform 160ms var(--ease-out, ease);
}

.nps__n:focus-visible { outline: 2px solid var(--focus, var(--accent)); outline-offset: 2px; }
.nps__n:active { transform: scale(0.94); }

/*
 * The chosen one in the same gold as the stars, because they are two answers to one card and
 * two accent colours would read as two unrelated controls.
 */
.nps__n--on {
  background: var(--star-gold, #F2B01E);
  border-color: var(--star-gold, #F2B01E);
  color: #1a1400;
}

@keyframes nps-pop {
  0%   { transform: scale(1); }
  45%  { transform: scale(1.18); }
  100% { transform: scale(1); }
}
.nps__n--hit { animation: nps-pop 300ms var(--ease-out, cubic-bezier(0.2, 0.8, 0.3, 1)); }

/* The anchors, because a bare 0..10 does not say which end is good. */
.nps__ends {
  display: flex;
  justify-content: space-between;
  width: 100%;
  font-size: 0.75rem;
  color: var(--text-muted, var(--text));
  opacity: 0.8;
}

@media (prefers-reduced-motion: reduce) {
  .nps__n,
  .nps__n--hit { animation: none; transition: none; }
}

/* =========================================================================
   GUEST SENTIMENT — what people said on the way out.

   Two cards: the building's own net promoter score, and a table of films.
   The visit sits first because it is the number an owner asks for by name,
   and because it is about what the reader can change this week.
   ========================================================================= */

.sentiment {
  display: grid;
  gap: 16px;
}

.sentiment__h {
  margin: 0 0 10px;
  font-size: 1rem;
  letter-spacing: 0.02em;
}

/* The visit's headline number, set at the size of a finding. */
.sentiment__big {
  margin: 0;
  font-size: clamp(2.4rem, 9vw, 3.4rem);
  font-weight: 700;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

/* Tone is applied only where a threshold is defensible: +50 is the number the
   industry treats as strong, and below zero means more detractors than
   promoters, which is arithmetic rather than opinion. Everything between is
   left uncoloured rather than given a verdict nobody agreed to. */
.score--good { color: var(--ok, #3fb27f); }
.score--bad { color: var(--danger, #d9534f); }

.sentiment__cap {
  margin: 6px 0 0;
  font-size: 0.78rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-muted, var(--text));
}

.sentiment__count {
  margin: 2px 0 0;
  font-size: 0.82rem;
  color: var(--text-muted, var(--text));
}

/* The bands under the headline: the shape, not just the score. */
.sentiment__bands {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 12px 0 0;
}

.band {
  padding: 2px 9px;
  border: 1px solid var(--border, rgba(128, 128, 128, 0.3));
  border-radius: 999px;
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
  color: var(--text-muted, var(--text));
}

.band--pro { border-color: color-mix(in srgb, var(--ok, #3fb27f) 55%, transparent); }
.band--det { border-color: color-mix(in srgb, var(--danger, #d9534f) 55%, transparent); }

/* Dashed, because a passive is the one band that is IN the sample and OUT of the score: 7s and
   8s are counted in the denominator and contribute nothing to the numerator. A solid outline
   like its neighbours would say it pulls the number one way or the other, and it does not. */
.band--pas { border-style: dashed; }

/* Why a number is missing, said where the number would have been. */
.sentiment__none {
  margin: 0;
  font-size: 0.9rem;
  color: var(--text-muted, var(--text));
  max-width: 56ch;
}

.sentiment__why { display: block; margin-top: 4px; font-size: 0.82rem; }

/* The standing caveat above the film table — that a score is not a sale. */
.sentiment__lead {
  margin: 0 0 14px;
  font-size: 0.88rem;
  color: var(--text-muted, var(--text));
  max-width: 70ch;
}

.sentiment__lead strong { color: var(--text); font-weight: 600; }

/* Wide content scrolls inside its own box; the page never scrolls sideways. */
.tablewrap { overflow-x: auto; }

.sentiment__table { margin-top: 2px; }
.sentiment__table th[scope='col'] {
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted, var(--text));
  white-space: nowrap;
}

.sentiment__film { font-weight: 600; }

/* A score and the count it rests on, always together: a number without its
   sample is the thing this whole feature exists to avoid printing. */
.score {
  font-size: 1.05rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.score--stars { color: var(--star-gold, var(--text)); }
.score--none { color: var(--text-muted, var(--text)); font-weight: 400; }

.sentiment__n {
  margin-left: 8px;
  font-size: 0.76rem;
  color: var(--text-muted, var(--text));
  font-variant-numeric: tabular-nums;
}

.sentiment__foot {
  margin: 12px 0 0;
  font-size: 0.8rem;
  color: var(--text-muted, var(--text));
  max-width: 70ch;
}

/* ------------------------------------------------------------------ invoices */

/*
 * THE ENTRY SCREEN, laid out around one fact: somebody is standing there holding a bill.
 *
 * The form is above the list on this page and below it on every other, which is deliberate —
 * see the note at the top of js/invoices.js. Everything here is spent on making the filing
 * quick, because a customer who asked for this feature and then stops using it has not decided
 * invoices are useless, they have decided the form is tiring.
 */
.invoice-add {
  margin-bottom: 18px;
}

/*
 * The document, first and biggest, as a label wrapping a hidden input — so the whole dashed
 * area is one tap target on a phone and one drop target on a desktop, without a second control
 * for the second gesture.
 */
.invoice-drop {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 26px 18px;
  border: 2px dashed var(--border-strong);
  border-radius: var(--radius-lg);
  background: var(--surface-2, transparent);
  cursor: pointer;
  text-align: center;
  transition: border-color 120ms ease, background 120ms ease;
}

.invoice-drop:hover,
.invoice-drop:focus-within,
.invoice-drop--over {
  border-color: var(--accent);
  background: var(--accent-soft, var(--glass));
}

/* Once a file is chosen the border stops asking for one — solid, and the accent goes to the name. */
.invoice-drop--has {
  border-style: solid;
  border-color: var(--accent);
}

.invoice-drop__icon {
  color: var(--text-dim);
  line-height: 0;
}

.invoice-drop--has .invoice-drop__icon {
  color: var(--accent);
}

.invoice-drop__text {
  font-weight: 600;
  /* A long filename wraps rather than pushing the card sideways on a phone. */
  overflow-wrap: anywhere;
  max-width: 100%;
}

.invoice-drop__hint {
  font-size: 0.82rem;
  color: var(--text-dim);
}

/*
 * Two columns where there is room and one where there is not. auto-fit rather than a media
 * query, so the same rule serves a phone, a split window and a desk monitor — and 15rem is
 * chosen as the width at which a date input stops truncating its own placeholder.
 */
.invoice-form__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: 0 18px;
  margin-top: 18px;
}

.invoice-form__grid .field {
  margin-bottom: 14px;
}

/* The currency, said once beside the amount rather than repeated down the list. */
.field__unit {
  font-weight: 400;
  color: var(--text-dim);
}

.field__optional {
  font-weight: 400;
  font-size: 0.82rem;
  color: var(--text-dim);
}

.invoice-form__acts {
  display: flex;
  justify-content: flex-end;
}

.invoice-form__note {
  color: var(--danger);
  font-weight: 600;
  margin: 0 0 12px;
}

/*
 * The duplicate answer. Warned rather than refused, and it carries two buttons because only a
 * person can tell a second visit from the same contractor from the same document twice.
 */
.invoice-dup {
  border: 1px solid var(--warn, var(--border-strong));
  border-radius: var(--radius-md, 10px);
  padding: 14px 16px;
  margin-bottom: 14px;
}

.invoice-dup__text {
  margin: 0 0 12px;
}

.invoice-dup__acts {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  justify-content: flex-end;
}

/* ------------------------------------------------------------- the list side */

.invoice-filters__row {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  align-items: center;
  margin: 12px 0 4px;
}

.invoice-filters__row .input {
  width: auto;
  flex: 1 1 9rem;
  min-width: 0;
}

/*
 * The downloads sit under the filters they carry rather than beside them, so the row of controls
 * somebody is adjusting stays one row of controls. Pushed right, because they are the end of that
 * sentence: narrow it, then take it away.
 */
.invoice-filters__row--exports {
  justify-content: flex-end;
  margin: 0 0 4px;
}

/*
 * What the model read off the document.
 *
 * Quiet by default and deliberately so: it is a note about how the boxes came to be filled, not
 * a result. The warn state is louder because it is only ever used for something that would
 * otherwise be silently wrong — a currency that is not the one this circuit files in.
 */
.invoice-read {
  grid-column: 1 / -1;
  margin: 0;
  font-size: 0.85rem;
  line-height: 1.5;
  color: var(--text-dim);
}

.invoice-read--busy {
  font-style: italic;
}

.invoice-read--warn {
  color: var(--warn);
  font-weight: 600;
}

/*
 * The total, in the same tabular figures the rows use so the digits line up when somebody
 * scrolls past a column of them.
 */
.invoice-total {
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  margin: 10px 0 14px;
}

.invoice-list {
  display: flex;
  flex-direction: column;
}

.invoice-row {
  display: flex;
  align-items: baseline;
  gap: 14px;
  padding: 11px 0;
  border-top: 1px solid var(--border);
}

.invoice-row__main {
  display: flex;
  flex-direction: column;
  gap: 2px;
  /* Takes the slack so the money and the link stay put down the column. */
  flex: 1 1 auto;
  min-width: 0;
}

.invoice-row__supplier {
  font-weight: 600;
  overflow-wrap: anywhere;
}

.invoice-row__meta {
  font-size: 0.84rem;
  color: var(--text-dim);
}

/* tabular-nums so a column of amounts lines up on the decimal point rather than drifting. */
.invoice-row__money {
  font-variant-numeric: tabular-nums;
  font-weight: 600;
  white-space: nowrap;
}

.invoice-row__open {
  white-space: nowrap;
}

/* A row whose document has gone says so, rather than offering a link that answers 404. */
.invoice-row__gone {
  font-size: 0.84rem;
  color: var(--text-dim);
  white-space: nowrap;
}

.invoice-empty {
  color: var(--text-dim);
  margin: 14px 0 0;
}

/*
 * The line that has to be on the page rather than only in the terms: a backup carries these
 * figures and not these documents, so the originals are the customer's to keep.
 */
.invoice-keep {
  margin: 22px 0 0;
  font-size: 0.84rem;
  color: var(--text-dim);
  max-width: 70ch;
}

@media (max-width: 40rem) {
  /*
   * The location gets its own row and the two dates share the next one. Left to wrap on their
   * own the three controls stranded one date beside Clear, which reads as though the second
   * date belongs to the button.
   */
  .invoice-filters__row select.input {
    flex: 1 1 100%;
  }

  .invoice-filters__row input.input {
    flex: 1 1 8rem;
  }

  /* On a phone the money drops under the supplier rather than squeezing the name to nothing. */
  .invoice-row {
    flex-wrap: wrap;
    gap: 4px 14px;
  }

  .invoice-row__main {
    flex: 1 0 100%;
  }
}

/*
 * ============================================================ THE ROTA
 *
 * Seven day columns that scroll sideways rather than reflowing, and that is the deliberate
 * part. A week is a shape a manager reads across — Friday is heavy BECAUSE Thursday is not —
 * and a grid that stacks to a list on a phone throws the comparison away exactly where it is
 * most wanted, which is standing in a foyer. So the columns keep their width and the strip
 * scrolls, inside its own container so the page body never moves.
 */
/* Draft and Publish, over the grid — inside the rota's own card, since they are about it. */
.rota__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin: 0 0 14px;
}

/* The week's two buttons and its name, in the range bar beside the cinema picker; the
   arrange buttons after it are pushed to the far end (`margin-inline-end: auto`). */
.rota__weeknav {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-inline-end: auto;
  /*
   * A flex item will not shrink below its content by default, and three unbreakable things in
   * a row — two buttons and a date span — held this bar wider than a phone, which pushed the
   * whole PAGE sideways. The grid below is allowed to scroll in its own container; the page
   * body is not.
   */
  min-width: 0;
}

.rota__weekname {
  font-weight: 600;
}

/* The words on a computer: the arrows are the phone's (below). */
.rota__arrow {
  display: none;
}

/*
 * THE WEEK ON A PHONE: a line of its own under the cinema picker, the week's name between two
 * round arrows — the scheduler's week, and for its reason.
 *
 * Written out, "Previous week", the name and "Next week" measure 387px. Under that the three
 * wrapped, and "Next week" took a row to itself; on a 440px phone they fitted BESIDE the picker,
 * whose basis is zero (see .rangebar__where), so it was squeezed to nine pixels and drawn under
 * the week's name. A line of its own settles the picker; the arrows let the week hold one row at
 * every phone width.
 *
 * The words stay in the buttons, out of sight but not out of the page, so each is still "Previous
 * week" and "Next week" to a screen reader. The order is put back to the written one: the range
 * bar sends its buttons to the end on a phone (`.rangebar .btn`), which here would stand both
 * arrows after the name.
 */
@media (max-width: 560px) {
  .rota__weeknav {
    flex: 1 0 100%;
    flex-wrap: nowrap;
    justify-content: space-between;
  }

  .rota__weeknav .rota__step {
    flex: none;
    order: 0;
    width: 38px;
    height: 38px;
    min-height: 38px;
    padding: 0;
    justify-content: center;
    border-radius: 50%;
  }

  .rota__arrow {
    display: block;
    flex: none;
  }

  .rota__steptext {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }

  .rota__weekname {
    flex: 1 1 auto;
    min-width: 0;
    text-align: center;
  }
}

.rota__status:empty {
  display: none;
}

.rota__scroll {
  overflow-x: auto;
  /* Room for the focus ring on the buttons at the edges, which a tight box clips. */
  padding: 3px;
  margin: -3px;
}

.rota__head {
  padding: 8px;
  border-radius: 8px;
  background: var(--p-mantle, var(--surface-2));
  border: 1px solid var(--p-hairline, var(--border));
}

.rota__head p {
  margin: 0;
  line-height: 1.35;
}

.rota__date {
  font-weight: 600;
}

.rota__crowd {
  font-size: 0.78rem;
  font-variant-numeric: tabular-nums;
}

.rota__crowd--figure {
  color: var(--p-text, var(--text));
  font-weight: 600;
}

.rota__crowd--peak,
.rota__crowd--quiet {
  color: var(--text-dim);
}

.rota__crowd--up {
  color: var(--p-green, var(--text-dim));
}

.rota__crowd--down {
  color: var(--p-peach, var(--text-dim));
}

/*
 * THE COVER LINE. Given its own rule and its own weight because it is the sentence the whole
 * strip above it exists to produce — the crowd is only worth the space because this sits
 * under it. A peak with nobody on it is the loudest thing on the page on purpose: it is the
 * one state that is still cheap to fix while the week is a draft.
 */
.rota__cover {
  margin-top: 4px !important;
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--p-text, var(--text));
}

.rota__cover--hole {
  color: var(--p-red, var(--danger));
}

/*
 * NOBODY IN CHARGE, said in the colour that means act on it.
 *
 * Amber and not red, and the distinction is the fix rather than the severity. A hole in cover
 * is red because the shift cannot run; this one CAN run — it is staffed — and what it needs is
 * one specific phone call. Red for both would teach a manager to read past the colour on the
 * days that are merely awkward, which is exactly when they would miss one that is not.
 *
 * Its OWN class rather than a modifier of .rota__cover, which is what it started as. Reusing
 * that class to inherit four lines of type made every `.rota__cover` selector on the page
 * ambiguous — a test asking a column for its cover line got two elements and failed. Sharing
 * a class is sharing an identity, and these are two different findings about two different
 * windows.
 */
.rota__unled {
  margin-top: 4px !important;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: 0.8rem;
  font-weight: 600;
  text-align: start;
  cursor: pointer;
  color: var(--p-yellow, var(--accent));
}

/*
 * Claimed. Quiet rather than green, and that is the point of the whole mechanism: a manager
 * covering a night is the ordinary state of a small building, not an achievement to be
 * congratulated on. It reads as settled so that the ones still amber stand out — which they
 * cannot do if every day in the row is shouting something.
 */
.rota__unled--covered {
  color: var(--text-dim);
  font-weight: 500;
}

/*
 * BY HOW MANY HOURS, on the shift that took them past it. Red where the draft flag beside it
 * is grey, because a draft is a proposal and this is a number with a law behind it.
 */
.rota__chipover {
  display: block;
  margin-top: 2px;
  font-size: 0.7rem;
  font-weight: 700;
  color: var(--p-red, var(--danger));
  font-variant-numeric: tabular-nums;
}

/* The person's week against their ceiling, under their name in the row head. */
.rota__whoname {
  display: block;
}

.rota__whohours {
  display: block;
  margin-top: 2px;
  font-size: 0.72rem;
  font-weight: 500;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}

.rota__whohours--over {
  color: var(--p-red, var(--danger));
  font-weight: 700;
}

/*
 * "(15h elsewhere)". Dim and lighter than the total it hangs off, including inside an OVER
 * badge — the red is about the ceiling, and where the hours were worked is context rather
 * than a second alarm.
 */
.rota__whoaway {
  color: var(--text-dim);
  font-weight: 400;
}

.rota__tally {
  font-size: 0.75rem;
  color: var(--text-dim);
}

.rota__flag,
.rota__why {
  font-size: 0.72rem;
  color: var(--text-dim);
}

/* A draft is dashed rather than greyed: greyed reads as disabled, and a draft is the one
   thing on this grid the manager can still freely change. */
.rota__times {
  display: flex;
  gap: 12px;
}

.rota__times .field {
  flex: 1;
}

.rota__despite {
  padding: 8px;
  border-radius: 8px;
  border: 1px solid var(--p-yellow, var(--accent));
}

.rota__refusal {
  margin: 0 0 6px;
  font-size: 0.85rem;
}

.rota__seen {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 8px;
  margin: 0;
  padding: 6px 0;
  border-bottom: 1px solid var(--p-hairline, var(--border));
}

.rota__seen:last-child {
  border-bottom: 0;
}

.rota__seenwho {
  font-weight: 600;
}

.rota__seenstate {
  font-size: 0.85rem;
  color: var(--text-dim);
}

/* Not-seen is the actionable state, so it is the one that is coloured. Seen is quiet on
   purpose — a page where everything is highlighted highlights nothing. */
.rota__seen--stale .rota__seenstate,
.rota__seen--never .rota__seenstate {
  color: var(--p-peach, var(--text-dim));
}

/* ============================================================ the crew's own page */

.myrota__title {
  margin-bottom: 2px;
}

.myrota__span,
.myrota__keep {
  color: var(--text-dim);
  font-size: 0.85rem;
}

.myrota__shift {
  padding: 10px 0;
  border-bottom: 1px solid var(--p-hairline, var(--border));
}

.myrota__shift p {
  margin: 0;
}

.myrota__day {
  font-weight: 600;
}

.myrota__time {
  font-variant-numeric: tabular-nums;
}

.myrota__note {
  font-size: 0.85rem;
  color: var(--text-dim);
}

.myrota__shift--dropped .myrota__day,
.myrota__shift--dropped .myrota__time {
  text-decoration: line-through;
  color: var(--text-dim);
}

.myrota__flag {
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--p-peach, var(--text-dim));
}

.myrota__shift--dropped .myrota__flag {
  color: var(--p-red, var(--danger));
}

.myrota__gone,
.myrota__empty {
  color: var(--text-dim);
}

.myrota__cal {
  margin-top: 16px;
}

/* Getting back in. The page a spent link lands on, so it carries the weight the shift list
   normally does — a heading, not a footnote. */
.myrota__back {
  margin-top: 20px;
  padding: 14px 16px;
  border: 1px solid var(--p-hairline, var(--border));
  border-radius: 10px;
}

.myrota__backtitle {
  margin: 0 0 8px;
  font-size: 1.05rem;
}

.myrota__backsay {
  margin: 0 0 14px;
  font-size: 0.88rem;
  color: var(--text-dim);
}

.myrota__backform .field {
  margin: 0 0 12px;
}

.myrota__backerr {
  margin: 12px 0 0;
  padding: 8px 10px;
  border-radius: 8px;
  border: 1px solid var(--p-red, var(--danger));
  color: var(--p-red, var(--danger));
  font-size: 0.85rem;
}

/* The home-screen offer. A strip rather than a dialog: it is an offer, and a modal would
   stand between somebody and the shifts they opened the page to read. */
.myrota__install {
  margin-top: 18px;
  padding: 12px 14px;
  border: 1px solid var(--p-hairline, var(--border));
  border-radius: 10px;
  background: var(--p-mantle, var(--surface-2));
}

.myrota__installsay {
  margin: 0 0 10px;
  font-size: 0.88rem;
}

.myrota__installdo {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin: 0;
}

/* Whose page this is, and the way off it. Quiet — it is reassurance on your own phone and
   only becomes the important line on a shared one. */
.myrota__whoami {
  margin-top: 18px;
  font-size: 0.85rem;
  color: var(--text-dim);
}

.myrota__calout {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 10px;
  margin-top: 10px;
}

.myrota__calnote {
  margin: 0;
  font-size: 0.85rem;
  color: var(--text-dim);
}

/*
 * THE WEEK AS A MATRIX — people down, days across.
 *
 * The stacked layout this replaces measured 1736px on a desktop and 3391px on a phone for one
 * real week (twenty crew, seventy-nine shifts). It grew with the busiest DAY, which nobody
 * controls. This grows with HEADCOUNT, which an operator does.
 *
 * A real table rather than a grid of divs, because it is a matrix and the semantics are free:
 * a screen reader announcing "Maya, Saturday, 5–1" is the whole page working, and no amount of
 * aria on divs is as reliable as a th with a scope on it.
 */
.rota__table {
  border-collapse: separate;
  border-spacing: 0;
  width: 100%;
  font-variant-numeric: tabular-nums;
}

/*
 * The name column stays put while the week scrolls under it. Without this a manager scrolled
 * to Sunday and lost track of whose row they were reading, which is the one thing this layout
 * exists to make easy.
 */
.rota__who,
.rota__corner {
  position: sticky;
  inset-inline-start: 0;
  z-index: 2;
  min-width: 88px;
  padding: 4px 8px 4px 0;
  background: var(--p-base, var(--bg));
  text-align: start;
  font-weight: 600;
  white-space: nowrap;
}

.rota__corner {
  z-index: 3;
  color: var(--text-dim);
  font-size: 0.78rem;
}

.rota__head {
  /*
   * Wide enough for "10:30am–12:00am", which is the longest a shift gets, and narrow enough
   * that seven of them plus the name column clear a desktop card. Both halves matter: a full
   * time that wraps is no clearer than a short one, and a week whose Sunday is cut off is a
   * week you cannot read in one go.
   */
  min-width: 144px;
  padding: 8px;
  border: 1px solid var(--p-hairline, var(--border));
  border-radius: 8px 8px 0 0;
  background: var(--p-mantle, var(--surface-2));
  text-align: start;
  vertical-align: top;
  font-weight: 400;
}

.rota__cell {
  padding: 2px;
  border-inline-end: 1px solid var(--p-hairline, var(--border));
  border-bottom: 1px solid var(--p-hairline, var(--border));
  vertical-align: top;
}

.rota__cell:first-of-type {
  border-inline-start: 1px solid var(--p-hairline, var(--border));
}

.rota__chip {
  display: flex;
  flex-direction: column;
  gap: 0;
  width: 100%;
  padding: 3px 6px;
  border: 0;
  border-inline-start: 3px solid transparent;
  border-radius: 5px;
  background: var(--p-surface1, var(--surface));
  color: inherit;
  font: inherit;
  font-size: 0.82rem;
  text-align: start;
  cursor: pointer;
}

.rota__chip + .rota__chip {
  margin-top: 2px;
}

/*
 * MORNING AND NIGHT, told apart by the edge rather than by reading the time. "Who is on
 * Saturday night" was eighteen cards to read in the stacked layout; here it is a colour down
 * one column. The boundary is a stated 3pm, not a clever guess from the schedule — a rule a
 * manager can predict beats one that is usually right.
 */
.rota__chip--morning {
  border-inline-start-color: var(--p-teal, var(--accent));
}

.rota__chip--night {
  border-inline-start-color: var(--p-lavender, var(--accent));
}

.rota__chip--draft {
  border-block: 1px dashed var(--p-hairline, var(--border));
  border-inline-end: 1px dashed var(--p-hairline, var(--border));
}

.rota__chip--dropped .rota__chiptime {
  text-decoration: line-through;
}

.rota__chip--despite {
  border-inline-start-color: var(--p-yellow, var(--accent));
}

.rota__chiptime {
  font-weight: 600;
}

.rota__chipnote,
.rota__chipflag {
  font-size: 0.7rem;
  color: var(--text-dim);
}

.rota__chip--dropped .rota__chipflag {
  color: var(--p-red, var(--danger));
}

/*
 * The empty cell IS the add button — the place you would point at is the place you press.
 * Nearly invisible until the row is hovered or the button is focused, because a hundred and
 * forty plus signs would be the loudest thing on a page whose subject is the shifts.
 */
.rota__put {
  width: 100%;
  padding: 3px 6px;
  border: 0;
  border-radius: 5px;
  background: none;
  color: transparent;
  font: inherit;
  font-size: 0.82rem;
  cursor: pointer;
}

.rota__row:hover .rota__put,
.rota__put:focus-visible {
  color: var(--text-dim);
  background: var(--p-mantle, var(--surface-2));
}

.rota__divide th {
  padding: 12px 0 4px;
  color: var(--text-dim);
  font-size: 0.78rem;
  font-weight: 400;
}

.rota__row:hover .rota__who {
  color: var(--p-blue, var(--accent));
}

/*
 * ON TOUCH THERE IS NO HOVER, so the plus that reveals itself on a desktop row would simply
 * never appear on the phone this is most often held on — an invisible button is the same as
 * no button. Coarse pointers get it faintly but always.
 */
@media (hover: none) {
  .rota__put {
    color: var(--text-dim);
    opacity: 0.5;
  }
}

/* ============================================================ the crew list */

.crew__row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 12px;
  width: 100%;
  padding: 10px 0;
  border: 0;
  border-bottom: 1px solid var(--p-hairline, var(--border));
  background: none;
  color: inherit;
  font: inherit;
  text-align: start;
  cursor: pointer;
}

.crew__name {
  font-weight: 600;
  margin-inline-end: auto;
}

.crew__hours,
.crew__link,
.crew__review {
  font-size: 0.85rem;
  color: var(--text-dim);
}

/*
 * Only ever drawn on the people who can, so it is a mark rather than a column. A rank printed
 * on every row — "Crew" eleven times and "Shift manager" twice — spends the list's ink on the
 * answer nobody scans it for.
 */
/*
 * REGULARLY OVER, on the crew row. Two weights, and the difference is the one that matters:
 * a few weeks over is worth knowing and is grey; an AVERAGE week over the ceiling is the shape
 * an insurer's test actually takes, and is red. Marking both the same would put the ordinary
 * awkward month in the same colour as the classification problem.
 */
.crew__over {
  font-size: 0.78rem;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}

.crew__over--regular {
  color: var(--p-red, var(--danger));
  font-weight: 600;
}

/*
 * The week-by-week list in the dialog. A three-column row so the numbers line up down it.
 *
 * `pastweek` and not `week`, and that is not fussiness: `.crew__week` is already the
 * availability fieldset three inches up this same dialog, so reusing the name would have laid
 * a twelve-row grid over the seven weekday rows. The same mistake as sharing `.rota__cover`
 * earlier today — caught this time by checking what the name already meant before writing it.
 */
.crew__past {
  margin: 14px 0 0;
  padding: 10px 12px;
  border: 1px solid var(--p-hairline, var(--border));
  border-radius: 8px;
}

.crew__pastweek {
  display: grid;
  grid-template-columns: 1fr auto auto;
  gap: 12px;
  margin: 0;
  padding: 3px 0;
  font-size: 0.82rem;
  font-variant-numeric: tabular-nums;
}

.crew__pastwhen {
  color: var(--text-dim);
}

.crew__pastover {
  min-width: 64px;
  text-align: end;
}

.crew__pastweek--over .crew__pasthours,
.crew__pastweek--over .crew__pastover {
  color: var(--p-red, var(--danger));
  font-weight: 600;
}

.crew__runs {
  font-size: 0.78rem;
  font-weight: 600;
  padding: 1px 8px;
  border-radius: 999px;
  border: 1px solid var(--p-hairline, var(--border));
  color: var(--p-blue, var(--accent));
  white-space: nowrap;
}

/*
 * SOMEBODY WITH NO HOURS IS THE LOUD CASE. A record that looks complete and cannot be rostered
 * onto anything is the failure this list exists to make visible — mayWork refuses every day of
 * the week for them, and a manager filling in a week would otherwise just find them missing.
 */
.crew__hours--warn,
.crew__link--warn {
  color: var(--p-peach, var(--text-dim));
}

.crew__row--gone .crew__name {
  color: var(--text-dim);
}

.crew__hours--gone {
  font-style: italic;
}

.crew__review {
  color: var(--p-yellow, var(--accent));
}

.crew__actions {
  margin: 14px 0 0;
}

.crew__day {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  padding: 4px 0;
}

.crew__dayname {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 130px;
}

/*
 * The two time boxes sit ON the day's row rather than under it. .input is full width by
 * default, which is right for a form and wrong for seven rows of it — unconstrained, one
 * person's week was three screens long and the dialog could not be read without scrolling
 * past the thing being edited.
 */
.crew__day .input {
  flex: 0 0 auto;
  width: 9rem;
}

/* The past-midnight box on an availability row. Small, because it is the exception. */
.crew__late {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 0.75rem;
  color: var(--text-dim);
  white-space: nowrap;
  cursor: pointer;
}

.crew__dash {
  color: var(--text-dim);
}

.crew__week,
.crew__off,
.crew__sites,
.crew__linkbox {
  margin: 14px 0 0;
  padding: 10px 12px;
  border: 1px solid var(--p-hairline, var(--border));
  border-radius: 8px;
}

/* The cinemas somebody works at. Same shape as the time-off list under it — a row per
   thing with the button at the end, and one add control below. */
.crew__siterow {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin: 0 0 6px;
}

.crew__sitename {
  margin-inline-end: auto;
}

.crew__siteadd {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 8px;
}

.crew__siteadd .field {
  margin: 0;
  min-width: 0;
  flex: 1 1 160px;
}

.crew__offrow {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin: 0 0 6px;
}

.crew__offwhy {
  color: var(--text-dim);
  font-size: 0.85rem;
  margin-inline-end: auto;
}

.crew__offadd {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 8px;
}

.crew__offadd .field {
  margin: 0;
  min-width: 0;
  flex: 1 1 130px;
}

.crew__caution {
  padding: 8px 10px;
  border-radius: 8px;
  border: 1px solid var(--p-yellow, var(--accent));
  font-size: 0.85rem;
}

/* The link itself, shown once. Breaks anywhere, because it is long and gets read off a screen. */
.crew__linkout {
  padding: 8px 10px;
  border-radius: 8px;
  background: var(--p-mantle, var(--surface-2));
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.8rem;
  overflow-wrap: anywhere;
}

.crew__linkstate {
  font-size: 0.85rem;
  color: var(--text-dim);
}

.crew__linkactions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

/*
 * WHAT HAPPENED TO THE EMAIL, and the two answers must not look alike.
 *
 * The whole reason that route awaits its send and reports honestly is so a manager cannot
 * walk away believing somebody has their rota. A failure drawn in the same quiet grey as
 * "Emailed to maya@example.com" would hand back the silence the await was spent buying —
 * which is exactly what shipped until the stylesheet guard caught the missing rule.
 */
.crew__linksent {
  margin-top: 8px;
  font-size: 0.85rem;
  color: var(--p-green, var(--success, var(--text-dim)));
}

.crew__linksent--no {
  color: var(--p-red, var(--danger));
  font-weight: 600;
}

/*
 * WHAT NARROWING SOMEBODY'S HOURS BROKE, and it is a report rather than an alarm: the app has
 * changed nothing, and the manager is being told so they can decide.
 */
.crew__stranded {
  margin-top: 12px;
  padding: 8px 10px;
  border-radius: 8px;
  border: 1px solid var(--p-peach, var(--text-dim));
  font-size: 0.85rem;
}

/* The seen card's headline: three counts, only two of which are anybody's job. */
.rota__seencount {
  display: flex;
  flex-wrap: wrap;
  gap: 6px 14px;
  margin: 0 0 10px;
  font-size: 0.9rem;
}

.rota__seenpart--done {
  color: var(--text-dim);
}

.rota__seenpart--stale,
.rota__seenpart--never {
  color: var(--p-peach, var(--text-dim));
  font-weight: 600;
}

.rota__seenall {
  margin: 0;
  color: var(--text-dim);
}

.rota__seenfold > summary {
  cursor: pointer;
  color: var(--text-dim);
  font-size: 0.85rem;
  padding: 4px 0;
}

/* ============================================================ speaking instead of typing

   The mic beside a box that feeds an agent — see js/dictate.js. Absent, not disabled, where the
   browser has no SpeechRecognition, so these rules simply never match on Firefox. */

.dictate {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  flex: none;
}

.dictate__glyph {
  flex: none;
}

/*
 * LISTENING. The accent plus a slow pulse, which is the only state in this app where something is
 * happening because of the person rather than to them — the pulse is the app saying "still here,
 * keep going", and a manager mid-sentence needs that more than they need a label.
 *
 * It is the button's own edge that breathes rather than a halo, for the reason the agent button's
 * glow is inset: a ring painted outside the border box makes the row it sits in read as misaligned.
 */
/*
 * WRITTEN AGAINST `.btn--secondary`, AND ITS HOVER, rather than on the modifier alone.
 *
 * `.btn--secondary:hover:not(:disabled)` is three classes deep and a bare `.dictate--live` is one,
 * so the plain modifier lost the whole lit state to the hover — and the moment it lost it is
 * exactly the moment it is most needed: you press the mic, your pointer is still on it, and the
 * button you just started goes grey under your own cursor. Caught by asking the page what it had
 * actually computed rather than by reading the rule and believing it.
 */
.dictate--live.btn--secondary,
.dictate--live.btn--secondary:hover:not(:disabled) {
  background: color-mix(in oklab, var(--accent) 16%, var(--surface));
  color: var(--accent);
  border-color: color-mix(in oklab, var(--accent) 55%, transparent);
  animation: dictate-live 1.8s ease-in-out infinite;
}

.dictate--live.btn--secondary:hover:not(:disabled) {
  background: color-mix(in oklab, var(--accent) 24%, var(--surface));
}

@keyframes dictate-live {
  0%,
  100% {
    box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--accent) 20%, transparent);
  }
  50% {
    box-shadow: inset 0 0 10px color-mix(in oklab, var(--accent) 38%, transparent);
  }
}

.dictate__hint {
  font-size: 0.8rem;
  color: var(--text-dim);
  line-height: 1.4;
}

.dictate__hint[hidden] {
  display: none;
}

/*
 * A reader who asked for no motion keeps the lit edge and loses the breathing. The words on the
 * button already say Listening; the pulse was only ever the faster way to read it.
 */
@media (prefers-reduced-motion: reduce) {
  .dictate--live {
    animation: none;
    box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--accent) 30%, transparent);
  }
}

/* ============================================================ three questions to start with

   The assistant's empty state, turned into an offer — see src/openers.js for which three, and
   why every one of them is gated against what the reading account may actually be told. */

.chat__openers {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 0 14px 10px;
}

.chat__openers[hidden] {
  display: none;
}

/*
 * A ROW, not a chip. These are whole questions — "Which titles are under-performing this week,
 * and what would you change?" — and a chip that long is a chip that wraps into an unreadable
 * blob. Full width, left-aligned, one per line: the shape of a list somebody reads down.
 */
.chat__opener {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 10px;
  width: 100%;
  padding: 10px 12px;
  border: 1px solid var(--p-hairline, var(--border));
  border-radius: 10px;
  background: var(--surface);
  color: var(--text);
  font: inherit;
  text-align: start;
  line-height: 1.4;
  cursor: pointer;
}

.chat__opener:hover {
  border-color: color-mix(in oklab, var(--accent) 45%, transparent);
  background: color-mix(in oklab, var(--accent) 7%, var(--surface));
}

/*
 * The suggestion and its dismiss sit side by side rather than one inside the other: a button
 * within a button is invalid, and the browsers that tolerate it disagree about which one a tap
 * belongs to. The × is laid over the row's end so it costs no width.
 */
.chat__openerrow {
  position: relative;
  display: flex;
}

.chat__openerdrop {
  position: absolute;
  inset-block: 0;
  inset-inline-end: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  border: 0;
  border-radius: 0 10px 10px 0;
  background: transparent;
  color: var(--text-faint);
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
}

/*
 * Quiet until the row is under the pointer, because it is the second thing anybody wants from a
 * suggestion and a row of × marks reads as a list of things to delete. It stays reachable by
 * keyboard whatever the pointer is doing — :focus-visible is not conditional on the hover.
 */
.chat__openerrow:hover .chat__openerdrop,
.chat__openerdrop:focus-visible {
  color: var(--text);
  background: color-mix(in oklab, var(--accent) 10%, transparent);
}

.chat__openerdrop:hover {
  color: var(--danger);
}

/* The arrow shifts aside so the two never overlap where the × actually sits. */
.chat__openerrow .chat__opener {
  padding-inline-end: 38px;
}

.chat__openertext {
  min-width: 0;
}

/* The mark that says a row goes somewhere. Accent, and only on hover does it come fully up —
   three of these at full strength would read as three buttons competing to be pressed. */
.chat__openergo {
  flex: none;
  color: var(--accent);
  opacity: 0.55;
}

.chat__opener:hover .chat__openergo {
  opacity: 1;
}

/* The assistant's own strip for it, above the composer: the composer is a tight flex line ending
   in Ask, and a sentence in there would push the send button onto a second row mid-press. */
.chat__tools {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 10px;
  padding: 0 14px;
}

.chat__tools:empty {
  display: none;
}

/* ============================================================ the AI staffing agent */

/*
 * The card wears the accent on its edge while the agent is out, which is the same sentence the
 * scheduler's write-up bar says with its running rule: something is happening and nothing is
 * broken. Inset, for the reason the live agent button's glow is inset — a halo outside the
 * border reads as the card's edge and puts this card visibly out of line with the ones above it.
 */
.staffagent--working {
  box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--accent) 45%, transparent);
}

/*
 * A button that cannot be pressed because it is BUSY is not the same as one that cannot be
 * pressed because there is nothing behind it, and the grey every other disabled control wears
 * says the second thing. The same rule the scheduler's agent button has, for the same reason:
 * while the agent is out, the thing under the reader's finger is the thing that is working.
 */
.staffagent--working .agentrow__btn[disabled] {
  opacity: 1;
  background: color-mix(in oklab, var(--accent) 13%, var(--surface));
  color: var(--accent);
  border-color: color-mix(in oklab, var(--accent) 32%, transparent);
  cursor: progress;
}

/* The hint above is about the whole card; the label below belongs to the box. Without this they
   set solid against each other and the label reads as the hint's last line. */
.staffagent__ask {
  margin-top: 16px;
}

.staffagent__box {
  width: 100%;
  /* A paragraph, not a line. The three-facts-in-one-sentence case is the one this is for, and a
     single-line box invites a single fact. */
  min-height: 88px;
  resize: vertical;
  font: inherit;
  line-height: 1.5;
}

.staffagent__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 12px;
  margin: 10px 0 0;
}

.staffagent__count {
  font-size: 0.82rem;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}

.staffagent__out {
  margin-top: 16px;
  padding-top: 14px;
  border-top: 1px solid var(--p-hairline, var(--border));
}

/* What the agent understood, in its own words, above the rows it turned that into. */
.staffagent__narrative {
  margin: 0 0 12px;
  line-height: 1.55;
}

.staffagent__rows {
  display: flex;
  flex-direction: column;
}

/*
 * ONE PROPOSED ROW, and the whole label is the hit area.
 *
 * A tick beside a sentence is a control somebody has to aim at; a row that toggles anywhere on
 * it is one a thumb can use. These are read on a phone in an office as often as at a desk.
 */
.staffagent__row {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 10px 2px;
  border-bottom: 1px solid var(--p-hairline, var(--border));
  cursor: pointer;
}

.staffagent__tick {
  flex: none;
  /* Nudged down onto the first line's baseline rather than floating at the box's top edge. */
  margin-top: 3px;
  width: 18px;
  height: 18px;
  accent-color: var(--accent);
}

.staffagent__what {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.staffagent__words {
  line-height: 1.45;
}

/* Their own sentence, kept beside the row it became — the check on whether the agent heard right. */
.staffagent__why {
  font-size: 0.82rem;
  color: var(--text-dim);
}

/*
 * WHAT THIS APP CANNOT HOLD. A panel rather than a line, because it is the half of the answer
 * the manager has to act on themselves: these are the rules that stay on the noticeboard.
 */
.staffagent__cant {
  margin-top: 14px;
  padding: 12px;
  border-radius: 8px;
  background: var(--p-mantle, var(--surface-2));
}

.staffagent__canthead {
  margin: 0 0 8px;
  font-size: 0.9rem;
  font-weight: 600;
}

.staffagent__cantrow {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 0 0 8px;
}

.staffagent__cantrow:last-child {
  margin-bottom: 0;
}

.staffagent__cantsaid {
  font-style: italic;
}

.staffagent__cantwhy {
  font-size: 0.82rem;
  color: var(--text-dim);
}

.staffagent__dropped {
  margin-top: 12px;
}

.staffagent__droprow {
  margin: 0 0 6px;
  font-size: 0.85rem;
  color: var(--text-dim);
}

.staffagent__apply {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 10px;
  margin: 14px 0 0;
}

.staffagent__say {
  margin: 10px 0 0;
  font-size: 0.88rem;
  color: var(--text-dim);
}

/* ============================================================ the shift patterns */

/*
 * WHAT ACTUALLY APPLIES, on a chosen day. Given its own band above the rows because it is the
 * answer, and the rows below it are the workings — six patterns with overlapping scopes are
 * easy to write and hard to hold in your head.
 */
.pattern__try {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 14px;
  margin-bottom: 12px;
  padding: 10px 12px;
  border-radius: 8px;
  background: var(--p-mantle, var(--surface-2));
}

.pattern__try .field {
  margin: 0;
  min-width: 0;
}

.pattern__applies {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin: 0;
  font-size: 0.85rem;
}

.pattern__appliesled {
  color: var(--text-dim);
}

.pattern__chip {
  padding: 2px 8px;
  border-radius: 999px;
  border: 1px solid var(--p-hairline, var(--border));
  font-variant-numeric: tabular-nums;
}

.pattern__row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 12px;
  width: 100%;
  padding: 10px 0;
  border: 0;
  border-bottom: 1px solid var(--p-hairline, var(--border));
  background: none;
  color: inherit;
  font: inherit;
  text-align: start;
  cursor: pointer;
}

.pattern__name {
  font-weight: 600;
  min-width: 72px;
}

.pattern__hours {
  font-variant-numeric: tabular-nums;
}

.pattern__on,
.pattern__when {
  font-size: 0.85rem;
  color: var(--text-dim);
}

.pattern__scope {
  margin-inline-start: auto;
  font-size: 0.78rem;
  color: var(--text-dim);
}

/* A row scoped to one building is the exception to the circuit's rule, so it is the one
   marked — the general case is the quiet one. */
.pattern__scope--mine {
  color: var(--p-blue, var(--accent));
}

.pattern__none {
  color: var(--text-dim);
}

.pattern__days,
.pattern__season {
  margin: 14px 0 0;
  padding: 10px 12px;
  border: 1px solid var(--p-hairline, var(--border));
  border-radius: 8px;
}

.pattern__day {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  margin-inline-end: 12px;
  white-space: nowrap;
}

/* ============================================================ the back-office tablet
 *
 * Sized for a screen on a counter, read at arm's length, tapped with a thumb while holding
 * something else. Everything here is bigger than the rest of the app on purpose — this is the
 * one surface nobody is sitting down at.
 */
.shopfloor {
  margin: 0;
  min-height: 100vh;
  background: var(--bg);
}

.shopfloor__panel {
  max-width: 560px;
  margin: 0 auto;
  padding: 24px 20px 40px;
}

.shopfloor__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 24px;
}

.shopfloor__place {
  margin: 0;
  font-size: 0.9rem;
  color: var(--text-dim);
}

.shopfloor__title {
  margin: 0 0 18px;
  font-size: 1.5rem;
}

.shopfloor__names {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 12px;
}

/* A name is a target, so it is a tile rather than a row: 56px is a thumb on a busy night. */
.shopfloor__name {
  min-height: 56px;
  padding: 12px 14px;
  border: 1px solid var(--p-hairline, var(--border));
  border-radius: 10px;
  background: var(--p-mantle, var(--surface-2));
  color: var(--text);
  font: inherit;
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;
}

/* Shown and disabled, never left out. Missing from the list reads as "the app has lost me"
   and produces a phone call; greyed out with a line underneath reads as an instruction. */
.shopfloor__name--nopin {
  opacity: 0.45;
  cursor: not-allowed;
}

.shopfloor__pinsay {
  margin: 0 0 10px;
  color: var(--text-dim);
}

.shopfloor__pinbox {
  /* max-width as well as width: .input sets width:100% and a bare width here loses to it in
     the cascade, which draws a four-character field the width of the tablet. */
  width: 8ch;
  max-width: 8ch;
  font-size: 1.6rem;
  letter-spacing: 0.4em;
  text-align: center;
}

.shopfloor__pad {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
  margin: 18px 0;
  max-width: 320px;
}

.shopfloor__key {
  min-height: 64px;
  border: 1px solid var(--p-hairline, var(--border));
  border-radius: 10px;
  background: var(--p-mantle, var(--surface-2));
  color: var(--text);
  font: inherit;
  font-size: 1.3rem;
  cursor: pointer;
}

.shopfloor__key--wide {
  font-size: 0.95rem;
}

.shopfloor__err {
  margin: 0 0 14px;
  padding: 10px 12px;
  border-radius: 8px;
  border: 1px solid var(--p-red, var(--danger));
  color: var(--p-red, var(--danger));
}

.shopfloor__insay {
  margin: 0 0 20px;
  color: var(--text-dim);
}

/* The way off, and deliberately the largest control on the screen: the timer is the backstop,
   handing the tablet to the next person is the ordinary case. */
.shopfloor__done {
  min-height: 56px;
  width: 100%;
  margin-top: 24px;
}

.shopfloor__gone {
  padding: 16px;
  border-radius: 10px;
  border: 1px solid var(--p-hairline, var(--border));
  color: var(--text-dim);
}

/* What the crew asked for, and where it stands. */
.myrota__ask,
.myrota__spare {
  margin-top: 22px;
  padding: 14px 16px;
  border: 1px solid var(--p-hairline, var(--border));
  border-radius: 10px;
}

/*
 * ON SHIFT RIGHT NOW — the door to the shift view.
 *
 * The one box on this page that is not a request or a record, so it is the one that reads as
 * live: the accent edge rather than the hairline every other card wears. It is on screen for
 * the length of a shift and gone the rest of the week, which is the whole reason it can afford
 * to be the loudest thing here without becoming wallpaper.
 */
.myrota__onnow {
  margin-top: 22px;
  padding: 14px 16px;
  border: 1px solid color-mix(in oklab, var(--accent) 40%, transparent);
  border-radius: 10px;
  background: color-mix(in oklab, var(--accent) 8%, transparent);
}

.myrota__onnowsay {
  margin: 0 0 10px;
  font-weight: 600;
}

/* A button that is really a link, so it has to centre its own text and drop its underline. */
.myrota__onnowgo {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: 48px;
  text-decoration: none;
}

.myrota__asktitle {
  margin: 0 0 10px;
  font-size: 1.05rem;
}

.myrota__askrow {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-bottom: 12px;
}

.myrota__req,
.myrota__sparerow {
  padding: 10px 0;
  border-top: 1px solid var(--p-hairline, var(--border));
}

.myrota__reqwhat,
.myrota__spareday {
  margin: 0;
  font-weight: 600;
}

.myrota__reqstate,
.myrota__reqwhy {
  margin: 2px 0 0;
  font-size: 0.85rem;
  color: var(--text-dim);
}

/* A decision reads differently from a wait. Not approved is not an error — somebody said no
   for a reason, and the reason is on the line underneath. */
.myrota__req--approved .myrota__reqstate {
  color: var(--p-green, var(--ok, var(--accent)));
  font-weight: 600;
}

.myrota__req--declined .myrota__reqstate {
  color: var(--p-yellow, var(--accent));
  font-weight: 600;
}

/* "Offer this shift up" sits inside a shift row, so it is quiet: the shift is the thing on
   the page and this is something you might do to it. */
.myrota__offer {
  padding-inline: 0;
  font-size: 0.85rem;
}

/* One ask in the manager's queue. */
.ask {
  padding: 12px 0;
  border-top: 1px solid var(--p-hairline, var(--border));
}

.ask:first-child {
  border-top: 0;
  padding-top: 0;
}

.ask__said {
  margin: 0;
}

.ask__note,
.ask__claim {
  margin: 3px 0 0;
  font-size: 0.85rem;
  color: var(--text-dim);
}

/* An unclaimed giveaway is the manager's cue to go and ask somebody, not to wait: approving
   one is nothing, and the shift is still uncovered on the night. */
.ask__claim--none {
  color: var(--p-yellow, var(--accent));
}

.ask__do {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 10px 0 0;
}

/* The reason an approval was refused. On the row and not in a toast: the manager has a
   decision to make and needs the sentence still there while they make it. */
.ask__trouble {
  margin: 10px 0 0;
  padding: 8px 10px;
  border-radius: 8px;
  border: 1px solid var(--p-yellow, var(--accent));
  font-size: 0.85rem;
}

/* A day's proposed hours: the tick, the two times, and the late box. Wraps on a phone,
   because five controls do not fit a 390px row and a squeezed time field is unusable. */
.myrota__hoursrow {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  padding: 8px 0;
  border-top: 1px solid var(--p-hairline, var(--border));
}

.myrota__hoursrow .input[type='time'] {
  width: auto;
  flex: 0 0 auto;
}

/* What the page says back — a refusal, or what a manager will see. On the page rather than
   in window.alert, which a browser can switch off. */
.myrota__say {
  margin-top: 16px;
  padding: 10px 12px;
  border-radius: 8px;
  border: 1px solid var(--p-yellow, var(--accent));
  font-size: 0.88rem;
}

/*
 * The checklists, on the manager's side: the lists, the lines inside them, and who owns
 * which. Two levels on screen because there are two levels in the data — a boxed list with
 * its lines indented under it, rather than one flat run of rows that would make a line and a
 * checklist look like the same kind of thing.
 */
.checklist__list {
  padding: 12px 14px;
  margin-bottom: 12px;
  border: 1px solid var(--p-hairline, var(--border));
  border-radius: 10px;
}

.checklist__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 12px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--p-hairline, var(--border));
}

.checklist__name {
  flex: 1 1 10rem;
  font-weight: 700;
}

.checklist__lines {
  padding-top: 4px;
}

.checklist__line {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 12px;
  padding: 6px 0 6px 12px;
}

.checklist__label {
  flex: 1 1 12rem;
}

.checklist__when,
.checklist__where,
.checklist__theirs {
  font-size: 0.85rem;
  color: var(--text-dim);
}

/* The circuit's lists, which this manager may read but not change. Quieter rather than
   hidden: they are part of what the building does every morning. */
.checklist__list--theirs {
  border-style: dashed;
}

.checklist__list--theirs .checklist__name {
  font-weight: 600;
}

.checklist__addline {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  padding: 8px 0 0 12px;
}

.checklist__addline .input {
  flex: 1 1 10rem;
}

.checklist__add {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  gap: 12px;
  margin-top: 16px;
}

.checklist__add .field {
  flex: 1 1 12rem;
  margin: 0;
}

.checklist__say {
  margin-top: 12px;
  padding: 10px 12px;
  border-radius: 8px;
  border: 1px solid var(--p-red, var(--danger));
  font-size: 0.88rem;
}

/*
 * THE ROUTINE. Tickboxes sized for a thumb on a greasy screen, like the PIN pad — 52px rows,
 * full width, so tapping the wrong line takes effort rather than being the default.
 */
/*
 * A PANEL. Exactly one is on screen and the switcher owns which — see showPanel in
 * js/floor.js. Siblings on one page rather than three documents, because the session, its
 * half-hour timer and the sign-out that ends it all belong to this page.
 */
.shopfloor__view {
  margin-top: 28px;
}

.shopfloor__group {
  margin-bottom: 18px;
}

/* The first screen: which checklist, and how much is left in it. Sized like the name tiles
   on the PIN screen, because it is the same kind of choice made in the same conditions. */
.shopfloor__listrow {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
  min-height: 60px;
  padding: 10px 16px;
  margin-bottom: 10px;
  text-align: start;
  font: inherit;
  color: var(--text);
  border: 1px solid var(--p-hairline, var(--border));
  border-radius: 10px;
  background: var(--p-mantle, var(--surface-2));
  cursor: pointer;
}

.shopfloor__listname {
  font-size: 1.05rem;
  font-weight: 650;
}

/* The count is the reason the extra level is affordable, so it is the second-loudest thing
   on the row rather than a footnote. */
.shopfloor__left {
  flex: 0 0 auto;
  font-size: 0.9rem;
  font-weight: 650;
  color: var(--p-yellow, var(--accent));
}

/* Said in words, not as a zero: "All done" reads across a room and "0 to do" does not. */
.shopfloor__left--done {
  color: var(--p-green, var(--success, var(--text-dim)));
}

.shopfloor__listrow--done .shopfloor__listname {
  color: var(--text-dim);
  font-weight: 600;
}

.shopfloor__back {
  margin-top: 16px;
  min-height: 44px;
}

.shopfloor__grouptitle {
  margin: 0 0 8px;
  font-size: 0.78rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-dim);
}

.shopfloor__jobs {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.shopfloor__tick {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  min-height: 52px;
  padding: 8px 14px;
  text-align: start;
  font: inherit;
  font-size: 1rem;
  color: var(--text);
  border: 1px solid var(--p-hairline, var(--border));
  border-radius: 10px;
  background: var(--p-mantle, var(--surface-2));
  cursor: pointer;
}

.shopfloor__tickbox {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 28px;
  width: 28px;
  height: 28px;
  border: 2px solid var(--p-hairline, var(--border));
  border-radius: 7px;
  font-weight: 700;
}

/* Done stays on the list rather than disappearing: a finished list should read as finished,
   not as empty. Quieter, ticked, and carrying the name of whoever did it. */
.shopfloor__tick--done {
  color: var(--text-dim);
}

.shopfloor__tick--done .shopfloor__tickbox {
  color: var(--p-green, var(--success, var(--accent)));
  border-color: var(--p-green, var(--success, var(--accent)));
}

.shopfloor__tick[disabled] {
  cursor: default;
}

.shopfloor__tickby {
  color: var(--text-dim);
  font-size: 0.85rem;
}

/* The floor's worklist: what a guest reported, still open, worst first. */
.shopfloor__work {
  margin-top: 28px;
}

.shopfloor__worktitle {
  margin: 0 0 12px;
  font-size: 1.1rem;
}

/*
 * A severity stripe rather than a coloured card. The whole list is things that are wrong, so
 * painting every row red says nothing — the stripe is what makes "this one first" readable
 * from arm's length without reading a word.
 */
.shopfloor__job {
  padding: 12px 14px;
  margin-bottom: 10px;
  border: 1px solid var(--p-hairline, var(--border));
  border-inline-start: 4px solid var(--p-hairline, var(--border));
  border-radius: 10px;
  background: var(--p-mantle, var(--surface-2));
}

/* A room nobody can watch a film in. Loudest stripe, and the widest, because it is the one
   row on this list that is not "when you get a minute". It had no rule at all and wore the
   default hairline — the same edge as a sticky floor. */
.shopfloor__job--emergency {
  border-inline-start-width: 7px;
  border-inline-start-color: var(--p-red, var(--danger));
}

.shopfloor__job--high {
  border-inline-start-color: var(--p-red, var(--danger));
}

.shopfloor__job--medium {
  border-inline-start-color: var(--p-yellow, var(--accent));
}

.shopfloor__jobwhere {
  margin: 0 0 4px;
  font-weight: 650;
}

.shopfloor__jobseat,
.shopfloor__jobago {
  font-weight: 400;
  color: var(--text-dim);
}

.shopfloor__jobsaid {
  margin: 0;
}

.shopfloor__jobfilm,
.shopfloor__jobnotes {
  margin: 4px 0 0;
  font-size: 0.85rem;
  color: var(--text-dim);
}

/*
 * THE FIX CLAIM at the foot of a job.
 *
 * Sized for a thumb on a greasy screen, like the PIN pad above it — 44px minimum on
 * everything tappable, and the sentence box full width rather than sharing a row with its
 * button, because at tablet width a side-by-side pair leaves a field nobody can type a
 * sentence into.
 */
.shopfloor__fixwrap,
.shopfloor__fixed {
  margin-top: 10px;
  padding-top: 10px;
  border-top: 1px solid var(--p-hairline, var(--border));
}

.shopfloor__fix,
.shopfloor__fixgo {
  min-height: 44px;
}

.shopfloor__fixform {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

.shopfloor__fixbox {
  flex: 1 1 220px;
  min-height: 44px;
}

/* Claimed: quieter than the job above it. The work is done and this row is now a note to
   whoever reads the list next, not a thing to act on. */
.shopfloor__fixedsay {
  margin: 0;
  font-weight: 650;
  color: var(--p-green, var(--success, var(--text-dim)));
}

.shopfloor__fixednote {
  margin: 2px 0 0;
  font-size: 0.85rem;
  color: var(--text-dim);
}

.shopfloor__undo {
  margin-top: 4px;
  min-height: 44px;
}

.shopfloor__clear {
  margin-top: 28px;
  color: var(--text-dim);
}

/* A rostered shift. The stripe is green where the guest reports' is red: this is the one
   list on the tablet that is not a list of things that are wrong. */
.shopfloor__shift {
  padding: 12px 14px;
  margin-bottom: 10px;
  border: 1px solid var(--p-hairline, var(--border));
  border-inline-start: 4px solid var(--p-green, var(--success, var(--border)));
  border-radius: 10px;
  background: var(--p-mantle, var(--surface-2));
}

/* Taken off the rota. Still listed, because somebody told in passing needs to see the app
   agree — greyed, with the line underneath saying it in words. */
.shopfloor__shift--off {
  border-inline-start-color: var(--p-hairline, var(--border));
  opacity: 0.6;
}

.shopfloor__shiftday {
  margin: 0 0 4px;
  font-weight: 650;
}

.shopfloor__shifttime {
  margin: 0;
  font-size: 1.05rem;
}

.shopfloor__shiftnote {
  margin: 6px 0 0;
  font-size: 0.85rem;
  color: var(--text-dim);
}

.shopfloor__shiftflag {
  margin: 6px 0 0;
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--p-yellow, var(--accent));
}

/* "The next two weeks", beside the heading rather than under it. */
.shopfloor__span {
  font-size: 0.85rem;
  font-weight: 400;
  color: var(--text-dim);
}

/* Where the things this panel deliberately does not do actually live. */
.shopfloor__minesay {
  margin-top: 20px;
  font-size: 0.85rem;
  color: var(--text-dim);
}

/* The crew's front door at staff.<base>: two boxes, one sentence, nothing else. */
.staff__say {
  margin: 0 0 18px;
  color: var(--text-dim);
}

.staff__other {
  margin-top: 24px;
  font-size: 0.85rem;
  color: var(--text-dim);
}

.staff__otherlink {
  color: inherit;
}

/* ------------------------------------------------------------------ the building's tablets
 *
 * A list, a form, and a code shown once. The list is the part that has to read well in a
 * hurry: the morning after a tablet goes missing, somebody is looking for the row that says
 * which one it was and the button that takes it away.
 */
.tablet__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 12px;
  padding: 10px 0;
  border-bottom: 1px solid var(--p-hairline, var(--border));
}

.tablet__which {
  flex: 1 1 12rem;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.tablet__name {
  font-weight: 700;
}

.tablet__seen {
  font-size: 0.85rem;
  color: var(--text-dim);
}

.tablet__add {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  gap: 12px;
  margin-top: 16px;
}

.tablet__add .field {
  flex: 1 1 12rem;
  margin: 0;
}

/*
 * The link, as a picture and as words. Bordered rather than bare because it is the one thing
 * on this page that stops being true when it is dismissed, and it should not read as part of
 * the list above it.
 */
.tablet__out {
  margin-top: 16px;
  padding: 16px;
  border: 1px solid var(--p-hairline, var(--border));
  border-radius: 12px;
  text-align: center;
}

.tablet__outsay {
  margin-bottom: 12px;
  font-size: 0.9rem;
}

/*
 * Capped well below the code's own 512px so it never fills a phone, and square so a scanner
 * meets the quiet zone the library drew rather than a stretched one. See QR_PIXELS.
 */
.tablet__qr {
  width: min(240px, 70vw);
  height: auto;
  aspect-ratio: 1;
  background: #fff;
  border-radius: 8px;
}

/* The address in full, for a tablet whose camera does not work — which is an ordinary thing
   in a back office. Wrapped, because it is long and must not push the card sideways. */
.tablet__link {
  margin: 12px 0;
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: 0.8rem;
  word-break: break-all;
  color: var(--text-dim);
}

.tablet__say {
  margin-top: 12px;
  font-size: 0.85rem;
  color: var(--text-dim);
}

/* ------------------------------------------------------- the bar, once the nav grew
 *
 * Eight links fitted; the ninth did not, and what fell off was the ACCOUNT — the avatar
 * dropped to a second row on its own while the links stayed on the first, which reads as the
 * bar being broken rather than as the bar being full. That happens because the row wraps and
 * the account is last in it, so the one control that must always be findable is the first
 * thing width takes away.
 *
 * ## The bar is allowed to be wider than the page
 *
 * It was capped at the same 1180px as the shell, so the wordmark lined up with the left edge
 * of the first card — tidy, and the reason the bar ran out of room on a 1920px screen with
 * 700px of it unused. The alignment was never load-bearing: the bar has its own background and
 * its own border, so it reads as its own surface rather than as the top of the column. Wider,
 * it holds nine links at the padding they were designed with instead of nine links squeezed.
 *
 * The shell is untouched at 1180 — the CONTENT's measure is a reading decision and has nothing
 * to do with how many pages there are.
 *
 * ## And the account still cannot be the thing that drops
 *
 * The row no longer wraps, so the account cannot be pushed off it; the nav becomes the flexible
 * item, so if a deployment ever does run out of width it is the LINKS that wrap onto a second
 * line, inside the nav, where a second row of links looks deliberate. `min-width: 0` is what
 * lets the nav shrink below its content at all — a flex item's default minimum is its content,
 * so without it the nav refuses to narrow and pushes the account off exactly as before.
 *
 * Measured: the nine links want 985px, and at 1400 the nav has 1056 — seventy pixels spare,
 * which is most of one more page. Below 700px the mobile block takes over, where Home rides the
 * wordmark's row and the rest fold behind a button; that arrangement is left alone.
 *
 * ## The wrap above did not happen, because the links are not the nav's children
 *
 * That paragraph describes a wrap that could never occur. Everything but Home lives inside
 * .nav__rest — a group that exists for the PHONE, where the strip has to move as one element so
 * Home can ride the wordmark's row — and that group is a plain flex row: no wrap, no min-width,
 * so it is one rigid block as wide as its content. The nav's own wrap can only ever break
 * between Home and the strip, which is not a wrap of the links at all.
 *
 * Measured on a circuit with every link (cinema edition, invoices, an admin who may see
 * takings): the strip wants 895px and is handed 895px at every width, because nothing lets it
 * narrow. So from 1150px down it crosses the account button, from about 1040px it runs past the
 * bar's own edge and the whole page scrolls sideways, and at 701px — one pixel above where the
 * phone layout would have rescued it — it is 391px over. A laptop at 1024, a tablet in
 * landscape, and a half-screen window on any monitor all land inside that band.
 *
 * Giving the group the wrap and the min-width the nav was given makes the intended thing happen
 * to the elements it was always meant to happen to. Scoped here rather than on the base rule
 * because the phone's .nav__rest is a horizontal scroller inside a fixed max-height: let it wrap
 * and the folded strip becomes several rows clipped to one.
 *
 * A WRAP ALONE IS NOT THE FIX, which the measurement said next: at the padding these links wear
 * on a wide screen, the strip needs three rows below 1100px and four below 800, and a 193px
 * sticky bar over somebody's reports is worse than the bug it replaced. So the band also wears
 * the tighter link the phone block already wears, for the same reason and to the same end —
 * see .nav__rest .nav__link down there, where 12px of side padding was trimmed to 8 because the
 * links came to more than a 390px phone has.
 *
 * With both, the bar is one row to about 1100px and two below it (the fullest bar, ten links since
 * Compliance joined it, to about 1210px), and two rows of links reads as a bar built for them. The
 * same links sliced off by the window's edge read as broken, and take the account menu with them.
 */
@media (min-width: 701px) {
  .topbar__inner {
    max-width: 1400px;
    flex-wrap: nowrap;
  }

  .nav {
    flex: 1 1 auto;
    min-width: 0;
    /*
     * Home stays on the row with the strip rather than being the thing that breaks off.
     *
     * The nav's own wrap can only break between Home and the group, and that is the worst of
     * the available breaks: it spends a whole row on one link and leaves the strip to wrap
     * below it anyway, which measured three rows where two were needed. The wrap belongs
     * inside the group, where there are eight links to distribute.
     */
    flex-wrap: nowrap;
  }

  .nav__rest {
    /*
     * And no `min-width: 0` beside it, though the nav above has one and I wrote one here
     * first. A fault plant removed it and every test still passed, which is the answer: the
     * automatic minimum of a flex item is its MIN-CONTENT, and once this group wraps its
     * min-content is the widest single link rather than the whole row. The nav needs the
     * override because it does not wrap; this does not, and a line of CSS that does nothing
     * under a comment claiming it is load-bearing is worse than no line at all.
     */
    flex-wrap: wrap;
    /* Wider than the 4px between links: on a second row the gap is what separates one line
       of links from the next, and 4px reads as a rendering fault rather than a decision. */
    row-gap: 6px;
  }
}

/*
 * The band where the bar has the links but not the room for them at full size.
 *
 * Upper edge measured, not chosen: it is where the strip at its wide padding stops fitting
 * beside the brand, Home and the account. Under it the links wear the phone's tighter padding
 * and a hair less type, which takes the strip from 895px to something a single row still holds
 * most of the way down — and the wrap above catches the rest.
 *
 * It was 1240px for nine links. Ten — Compliance joined the bar — want a 1043px track at the wide
 * padding, which under Studio is a 1299px window, so at 1280 (a laptop's whole screen) the bar
 * went to two rows with room to spare at the tighter padding: those want 953px, one row to 1209px.
 * The edge sits a little above 1299 so type that sets a few pixels wider on another machine finds
 * the tighter link before it finds the wrap.
 */
@media (min-width: 701px) and (max-width: 1320px) {
  .nav__rest .nav__link {
    padding: 6px 9px;
    font-size: 0.84rem;
  }
}

/* ------------------------------------------------------------------- the app's own tooltip
 *
 * The box that replaced the browser's. See tips() in js/util.js for why it exists; this is
 * only what it looks like — the app's surface, the app's border, the app's type, so an
 * explanation reads as part of the page rather than as the operating system interrupting it.
 *
 * Fixed, because it explains things inside tables that scroll sideways in their own boxes.
 * A very high stacking order for the same reason: it sits above the sticky bar, the dock and
 * any open card, all of which it may need to overlap to point at what it is explaining.
 */
.tip {
  position: fixed;
  z-index: 1200;
  max-width: min(320px, calc(100vw - 24px));
  padding: 8px 11px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  font-size: 0.82rem;
  line-height: 1.45;
  /* The measure is short and the box is narrow, so the words break rather than the layout. */
  overflow-wrap: anywhere;
  box-shadow: 0 10px 28px rgb(0 0 0 / 34%);
  /* It is drawn where the pointer already is, so it must never be the thing under it. */
  pointer-events: none;
}

/* Anything carrying an explanation says so with the cursor, which is the convention the
   badges here already used while their tooltip was the browser's. */
[data-tip] {
  cursor: help;
}

/*
 * Except the ones that are already something you do. A button with a hint is still a button,
 * and a help cursor on it would promise the wrong thing.
 */
button[data-tip],
a[data-tip],
label[data-tip],
summary[data-tip] {
  cursor: revert;
}

/* =================================================================== Spotlight
 *
 * The CineScope edition's guest pages: the house lights going down. A projector beam in the
 * circuit's own colour behind a ticket that says where the guest is and what is on, the issue
 * types as tiles with a drawing each, and Archivo Expanded for everything that is a heading.
 *
 * EVERY RULE BELOW IS UNDER `:root[data-edition='cinema'] .guest--spot`, and that is the whole of
 * how Foyer keeps its own page. The edition is stamped on the root by the blocking theme script
 * before first paint (themeScript in src/app.js), so nothing here arrives late; the class is on
 * the four pages that have been given this look, so a guest page that has not is left alone. A
 * rule written without the prefix would reach a Foyer guest, which is exactly what was asked not
 * to happen — Foyer is to have a style of its own rather than a cinema's.
 *
 * NOTHING HERE NAMES A COLOUR the theme does not own. The Spotlight tokens are mixes of the
 * theme's text, accent and surfaces, so the page is the circuit's brand colour on whichever
 * ground its guest theme is — dark by default, and the light block below re-mixes the few that
 * a light ground needs the other way round (white glass rather than a veil of ink).
 */

/*
 * Archivo, self-hosted: the CSP allows fonts from this origin only, and a guest standing in a
 * cinema's basement should not wait on a third party for the headings. One variable file per
 * script range, both axes — weight 400 to 800 and width 100% to 125% — so the expanded headings
 * and the plain body text are the same download. The licence is beside the files (OFL).
 *
 * Only fetched by a page that sets text in it, which is the four Spotlight pages: a staff page
 * and a Foyer guest never ask for it.
 */
@font-face {
  font-family: 'Archivo';
  font-style: normal;
  font-weight: 400 800;
  font-stretch: 100% 125%;
  font-display: swap;
  src: url('/fonts/archivo-latin-v25.woff2') format('woff2');
  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: 'Archivo';
  font-style: normal;
  font-weight: 400 800;
  font-stretch: 100% 125%;
  font-display: swap;
  src: url('/fonts/archivo-latin-ext-v25.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/*
 * One drawing from public/js/icons.js, wherever it is put: a tile, the ticket, the head of the
 * form, a row in Settings. Stroked in the colour of the text around it and never filled, so every
 * drawing reads in every theme. Its size is the container's to say, through --glyph.
 */
.glyph {
  width: var(--glyph, 24px);
  height: var(--glyph, 24px);
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* A name with no drawing: its first letter in the rounded square every drawing sits in. */
.glyph--letter {
  display: inline-grid;
  place-items: center;
  box-sizing: border-box;
  border: 1.6px solid currentColor;
  border-radius: 27%;
  font-size: calc(var(--glyph, 24px) * 0.5);
  font-weight: 800;
  font-stretch: 125%;
  line-height: 1;
}

/* ---------------------------------------------------------------- the tokens */

:root[data-edition='cinema'] .guest--spot {
  --spot-glass: color-mix(in srgb, var(--text) 5%, transparent);
  --spot-glass-hover: color-mix(in srgb, var(--text) 9%, transparent);
  --spot-ticket-hi: color-mix(in srgb, var(--text) 11%, transparent);
  --spot-ticket-lo: color-mix(in srgb, var(--text) 4%, transparent);
  --spot-rim: color-mix(in srgb, var(--text) 14%, transparent);
  --spot-line: color-mix(in srgb, var(--text) 10%, transparent);
  --spot-line-strong: color-mix(in srgb, var(--text) 16%, transparent);
  --spot-dash: color-mix(in srgb, var(--text) 22%, transparent);
  --spot-beam-top: color-mix(in srgb, var(--accent) 50%, transparent);
  --spot-beam-mid: color-mix(in srgb, var(--accent) 16%, transparent);
  --spot-ray: color-mix(in srgb, var(--text) 6%, transparent);
  --spot-ray-soft: color-mix(in srgb, var(--text) 4.5%, transparent);
  --spot-mote: color-mix(in srgb, var(--text) 62%, transparent);
  --spot-glow: color-mix(in srgb, var(--accent) 60%, transparent);
  --spot-sheet: color-mix(in srgb, var(--surface) 55%, var(--bg));
  --spot-sheet-shadow: rgb(0 0 0 / 55%);
  --spot-pad: 20px;
  /* The ticket's stub, and so where its notches are cut: one number, so the two cannot part. */
  --stub: 104px;
  font-family: 'Archivo', var(--font);
}

/*
 * A light ground wants the glass the other way round: a veil of the theme's ink on a pale page is
 * a grey smudge, where a veil of white is a card catching the light. The beam is gentler for the
 * same reason — half-strength accent on a pale ground is a stain rather than a glow.
 *
 * Three ways a guest page can be light, and all three are here: Light, "Your brand" on a light
 * base, and System on a device set to light.
 */
:root[data-edition='cinema'][data-theme='light'] .guest--spot,
:root[data-edition='cinema'][data-theme='custom'][data-brand-base='light'] .guest--spot {
  --spot-glass: color-mix(in srgb, white 55%, transparent);
  --spot-glass-hover: color-mix(in srgb, white 82%, transparent);
  --spot-ticket-hi: color-mix(in srgb, white 90%, transparent);
  --spot-ticket-lo: color-mix(in srgb, white 58%, transparent);
  --spot-rim: color-mix(in srgb, white 95%, transparent);
  --spot-line: color-mix(in srgb, var(--text) 14%, transparent);
  --spot-line-strong: color-mix(in srgb, var(--text) 22%, transparent);
  --spot-dash: color-mix(in srgb, var(--text) 28%, transparent);
  --spot-beam-top: color-mix(in srgb, var(--accent) 32%, transparent);
  --spot-beam-mid: color-mix(in srgb, var(--accent) 10%, transparent);
  --spot-ray: color-mix(in srgb, var(--accent) 9%, transparent);
  --spot-ray-soft: color-mix(in srgb, var(--accent) 6%, transparent);
  --spot-mote: color-mix(in srgb, var(--accent) 55%, transparent);
  --spot-glow: color-mix(in srgb, var(--accent) 32%, transparent);
  --spot-sheet: var(--surface-2);
  --spot-sheet-shadow: rgb(76 79 105 / 18%);
}

@media (prefers-color-scheme: light) {
  :root[data-edition='cinema'][data-theme='system'] .guest--spot {
    --spot-glass: color-mix(in srgb, white 55%, transparent);
    --spot-glass-hover: color-mix(in srgb, white 82%, transparent);
    --spot-ticket-hi: color-mix(in srgb, white 90%, transparent);
    --spot-ticket-lo: color-mix(in srgb, white 58%, transparent);
    --spot-rim: color-mix(in srgb, white 95%, transparent);
    --spot-line: color-mix(in srgb, var(--text) 14%, transparent);
    --spot-line-strong: color-mix(in srgb, var(--text) 22%, transparent);
    --spot-dash: color-mix(in srgb, var(--text) 28%, transparent);
    --spot-beam-top: color-mix(in srgb, var(--accent) 32%, transparent);
    --spot-beam-mid: color-mix(in srgb, var(--accent) 10%, transparent);
    --spot-ray: color-mix(in srgb, var(--accent) 9%, transparent);
    --spot-ray-soft: color-mix(in srgb, var(--accent) 6%, transparent);
    --spot-mote: color-mix(in srgb, var(--accent) 55%, transparent);
    --spot-glow: color-mix(in srgb, var(--accent) 32%, transparent);
    --spot-sheet: var(--surface-2);
    --spot-sheet-shadow: rgb(76 79 105 / 18%);
  }
}

@media (max-width: 560px) {
  :root[data-edition='cinema'] .guest--spot {
    --spot-pad: 16px;
  }
}

/* The narrowest phones give the stub back a little, so a reference beside it still fits. */
@media (max-width: 374px) {
  :root[data-edition='cinema'] .guest--spot {
    --stub: 88px;
  }
}

/* ------------------------------------------------------------------ the beam
 *
 * A cone of the accent from the top of the page, blurred until it has no edge; faint rays inside
 * it that sway a degree or so either way; and a few motes of dust drifting up through the light.
 * The cone breathes on a seven-second cycle with two quick dips in it — the flicker of a lamp,
 * rare and faint enough to be felt rather than seen.
 *
 * Only `opacity` and `transform` move, which the compositor does on its own. On a phone set to
 * reduce motion all three hold still, with the motes left where they were at a third strength.
 *
 * Positioned against the page, not the window: it lights the top of the document and scrolls away
 * with it, the way a beam over the seats stays over the seats. Clipped to its own box, because the
 * blurred cone is wider than a phone and would otherwise let the whole page scroll sideways.
 */
.spot {
  display: none;
}

:root[data-edition='cinema'] .guest--spot .spot {
  display: block;
  position: absolute;
  top: 0;
  left: 50%;
  z-index: -1;
  width: min(100%, 560px);
  height: 640px;
  transform: translateX(-50%);
  overflow: clip;
  pointer-events: none;
}

:root[data-edition='cinema'] .guest--spot .spot__beam {
  position: absolute;
  inset: -20px 0 auto;
  height: 600px;
  filter: blur(24px);
}

/* The cone is a clip on a pseudo-element under the blur, so the blur softens its edges rather
   than the clip cutting them back to hard lines. */
:root[data-edition='cinema'] .guest--spot .spot__beam::before {
  content: '';
  position: absolute;
  inset: 0;
  clip-path: polygon(43% 0, 57% 0, 108% 100%, -8% 100%);
  background: linear-gradient(180deg, var(--spot-beam-top) 0%, var(--spot-beam-mid) 42%, transparent 100%);
  animation: spot-breathe 7s ease-in-out infinite;
}

:root[data-edition='cinema'] .guest--spot .spot__rays {
  position: absolute;
  inset: -20px 0 auto;
  height: 560px;
  filter: blur(3px);
  -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 25%, transparent 80%);
  mask-image: linear-gradient(180deg, #000 0%, #000 25%, transparent 80%);
}

/* The rays move inside a cone that does not: the clip is here and the sway is on the layer in it. */
:root[data-edition='cinema'] .guest--spot .spot__sway {
  position: absolute;
  inset: 0;
  overflow: hidden;
  clip-path: polygon(44% 0, 56% 0, 104% 100%, -4% 100%);
}

:root[data-edition='cinema'] .guest--spot .spot__sway::before {
  content: '';
  position: absolute;
  inset: 0;
  transform-origin: 50% 0;
  background:
    repeating-conic-gradient(
      from 150deg at 50% 0%,
      transparent 0deg,
      var(--spot-ray) 0.9deg,
      transparent 2deg,
      transparent 4.3deg
    ),
    repeating-conic-gradient(
      from 151deg at 50% 0%,
      transparent 0deg,
      var(--spot-ray-soft) 1.4deg,
      transparent 3deg,
      transparent 7.1deg
    );
  animation: spot-sway 9s ease-in-out infinite alternate;
}

:root[data-edition='cinema'] .guest--spot .spot__mote {
  position: absolute;
  width: 2px;
  height: 2px;
  border-radius: 50%;
  background: var(--spot-mote);
  opacity: 0;
  animation: spot-drift 14s linear infinite;
}

/* Placed by hand, and for three reasons: a field that is the same on every load can be
   screenshotted twice, it does not clump, and six different periods decorrelate within a minute
   anyway. */
:root[data-edition='cinema'] .guest--spot .spot__mote--1 {
  left: 46%;
  top: 250px;
  animation-duration: 15s;
  animation-delay: -3s;
}

:root[data-edition='cinema'] .guest--spot .spot__mote--2 {
  left: 55%;
  top: 190px;
  animation-duration: 12s;
  animation-delay: -8s;
}

:root[data-edition='cinema'] .guest--spot .spot__mote--3 {
  left: 38%;
  top: 330px;
  width: 3px;
  height: 3px;
  animation-duration: 18s;
  animation-delay: -1s;
}

:root[data-edition='cinema'] .guest--spot .spot__mote--4 {
  left: 61%;
  top: 300px;
  animation-duration: 14s;
  animation-delay: -11s;
}

:root[data-edition='cinema'] .guest--spot .spot__mote--5 {
  left: 50%;
  top: 390px;
  animation-duration: 16s;
  animation-delay: -5s;
}

:root[data-edition='cinema'] .guest--spot .spot__mote--6 {
  left: 33%;
  top: 420px;
  animation-duration: 20s;
  animation-delay: -14s;
}

@keyframes spot-breathe {
  0% { opacity: 0.84; }
  22% { opacity: 0.96; }
  23% { opacity: 0.86; }
  24.5% { opacity: 0.96; }
  50% { opacity: 1; }
  71% { opacity: 0.94; }
  72% { opacity: 0.83; }
  73.5% { opacity: 0.95; }
  100% { opacity: 0.84; }
}

@keyframes spot-sway {
  from { transform: rotate(-1.4deg); }
  to { transform: rotate(1.4deg); }
}

@keyframes spot-drift {
  0% { transform: translate(0, 0); opacity: 0; }
  15% { opacity: 0.8; }
  75% { opacity: 0.55; }
  100% { transform: translate(22px, -150px); opacity: 0; }
}

@keyframes spot-rise {
  from { transform: translateY(28px); opacity: 0; }
  to { transform: none; opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  :root[data-edition='cinema'] .guest--spot .spot__beam::before,
  :root[data-edition='cinema'] .guest--spot .spot__sway::before,
  :root[data-edition='cinema'] .guest--spot .spot__mote {
    animation: none;
  }

  :root[data-edition='cinema'] .guest--spot .spot__mote {
    opacity: 0.35;
  }
}

/* --------------------------------------------------------------- the header
 *
 * One row for the mark and the language, then the ticket, then the question — laid out by grid
 * area rather than by the order they sit in the markup, which is Foyer's order and stays it.
 */
:root[data-edition='cinema'] .guest--spot .guest-hero {
  overflow: visible;
  padding: 18px var(--spot-pad) 0;
  background: none;
  border-bottom: 0;
}

:root[data-edition='cinema'] .guest--spot .guest-hero::after {
  display: none;
}

:root[data-edition='cinema'] .guest--spot .guest-hero__inner {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas:
    'mark lang'
    'ticket ticket'
    'title title'
    'record record';
  align-items: center;
  max-width: 600px;
  min-height: 44px;
}

:root[data-edition='cinema'] .guest--spot .guest-hero .wordmark,
:root[data-edition='cinema'] .guest--spot .guest-hero .brand-logo {
  grid-area: mark;
  justify-self: start;
}

/*
 * The mark keeps its own letters. It is the name set in the app's face, with the reticle placed
 * from that face's measurements (see .wordmark__reticle), so it takes neither the page's Archivo
 * nor its width: stretched and spaced out, it was a different logo from the one on every other
 * screen, with an O that no longer sat in its word. Its size and spacing stay the guest page's
 * own (.guest-hero .wordmark); only its place in the header is Spotlight's.
 */
:root[data-edition='cinema'] .guest--spot .guest-hero .wordmark {
  font-family: var(--font);
}

:root[data-edition='cinema'] .guest--spot .guest-hero h1 {
  grid-area: title;
  margin: 26px 0 0;
  font-size: clamp(1.75rem, 1.2rem + 3vw, 2.25rem);
  font-weight: 700;
  font-stretch: 125%;
  line-height: 1.06;
  letter-spacing: -0.015em;
  text-align: center;
  text-wrap: balance;
}

/* The language, as a pill in the corner. The words "Language" stay for a screen reader — the
   select is named by its label — and leave the row for anyone who can see the pill. */
:root[data-edition='cinema'] .guest--spot .guest-lang {
  grid-area: lang;
  margin: 0;
}

:root[data-edition='cinema'] .guest--spot .guest-lang__label {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

:root[data-edition='cinema'] .guest--spot .guest-lang__select {
  -webkit-appearance: none;
  appearance: none;
  height: 44px;
  padding: 0 34px 0 16px;
  border-radius: 22px;
  border: 1px solid var(--spot-line-strong);
  background-color: var(--spot-glass);
  /* A chevron drawn in the text's own colour, so it follows the theme with the words. */
  background-image:
    linear-gradient(45deg, transparent 50%, currentColor 50%),
    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position:
    calc(100% - 19px) 52%,
    calc(100% - 14px) 52%;
  background-size: 5px 5px;
  background-repeat: no-repeat;
  color: var(--text);
  font: inherit;
  font-size: 0.8125rem;
  font-weight: 500;
  cursor: pointer;
}

/* What this cinema's own record says: under the question, with a clock, in the accent. */
:root[data-edition='cinema'] .guest--spot .guest-hero__record,
:root[data-edition='cinema'] .guest--spot .thanks__record {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  font-size: 0.8125rem;
  line-height: 1.4;
  color: var(--text-dim);
  text-align: center;
}

:root[data-edition='cinema'] .guest--spot .guest-hero__record {
  grid-area: record;
  margin: 14px 0 0;
}

:root[data-edition='cinema'] .guest--spot .guest-hero__record::before,
:root[data-edition='cinema'] .guest--spot .thanks__record::before {
  content: '';
  width: 16px;
  height: 16px;
  flex: none;
  background: var(--accent);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3.5a8.5 8.5 0 1 1 0 17a8.5 8.5 0 1 1 0-17z M12 7.5V12l3 2'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3.5a8.5 8.5 0 1 1 0 17a8.5 8.5 0 1 1 0-17z M12 7.5V12l3 2'/%3E%3C/svg%3E") center / contain no-repeat;
}

/* ------------------------------------------------------------------ the ticket
 *
 * Glass with two notches cut where the stub tears off, and a perforation between them: a real
 * ticket's shape, drawn with a mask so the beam still glows through it. Until report.js has read
 * the scan the row is the plain chip it is in Foyer — "Checking your QR code…", or a refusal —
 * and it only becomes a ticket once there is a place to print on it.
 */
:root[data-edition='cinema'] .guest--spot .guest-hero__place {
  grid-area: ticket;
  margin-top: 22px;
}

:root[data-edition='cinema'] .guest--spot .guest-hero__place--ticket {
  flex-wrap: nowrap;
  align-items: stretch;
  gap: 0;
  min-height: 104px;
  border-radius: 18px;
  background: linear-gradient(135deg, var(--spot-ticket-hi) 0%, var(--spot-ticket-lo) 100%);
  box-shadow: inset 0 1px 0 var(--spot-rim);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  -webkit-mask:
    radial-gradient(circle 10px at var(--stub) 0, transparent 97%, #000 100%) 0 0 / 100% 51% no-repeat,
    radial-gradient(circle 10px at var(--stub) 100%, transparent 97%, #000 100%) 0 100% / 100% 51% no-repeat;
  mask:
    radial-gradient(circle 10px at var(--stub) 0, transparent 97%, #000 100%) 0 0 / 100% 51% no-repeat,
    radial-gradient(circle 10px at var(--stub) 100%, transparent 97%, #000 100%) 0 100% / 100% 51% no-repeat;
}

/* The chip's words are the stub and the lines beside it now. Still in the document — the page
   reads the place from it and a refusal still writes into it. */
:root[data-edition='cinema'] .guest--spot .guest-hero__place--ticket .guest-hero__where {
  display: none;
}

:root[data-edition='cinema'] .guest--spot .guest-hero__stub {
  position: relative;
  width: var(--stub);
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  color: var(--text);
  --glyph: 46px;
}

/* The perforation, on the stub's own edge so it is exactly where the notches are. */
:root[data-edition='cinema'] .guest--spot .guest-hero__stub::after {
  content: '';
  position: absolute;
  top: 16px;
  bottom: 16px;
  right: 0;
  border-left: 1.5px dashed var(--spot-dash);
}

:root[data-edition='cinema'] .guest--spot .guest-hero__kind {
  max-width: calc(var(--stub) - 8px);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.6rem;
  font-weight: 600;
  font-stretch: 112%;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-dim);
}

:root[data-edition='cinema'] .guest--spot .guest-hero__number {
  font-size: 3rem;
  font-weight: 800;
  font-stretch: 125%;
  line-height: 0.95;
  font-variant-numeric: tabular-nums;
  text-shadow: 0 0 22px var(--spot-glow);
}

/* Anywhere that is not a screen: its drawing where the number would be, glowing the same way. */
:root[data-edition='cinema'] .guest--spot .guest-hero__stub--place .guest-hero__kind,
:root[data-edition='cinema'] .guest--spot .guest-hero__stub--place .guest-hero__number {
  display: none;
}

:root[data-edition='cinema'] .guest--spot .guest-hero__stub--place .glyph {
  stroke-width: 1.3;
  filter: drop-shadow(0 0 12px var(--spot-glow));
}

/* The right half: the film when there is one, where they are when not. Same three lines each. */
:root[data-edition='cinema'] .guest--spot .guest-hero__place--ticket .onnow,
:root[data-edition='cinema'] .guest--spot .guest-hero__here {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-wrap: wrap;
  align-content: center;
  gap: 5px 0;
  margin: 0;
  padding: 14px 18px;
  font-size: 0.8125rem;
  line-height: 1.35;
  color: var(--text-dim);
}

:root[data-edition='cinema'] .guest--spot .guest-hero__place--ticket .onnow__label,
:root[data-edition='cinema'] .guest--spot .guest-hero__eyebrow {
  flex: 0 0 100%;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: 0.6rem;
  font-weight: 600;
  font-stretch: 125%;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--text-dim);
  opacity: 1;
}

/* A lit dot before "Now showing": the one thing on the ticket that says something is on. */
:root[data-edition='cinema'] .guest--spot .guest-hero__place--ticket .onnow__label::before {
  content: '';
  width: 6px;
  height: 6px;
  flex: none;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 10px var(--accent);
}

:root[data-edition='cinema'] .guest--spot .guest-hero__place--ticket .onnow__film,
:root[data-edition='cinema'] .guest--spot .guest-hero__title {
  flex: 0 0 100%;
  font-size: 1.125rem;
  font-weight: 700;
  font-stretch: 125%;
  line-height: 1.12;
  letter-spacing: -0.01em;
  color: var(--text);
  overflow-wrap: break-word;
  text-wrap: balance;
}

:root[data-edition='cinema'] .guest--spot .guest-hero__title {
  font-size: 1.25rem;
}

/* Balanced, so a line that has to wrap does not leave the last word of a cinema's name — "4" of
   "Alpha Cinema 4" — alone underneath. */
:root[data-edition='cinema'] .guest--spot .guest-hero__meta {
  flex: 0 0 100%;
  text-wrap: balance;
}

:root[data-edition='cinema'] .guest--spot .guest-hero__place--ticket .onnow__time {
  opacity: 1;
  font-variant-numeric: tabular-nums;
}

/* The cinema after the clock. Foyer's film line never names it — the chip beside it already has. */
.onnow__at {
  display: none;
}

:root[data-edition='cinema'] .guest--spot .guest-hero__place--ticket .onnow__at {
  display: inline;
}

:root[data-edition='cinema'] .guest--spot .guest-hero__place--ticket .onnow__at::before {
  content: '\00a0\00b7\00a0';
}

/* ------------------------------------------------------------------ the tiles */

/* The full column's width, not its contents': an auto-margined item in the body's flex column
   shrinks to fit what is in it, and the tiles would come out narrower than the ticket above. */
:root[data-edition='cinema'] .guest--spot #main {
  display: flex;
  flex-direction: column;
  width: 100%;
  padding-bottom: 0;
}

:root[data-edition='cinema'] .guest--spot .guest-form {
  flex: 1 0 auto;
  display: flex;
  flex-direction: column;
  gap: 20px;
  padding-top: 24px;
}

/* The question is the page's heading, so the group's own legend is for a screen reader alone. */
:root[data-edition='cinema'] .guest--spot #category-group > legend {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

/* No numbered steps: the form is two screens now, and each says what it is. */
:root[data-edition='cinema'] .guest--spot .step__title::before {
  display: none;
}

:root[data-edition='cinema'] .guest--spot .pills__grid--tiles {
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 10px;
}

:root[data-edition='cinema'] .guest--spot .pills__grid--roomy {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

:root[data-edition='cinema'] .guest--spot .pill .pill__face {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: space-between;
  gap: 10px;
  min-height: 104px;
  height: 100%;
  padding: 14px 12px 12px 14px;
  border: 1px solid var(--spot-line);
  border-radius: 18px;
  background: var(--spot-glass);
  color: var(--text);
  text-align: left;
  font-size: 0.8125rem;
  line-height: 1.22;
  font-weight: 500;
  --glyph: 26px;
}

:root[data-edition='cinema'] .guest--spot .pills__grid--roomy .pill__face {
  min-height: 128px;
  padding: 16px 14px 14px 16px;
  border-radius: 20px;
  font-size: 1rem;
  font-weight: 600;
  line-height: 1.2;
  --glyph: 30px;
}

/* The words and the letter are spans inside the tile, and the base pill rule would make each of
   them a tile of its own. */
:root[data-edition='cinema'] .guest--spot .pill .pill__text {
  display: block;
  min-height: 0;
  height: auto;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  overflow-wrap: break-word;
  hyphens: auto;
}

:root[data-edition='cinema'] .guest--spot .pill .glyph--letter {
  display: inline-grid;
  min-height: 0;
  height: var(--glyph, 24px);
  padding: 0;
  border: 1.6px solid currentColor;
  border-radius: 27%;
  background: none;
  color: inherit;
  font-size: calc(var(--glyph, 24px) * 0.5);
}

:root[data-edition='cinema'] .guest--spot .pill:hover .pill__face {
  border-color: var(--spot-line-strong);
  background: var(--spot-glass-hover);
  color: var(--text);
}

:root[data-edition='cinema'] .guest--spot .pill input:checked + .pill__face {
  border-color: color-mix(in srgb, var(--accent) 70%, transparent);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  color: var(--text);
  font-weight: inherit;
  box-shadow: 0 0 22px color-mix(in srgb, var(--accent) 22%, transparent);
}

:root[data-edition='cinema'] .guest--spot .pill input:focus-visible + .pill__face {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* "Something else", as the row under the grid rather than one more tile in it. */
:root[data-edition='cinema'] .guest--spot .pills__else {
  margin-top: 10px;
}

:root[data-edition='cinema'] .guest--spot .pill--wide .pill__face {
  flex-direction: row;
  align-items: center;
  justify-content: flex-start;
  gap: 12px;
  min-height: 56px;
  padding: 0 18px 0 16px;
  border-radius: 16px;
  font-size: 0.9375rem;
  --glyph: 22px;
}

:root[data-edition='cinema'] .guest--spot .pill--wide .pill__text {
  flex: 1 1 auto;
}

:root[data-edition='cinema'] .guest--spot .pill--wide .pill__face::after {
  content: '';
  width: 8px;
  height: 8px;
  flex: none;
  border-top: 1.8px solid var(--text-faint);
  border-right: 1.8px solid var(--text-faint);
  transform: rotate(45deg);
}

/* ------------------------------------------------------------ the two choices
 *
 * Where the location takes orders at the seat, the page opens on these instead of the tiles —
 * Report a problem, and Order food and drinks (js/report.js, src/seat-ordering.js). Two rows of the
 * tiles' glass, larger, each with a line saying what it opens: the form, or the location's ordering
 * page by its name, with an arrow out for the one that leaves the page.
 */
.guest-start {
  display: grid;
  gap: 12px;
  padding-top: 24px;
}

.guest-start__choice {
  display: flex;
  align-items: center;
  gap: 14px;
  width: 100%;
  min-height: 84px;
  padding: 16px 18px 16px 16px;
  border: 1px solid var(--border);
  border-radius: 20px;
  background: var(--surface);
  color: var(--text);
  font: inherit;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
  --glyph: 30px;
}

.guest-start__choice:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.guest-start__glyph {
  display: inline-grid;
  flex: none;
  place-items: center;
}

.guest-start__words {
  display: grid;
  flex: 1 1 auto;
  gap: 3px;
  min-width: 0;
}

.guest-start__title {
  font-size: 1.0625rem;
  font-weight: 600;
  line-height: 1.25;
}

/* The ordering page's name can be long, and is one word to a browser: it breaks rather than widens. */
.guest-start__hint {
  color: var(--text-dim);
  font-size: 0.875rem;
  line-height: 1.35;
  overflow-wrap: anywhere;
}

/* A chevron for the form, which is a step on this page; an arrow out for the page that is not. */
.guest-start__choice::after {
  content: '';
  width: 8px;
  height: 8px;
  flex: none;
  border-top: 1.8px solid var(--text-faint);
  border-right: 1.8px solid var(--text-faint);
  transform: rotate(45deg);
}

/* The arrow out: the chevron's corner as its head, turned to point up and away, and a shaft drawn
   corner to corner across its box. */
a.guest-start__choice::after {
  width: 11px;
  height: 11px;
  transform: none;
  background: linear-gradient(
    to bottom right,
    transparent calc(50% - 0.9px),
    var(--text-faint) calc(50% - 0.9px),
    var(--text-faint) calc(50% + 0.9px),
    transparent calc(50% + 0.9px)
  );
}

/* Back to the two choices, at the head of the form: a quiet link on the left, where Back goes, and
   never a button as heavy as the choices it leads to. */
.guest-start__back {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding-inline: 2px;
}

.guest-start__back::before {
  content: '';
  width: 7px;
  height: 7px;
  border-bottom: 1.8px solid currentColor;
  border-left: 1.8px solid currentColor;
  transform: rotate(45deg);
}

:root[data-edition='cinema'] .guest--spot .guest-start__choice {
  border-color: var(--spot-line);
  background: var(--spot-glass);
}

:root[data-edition='cinema'] .guest--spot .guest-start__choice:hover {
  border-color: var(--spot-line-strong);
  background: var(--spot-glass-hover);
}

/* ------------------------------------------------------------------ the steps
 *
 * `data-stage` on <body>, set by report.js: 'choose' shows the tiles and the credit and nothing
 * else; 'details' shows everything but the tiles, raised as a sheet over the page; 'sent' puts
 * the ticket away for the thank-you. Every field stays in the document throughout, so Change
 * loses nothing anybody typed.
 */
:root[data-edition='cinema'] .guest--spot[data-stage='choose'] .guest-form > :not(#category-step, .guest-send, #start-back) {
  display: none;
}

/*
 * 'start' is the two choices above the form, where the location takes orders at the seat: the form
 * stays in the page with nothing of it showing but the credit, which stays on screen in every step.
 */
:root[data-edition='cinema'] .guest--spot[data-stage='start'] .guest-form > :not(.guest-send) {
  display: none;
}

:root[data-edition='cinema'] .guest--spot[data-stage='choose'] .guest-send,
:root[data-edition='cinema'] .guest--spot[data-stage='start'] .guest-send {
  position: static;
  margin: auto 0 0;
  padding: 26px 0 calc(20px + env(safe-area-inset-bottom, 0px));
  background: none;
}

/* The button and the anonymity line wait for the second step, and each is named: the credit is
   under them and stays on screen in every step, which a rule written as "all but the credit"
   would put one careless edit away from taking down too (see test/brand.test.js). */
:root[data-edition='cinema'] .guest--spot[data-stage='choose'] .guest-send > #submit,
:root[data-edition='cinema'] .guest--spot[data-stage='choose'] .guest-send > .guest-send__note,
:root[data-edition='cinema'] .guest--spot[data-stage='start'] .guest-send > #submit,
:root[data-edition='cinema'] .guest--spot[data-stage='start'] .guest-send > .guest-send__note {
  display: none;
}

:root[data-edition='cinema'] .guest--spot[data-stage='details'] #category-step {
  display: none;
}

/* The heading made way for the chosen type: still the page's heading for a screen reader. */
:root[data-edition='cinema'] .guest--spot[data-stage='details'] .guest-hero h1,
:root[data-edition='cinema'] .guest--spot[data-stage='sent'] .guest-hero h1 {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

:root[data-edition='cinema'] .guest--spot[data-stage='details'] .guest-hero__record,
:root[data-edition='cinema'] .guest--spot[data-stage='sent'] .guest-hero__record,
:root[data-edition='cinema'] .guest--spot[data-stage='sent'] .guest-hero__place {
  display: none;
}

/*
 * The sheet. In the page's flow rather than fixed over it: a fixed panel with a text box in it is
 * the thing an iPhone's keyboard handles worst, scrolling the page out from under the field being
 * typed in. This scrolls like the page it is, and the send bar still rides the bottom edge.
 */
:root[data-edition='cinema'] .guest--spot[data-stage='details'] .guest-form {
  gap: 18px;
  margin: 22px calc(-1 * var(--shell-pad)) 0;
  padding: 12px var(--spot-pad) 0;
  border-top: 1px solid var(--spot-line-strong);
  border-radius: 28px 28px 0 0;
  background: var(--spot-sheet);
  box-shadow: 0 -24px 60px var(--spot-sheet-shadow);
  animation: spot-rise 360ms var(--ease-arrive);
}

/* The handle: says "this came up from below", and nothing more — it is not a control. */
:root[data-edition='cinema'] .guest--spot[data-stage='details'] .guest-form::before {
  content: '';
  align-self: center;
  width: 38px;
  height: 5px;
  flex: none;
  border-radius: 3px;
  background: var(--spot-dash);
}

:root[data-edition='cinema'] .guest--spot .picked {
  display: flex;
  align-items: center;
  gap: 14px;
  padding-bottom: 16px;
  border-bottom: 1px solid var(--spot-line);
}

:root[data-edition='cinema'] .guest--spot .picked__glyph {
  width: 48px;
  height: 48px;
  flex: none;
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: 1px solid color-mix(in srgb, var(--accent) 55%, transparent);
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  color: var(--accent);
  box-shadow: 0 0 26px color-mix(in srgb, var(--accent) 30%, transparent);
  --glyph: 24px;
}

:root[data-edition='cinema'] .guest--spot .picked__words {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 3px;
}

:root[data-edition='cinema'] .guest--spot .picked__eyebrow {
  font-size: 0.65rem;
  font-weight: 600;
  font-stretch: 125%;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--text-dim);
}

:root[data-edition='cinema'] .guest--spot .picked__type {
  margin: 0;
  font-size: 1.375rem;
  font-weight: 700;
  font-stretch: 125%;
  line-height: 1.1;
  color: var(--text);
  overflow-wrap: break-word;
}

/* Focused by the page when the step opens, for a screen reader's sake; a ring on a heading
   nobody pressed would look like a fault. */
:root[data-edition='cinema'] .guest--spot .picked__type:focus {
  outline: none;
}

:root[data-edition='cinema'] .guest--spot .picked__change {
  flex: none;
  min-height: 44px;
  padding: 0 16px;
  border-radius: 22px;
  border: 1px solid var(--spot-line-strong);
  background: transparent;
  color: var(--text);
  font-size: 0.875rem;
  font-weight: 500;
}

:root[data-edition='cinema'] .guest--spot .guest-form .scope-note {
  padding: 0;
  border: 0;
  background: none;
  color: var(--text-dim);
  font-size: 0.875rem;
}

/* ------------------------------------------------------------------ the fields */

:root[data-edition='cinema'] .guest--spot .guest-form .step__title {
  margin: 0 0 8px;
  font-size: 0.875rem;
  font-weight: 600;
  letter-spacing: 0;
}

:root[data-edition='cinema'] .guest--spot .guest-form input[type='text'],
:root[data-edition='cinema'] .guest--spot .guest-form input[type='email'],
:root[data-edition='cinema'] .guest--spot .thanks__email input[type='email'],
:root[data-edition='cinema'] .guest--spot .guest-form textarea {
  padding: 14px 16px;
  border-radius: 14px;
  border: 1px solid var(--spot-line-strong);
  background: var(--spot-glass);
  color: var(--text);
  font-family: inherit;
  font-size: 1rem;
}

:root[data-edition='cinema'] .guest--spot .guest-form textarea {
  min-height: 150px;
}

:root[data-edition='cinema'] .guest--spot .guest-form input[type='text']:focus,
:root[data-edition='cinema'] .guest--spot .guest-form input[type='email']:focus,
:root[data-edition='cinema'] .guest--spot .thanks__email input[type='email']:focus,
:root[data-edition='cinema'] .guest--spot .guest-form textarea:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 18%, transparent);
}

:root[data-edition='cinema'] .guest--spot .guest-form__meta .field__hint {
  font-size: 0.78rem;
  line-height: 1.45;
  color: var(--text-dim);
}

/* The rule about personal details, beside a shield rather than in the warning colour: it is read
   before anything has gone wrong, and on this page it is a promise as much as a rule. */
:root[data-edition='cinema'] .guest--spot .field__hint--privacy {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin-top: 10px;
  font-size: 0.78rem;
  line-height: 1.45;
  color: var(--text-dim);
}

:root[data-edition='cinema'] .guest--spot .field__hint--privacy::before {
  content: '';
  width: 16px;
  height: 16px;
  flex: none;
  margin-top: 1px;
  background: var(--accent);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3.5l7 2.8v5.2c0 4.4-2.9 7.9-7 9.5-4.1-1.6-7-5.1-7-9.5V6.3z M9 12l2 2 4-4'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3.5l7 2.8v5.2c0 4.4-2.9 7.9-7 9.5-4.1-1.6-7-5.1-7-9.5V6.3z M9 12l2 2 4-4'/%3E%3C/svg%3E") center / contain no-repeat;
}

:root[data-edition='cinema'] .guest--spot .onnow__not {
  margin-top: 12px;
  min-height: 36px;
  padding: 0 14px;
  border-radius: 18px;
  border: 1px solid var(--spot-line-strong);
  color: var(--text);
  text-decoration: none;
  opacity: 1;
}

/* The camera, as a dashed row that says it is optional without a box around the word. */
:root[data-edition='cinema'] .guest--spot .photo__pick {
  flex: 1 1 auto;
  justify-content: flex-start;
  gap: 10px;
  min-height: 52px;
  padding: 0 16px;
  border-radius: 14px;
  border: 1px dashed var(--spot-dash);
  background: transparent;
  color: var(--text);
  font-size: 0.9375rem;
  font-weight: 500;
}

:root[data-edition='cinema'] .guest--spot .photo__pick::before {
  content: '';
  width: 20px;
  height: 20px;
  flex: none;
  background: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4.5 8h2.8l1.7-2.5h6l1.7 2.5h2.8A1.5 1.5 0 0 1 21 9.5v8.5a1.5 1.5 0 0 1-1.5 1.5h-15A1.5 1.5 0 0 1 3 18V9.5A1.5 1.5 0 0 1 4.5 8z M12 10.2a3.6 3.6 0 1 1 0 7.2a3.6 3.6 0 1 1 0-7.2z'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4.5 8h2.8l1.7-2.5h6l1.7 2.5h2.8A1.5 1.5 0 0 1 21 9.5v8.5a1.5 1.5 0 0 1-1.5 1.5h-15A1.5 1.5 0 0 1 3 18V9.5A1.5 1.5 0 0 1 4.5 8z M12 10.2a3.6 3.6 0 1 1 0 7.2a3.6 3.6 0 1 1 0-7.2z'/%3E%3C/svg%3E") center / contain no-repeat;
}

/* ------------------------------------------------------------------ sending */

:root[data-edition='cinema'] .guest--spot[data-stage='details'] .guest-send {
  margin: auto calc(-1 * var(--spot-pad)) 0;
  padding: 22px var(--spot-pad) calc(16px + env(safe-area-inset-bottom, 0px));
  background: linear-gradient(
    180deg,
    transparent,
    color-mix(in srgb, var(--spot-sheet) 88%, transparent) 38%,
    var(--spot-sheet) 70%
  );
}

:root[data-edition='cinema'] .guest--spot .guest-send .btn--cta {
  gap: 10px;
  min-height: 58px;
  border-radius: 16px;
  font-size: 1.0625rem;
  font-weight: 700;
  font-stretch: 112%;
  letter-spacing: 0.01em;
  box-shadow: 0 12px 34px color-mix(in srgb, var(--accent-fill) 35%, transparent);
}

:root[data-edition='cinema'] .guest--spot .guest-send .btn--cta::after,
:root[data-edition='cinema'] .guest--spot .thanks__trace .btn::after {
  content: '';
  width: 18px;
  height: 18px;
  flex: none;
  background: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14 M13.5 6.5L19 12l-5.5 5.5'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14 M13.5 6.5L19 12l-5.5 5.5'/%3E%3C/svg%3E") center / contain no-repeat;
}

:root[data-edition='cinema'] .guest--spot .guest-send__note {
  margin-top: 10px;
  font-size: 0.78rem;
  line-height: 1.4;
  color: var(--text-dim);
}

:root[data-edition='cinema'] .guest--spot .guest-send__by,
:root[data-edition='cinema'] .guest--spot .thanks__by {
  margin: 6px 0 0;
  text-align: center;
  font-size: 0.6875rem;
  letter-spacing: 0.06em;
  color: var(--text-faint);
}

:root[data-edition='cinema'] .guest--spot .guest-send__by strong,
:root[data-edition='cinema'] .guest--spot .thanks__by strong {
  font-weight: 700;
  color: var(--text-dim);
}

/* ------------------------------------------------------------------ the thank-you
 *
 * A screen of its own rather than a card on the form's: the mark glows in the accent, the
 * reference is a ticket of its own with its Copy beside it, and the way back to the report is a
 * full-width button. The line about somebody else scanning again sits at the foot with the credit.
 */
.thanks__by {
  display: none;
}

:root[data-edition='cinema'] .guest--spot .thanks__by {
  display: block;
}

:root[data-edition='cinema'] .guest--spot .thanks {
  flex: 1 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-top: 34px;
  padding: 0 0 calc(20px + env(safe-area-inset-bottom, 0px));
  border: 0;
  background: none;
  box-shadow: none;
  text-align: center;
}

:root[data-edition='cinema'] .guest--spot .thanks__mark {
  width: 92px;
  height: 92px;
  margin: 0 0 26px;
  border: 1.5px solid var(--accent);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  color: var(--text);
  box-shadow:
    0 0 0 10px color-mix(in srgb, var(--accent) 6%, transparent),
    0 0 64px color-mix(in srgb, var(--accent) 45%, transparent);
}

:root[data-edition='cinema'] .guest--spot .thanks__mark svg {
  width: 40px;
  height: 40px;
}

:root[data-edition='cinema'] .guest--spot .thanks h2 {
  margin: 0;
  font-size: 2.25rem;
  font-weight: 700;
  font-stretch: 125%;
  line-height: 1.04;
  letter-spacing: -0.015em;
}

:root[data-edition='cinema'] .guest--spot .thanks__where {
  margin: 8px 0 0;
  font-size: 0.9375rem;
  line-height: 1.45;
}

:root[data-edition='cinema'] .guest--spot .thanks__photo {
  margin: 8px 0 0;
}

:root[data-edition='cinema'] .guest--spot .thanks .ticket {
  align-self: stretch;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas:
    'label copy'
    'ref copy';
  justify-items: start;
  align-items: center;
  gap: 4px 12px;
  margin: 28px 0 0;
  padding: 16px 16px 16px 20px;
  border-radius: 20px;
  border: 1px solid var(--spot-line);
  background: var(--spot-glass);
  text-align: left;
}

:root[data-edition='cinema'] .guest--spot .ticket__label {
  grid-area: label;
  font-size: 0.65rem;
  font-weight: 600;
  font-stretch: 125%;
  letter-spacing: 0.2em;
}

:root[data-edition='cinema'] .guest--spot .thanks__ref {
  grid-area: ref;
  font-family: inherit;
  /* One line, always: a reference broken at its hyphen reads as two references. Sized to fit
     beside Copy on the narrowest phone, and no larger than the heading's weight needs. */
  font-size: clamp(1.1rem, 5.2vw, 1.5rem);
  font-weight: 700;
  font-stretch: 112%;
  letter-spacing: 0.05em;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

:root[data-edition='cinema'] .guest--spot .ticket__copy {
  grid-area: copy;
  min-height: 48px;
  padding: 0 16px;
  border-radius: 14px;
  border: 1px solid var(--spot-line-strong);
  background: var(--spot-glass);
  color: var(--text);
  font-size: 0.875rem;
}

:root[data-edition='cinema'] .guest--spot .thanks__record {
  margin: 14px 0 0;
}

:root[data-edition='cinema'] .guest--spot .thanks > .field__hint[data-reword] {
  max-width: 320px;
  margin: 18px auto 0;
  font-size: 0.9rem;
  line-height: 1.55;
  color: var(--text-dim);
}

:root[data-edition='cinema'] .guest--spot .thanks__code,
:root[data-edition='cinema'] .guest--spot .thanks__trace {
  align-self: stretch;
}

/* No rule above the way back: on this screen it is the next thing, not a new section. */
:root[data-edition='cinema'] .guest--spot .thanks__trace {
  margin-top: 22px;
  padding-top: 0;
  border-top: 0;
}

:root[data-edition='cinema'] .guest--spot .thanks__trace .btn {
  gap: 10px;
  min-height: 54px;
  border-radius: 16px;
  border: 1px solid var(--spot-line-strong);
  background: var(--spot-glass);
  color: var(--text);
  font-size: 1rem;
}

:root[data-edition='cinema'] .guest--spot .thanks__trace .field__hint {
  font-size: 0.75rem;
  color: var(--text-faint);
}

:root[data-edition='cinema'] .guest--spot #another-hint {
  margin: auto 0 0;
  padding-top: 28px;
  font-size: 0.8125rem;
  color: var(--text-dim);
}

/* ------------------------------------------------------------ the rating card
 *
 * The ticket carries the film, the showing and the cinema, so the card's own copies of them stand
 * down; the two questions share one glass card with a hairline between them; and the recommend
 * row is a single segmented bar — nought to ten on one line, the chosen number a lit pill.
 */
:root[data-edition='cinema'] .guest--spot .guest-main {
  width: 100%;
  max-width: 640px;
  margin: 0 auto;
  padding: 0 var(--spot-pad) calc(24px + env(safe-area-inset-bottom, 0px));
}

:root[data-edition='cinema'] .guest--spot .rate,
:root[data-edition='cinema'] .guest--spot .verdict,
:root[data-edition='cinema'] .guest--spot .rate-nope {
  min-height: 0;
  align-content: start;
  justify-items: stretch;
  padding: 20px 0 0;
}

:root[data-edition='cinema'] .guest--spot:has(.guest-hero__place--ticket) .rate > .rate__label,
:root[data-edition='cinema'] .guest--spot:has(.guest-hero__place--ticket) .rate > .rate__film,
:root[data-edition='cinema'] .guest--spot:has(.guest-hero__place--ticket) .rate > .rate__showtime,
:root[data-edition='cinema'] .guest--spot:has(.guest-hero__place--ticket) .rate > .rate__venue {
  display: none;
}

:root[data-edition='cinema'] .guest--spot .rate .asked,
:root[data-edition='cinema'] .guest--spot .rate .nps {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  max-width: none;
  margin: 0;
  padding: 18px 12px;
  border: 1px solid var(--spot-line);
  border-radius: 22px;
  background: var(--spot-glass);
}

/* Both questions: one card, the hairline between them drawn inside it rather than as its edge. */
:root[data-edition='cinema'] .guest--spot .rate .asked:has(+ .nps:not([hidden])) {
  padding-bottom: 16px;
  border-bottom: 0;
  border-radius: 22px 22px 0 0;
}

:root[data-edition='cinema'] .guest--spot .rate .asked:not([hidden]) + .nps {
  position: relative;
  padding-top: 16px;
  border-top: 0;
  border-radius: 0 0 22px 22px;
}

:root[data-edition='cinema'] .guest--spot .rate .asked:not([hidden]) + .nps::before {
  content: '';
  position: absolute;
  top: 0;
  left: 12px;
  right: 12px;
  height: 1px;
  background: var(--spot-line);
}

:root[data-edition='cinema'] .guest--spot .rate__ask {
  margin: 0;
  max-width: 280px;
  font-size: 0.9375rem;
  line-height: 1.35;
  font-weight: 600;
  text-align: center;
}

:root[data-edition='cinema'] .guest--spot .stars {
  gap: 4px;
  padding: 10px 0 0;
}

:root[data-edition='cinema'] .guest--spot .star {
  width: 56px;
  height: 56px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  color: transparent;
}

/* An outline until it is lit, so five unlit stars read as five places to tap rather than as a
   grey rating somebody else gave. */
:root[data-edition='cinema'] .guest--spot .star__glyph {
  width: 42px;
  height: 42px;
  stroke: color-mix(in srgb, var(--text) 40%, transparent);
  stroke-width: 1.3;
  stroke-linejoin: round;
}

:root[data-edition='cinema'] .guest--spot .star--on {
  color: var(--star-gold);
}

:root[data-edition='cinema'] .guest--spot .star--on .star__glyph {
  stroke: var(--star-gold);
  filter: drop-shadow(0 0 10px color-mix(in srgb, var(--star-gold) 50%, transparent));
}

:root[data-edition='cinema'] .guest--spot .rate__hint {
  margin: 6px 0 0;
  min-height: 1.4rem;
  font-size: 0.9375rem;
  font-weight: 700;
  font-stretch: 112%;
}

:root[data-edition='cinema'] .guest--spot .nps {
  gap: 0;
}

/* Eleven cells in one bar, a hairline between neighbours; none beside the lit one, whose rounded
   edge a line would run into. */
:root[data-edition='cinema'] .guest--spot .nps__row {
  align-self: stretch;
  gap: 0;
  height: 52px;
  margin-top: 12px;
  padding: 3px;
  border-radius: 14px;
  border: 1px solid var(--spot-line-strong);
  background: var(--spot-glass);
}

:root[data-edition='cinema'] .guest--spot .nps__n {
  min-height: 0;
  height: 100%;
  padding: 0;
  border: 0;
  border-left: 1px solid var(--spot-line);
  border-radius: 0;
  font-family: inherit;
  font-size: 0.9375rem;
  font-weight: 700;
}

:root[data-edition='cinema'] .guest--spot .nps__n:first-child,
:root[data-edition='cinema'] .guest--spot .nps__n--on,
:root[data-edition='cinema'] .guest--spot .nps__n--on + .nps__n {
  border-left-color: transparent;
}

:root[data-edition='cinema'] .guest--spot .nps__n--on {
  border-radius: 10px;
  background: var(--accent-fill);
  color: var(--accent-ink);
  box-shadow: 0 4px 16px color-mix(in srgb, var(--accent) 50%, transparent);
}

:root[data-edition='cinema'] .guest--spot .nps__ends {
  margin-top: 8px;
  padding: 0 4px;
  font-size: 0.72rem;
  color: var(--text-faint);
  opacity: 1;
}

:root[data-edition='cinema'] .guest--spot .rate__send {
  gap: 10px;
  width: 100%;
  min-height: 56px;
  margin-top: 18px;
  border: 0;
  border-radius: 16px;
  background: var(--accent-fill);
  color: var(--accent-ink);
  font-size: 1.0625rem;
  font-weight: 700;
  font-stretch: 112%;
  box-shadow: 0 12px 34px color-mix(in srgb, var(--accent-fill) 35%, transparent);
}

:root[data-edition='cinema'] .guest--spot .rate__send::after,
:root[data-edition='cinema'] .guest--spot .also__more::after {
  content: '';
  width: 18px;
  height: 18px;
  flex: none;
  background: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14 M13.5 6.5L19 12l-5.5 5.5'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14 M13.5 6.5L19 12l-5.5 5.5'/%3E%3C/svg%3E") center / contain no-repeat;
}

:root[data-edition='cinema'] .guest--spot .rate__quiet {
  margin: 10px 0 0;
  font-size: 0.78rem;
  color: var(--text-dim);
  opacity: 1;
}

/* ------------------------------------------------------------ after the tap
 *
 * What they said beside what everybody else said, as a ticket torn down the middle. Below the
 * floor there is only one half, so it is a plain card — and the page opens on a lit star instead,
 * because being early is the news.
 */
:root[data-edition='cinema'] .guest--spot .verdict__title {
  margin: 12px 0 0;
  font-size: clamp(1.625rem, 1.2rem + 2.4vw, 2rem);
  font-weight: 700;
  font-stretch: 125%;
  line-height: 1.08;
  letter-spacing: -0.015em;
  text-align: center;
}

:root[data-edition='cinema'] .guest--spot .verdict__film,
:root[data-edition='cinema'] .guest--spot .verdict__venue {
  margin: 8px 0 0;
  font-size: 0.9375rem;
  letter-spacing: 0;
  color: var(--text-dim);
  text-align: center;
}

:root[data-edition='cinema'] .guest--spot .verdict:has(#verdict-circuit-side[hidden])::before {
  content: '';
  justify-self: center;
  width: 92px;
  height: 92px;
  margin-top: 16px;
  border-radius: 50%;
  border: 1.5px solid var(--accent);
  background:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23F2B01E' stroke='%23F2B01E' stroke-width='1.2' stroke-linejoin='round' d='M12 2.6l2.9 5.88 6.49.95-4.7 4.58 1.11 6.46L12 17.42 6.2 20.47l1.11-6.46-4.7-4.58 6.49-.95z'/%3E%3C/svg%3E")
      center / 40px no-repeat,
    color-mix(in srgb, var(--accent) 14%, transparent);
  box-shadow:
    0 0 0 10px color-mix(in srgb, var(--accent) 6%, transparent),
    0 0 64px color-mix(in srgb, var(--accent) 45%, transparent);
}

:root[data-edition='cinema'] .guest--spot .verdict__pair {
  flex-wrap: nowrap;
  align-items: stretch;
  gap: 0;
  min-height: 124px;
  margin-top: 22px;
  border-radius: 18px;
  background: linear-gradient(135deg, var(--spot-ticket-hi) 0%, var(--spot-ticket-lo) 100%);
  box-shadow: inset 0 1px 0 var(--spot-rim);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  -webkit-mask:
    radial-gradient(circle 10px at 50% 0, transparent 97%, #000 100%) 0 0 / 100% 51% no-repeat,
    radial-gradient(circle 10px at 50% 100%, transparent 97%, #000 100%) 0 100% / 100% 51% no-repeat;
  mask:
    radial-gradient(circle 10px at 50% 0, transparent 97%, #000 100%) 0 0 / 100% 51% no-repeat,
    radial-gradient(circle 10px at 50% 100%, transparent 97%, #000 100%) 0 100% / 100% 51% no-repeat;
}

:root[data-edition='cinema'] .guest--spot .verdict__pair:has(#verdict-circuit-side[hidden]) {
  border: 1px solid var(--spot-line);
  border-radius: 20px;
  background: var(--spot-glass);
  box-shadow: none;
  -webkit-mask: none;
  mask: none;
}

:root[data-edition='cinema'] .guest--spot .verdict__side {
  position: relative;
  flex: 1 1 0;
  min-width: 0;
  align-content: center;
  justify-items: center;
  gap: 6px;
  padding: 16px 8px;
}

:root[data-edition='cinema'] .guest--spot .verdict__side + .verdict__side::before {
  content: '';
  position: absolute;
  top: 18px;
  bottom: 18px;
  left: 0;
  border-left: 1.5px dashed var(--spot-dash);
}

:root[data-edition='cinema'] .guest--spot .verdict__cap {
  font-size: 0.625rem;
  font-weight: 600;
  font-stretch: 125%;
  letter-spacing: 0.2em;
  color: var(--text-dim);
}

/* What they said, as the stars they tapped, in the gold they tapped them in. */
:root[data-edition='cinema'] .guest--spot #verdict-yours {
  font-size: 1.5rem;
  letter-spacing: 0.08em;
  color: var(--star-gold);
  text-shadow: 0 0 10px color-mix(in srgb, var(--star-gold) 45%, transparent);
}

:root[data-edition='cinema'] .guest--spot .verdict[data-by='recommend'] #verdict-yours {
  font-stretch: 125%;
  font-weight: 800;
  letter-spacing: 0;
  color: var(--text);
  text-shadow: none;
}

:root[data-edition='cinema'] .guest--spot #verdict-circuit {
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
  font-size: 2.5rem;
  font-weight: 800;
  font-stretch: 125%;
  line-height: 1;
  color: var(--text);
}

/* Everybody else's stars carry the star; a promoter score is a number of its own and does not. */
:root[data-edition='cinema'] .guest--spot .verdict[data-by='stars'] #verdict-circuit::after {
  content: '\2605';
  font-size: 1.25rem;
  color: var(--star-gold);
}

:root[data-edition='cinema'] .guest--spot .verdict__count {
  font-size: 0.75rem;
  color: var(--text-faint);
}

:root[data-edition='cinema'] .guest--spot .verdict__note {
  margin: 14px 0 0;
  font-size: 0.9rem;
  color: var(--text-dim);
  text-align: center;
}

/*
 * Rate your visit, after the tap. One answer and nobody else's — the reply carries no score to set
 * beside it (see venue.js) — so no torn ticket: the report form's lit check, and what they said on
 * a card of its own, as large as the film card sets everybody's score.
 *
 * The check is markup on a page Foyer serves too, so a bare rule takes it away and the stamped one
 * brings it back, like the beam's.
 */
.verdict__mark {
  display: none;
}

:root[data-edition='cinema'] .guest--spot .verdict__mark {
  display: grid;
  place-items: center;
  justify-self: center;
  width: 92px;
  height: 92px;
  margin: 16px 0 8px;
  border-radius: 50%;
  border: 1.5px solid var(--accent);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  color: var(--text);
  box-shadow:
    0 0 0 10px color-mix(in srgb, var(--accent) 6%, transparent),
    0 0 64px color-mix(in srgb, var(--accent) 45%, transparent);
}

:root[data-edition='cinema'] .guest--spot .verdict__mark svg {
  width: 40px;
  height: 40px;
}

:root[data-edition='cinema'] .guest--spot .verdict--visit .verdict__pair {
  min-height: 0;
  border: 1px solid var(--spot-line);
  border-radius: 20px;
  background: var(--spot-glass);
  box-shadow: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  -webkit-mask: none;
  mask: none;
}

:root[data-edition='cinema'] .guest--spot .verdict--visit .verdict__side {
  padding: 20px 8px 22px;
}

:root[data-edition='cinema'] .guest--spot .verdict--visit #verdict-yours {
  font-size: 2.5rem;
  font-weight: 800;
  font-stretch: 125%;
  line-height: 1;
  letter-spacing: 0;
  color: var(--text);
  text-shadow: none;
}

:root[data-edition='cinema'] .guest--spot .also {
  justify-items: stretch;
  max-width: none;
  margin-top: 26px;
  gap: 10px;
}

:root[data-edition='cinema'] .guest--spot .also__cap {
  font-size: 0.65rem;
  font-weight: 600;
  font-stretch: 125%;
  letter-spacing: 0.2em;
  color: var(--text-dim);
  text-align: left;
}

:root[data-edition='cinema'] .guest--spot .also__more {
  gap: 10px;
  min-height: 52px;
  margin-top: 6px;
  border-radius: 16px;
  border: 1px solid var(--spot-line-strong);
  background: var(--spot-glass);
  color: var(--text);
  font-size: 1rem;
  font-weight: 600;
}

/* The email box takes the list's width and spacing in this look, as everything on the screen does. */
:root[data-edition='cinema'] .guest--spot .rated-email {
  max-width: none;
  margin-top: 26px;
  gap: 10px;
}

:root[data-edition='cinema'] .guest--spot .rated-email .field__hint {
  font-size: 0.8rem;
  line-height: 1.5;
  color: var(--text-dim);
}

:root[data-edition='cinema'] .guest--spot .verdict > .thanks__email-done {
  max-width: none;
  justify-self: stretch;
  text-align: left;
}

:root[data-edition='cinema'] .guest--spot .rate-nope__text {
  padding: 22px 20px;
  border-radius: 20px;
  border: 1px solid var(--spot-line);
  background: var(--spot-glass);
  color: var(--text-dim);
}

/* The links page's card: the same glass as the box above, the button the rating card's own. */
:root[data-edition='cinema'] .guest--spot .rated-link {
  margin-top: 8px;
  padding: 24px 20px;
  border-radius: 20px;
  border: 1px solid var(--spot-line);
  background: var(--spot-glass);
}

:root[data-edition='cinema'] .guest--spot .rated-link__text {
  color: var(--text);
}

/* ------------------------------------------------------------ the ranked list
 *
 * Rows of glass, the place in the accent down the left, the score and its star down the right.
 * The first film on the public page is the featured ticket: why it is first, its name set large,
 * and its score on the stub.
 */
:root[data-edition='cinema'] .guest--spot .rated-list {
  gap: 8px;
}

:root[data-edition='cinema'] .guest--spot .rated-list__film {
  align-items: center;
  column-gap: 14px;
  row-gap: 2px;
  min-height: 58px;
  padding: 10px 16px 10px 14px;
  border: 1px solid var(--spot-line);
  border-radius: 16px;
  background: var(--spot-glass);
}

:root[data-edition='cinema'] .guest--spot .rated-list--ranked .rated-list__film::before {
  min-width: 20px;
  font-size: 0.9375rem;
  font-weight: 800;
  font-stretch: 125%;
  text-align: center;
  color: var(--accent);
}

:root[data-edition='cinema'] .guest--spot .rated-list__name {
  font-size: 0.9375rem;
  font-weight: 600;
  line-height: 1.25;
  color: var(--text);
}

:root[data-edition='cinema'] .guest--spot .rated-list--ranked .rated-list__count {
  grid-column: 2;
}

:root[data-edition='cinema'] .guest--spot .rated-list__count {
  font-size: 0.75rem;
  color: var(--text-faint);
}

:root[data-edition='cinema'] .guest--spot .rated-list--ranked .rated-list__score {
  grid-column: 3;
  grid-row: 1 / span 2;
}

:root[data-edition='cinema'] .guest--spot .rated-list__score {
  font-size: 1rem;
  font-weight: 700;
  font-stretch: 112%;
  color: var(--text);
}

:root[data-edition='cinema'] .guest--spot .rated-list__star {
  color: var(--star-gold);
}

:root[data-edition='cinema'] .guest--spot .rated-list__film--top {
  grid-template-columns: minmax(0, 1fr) 112px;
  grid-template-areas:
    'why stub'
    'name stub'
    'count stub'
    'rising stub';
  align-content: center;
  align-items: center;
  justify-items: start;
  row-gap: 6px;
  column-gap: 0;
  min-height: 156px;
  margin-bottom: 6px;
  padding: 18px 0 18px 18px;
  border: 0;
  border-radius: 20px;
  background: linear-gradient(135deg, var(--spot-ticket-hi) 0%, var(--spot-ticket-lo) 100%);
  box-shadow: inset 0 1px 0 var(--spot-rim);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  -webkit-mask:
    radial-gradient(circle 10px at calc(100% - 112px) 0, transparent 97%, #000 100%) 0 0 / 100% 51% no-repeat,
    radial-gradient(circle 10px at calc(100% - 112px) 100%, transparent 97%, #000 100%) 0 100% / 100% 51% no-repeat;
  mask:
    radial-gradient(circle 10px at calc(100% - 112px) 0, transparent 97%, #000 100%) 0 0 / 100% 51% no-repeat,
    radial-gradient(circle 10px at calc(100% - 112px) 100%, transparent 97%, #000 100%) 0 100% / 100% 51% no-repeat;
}

/* Its place, as a lit disc at the start of the line that says why it is first. */
:root[data-edition='cinema'] .guest--spot .rated-list--ranked .rated-list__film--top::before {
  grid-area: why;
  display: grid;
  place-items: center;
  width: 22px;
  height: 22px;
  min-width: 0;
  border-radius: 50%;
  background: var(--accent-fill);
  color: var(--accent-ink);
  font-size: 0.75rem;
}

:root[data-edition='cinema'] .guest--spot .rated-list__why {
  grid-area: why;
  padding-left: 30px;
  font-size: 0.625rem;
  font-weight: 600;
  font-stretch: 125%;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--text-dim);
}

:root[data-edition='cinema'] .guest--spot .rated-list__film--top .rated-list__name {
  grid-area: name;
  padding-right: 16px;
  font-size: 1.3rem;
  font-weight: 700;
  font-stretch: 125%;
  line-height: 1.12;
  letter-spacing: -0.01em;
}

:root[data-edition='cinema'] .guest--spot .rated-list--ranked .rated-list__film--top .rated-list__count {
  grid-area: count;
  font-size: 0.78rem;
  color: var(--text-dim);
}

:root[data-edition='cinema'] .guest--spot .rated-list__rising {
  grid-area: rising;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 26px;
  padding: 0 10px;
  border-radius: 13px;
  border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--text);
}

:root[data-edition='cinema'] .guest--spot .rated-list__rising::before {
  content: '';
  width: 14px;
  height: 14px;
  flex: none;
  background: var(--accent);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 16l5-5 4 4 7-7 M14 8h6v6'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 16l5-5 4 4 7-7 M14 8h6v6'/%3E%3C/svg%3E") center / contain no-repeat;
}

:root[data-edition='cinema'] .guest--spot .rated-list__stub {
  position: relative;
  grid-area: stub;
  align-self: stretch;
  justify-self: stretch;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
}

:root[data-edition='cinema'] .guest--spot .rated-list__stub::before {
  content: '';
  position: absolute;
  top: -2px;
  bottom: -2px;
  left: 0;
  border-left: 1.5px dashed var(--spot-dash);
}

/* The number over its star: the text node and the star become two lines of one column. */
:root[data-edition='cinema'] .guest--spot .rated-list__stub .rated-list__score {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  font-size: 2.375rem;
  font-weight: 800;
  font-stretch: 125%;
  line-height: 1;
}

:root[data-edition='cinema'] .guest--spot .rated-list__stub .rated-list__star {
  font-size: 1.375rem;
  text-shadow: 0 0 8px color-mix(in srgb, var(--star-gold) 50%, transparent);
}

:root[data-edition='cinema'] .guest--spot .rated-list__of {
  font-size: 0.6875rem;
  color: var(--text-faint);
}

/* ------------------------------------------------------------ community ratings
 *
 * Which cinema, as a line over the heading rather than a chip under it; the rules under the
 * heading; the featured ticket; the rest; the method. On a computer — it is also a link from the
 * cinema's own site — the ticket takes the left and the rest sit in two columns beside it.
 */
:root[data-edition='cinema'] .guest--spot .guest-hero--rated .guest-hero__place {
  justify-self: center;
  margin-top: 30px;
}

:root[data-edition='cinema'] .guest--spot .guest-hero--rated .guest-hero__where {
  padding: 0;
  border: 0;
  background: none;
  font-size: 0.6875rem;
  font-weight: 600;
  font-stretch: 125%;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--text-dim);
}

:root[data-edition='cinema'] .guest--spot .guest-hero--rated .guest-hero__pin {
  display: none;
}

:root[data-edition='cinema'] .guest--spot .guest-hero--rated h1 {
  margin-top: 10px;
}

:root[data-edition='cinema'] .guest--spot .rated {
  justify-items: stretch;
  gap: 0;
  padding: 10px 0 0;
}

:root[data-edition='cinema'] .guest--spot .rated .rated-list {
  max-width: none;
  margin-top: 24px;
}

:root[data-edition='cinema'] .guest--spot .rated__eyebrow {
  font-size: 0.78rem;
  letter-spacing: 0;
  text-transform: none;
  color: var(--text-dim);
}

:root[data-edition='cinema'] .guest--spot .rated__foot {
  max-width: none;
  margin: 16px 0 0;
  font-size: 0.78rem;
  color: var(--text-faint);
}

@media (min-width: 900px) {
  :root[data-edition='cinema'] .guest--spot .guest-hero--rated {
    padding: 28px 56px 0;
  }

  :root[data-edition='cinema'] .guest--spot .guest-hero--rated .guest-hero__inner {
    max-width: 1080px;
  }

  :root[data-edition='cinema'] .guest--spot .guest-hero--rated .guest-hero__place {
    margin-top: 34px;
  }

  :root[data-edition='cinema'] .guest--spot .guest-hero--rated .guest-hero__where {
    font-size: 0.8125rem;
    padding: 0;
  }

  :root[data-edition='cinema'] .guest--spot .guest-hero--rated h1 {
    margin: 14px 0 0;
    font-size: 3.75rem;
    font-weight: 800;
    letter-spacing: -0.02em;
  }

  :root[data-edition='cinema'] .guest--spot:has(.rated) .guest-main {
    max-width: 1192px;
    padding: 0 56px 36px;
  }

  :root[data-edition='cinema'] .guest--spot .rated {
    padding-top: 14px;
  }

  :root[data-edition='cinema'] .guest--spot .rated__eyebrow {
    font-size: 0.9375rem;
  }

  :root[data-edition='cinema'] .guest--spot .rated .rated-list {
    max-width: none;
    margin-top: 40px;
    grid-template-columns: minmax(0, 1.15fr) repeat(2, minmax(0, 0.5fr));
    gap: 20px;
  }

  :root[data-edition='cinema'] .guest--spot .rated .rated-list__film {
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-areas:
      'rank score'
      'name name'
      'count count';
    grid-template-rows: auto 1fr auto;
    align-items: end;
    row-gap: 4px;
    min-height: 140px;
    padding: 18px 20px;
    border: 1px solid var(--spot-line);
    border-radius: 20px;
  }

  :root[data-edition='cinema'] .guest--spot .rated .rated-list--ranked .rated-list__film::before {
    grid-area: rank;
    align-self: start;
    justify-self: start;
    font-size: 1rem;
    min-width: 0;
  }

  :root[data-edition='cinema'] .guest--spot .rated .rated-list--ranked .rated-list__score {
    grid-area: score;
    align-self: start;
    justify-self: end;
    font-size: 1.375rem;
    font-weight: 800;
  }

  :root[data-edition='cinema'] .guest--spot .rated .rated-list__name {
    grid-area: name;
    font-size: 1.125rem;
    font-weight: 700;
    font-stretch: 112%;
  }

  :root[data-edition='cinema'] .guest--spot .rated .rated-list--ranked .rated-list__count {
    grid-area: count;
    font-size: 0.8125rem;
  }

  /* The featured one takes the left of the first two rows, wherever the rest end. */
  :root[data-edition='cinema'] .guest--spot .rated .rated-list__film--top {
    grid-column: 1;
    grid-row: 1 / span 2;
    grid-template-columns: minmax(0, 1fr) 190px;
    grid-template-rows: none;
    grid-template-areas:
      'why stub'
      'name stub'
      'count stub'
      'rising stub';
    align-content: center;
    align-items: center;
    row-gap: 10px;
    min-height: 300px;
    margin: 0;
    padding: 32px 0 32px 36px;
    border: 0;
    border-radius: 24px;
    -webkit-mask:
      radial-gradient(circle 13px at calc(100% - 190px) 0, transparent 97%, #000 100%) 0 0 / 100% 51% no-repeat,
      radial-gradient(circle 13px at calc(100% - 190px) 100%, transparent 97%, #000 100%) 0 100% / 100% 51% no-repeat;
    mask:
      radial-gradient(circle 13px at calc(100% - 190px) 0, transparent 97%, #000 100%) 0 0 / 100% 51% no-repeat,
      radial-gradient(circle 13px at calc(100% - 190px) 100%, transparent 97%, #000 100%) 0 100% / 100% 51% no-repeat;
  }

  /* One film and nothing beside it: the ticket takes the row rather than a third of it. */
  :root[data-edition='cinema'] .guest--spot .rated .rated-list__film--top:only-child {
    grid-column: 1 / -1;
    grid-row: auto;
  }

  /* Fewer than four: not enough to fill two rows beside the ticket, so it takes one row, and with
     only one film beside it that film takes the rest of the row rather than a column of it. */
  :root[data-edition='cinema'] .guest--spot .rated .rated-list:not(:has(> :nth-child(4))) .rated-list__film--top {
    grid-row: auto;
  }

  :root[data-edition='cinema'] .guest--spot .rated .rated-list:not(:has(> :nth-child(3))) {
    grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  }

  :root[data-edition='cinema'] .guest--spot .rated .rated-list--ranked .rated-list__film--top::before {
    grid-area: why;
    align-self: center;
    width: 28px;
    height: 28px;
    font-size: 0.875rem;
  }

  :root[data-edition='cinema'] .guest--spot .rated .rated-list__why {
    padding-left: 38px;
    font-size: 0.75rem;
  }

  :root[data-edition='cinema'] .guest--spot .rated .rated-list__film--top .rated-list__name {
    padding-right: 28px;
    font-size: 2.5rem;
    font-weight: 800;
    font-stretch: 125%;
    line-height: 1.05;
    text-wrap: balance;
  }

  :root[data-edition='cinema'] .guest--spot .rated .rated-list--ranked .rated-list__film--top .rated-list__count {
    font-size: 0.9375rem;
  }

  :root[data-edition='cinema'] .guest--spot .rated .rated-list__stub .rated-list__score {
    font-size: 4.75rem;
  }

  :root[data-edition='cinema'] .guest--spot .rated .rated-list__stub .rated-list__star {
    font-size: 1.875rem;
  }

  :root[data-edition='cinema'] .guest--spot .rated .rated-list__of {
    font-size: 0.8125rem;
  }

  /* The base sheet stretches an odd last card across its two columns; here the columns are the
     ticket's neighbours, and a card across them would sit under the ticket's edge. */
  :root[data-edition='cinema'] .guest--spot .rated .rated-list__film:last-child:nth-child(even) {
    grid-column: auto;
  }

  :root[data-edition='cinema'] .guest--spot .rated__foot {
    margin-top: 28px;
    font-size: 0.875rem;
  }
}

/* ------------------------------------------------------------ the tracking page
 *
 * The heading, then the reference as a ticket, then the rail on a card of its own that turns green
 * with the rail when the report is resolved — and the closing line, when there is one, inside the
 * same card under the step it closes. Nothing about what the page says changes.
 */
:root[data-edition='cinema'] .guest--spot.guest--trace #main {
  justify-content: flex-start;
}

/* The page starts under its heading rather than floating in the middle of the screen: the heading
   is above it now, and a report half a screen below its own title reads as two pages. */
:root[data-edition='cinema'] .guest--spot.guest--trace #main::before,
:root[data-edition='cinema'] .guest--spot.guest--trace #main::after {
  display: none;
}

:root[data-edition='cinema'] .guest--spot.guest--trace .guest-hero__where {
  display: none;
}

:root[data-edition='cinema'] .guest--spot.guest--trace #main > .card {
  display: flex;
  flex-direction: column;
  gap: 18px;
  margin-top: 26px;
  padding: 0 0 calc(24px + env(safe-area-inset-bottom, 0px));
  border: 0;
  background: none;
  box-shadow: none;
}

:root[data-edition='cinema'] .guest--spot .trace-card {
  --trace-rail: 26px;
  --trace-orb: 26px;
  --trace-gap: 14px;
  --trace-clear: 4px;
}

:root[data-edition='cinema'] .guest--spot .trace-card > .ticket--stub {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas:
    'label copy'
    'ref copy'
    'what copy';
  align-content: center;
  align-items: center;
  justify-items: start;
  gap: 5px 10px;
  min-height: 104px;
  margin: 0;
  padding: 14px 12px 14px calc(var(--stub) + 18px);
  border: 0;
  border-radius: 18px;
  background: linear-gradient(135deg, var(--spot-ticket-hi) 0%, var(--spot-ticket-lo) 100%);
  box-shadow: inset 0 1px 0 var(--spot-rim);
  text-align: left;
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  -webkit-mask:
    radial-gradient(circle 10px at var(--stub) 0, transparent 97%, #000 100%) 0 0 / 100% 51% no-repeat,
    radial-gradient(circle 10px at var(--stub) 100%, transparent 97%, #000 100%) 0 100% / 100% 51% no-repeat;
  mask:
    radial-gradient(circle 10px at var(--stub) 0, transparent 97%, #000 100%) 0 0 / 100% 51% no-repeat,
    radial-gradient(circle 10px at var(--stub) 100%, transparent 97%, #000 100%) 0 100% / 100% 51% no-repeat;
}

:root[data-edition='cinema'] .guest--spot .trace-card > .ticket--stub > .guest-hero__stub {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
}

:root[data-edition='cinema'] .guest--spot .trace-card > .ticket--stub .thanks__ref {
  font-size: clamp(0.875rem, 4vw, 1.0625rem);
  font-stretch: 125%;
  letter-spacing: 0.03em;
}

:root[data-edition='cinema'] .guest--spot .trace-card > .ticket--stub .ticket__label {
  letter-spacing: 0.16em;
}

:root[data-edition='cinema'] .guest--spot .ticket__what {
  grid-area: what;
  max-width: 100%;
  font-size: 0.8125rem;
  line-height: 1.35;
  color: var(--text-dim);
  overflow-wrap: anywhere;
}

/*
 * Copy, as a square with the drawing for it: the ticket beside it is narrow, and the reference is
 * the thing that must fit. The word stays in the button for a screen reader and for the moment it
 * says "Copied" — which the drawing says too, by becoming a tick.
 */
/* Flex rather than a grid: the word is a second item, and a grid gave it a row of its own under
   the drawing, which pushed the drawing above the middle. */
:root[data-edition='cinema'] .guest--spot .trace-card > .ticket--stub .ticket__copy {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  min-height: 44px;
  padding: 0;
  border-radius: 12px;
  font-size: 0;
}

:root[data-edition='cinema'] .guest--spot .trace-card > .ticket--stub .ticket__copy::before {
  content: '';
  width: 18px;
  height: 18px;
  background: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 9h10.5v10.5H9z M5 15V4.5h10.5'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 9h10.5v10.5H9z M5 15V4.5h10.5'/%3E%3C/svg%3E") center / contain no-repeat;
}

:root[data-edition='cinema'] .guest--spot .trace-card > .ticket--stub .ticket__copy--done::before {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12.5l4.3 4.3L19 7.3'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12.5l4.3 4.3L19 7.3'/%3E%3C/svg%3E");
}

:root[data-edition='cinema'] .guest--spot .trace-card .trace {
  margin: 0;
  padding: 6px 18px;
  border: 1px solid var(--spot-line);
  border-radius: 22px;
  background: var(--spot-glass);
}

:root[data-edition='cinema'] .guest--spot .trace-card .trace--complete {
  border-color: color-mix(in srgb, var(--ok) 30%, transparent);
  box-shadow: 0 0 40px color-mix(in srgb, var(--ok) 10%, transparent);
}

/* With a closing line under it the glow moves down to the line's edge; along the seam it would
   draw a band straight across the card. */
:root[data-edition='cinema'] .guest--spot .trace-card .trace:has(+ .trace__outcome:not([hidden])) {
  box-shadow: none;
}

/* The closing line continues the rail's card rather than starting one of its own. */
:root[data-edition='cinema'] .guest--spot .trace-card .trace:has(+ .trace__outcome:not([hidden])) {
  padding-bottom: 2px;
  border-bottom: 0;
  border-radius: 22px 22px 0 0;
}

:root[data-edition='cinema'] .guest--spot .trace-card .trace + .trace__outcome {
  margin: -18px 0 0;
  padding: 4px 18px 18px calc(18px + var(--trace-rail) + var(--trace-gap));
  border: 1px solid color-mix(in srgb, var(--ok) 30%, transparent);
  border-top: 0;
  border-radius: 0 0 22px 22px;
  background: var(--spot-glass);
  box-shadow: 0 16px 40px -20px color-mix(in srgb, var(--ok) 18%, transparent);
}

:root[data-edition='cinema'] .guest--spot .trace__outcomeLabel {
  font-size: 0.625rem;
  font-weight: 600;
  font-stretch: 125%;
  letter-spacing: 0.2em;
  color: var(--ok);
}

:root[data-edition='cinema'] .guest--spot .trace__outcomeText {
  margin-top: 6px;
  font-size: 0.9375rem;
  line-height: 1.5;
}

:root[data-edition='cinema'] .guest--spot .trace__orb {
  border-color: var(--spot-line-strong);
  background: transparent;
}

:root[data-edition='cinema'] .guest--spot .trace__step--done .trace__orb {
  border-color: var(--trace-ink);
  background: var(--trace-ink);
}

/* A tick in every step that has happened, cut out of the orb in the page's own ground. Above the
   halo, which would otherwise wash over it once a pulse. */
:root[data-edition='cinema'] .guest--spot .trace__step--done .trace__orb::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  margin: auto;
  width: 14px;
  height: 14px;
  background: var(--bg);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12.5l4.3 4.3L19 7.3'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12.5l4.3 4.3L19 7.3'/%3E%3C/svg%3E") center / contain no-repeat;
}

/* The pulse starts inside the orb and leaves it as a ring, rather than a halo two and a half
   times the size of an orb this much bigger than the one it was drawn for. */
:root[data-edition='cinema'] .guest--spot .trace__step--current .trace__orb::after {
  inset: 6px;
}

:root[data-edition='cinema'] .guest--spot .trace__what {
  font-size: 0.9375rem;
  color: var(--text);
}

:root[data-edition='cinema'] .guest--spot .trace__step:not(.trace__step--done) .trace__what {
  color: var(--text-faint);
}

:root[data-edition='cinema'] .guest--spot .trace__when {
  font-size: 0.78rem;
  color: var(--text-dim);
}

:root[data-edition='cinema'] .guest--spot .trace-card > .field__hint {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  margin: 0;
  font-size: 0.8125rem;
  line-height: 1.5;
  color: var(--text-dim);
}

:root[data-edition='cinema'] .guest--spot .trace-card > .field__hint::before {
  content: '';
  width: 16px;
  height: 16px;
  flex: none;
  margin-top: 2px;
  background: var(--accent);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3.5l7 2.8v5.2c0 4.4-2.9 7.9-7 9.5-4.1-1.6-7-5.1-7-9.5V6.3z M9 12l2 2 4-4'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3.5l7 2.8v5.2c0 4.4-2.9 7.9-7 9.5-4.1-1.6-7-5.1-7-9.5V6.3z M9 12l2 2 4-4'/%3E%3C/svg%3E") center / contain no-repeat;
}

:root[data-edition='cinema'] .guest--spot.guest--trace #gone {
  margin-top: 26px;
  border: 1px solid var(--spot-line);
  background: var(--spot-glass);
}

/* =================================================================== Aurora
 *
 * Foyer's guest pages: the business's own colour as soft light drifting behind frosted glass, like
 * a lit window at night. A card saying where the guest is, the issue types as tiles each with its
 * drawing in a small glowing circle, everything reading from the left, and Manrope throughout.
 * When a report is resolved, the light turns green.
 *
 * EVERY RULE BELOW IS UNDER `:root[data-edition='standalone'] .guest--spot`, exactly as Spotlight's
 * are under the cinema stamp, and for the same promise the other way round: Foyer was never to
 * wear a cinema's look, and CineScope's guests are never to see this one. The pages, the scripts
 * and the steps are shared (see `premium` in js/report.js); the look is the edition's. Two
 * sections rather than one with the differences picked out, so neither can be changed by an edit
 * made for the other — CineScope's is live and was asked to stay exactly as it is.
 *
 * NOTHING HERE NAMES A COLOUR the theme does not own. The light is the theme's accent — which for
 * "Your brand" is the circuit's own colour, already moved in lightness until it reads on the
 * ground (see brandTokens in js/brand-colour.js), so a dark navy or a pale gold still glows. The
 * glass is a veil of the theme's ink on a dark ground and of white on a light one.
 */

/*
 * Manrope, self-hosted, for the reason Archivo is: the CSP admits fonts from this origin only.
 * One variable file per script range, weight 400 to 800, fetched only by a page that sets text in
 * it — which is a Foyer guest page, and nothing else. The licence is beside the files (OFL).
 */
@font-face {
  font-family: 'Manrope';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url('/fonts/manrope-latin-v20.woff2') format('woff2');
  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: 'Manrope';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url('/fonts/manrope-latin-ext-v20.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/*
 * The pieces only Aurora draws, taken away everywhere else by a bare rule and brought back under
 * the stamp — the beam's arrangement. The light is markup on all five guest pages; the caption over
 * a thank-you code is markup on the two confirmation pages that did not carry one.
 */
.aurora {
  display: none;
}

.thanks__code-label--aurora {
  display: none;
}

/* ---------------------------------------------------------------- the tokens */

:root[data-edition='standalone'] .guest--spot {
  /* The glass: a card, a tile, a well, and the pane under a pointer. */
  --aura-glass: color-mix(in srgb, var(--text) 6%, transparent);
  --aura-tile: color-mix(in srgb, var(--text) 5.5%, transparent);
  --aura-well: color-mix(in srgb, var(--text) 4%, transparent);
  --aura-hover: color-mix(in srgb, var(--text) 9%, transparent);
  --aura-button: color-mix(in srgb, var(--text) 8%, transparent);
  /* Its edges: the light caught along the top, the hairline all round, and a drop beneath. */
  --aura-rim: color-mix(in srgb, var(--text) 16%, transparent);
  --aura-rim-soft: color-mix(in srgb, var(--text) 8%, transparent);
  --aura-edge: color-mix(in srgb, var(--text) 8%, transparent);
  --aura-line: color-mix(in srgb, var(--text) 10%, transparent);
  --aura-ring: color-mix(in srgb, var(--text) 18%, transparent);
  --aura-drop: 0 30px 60px -34px rgb(0 0 0 / 80%);
  /* The accent as the glow in a drawing's circle, the ring round the place's square, its halo. */
  --aura-halo: color-mix(in srgb, var(--accent) 16%, transparent);
  --aura-halo-ring: color-mix(in srgb, var(--accent) 45%, transparent);
  --aura-halo-soft: color-mix(in srgb, var(--accent) 22%, transparent);
  /* The light itself: a bright version of the colour, a deep one, and a neighbour (see below). */
  --aura-glow-1: color-mix(in srgb, var(--accent) 50%, transparent);
  --aura-glow-2: color-mix(in srgb, color-mix(in srgb, var(--accent) 45%, black) 60%, transparent);
  --aura-glow-3: color-mix(in srgb, var(--accent) 24%, transparent);
  /* The one lit control on a screen: the colour, lighter at the top, with the ground's own ink on
     it. The accent is the colour tuned to be READ on a dark ground, so the ground reads on it. */
  --aura-fill: linear-gradient(180deg, color-mix(in srgb, var(--accent) 70%, white) 0%, var(--accent) 100%);
  --aura-on-fill: var(--bg);
  --aura-fill-glow: color-mix(in srgb, var(--accent) 60%, transparent);
  --aura-fill-text: linear-gradient(180deg, color-mix(in srgb, var(--accent) 45%, white) 0%, var(--accent) 100%);
  --aura-pad: 20px;
  font-family: 'Manrope', var(--font);
}

/*
 * A light ground wants the glass the other way round — a veil of white rather than of ink — and
 * gentler light, because half-strength accent on a pale page is a stain rather than a glow. The lit
 * control takes the theme's own button pair there, whose ink is chosen to read on its fill.
 *
 * Three ways a guest page can be light, and all three are here: Light, "Your brand" on a light
 * base, and System on a device set to light.
 */
:root[data-edition='standalone'][data-theme='light'] .guest--spot,
:root[data-edition='standalone'][data-theme='custom'][data-brand-base='light'] .guest--spot {
  --aura-glass: color-mix(in srgb, white 58%, transparent);
  --aura-tile: color-mix(in srgb, white 50%, transparent);
  --aura-well: color-mix(in srgb, white 38%, transparent);
  --aura-hover: color-mix(in srgb, white 82%, transparent);
  --aura-button: color-mix(in srgb, white 70%, transparent);
  --aura-rim: color-mix(in srgb, white 95%, transparent);
  --aura-rim-soft: color-mix(in srgb, white 80%, transparent);
  --aura-edge: color-mix(in srgb, var(--text) 10%, transparent);
  --aura-line: color-mix(in srgb, var(--text) 13%, transparent);
  --aura-ring: color-mix(in srgb, var(--text) 24%, transparent);
  --aura-drop: 0 24px 50px -30px rgb(76 79 105 / 35%);
  --aura-halo: color-mix(in srgb, var(--accent) 12%, transparent);
  --aura-halo-ring: color-mix(in srgb, var(--accent) 35%, transparent);
  --aura-halo-soft: color-mix(in srgb, var(--accent) 14%, transparent);
  --aura-glow-1: color-mix(in srgb, var(--accent) 30%, transparent);
  --aura-glow-2: color-mix(in srgb, var(--accent) 16%, transparent);
  --aura-glow-3: color-mix(in srgb, var(--accent) 14%, transparent);
  --aura-fill: linear-gradient(180deg, color-mix(in srgb, var(--accent-fill) 90%, white) 0%, var(--accent-fill) 100%);
  --aura-on-fill: var(--accent-ink);
  --aura-fill-glow: color-mix(in srgb, var(--accent-fill) 45%, transparent);
  --aura-fill-text: linear-gradient(180deg, var(--accent) 0%, color-mix(in srgb, var(--accent) 78%, black) 100%);
}

@media (prefers-color-scheme: light) {
  :root[data-edition='standalone'][data-theme='system'] .guest--spot {
    --aura-glass: color-mix(in srgb, white 58%, transparent);
    --aura-tile: color-mix(in srgb, white 50%, transparent);
    --aura-well: color-mix(in srgb, white 38%, transparent);
    --aura-hover: color-mix(in srgb, white 82%, transparent);
    --aura-button: color-mix(in srgb, white 70%, transparent);
    --aura-rim: color-mix(in srgb, white 95%, transparent);
    --aura-rim-soft: color-mix(in srgb, white 80%, transparent);
    --aura-edge: color-mix(in srgb, var(--text) 10%, transparent);
    --aura-line: color-mix(in srgb, var(--text) 13%, transparent);
    --aura-ring: color-mix(in srgb, var(--text) 24%, transparent);
    --aura-drop: 0 24px 50px -30px rgb(76 79 105 / 35%);
    --aura-halo: color-mix(in srgb, var(--accent) 12%, transparent);
    --aura-halo-ring: color-mix(in srgb, var(--accent) 35%, transparent);
    --aura-halo-soft: color-mix(in srgb, var(--accent) 14%, transparent);
    --aura-glow-1: color-mix(in srgb, var(--accent) 30%, transparent);
    --aura-glow-2: color-mix(in srgb, var(--accent) 16%, transparent);
    --aura-glow-3: color-mix(in srgb, var(--accent) 14%, transparent);
    --aura-fill: linear-gradient(180deg, color-mix(in srgb, var(--accent-fill) 90%, white) 0%, var(--accent-fill) 100%);
    --aura-on-fill: var(--accent-ink);
    --aura-fill-glow: color-mix(in srgb, var(--accent-fill) 45%, transparent);
    --aura-fill-text: linear-gradient(180deg, var(--accent) 0%, color-mix(in srgb, var(--accent) 78%, black) 100%);
  }
}

/*
 * The third glow is the colour's neighbour, a little round the wheel — what keeps a single-hue
 * light from reading as a flat wash. Only where the browser can turn a hue; anywhere else it is
 * a fainter version of the same colour, which still looks deliberate.
 *
 * Behind @supports rather than written after the plain token, because a custom property is only
 * checked when it is USED: an unsupported value would not fall back to the line above it, it
 * would make the glow's background nothing at all.
 */
@supports (color: hsl(from red calc(h + 30) s l / 0.24)) {
  :root[data-edition='standalone'] .guest--spot {
    --aura-glow-3: hsl(from var(--accent) calc(h + 30) s l / 0.26);
  }

  :root[data-edition='standalone'][data-theme='light'] .guest--spot,
  :root[data-edition='standalone'][data-theme='custom'][data-brand-base='light'] .guest--spot {
    --aura-glow-3: hsl(from var(--accent) calc(h + 30) s l / 0.16);
  }

  @media (prefers-color-scheme: light) {
    :root[data-edition='standalone'][data-theme='system'] .guest--spot {
      --aura-glow-3: hsl(from var(--accent) calc(h + 30) s l / 0.16);
    }
  }
}

/* The page's own side padding, so the header lines up with the column under it. */
@media (max-width: 560px) {
  :root[data-edition='standalone'] .guest--spot {
    --aura-pad: 16px;
  }
}

/* ----------------------------------------------------------------- the light
 *
 * Three soft glows at the top of the page, blurred until they have no edge, each drifting on its
 * own slow cycle — twenty-two, twenty-seven and thirty-one seconds, so the three never line up
 * the same way twice in a minute. Under them a fade into the page's own ground, so the light ends
 * rather than stopping.
 *
 * Only `transform` moves, which the compositor does on its own. On a phone set to reduce motion
 * all three hold still where they are.
 *
 * Positioned against the page rather than the window, as the beam is: it lights the top of the
 * document and scrolls away with it. The full width of the page, with the glows placed on the
 * column the content sits in (`--aura-col`), so on a computer the light spreads past the column's
 * edges instead of stopping at them. Clipped to its own box, or the blur would let the page
 * scroll sideways.
 *
 * The fade is a MASK on the glows rather than a veil of the page's colour laid over them: the page
 * is painted by the theme with a tint of its own, and a veil of the flat ground drew a darker box
 * wherever the two differed.
 */
:root[data-edition='standalone'] .guest--spot .aurora {
  --aura-col: min(100%, 640px);
  display: block;
  position: absolute;
  top: 0;
  left: 0;
  z-index: -1;
  width: 100%;
  height: 700px;
  overflow: clip;
  pointer-events: none;
  -webkit-mask-image: linear-gradient(180deg, #000 30%, transparent 92%);
  mask-image: linear-gradient(180deg, #000 30%, transparent 92%);
  transition: opacity 600ms var(--ease-arrive);
}

:root[data-edition='standalone'] .guest--spot .aurora__glow {
  position: absolute;
  border-radius: 50%;
  will-change: transform;
}

/* Placed as fractions of the column, so the same light falls on a phone and on a desktop. */
:root[data-edition='standalone'] .guest--spot .aurora__glow--1 {
  left: calc(50% - 0.17 * var(--aura-col));
  top: -170px;
  width: calc(1.03 * var(--aura-col));
  height: 380px;
  background: var(--aura-glow-1);
  filter: blur(64px);
  animation: aura-drift 22s ease-in-out infinite alternate;
}

:root[data-edition='standalone'] .guest--spot .aurora__glow--2 {
  left: calc(50% - 0.94 * var(--aura-col));
  top: 20px;
  width: calc(0.97 * var(--aura-col));
  height: 330px;
  background: var(--aura-glow-2);
  filter: blur(72px);
  animation: aura-sway 27s ease-in-out infinite alternate;
}

:root[data-edition='standalone'] .guest--spot .aurora__glow--3 {
  left: calc(50% - 0.4 * var(--aura-col));
  top: 150px;
  width: calc(0.77 * var(--aura-col));
  height: 200px;
  background: var(--aura-glow-3);
  filter: blur(70px);
  animation: aura-drift 31s ease-in-out infinite alternate-reverse;
}

/* Quieter while somebody is writing: the words they are typing are the page then. */
:root[data-edition='standalone'] .guest--spot[data-stage='details'] .aurora {
  opacity: 0.72;
}

/* Resolved turns the light green, as it turns the rail — the one state a guest was hoping for. */
:root[data-edition='standalone'] .guest--spot.guest--resolved .aurora {
  --aura-glow-1: color-mix(in srgb, var(--ok) 42%, transparent);
  --aura-glow-2: color-mix(in srgb, color-mix(in srgb, var(--ok) 45%, black) 55%, transparent);
  --aura-glow-3: color-mix(in srgb, var(--ok) 18%, transparent);
}

/* Gentler on a light ground, for the reason the colour's own light is. */
:root[data-edition='standalone'][data-theme='light'] .guest--spot.guest--resolved .aurora,
:root[data-edition='standalone'][data-theme='custom'][data-brand-base='light'] .guest--spot.guest--resolved .aurora {
  --aura-glow-1: color-mix(in srgb, var(--ok) 26%, transparent);
  --aura-glow-2: color-mix(in srgb, var(--ok) 14%, transparent);
  --aura-glow-3: color-mix(in srgb, var(--ok) 10%, transparent);
}

@media (prefers-color-scheme: light) {
  :root[data-edition='standalone'][data-theme='system'] .guest--spot.guest--resolved .aurora {
    --aura-glow-1: color-mix(in srgb, var(--ok) 26%, transparent);
    --aura-glow-2: color-mix(in srgb, var(--ok) 14%, transparent);
    --aura-glow-3: color-mix(in srgb, var(--ok) 10%, transparent);
  }
}

@keyframes aura-drift {
  from { transform: translate(0, 0) scale(1); }
  to { transform: translate(-46px, 34px) scale(1.14); }
}

@keyframes aura-sway {
  from { transform: translate(0, 0) scale(1.06); }
  to { transform: translate(52px, -22px) scale(0.94); }
}

@media (prefers-reduced-motion: reduce) {
  :root[data-edition='standalone'] .guest--spot .aurora__glow {
    animation: none;
  }
}

/*
 * THE GRAIN the glass frosts: a whisper of noise over the whole window, behind the light and
 * everything on it. It is what stops the dark gradients and veils banding on OLED, and what the
 * glass has to blur — a frosted pane over a perfectly flat ground reads as a flat pane.
 *
 * On body's own ::before, fixed and window-sized at `z-index: -1` — first in the body, so the light
 * paints over it — because body may not paint a background of its own (see the root's ground at
 * the top of this file). It rode the page-wide dust layer until that went; the guest pages have no
 * dock, whose veils are the other thing on body's pseudo-elements. On screen only: a printout has
 * no glass.
 */
@media screen {
  :root[data-edition='standalone'] body.guest--spot::before {
    content: '';
    position: fixed;
    inset: 0;
    z-index: -1;
    pointer-events: none;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.05'/%3E%3C/svg%3E");
  }
}

/* --------------------------------------------------------------- the header
 *
 * One row for the mark and the language, then the card saying where they are, then the question —
 * laid out by grid area rather than by the order they sit in the markup, which is shared.
 */
:root[data-edition='standalone'] .guest--spot .guest-hero {
  overflow: visible;
  padding: 20px var(--aura-pad) 0;
  background: none;
  border-bottom: 0;
}

:root[data-edition='standalone'] .guest--spot .guest-hero::after {
  display: none;
}

:root[data-edition='standalone'] .guest--spot .guest-hero__inner {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas:
    'mark lang'
    'ticket ticket'
    'title title'
    'where where'
    'record record';
  align-items: center;
  max-width: 600px;
  min-height: 40px;
}

:root[data-edition='standalone'] .guest--spot .guest-hero .wordmark,
:root[data-edition='standalone'] .guest--spot .guest-hero .brand-logo {
  grid-area: mark;
  justify-self: start;
}

/* The mark keeps its own letters, as it does under the beam: it is the product's name set in the
   app's face, and in Manrope it would be a different logo from the one on every other screen. */
:root[data-edition='standalone'] .guest--spot .guest-hero .wordmark {
  font-family: var(--font);
}

:root[data-edition='standalone'] .guest--spot .guest-hero h1 {
  grid-area: title;
  margin: 30px 2px 0;
  font-size: clamp(1.875rem, 1.35rem + 2.3vw, 2.25rem);
  font-weight: 760;
  line-height: 1.08;
  letter-spacing: -0.03em;
  text-wrap: balance;
}

/* The language, as a pill in the corner. "Language" stays for a screen reader — the select is
   named by its label — and leaves the row for anyone who can see the pill. */
:root[data-edition='standalone'] .guest--spot .guest-lang {
  grid-area: lang;
  margin: 0;
}

:root[data-edition='standalone'] .guest--spot .guest-lang__label {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

:root[data-edition='standalone'] .guest--spot .guest-lang__select {
  -webkit-appearance: none;
  appearance: none;
  height: 40px;
  padding: 0 34px 0 16px;
  border: 0;
  border-radius: 20px;
  background-color: var(--aura-tile);
  /* A chevron drawn in the text's own colour, so it follows the theme with the words. */
  background-image:
    linear-gradient(45deg, transparent 50%, currentColor 50%),
    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position:
    calc(100% - 19px) 52%,
    calc(100% - 14px) 52%;
  background-size: 5px 5px;
  background-repeat: no-repeat;
  box-shadow: inset 0 0 0 1px var(--aura-line);
  color: var(--text);
  font: inherit;
  font-size: 0.8125rem;
  font-weight: 600;
  cursor: pointer;
}

/* What this place's own record says: a line under the question, after a point of the light. */
:root[data-edition='standalone'] .guest--spot .guest-hero__record,
:root[data-edition='standalone'] .guest--spot .thanks__record {
  display: flex;
  align-items: center;
  gap: 9px;
  font-size: 0.84rem;
  font-weight: 500;
  line-height: 1.4;
  color: var(--text-dim);
  text-align: left;
}

:root[data-edition='standalone'] .guest--spot .guest-hero__record {
  grid-area: record;
  margin: 14px 2px 0;
}

:root[data-edition='standalone'] .guest--spot .guest-hero__record::before,
:root[data-edition='standalone'] .guest--spot .thanks__record::before {
  content: '';
  width: 6px;
  height: 6px;
  flex: none;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 10px var(--accent);
}

/* ------------------------------------------------------ the card saying where
 *
 * Until the scan is read, the plain chip — "Checking your QR code…", or a refusal — and once there
 * is a place, one pane of glass: a glowing square with the room's number or the place's drawing,
 * and beside it where they are. The same elements as CineScope's ticket; only the shape differs.
 */
:root[data-edition='standalone'] .guest--spot .guest-hero__place {
  grid-area: ticket;
  margin-top: 26px;
}

:root[data-edition='standalone'] .guest--spot .guest-hero__where {
  padding: 8px 15px 8px 12px;
  border: 0;
  border-radius: 999px;
  background: var(--aura-tile);
  box-shadow: inset 0 0 0 1px var(--aura-edge);
  font-size: 0.84rem;
  font-weight: 600;
}

/* A chip with nothing in it says nothing: the rating pages leave it empty when they cannot say. */
:root[data-edition='standalone'] .guest--spot .guest-hero__place:has(#where-text:empty) {
  display: none;
}

:root[data-edition='standalone'] .guest--spot .guest-hero__place--ticket {
  flex-wrap: nowrap;
  align-items: center;
  gap: 16px;
  padding: 14px 16px 14px 14px;
  border-radius: 26px;
  background: var(--aura-glass);
  box-shadow:
    inset 0 1px 0 var(--aura-rim),
    inset 0 0 0 1px var(--aura-edge),
    var(--aura-drop);
  -webkit-backdrop-filter: blur(24px) saturate(160%);
  backdrop-filter: blur(24px) saturate(160%);
}

/* The chip's words are the square and the lines beside it now. Still in the document — the page
   reads the place from it and a refusal still writes into it. */
:root[data-edition='standalone'] .guest--spot .guest-hero__place--ticket .guest-hero__where {
  display: none;
}

/* The square: grows sideways for a long room number rather than squeezing it, and is square for
   anything short. */
:root[data-edition='standalone'] .guest--spot .guest-hero__stub {
  min-width: 68px;
  max-width: 45%;
  height: 68px;
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  padding: 0 10px;
  border-radius: 20px;
  background: var(--aura-button);
  box-shadow:
    inset 0 0 0 1px var(--aura-halo-ring),
    inset 0 0 22px var(--aura-halo-soft),
    0 0 26px var(--aura-halo-soft);
  color: var(--text);
  --glyph: 28px;
}

:root[data-edition='standalone'] .guest--spot .guest-hero__kind {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  /* The letter-spacing trails the last letter, so the same again on the left puts the word in the
     middle. Padding rather than a negative margin on the right: that made the box narrower than
     its own text, and the word was cut to "GUEST ROO…". */
  padding-left: 0.2em;
  font-size: 0.53rem;
  font-weight: 800;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--text-dim);
}

:root[data-edition='standalone'] .guest--spot .guest-hero__number {
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 1.625rem;
  font-weight: 800;
  line-height: 1.05;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}

/* Anywhere that is not a numbered door: its drawing, or its first letter, in the colour. */
:root[data-edition='standalone'] .guest--spot .guest-hero__stub--place {
  color: var(--accent);
}

:root[data-edition='standalone'] .guest--spot .guest-hero__stub--place .guest-hero__kind,
:root[data-edition='standalone'] .guest--spot .guest-hero__stub--place .guest-hero__number {
  display: none;
}

:root[data-edition='standalone'] .guest--spot .guest-hero__stub--place .glyph {
  stroke-width: 1.5;
}

:root[data-edition='standalone'] .guest--spot .guest-hero__stub--place .glyph--letter {
  width: auto;
  height: auto;
  border: 0;
  font-size: 1.75rem;
  font-stretch: 100%;
}

:root[data-edition='standalone'] .guest--spot .guest-hero__here {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 3px;
  margin: 0;
}

:root[data-edition='standalone'] .guest--spot .guest-hero__eyebrow {
  font-size: 0.625rem;
  font-weight: 800;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--text-faint);
}

:root[data-edition='standalone'] .guest--spot .guest-hero__title {
  font-size: 1.0625rem;
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: -0.01em;
  color: var(--text);
  overflow-wrap: break-word;
  text-wrap: balance;
}

/* Balanced, so a line that has to wrap does not leave the last word of a name alone underneath. */
:root[data-edition='standalone'] .guest--spot .guest-hero__meta {
  font-size: 0.84rem;
  font-weight: 500;
  line-height: 1.35;
  color: var(--text-dim);
  text-wrap: balance;
}

/* ------------------------------------------------------------------ the tiles */

/* The full column's width, not its contents': an auto-margined item in the body's flex column
   shrinks to fit what is in it, and the tiles would come out narrower than the card above. */
:root[data-edition='standalone'] .guest--spot #main {
  display: flex;
  flex-direction: column;
  width: 100%;
  padding-bottom: 0;
}

:root[data-edition='standalone'] .guest--spot .guest-form {
  flex: 1 0 auto;
  display: flex;
  flex-direction: column;
  gap: 22px;
  padding-top: 22px;
}

/* The question is the page's heading, so the group's own legend is for a screen reader alone. */
:root[data-edition='standalone'] .guest--spot #category-group > legend {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

/* No numbered steps: the form is two screens now, and each says what it is. */
:root[data-edition='standalone'] .guest--spot .step__title::before {
  display: none;
}

/*
 * Two a row whatever the count, each a drawing beside its name — a business's own types are
 * phrases more often than words, and a phrase reads beside a drawing where it would be squeezed
 * under one. An odd last tile takes the row, so five types never leave a hole.
 */
:root[data-edition='standalone'] .guest--spot .pills__grid--tiles {
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
}

:root[data-edition='standalone'] .guest--spot .pills__grid--tiles > .pill:last-child:nth-child(odd) {
  grid-column: 1 / -1;
}

:root[data-edition='standalone'] .guest--spot .pill .pill__face {
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: flex-start;
  gap: 12px;
  min-height: 66px;
  height: 100%;
  padding: 10px 12px;
  border: 0;
  border-radius: 18px;
  background: var(--aura-tile);
  box-shadow:
    inset 0 1px 0 var(--aura-rim-soft),
    inset 0 0 0 1px var(--aura-edge);
  color: var(--text);
  text-align: left;
  font-size: 0.9063rem;
  font-weight: 600;
  line-height: 1.2;
  --glyph: 20px;
}

/* Fewer, so each is given more room: a restroom's short list. */
:root[data-edition='standalone'] .guest--spot .pills__grid--roomy .pill__face {
  min-height: 74px;
  font-size: 0.9688rem;
}

/* The words and the letter are spans inside the tile, and the base pill rule would make each of
   them a tile of its own. */
:root[data-edition='standalone'] .guest--spot .pill .pill__text {
  display: block;
  min-width: 0;
  min-height: 0;
  height: auto;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  overflow-wrap: break-word;
  hyphens: auto;
}

/* The drawing in a small glowing circle: the stroke in the colour, the circle a breath of it.
   content-box, so the circle is the drawing plus its margin whatever the page's sizing rule. */
:root[data-edition='standalone'] .guest--spot .pill .glyph {
  box-sizing: content-box;
  padding: 9px;
  border-radius: 50%;
  background: var(--aura-halo);
  color: var(--accent);
}

:root[data-edition='standalone'] .guest--spot .pill .glyph--letter {
  display: inline-grid;
  place-items: center;
  box-sizing: border-box;
  width: 38px;
  height: 38px;
  min-height: 0;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--aura-halo);
  color: var(--accent);
  font-size: 1rem;
  font-weight: 800;
  font-stretch: 100%;
}

:root[data-edition='standalone'] .guest--spot .pill:hover .pill__face {
  background: var(--aura-hover);
  color: var(--text);
}

/* The one chosen, when a guest comes back to change it: lit round its edge in the colour. */
:root[data-edition='standalone'] .guest--spot .pill input:checked + .pill__face {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  color: var(--text);
  font-weight: 600;
  box-shadow:
    inset 0 0 0 1px color-mix(in srgb, var(--accent) 60%, transparent),
    0 0 22px color-mix(in srgb, var(--accent) 22%, transparent);
}

:root[data-edition='standalone'] .guest--spot .pill input:focus-visible + .pill__face {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

/* ------------------------------------------------------------------ the steps
 *
 * `data-stage` on <body>, set by report.js: 'choose' shows the tiles and the credit and nothing
 * else; 'details' shows everything but the tiles, under a smaller card; 'sent' puts the card away
 * for the thank-you. No sheet rising over the page, unlike CineScope's: the light is the page's
 * one flourish, and the form is simply the next thing on it. Every field stays in the document
 * throughout, so Change loses nothing anybody typed.
 */
:root[data-edition='standalone'] .guest--spot[data-stage='choose'] .guest-form > :not(#category-step, .guest-send) {
  display: none;
}

:root[data-edition='standalone'] .guest--spot[data-stage='choose'] .guest-send {
  position: static;
  margin: auto 0 0;
  padding: 26px 0 calc(20px + env(safe-area-inset-bottom, 0px));
  background: none;
}

/* The button and the anonymity line wait for the second step, and each is named: the credit is
   under them and stays on screen in every step (see test/brand.test.js). */
:root[data-edition='standalone'] .guest--spot[data-stage='choose'] .guest-send > #submit,
:root[data-edition='standalone'] .guest--spot[data-stage='choose'] .guest-send > .guest-send__note {
  display: none;
}

:root[data-edition='standalone'] .guest--spot[data-stage='details'] #category-step {
  display: none;
}

/* The heading made way for the chosen type: still the page's heading for a screen reader. */
:root[data-edition='standalone'] .guest--spot[data-stage='details'] .guest-hero h1,
:root[data-edition='standalone'] .guest--spot[data-stage='sent'] .guest-hero h1 {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

:root[data-edition='standalone'] .guest--spot[data-stage='details'] .guest-hero__record,
:root[data-edition='standalone'] .guest--spot[data-stage='sent'] .guest-hero__record,
:root[data-edition='standalone'] .guest--spot[data-stage='sent'] .guest-hero__place {
  display: none;
}

/* The card, smaller, while the form is being filled in: where they are is settled by now. */
:root[data-edition='standalone'] .guest--spot[data-stage='details'] .guest-hero__place--ticket {
  margin-top: 18px;
  gap: 12px;
  padding: 9px 14px 9px 9px;
  border-radius: 20px;
  box-shadow:
    inset 0 1px 0 var(--aura-rim),
    inset 0 0 0 1px var(--aura-edge);
}

:root[data-edition='standalone'] .guest--spot[data-stage='details'] .guest-hero__stub {
  min-width: 44px;
  height: 44px;
  padding: 0 7px;
  border-radius: 14px;
  box-shadow:
    inset 0 0 0 1px var(--aura-halo-ring),
    inset 0 0 14px var(--aura-halo-soft);
  --glyph: 22px;
}

:root[data-edition='standalone'] .guest--spot[data-stage='details'] .guest-hero__kind,
:root[data-edition='standalone'] .guest--spot[data-stage='details'] .guest-hero__eyebrow {
  display: none;
}

:root[data-edition='standalone'] .guest--spot[data-stage='details'] .guest-hero__number {
  font-size: 1rem;
}

:root[data-edition='standalone'] .guest--spot[data-stage='details'] .guest-hero__stub--place .glyph--letter {
  font-size: 1.125rem;
}

:root[data-edition='standalone'] .guest--spot[data-stage='details'] .guest-hero__title {
  font-size: 0.9375rem;
}

:root[data-edition='standalone'] .guest--spot[data-stage='details'] .guest-hero__meta {
  font-size: 0.78rem;
}

/* Room above the chosen type for its label, which sits over the card rather than inside it. */
:root[data-edition='standalone'] .guest--spot[data-stage='details'] .guest-form {
  padding-top: 48px;
}

/* ------------------------------------------------------------ what they chose */

:root[data-edition='standalone'] .guest--spot .picked {
  position: relative;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 10px 10px 12px;
  border-radius: 20px;
  background: var(--aura-tile);
  box-shadow:
    inset 0 1px 0 var(--aura-rim-soft),
    inset 0 0 0 1px var(--aura-edge);
}

:root[data-edition='standalone'] .guest--spot .picked__glyph {
  width: 42px;
  height: 42px;
  flex: none;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--aura-halo);
  color: var(--accent);
  --glyph: 21px;
}

:root[data-edition='standalone'] .guest--spot .picked__glyph .glyph--letter {
  width: auto;
  height: auto;
  border: 0;
  font-size: 1.05rem;
  font-stretch: 100%;
}

:root[data-edition='standalone'] .guest--spot .picked__words {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

/* The label over the card, as the mockups have it: "Your report", then the report. */
:root[data-edition='standalone'] .guest--spot .picked__eyebrow {
  position: absolute;
  left: 2px;
  bottom: calc(100% + 10px);
  font-size: 0.625rem;
  font-weight: 800;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--text-faint);
  white-space: nowrap;
}

:root[data-edition='standalone'] .guest--spot .picked__type {
  margin: 0;
  font-size: 1.125rem;
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: -0.015em;
  color: var(--text);
  overflow-wrap: break-word;
}

/* Focused by the page when the step opens, for a screen reader's sake; a ring on a heading
   nobody pressed would look like a fault. */
:root[data-edition='standalone'] .guest--spot .picked__type:focus {
  outline: none;
}

:root[data-edition='standalone'] .guest--spot .picked__change {
  flex: none;
  min-height: 44px;
  padding: 0 16px;
  border: 0;
  border-radius: 14px;
  background: var(--aura-button);
  box-shadow: inset 0 0 0 1px var(--aura-line);
  color: var(--text);
  font-size: 0.875rem;
  font-weight: 700;
}

:root[data-edition='standalone'] .guest--spot .picked__change:hover:not(:disabled) {
  background: var(--aura-hover);
  color: var(--text);
}

:root[data-edition='standalone'] .guest--spot .guest-form .scope-note {
  padding: 0;
  border: 0;
  background: none;
  color: var(--text-dim);
  font-size: 0.9rem;
}

/* ------------------------------------------------------------------ the fields */

:root[data-edition='standalone'] .guest--spot .guest-form .step__title {
  margin: 0 0 10px;
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: -0.01em;
}

:root[data-edition='standalone'] .guest--spot .guest-form input[type='text'],
:root[data-edition='standalone'] .guest--spot .guest-form input[type='email'],
:root[data-edition='standalone'] .guest--spot .thanks__email input[type='email'],
:root[data-edition='standalone'] .guest--spot .guest-form textarea {
  padding: 14px 16px;
  border: 1px solid var(--aura-line);
  border-radius: 18px;
  background: var(--aura-well);
  color: var(--text);
  font-family: inherit;
  font-size: 1rem;
  font-weight: 500;
}

:root[data-edition='standalone'] .guest--spot .guest-form textarea {
  min-height: 124px;
  line-height: 1.45;
  /* The grip sat outside the rounded corner as a stray dot; the box scrolls for a long report. */
  resize: none;
}

/* Focus glows in the colour — the ring and a breath of light round it. */
:root[data-edition='standalone'] .guest--spot .guest-form input[type='text']:focus,
:root[data-edition='standalone'] .guest--spot .guest-form input[type='email']:focus,
:root[data-edition='standalone'] .guest--spot .thanks__email input[type='email']:focus,
:root[data-edition='standalone'] .guest--spot .guest-form textarea:focus {
  border-color: color-mix(in srgb, var(--accent) 80%, transparent);
  box-shadow:
    0 0 0 4px color-mix(in srgb, var(--accent) 12%, transparent),
    0 0 28px color-mix(in srgb, var(--accent) 12%, transparent);
}

:root[data-edition='standalone'] .guest--spot .guest-form__meta .field__hint {
  font-size: 0.78rem;
  line-height: 1.4;
  color: var(--text-dim);
}

:root[data-edition='standalone'] .guest--spot .guest-form__meta .counter {
  color: var(--text-faint);
  font-variant-numeric: tabular-nums;
}

/* The rule about personal details, after a padlock rather than in the warning colour: it is read
   before anything has gone wrong, and on this page it is a promise as much as a rule. */
:root[data-edition='standalone'] .guest--spot .field__hint--privacy {
  display: flex;
  align-items: flex-start;
  gap: 9px;
  margin-top: 12px;
  font-size: 0.78rem;
  line-height: 1.45;
  color: var(--text-faint);
}

:root[data-edition='standalone'] .guest--spot .field__hint--privacy::before {
  content: '';
  width: 15px;
  height: 15px;
  flex: none;
  margin-top: 1px;
  background: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7.5 11V8a4.5 4.5 0 0 1 9 0v3 M6 11h12a1 1 0 0 1 1 1v7.5a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V12a1 1 0 0 1 1-1z'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7.5 11V8a4.5 4.5 0 0 1 9 0v3 M6 11h12a1 1 0 0 1 1 1v7.5a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V12a1 1 0 0 1 1-1z'/%3E%3C/svg%3E") center / contain no-repeat;
}

/* The camera, as a row with its drawing in the same glowing circle the tiles use. */
:root[data-edition='standalone'] .guest--spot .photo__pick {
  position: relative;
  flex: 1 1 auto;
  justify-content: flex-start;
  min-height: 54px;
  padding: 0 16px 0 58px;
  border: 0;
  border-radius: 18px;
  background: var(--aura-well);
  box-shadow: inset 0 0 0 1px var(--aura-edge);
  color: var(--text);
  font-size: 0.9375rem;
  font-weight: 600;
}

:root[data-edition='standalone'] .guest--spot .photo__pick:hover:not(:disabled) {
  background: var(--aura-hover);
  color: var(--text);
}

:root[data-edition='standalone'] .guest--spot .photo__pick::before,
:root[data-edition='standalone'] .guest--spot .photo__pick::after {
  content: '';
  position: absolute;
  top: 50%;
}

:root[data-edition='standalone'] .guest--spot .photo__pick::before {
  left: 12px;
  width: 34px;
  height: 34px;
  margin-top: -17px;
  border-radius: 50%;
  background: var(--aura-halo);
}

:root[data-edition='standalone'] .guest--spot .photo__pick::after {
  left: 20px;
  width: 18px;
  height: 18px;
  margin-top: -9px;
  background: var(--accent);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 8.5h3l1.5-2.5h7L17 8.5h3a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-9a1 1 0 0 1 1-1z M12 10.5a3.5 3.5 0 1 1 0 7a3.5 3.5 0 1 1 0-7z'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 8.5h3l1.5-2.5h7L17 8.5h3a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-9a1 1 0 0 1 1-1z M12 10.5a3.5 3.5 0 1 1 0 7a3.5 3.5 0 1 1 0-7z'/%3E%3C/svg%3E") center / contain no-repeat;
}

:root[data-edition='standalone'] .guest--spot .photo__thumb {
  border: 0;
  border-radius: 18px;
}

/* ------------------------------------------------------------------ sending */

:root[data-edition='standalone'] .guest--spot[data-stage='details'] .guest-send {
  margin-top: auto;
}

/* The one lit control on the page: the colour, catching the light along its top edge. */
:root[data-edition='standalone'] .guest--spot .guest-send .btn--cta,
:root[data-edition='standalone'] .guest--spot .rate__send {
  min-height: 56px;
  border: 0;
  border-radius: 18px;
  background: var(--aura-fill);
  color: var(--aura-on-fill);
  font-size: 1.03rem;
  font-weight: 800;
  letter-spacing: -0.01em;
  box-shadow:
    inset 0 1px 0 rgb(255 255 255 / 45%),
    0 14px 36px -14px var(--aura-fill-glow);
}

:root[data-edition='standalone'] .guest--spot .guest-send .btn--cta:hover:not(:disabled),
:root[data-edition='standalone'] .guest--spot .rate__send:hover:not(:disabled) {
  background: var(--aura-fill);
  color: var(--aura-on-fill);
  filter: brightness(1.06);
}

:root[data-edition='standalone'] .guest--spot .guest-send__note {
  margin-top: 10px;
  padding: 0 8px;
  font-size: 0.75rem;
  font-weight: 500;
  line-height: 1.45;
  color: var(--text-dim);
}

/* The credit, under everything, with the product's name set as a mark rather than a word. */
:root[data-edition='standalone'] .guest--spot .guest-send__by,
:root[data-edition='standalone'] .guest--spot .thanks__by {
  margin: 12px 0 0;
  text-align: center;
  font-size: 0.75rem;
  font-weight: 500;
  letter-spacing: 0;
  color: var(--text-faint);
}

:root[data-edition='standalone'] .guest--spot .guest-send__by strong,
:root[data-edition='standalone'] .guest--spot .thanks__by strong {
  font-weight: 800;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-dim);
}

/* ------------------------------------------------------------------ the thank-you
 *
 * A screen of its own, reading from the left: a lit check, the thanks, where it was reported, the
 * reference on glass with Copy beside it, and the way back to the report as a full-width row. The
 * line about scanning again sits at the foot with the credit.
 */
:root[data-edition='standalone'] .guest--spot .thanks__by {
  display: block;
}

:root[data-edition='standalone'] .guest--spot .thanks {
  flex: 1 0 auto;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  margin-top: 26px;
  padding: 0 0 calc(20px + env(safe-area-inset-bottom, 0px));
  border: 0;
  background: none;
  box-shadow: none;
  text-align: left;
}

:root[data-edition='standalone'] .guest--spot .thanks__mark {
  width: 64px;
  height: 64px;
  margin: 12px 0 24px;
  border: 0;
  background: var(--aura-fill);
  color: var(--aura-on-fill);
  box-shadow:
    inset 0 1px 0 rgb(255 255 255 / 50%),
    0 0 44px var(--aura-fill-glow);
}

:root[data-edition='standalone'] .guest--spot .thanks__mark svg {
  width: 30px;
  height: 30px;
}

:root[data-edition='standalone'] .guest--spot .thanks h2 {
  margin: 0 2px;
  font-size: 2.625rem;
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.035em;
}

:root[data-edition='standalone'] .guest--spot .thanks__where {
  margin: 10px 2px 0;
  font-size: 0.97rem;
  font-weight: 500;
  line-height: 1.45;
  color: var(--text-dim);
}

:root[data-edition='standalone'] .guest--spot .thanks__photo {
  margin: 8px 2px 0;
}

/* The reference, on the glass the place was on: its label, the number, and Copy beside both. */
:root[data-edition='standalone'] .guest--spot .ticket {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas:
    'label copy'
    'ref copy';
  justify-items: start;
  align-items: center;
  gap: 6px 14px;
  margin: 0;
  padding: 16px 14px 16px 20px;
  border: 0;
  border-radius: 24px;
  background: var(--aura-glass);
  box-shadow:
    inset 0 1px 0 var(--aura-rim),
    inset 0 0 0 1px var(--aura-edge);
  text-align: left;
  -webkit-backdrop-filter: blur(24px) saturate(160%);
  backdrop-filter: blur(24px) saturate(160%);
}

:root[data-edition='standalone'] .guest--spot .thanks .ticket {
  margin-top: 28px;
}

:root[data-edition='standalone'] .guest--spot .ticket__label {
  grid-area: label;
  font-size: 0.625rem;
  font-weight: 800;
  letter-spacing: 0.2em;
  color: var(--text-faint);
}

:root[data-edition='standalone'] .guest--spot .thanks__ref {
  grid-area: ref;
  font-family: inherit;
  /* One line, always: a reference broken at its hyphen reads as two references. Sized to fit
     beside Copy on the narrowest phone. */
  font-size: clamp(1.1rem, 5.6vw, 1.5rem);
  font-weight: 800;
  line-height: 1;
  letter-spacing: 0.03em;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

:root[data-edition='standalone'] .guest--spot .ticket__copy {
  grid-area: copy;
  min-height: 44px;
  padding: 0 16px;
  border: 0;
  border-radius: 14px;
  background: var(--aura-button);
  box-shadow: inset 0 0 0 1px var(--aura-line);
  color: var(--text);
  font-size: 0.875rem;
  font-weight: 700;
}

:root[data-edition='standalone'] .guest--spot .ticket__copy:hover {
  background: var(--aura-hover);
}

:root[data-edition='standalone'] .guest--spot .ticket__copy--done {
  color: var(--ok);
}

:root[data-edition='standalone'] .guest--spot .thanks > .field__hint[data-reword] {
  margin: 16px 2px 0;
  font-size: 0.84rem;
  line-height: 1.5;
  color: var(--text-dim);
}

:root[data-edition='standalone'] .guest--spot .thanks__record {
  margin: 16px 2px 0;
}

/* No rule above the way back: on this screen it is the next thing, not a new section. */
:root[data-edition='standalone'] .guest--spot .thanks__trace {
  margin-top: 22px;
  padding-top: 0;
  border-top: 0;
}

:root[data-edition='standalone'] .guest--spot .thanks__trace .btn {
  justify-content: space-between;
  min-height: 56px;
  margin: 0;
  padding: 0 18px 0 20px;
  border: 0;
  border-radius: 18px;
  background: var(--aura-tile);
  box-shadow:
    inset 0 1px 0 var(--aura-rim-soft),
    inset 0 0 0 1px var(--aura-line);
  color: var(--text);
  font-size: 0.97rem;
  font-weight: 700;
}

:root[data-edition='standalone'] .guest--spot .thanks__trace .btn:hover {
  background: var(--aura-hover);
  color: var(--text);
}

:root[data-edition='standalone'] .guest--spot .thanks__trace .btn::after {
  content: '';
  width: 18px;
  height: 18px;
  flex: none;
  background: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4.5 12h15 M13.5 6l6 6-6 6'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4.5 12h15 M13.5 6l6 6-6 6'/%3E%3C/svg%3E") center / contain no-repeat;
}

:root[data-edition='standalone'] .guest--spot .thanks__trace .field__hint {
  margin: 10px 2px 0;
  font-size: 0.78rem;
  line-height: 1.45;
  color: var(--text-faint);
}

:root[data-edition='standalone'] .guest--spot #another-hint {
  margin: auto 0 0;
  padding-top: 28px;
  text-align: center;
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--text-dim);
}

/* ---------------------------------------------------------- the thank-you code
 *
 * What the code is for on the left — the caption in the colour and the business's own sentence
 * under it — and the code itself on the right, in a dashed outline: a thing to show at a counter,
 * not a button to press. On every page that can hand one over.
 */
:root[data-edition='standalone'] .guest--spot .thanks__code {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas:
    'label value'
    'words value';
  align-items: center;
  gap: 5px 14px;
  margin-top: 12px;
  padding: 16px 16px 16px 20px;
  border: 0;
  border-radius: 24px;
  background: var(--aura-tile);
  box-shadow:
    inset 0 1px 0 var(--aura-rim-soft),
    inset 0 0 0 1px var(--aura-edge);
  text-align: left;
}

/* Shown under the stamp for the two pages whose caption ships hidden (.thanks__code-label--aurora):
   the caption carries the plain class too, so this one rule is what lets it through. */
:root[data-edition='standalone'] .guest--spot .thanks__code-label {
  display: block;
  grid-area: label;
  font-size: 0.625rem;
  font-weight: 800;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--accent);
}

:root[data-edition='standalone'] .guest--spot .thanks__code-words {
  grid-area: words;
  margin: 0;
  font-size: 0.8125rem;
  font-weight: 500;
  line-height: 1.4;
  color: var(--text-dim);
}

:root[data-edition='standalone'] .guest--spot .thanks__code-list {
  grid-area: value;
  justify-items: end;
}

:root[data-edition='standalone'] .guest--spot .thanks__code > .thanks__code-value {
  grid-area: value;
}

:root[data-edition='standalone'] .guest--spot .thanks__code-value {
  padding: 9px 12px;
  border: 1px dashed color-mix(in srgb, var(--accent) 70%, transparent);
  border-radius: 12px;
  font-family: inherit;
  font-size: 0.9063rem;
  font-weight: 800;
  letter-spacing: 0.04em;
  color: var(--text);
}

/* ------------------------------------------------------------ the tracking page
 *
 * The heading, where the report was made under it, the reference on glass with what it was about
 * beside that issue's drawing, and the rail on a well of its own — which turns green with the
 * light when the report is resolved.
 */
:root[data-edition='standalone'] .guest--spot.guest--trace .guest-hero h1 {
  margin-top: 40px;
  font-size: clamp(2rem, 1.5rem + 2.4vw, 2.375rem);
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.035em;
}

:root[data-edition='standalone'] .guest--spot .guest-hero__inner > .guest-hero__where {
  grid-area: where;
  justify-self: start;
  margin: 10px 2px 0;
  padding: 0;
  border: 0;
  background: none;
  box-shadow: none;
  font-size: 0.9063rem;
  font-weight: 500;
  color: var(--text-dim);
}

:root[data-edition='standalone'] .guest--spot .guest-hero__inner > .guest-hero__where .guest-hero__pin {
  display: none;
}

:root[data-edition='standalone'] .guest--spot.guest--trace #main {
  justify-content: flex-start;
}

/* The page starts under its heading rather than floating in the middle of the screen: the heading
   is above it, and a report half a screen below its own title reads as two pages. */
:root[data-edition='standalone'] .guest--spot.guest--trace #main::before,
:root[data-edition='standalone'] .guest--spot.guest--trace #main::after {
  display: none;
}

:root[data-edition='standalone'] .guest--spot.guest--trace #main > .card {
  flex: 1 0 auto;
  display: flex;
  flex-direction: column;
  gap: 12px;
  margin-top: 24px;
  padding: 0 0 calc(22px + env(safe-area-inset-bottom, 0px));
  border: 0;
  background: none;
  box-shadow: none;
}

:root[data-edition='standalone'] .guest--spot .trace-card {
  --trace-rail: 24px;
  --trace-orb: 24px;
  --trace-gap: 14px;
  --trace-clear: 5px;
}

/* What the report was about, under a hairline in the reference's card: the type's drawing in its
   circle, and its name. */
:root[data-edition='standalone'] .guest--spot .trace-card > .ticket {
  grid-template-areas:
    'label copy'
    'ref copy'
    'what what';
  padding-bottom: 14px;
}

:root[data-edition='standalone'] .guest--spot .ticket__what {
  grid-area: what;
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  margin-top: 8px;
  padding-top: 12px;
  border-top: 1px solid var(--aura-line);
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--text-dim);
  --glyph: 15px;
}

:root[data-edition='standalone'] .guest--spot .ticket__what .glyph {
  box-sizing: content-box;
  padding: 6.5px;
  border-radius: 50%;
  background: var(--aura-halo);
  color: var(--accent);
}

:root[data-edition='standalone'] .guest--spot .ticket__what .glyph--letter {
  box-sizing: border-box;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 0;
  font-size: 0.8125rem;
  font-stretch: 100%;
}

/* The rail, on a well: each step's name with its date under it, the orb beside both. */
:root[data-edition='standalone'] .guest--spot .trace-card .trace {
  margin: 0;
  padding: 8px 18px;
  border-radius: 24px;
  background: var(--aura-well);
  box-shadow: inset 0 0 0 1px var(--aura-edge);
}

:root[data-edition='standalone'] .guest--spot .trace__step {
  grid-template-columns: var(--trace-rail) minmax(0, 1fr);
  grid-template-rows: auto auto;
  row-gap: 3px;
  padding: 14px 0;
}

:root[data-edition='standalone'] .guest--spot .trace__orb {
  grid-row: 1 / span 2;
  align-self: center;
  box-sizing: border-box;
  border: 2px solid var(--aura-ring);
  background: transparent;
}

:root[data-edition='standalone'] .guest--spot .trace__step--done .trace__orb {
  border-color: var(--trace-ink);
  background: var(--trace-ink);
}

/* A tick in every step that has happened, cut out of the orb in the page's own ground. Above the
   halo, which would otherwise wash over it once a pulse. */
:root[data-edition='standalone'] .guest--spot .trace__step--done .trace__orb::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  margin: auto;
  width: 12px;
  height: 12px;
  background: var(--bg);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12.5l4.3 4.3L19 7.3'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12.5l4.3 4.3L19 7.3'/%3E%3C/svg%3E") center / contain no-repeat;
}

/*
 * The step it is on now, while it is still on one: a lit ring rather than a tick, because it is
 * happening rather than done, with the pulse starting inside it and leaving as a ring.
 */
:root[data-edition='standalone'] .guest--spot .trace:not(.trace--complete) .trace__step--current .trace__orb::before {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  -webkit-mask: none;
  mask: none;
}

:root[data-edition='standalone'] .guest--spot .trace__step--current .trace__orb::after {
  inset: 5px;
}

/* Resolved: nothing is moving any more, so the last orb glows rather than pulses. */
:root[data-edition='standalone'] .guest--spot .trace--complete .trace__step--current .trace__orb {
  box-shadow: 0 0 16px color-mix(in srgb, var(--ok) 60%, transparent);
}

:root[data-edition='standalone'] .guest--spot .trace--complete .trace__step--current .trace__orb::after {
  display: none;
}

:root[data-edition='standalone'] .guest--spot .trace__what {
  font-size: 1rem;
  font-weight: 700;
  color: var(--text);
}

:root[data-edition='standalone'] .guest--spot .trace__step:not(.trace__step--done) .trace__what {
  color: var(--text-faint);
}

:root[data-edition='standalone'] .guest--spot .trace--complete .trace__step--current .trace__what {
  color: var(--ok);
}

:root[data-edition='standalone'] .guest--spot .trace__when {
  grid-column: 2;
  font-size: 0.78rem;
  font-weight: 500;
  color: var(--text-faint);
}

/* What was done, on a card of its own in the colour of done. */
:root[data-edition='standalone'] .guest--spot .trace-card .trace__outcome {
  margin: 0;
  padding: 16px 20px;
  border-radius: 24px;
  background: color-mix(in srgb, var(--ok) 7%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ok) 20%, transparent);
}

:root[data-edition='standalone'] .guest--spot .trace__outcomeLabel {
  font-size: 0.625rem;
  font-weight: 800;
  letter-spacing: 0.2em;
  color: var(--ok);
}

:root[data-edition='standalone'] .guest--spot .trace__outcomeText {
  margin-top: 7px;
  font-size: 0.9375rem;
  font-weight: 500;
  line-height: 1.45;
}

/* The anonymity note, at the foot of the page where a promise about the page belongs. */
:root[data-edition='standalone'] .guest--spot .trace-card > .field__hint {
  margin: auto 2px 0;
  padding-top: 18px;
  font-size: 0.78rem;
  font-weight: 500;
  line-height: 1.5;
  color: var(--text-faint);
}

:root[data-edition='standalone'] .guest--spot.guest--trace #main > #gone {
  flex: none;
  margin-top: 24px;
  padding: 20px 22px;
  border-radius: 24px;
  background: var(--aura-glass);
  box-shadow:
    inset 0 1px 0 var(--aura-rim),
    inset 0 0 0 1px var(--aura-edge);
}

/* ------------------------------------------------------------ rate your visit
 *
 * The building on the card at the top, so the rating card's own copies of it stand down; the
 * question under the heading as its subtitle; eleven lit-glass buttons on one line, the chosen one
 * in the colour; and after the tap, the check and what they said, set large.
 */
:root[data-edition='standalone'] .guest--spot .guest-main {
  width: 100%;
  max-width: 640px;
  margin: 0 auto;
  padding: 0 var(--aura-pad) calc(24px + env(safe-area-inset-bottom, 0px));
}

:root[data-edition='standalone'] .guest--spot .rate,
:root[data-edition='standalone'] .guest--spot .verdict,
:root[data-edition='standalone'] .guest--spot .rate-nope {
  min-height: 0;
  align-content: start;
  justify-items: stretch;
  padding: 10px 0 0;
  text-align: left;
}

:root[data-edition='standalone'] .guest--spot .rate > .rate__label,
:root[data-edition='standalone'] .guest--spot .rate > .rate__venue,
:root[data-edition='standalone'] .guest--spot .rate > .rate__film,
:root[data-edition='standalone'] .guest--spot .rate > .rate__showtime {
  display: none;
}

:root[data-edition='standalone'] .guest--spot .rate .nps {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 0;
  width: 100%;
  max-width: none;
  margin: 0;
}

:root[data-edition='standalone'] .guest--spot .rate__ask {
  margin: 0 2px;
  font-size: 0.97rem;
  font-weight: 500;
  line-height: 1.45;
  color: var(--text-dim);
  text-align: left;
  text-wrap: pretty;
}

:root[data-edition='standalone'] .guest--spot .nps__row {
  display: grid;
  grid-template-columns: repeat(11, minmax(0, 1fr));
  gap: 4px;
  margin-top: 20px;
}

:root[data-edition='standalone'] .guest--spot .nps__n {
  min-height: 56px;
  padding: 0;
  border: 0;
  border-radius: 12px;
  background: var(--aura-tile);
  box-shadow:
    inset 0 1px 0 var(--aura-rim-soft),
    inset 0 0 0 1px var(--aura-edge);
  color: var(--text);
  font-family: inherit;
  font-size: 0.97rem;
  font-weight: 600;
}

:root[data-edition='standalone'] .guest--spot .nps__n:hover {
  background: var(--aura-hover);
}

:root[data-edition='standalone'] .guest--spot .nps__n--on,
:root[data-edition='standalone'] .guest--spot .nps__n--on:hover {
  background: var(--aura-fill);
  color: var(--aura-on-fill);
  font-weight: 800;
  box-shadow:
    inset 0 1px 0 rgb(255 255 255 / 45%),
    0 0 24px var(--aura-fill-glow);
}

:root[data-edition='standalone'] .guest--spot .nps__ends {
  margin-top: 9px;
  padding: 0 2px;
  font-size: 0.78rem;
  font-weight: 500;
  color: var(--text-faint);
  opacity: 1;
}

:root[data-edition='standalone'] .guest--spot .rate__send {
  width: 100%;
  margin-top: 26px;
}

:root[data-edition='standalone'] .guest--spot .rate__quiet {
  margin: 14px 0 0;
  text-align: center;
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--text-faint);
  opacity: 1;
}

/* After the tap: the check the report's thank-you carries, lit in the colour. */
:root[data-edition='standalone'] .guest--spot .verdict__mark {
  display: grid;
  place-items: center;
  justify-self: start;
  width: 64px;
  height: 64px;
  margin: 12px 0 0;
  border-radius: 50%;
  background: var(--aura-fill);
  color: var(--aura-on-fill);
  box-shadow:
    inset 0 1px 0 rgb(255 255 255 / 50%),
    0 0 44px var(--aura-fill-glow);
}

:root[data-edition='standalone'] .guest--spot .verdict__mark svg {
  width: 30px;
  height: 30px;
}

:root[data-edition='standalone'] .guest--spot .verdict__title {
  margin: 24px 2px 0;
  font-size: clamp(1.875rem, 1.35rem + 2.3vw, 2.25rem);
  font-weight: 800;
  line-height: 1.05;
  letter-spacing: -0.035em;
  text-align: left;
}

:root[data-edition='standalone'] .guest--spot .verdict__venue {
  margin: 10px 2px 0;
  font-size: 0.97rem;
  font-weight: 500;
  letter-spacing: 0;
  color: var(--text-dim);
}

/*
 * What they said, on the glass: the caption and the sentence about it on the left, the answer
 * large on the right. The sentence is the card's sibling in the markup (it is CineScope's line
 * under its card), so it is laid into the same row of the grid as the card rather than moved —
 * over the card's left half, with the column beside it kept for the number.
 */
:root[data-edition='standalone'] .guest--spot .verdict {
  grid-template-columns: minmax(0, 1fr) 8.5rem;
  grid-template-areas:
    'mark mark'
    'title title'
    'venue venue'
    'said said'
    'code code';
}

:root[data-edition='standalone'] .guest--spot .verdict__mark {
  grid-area: mark;
}

:root[data-edition='standalone'] .guest--spot .verdict__title {
  grid-area: title;
}

:root[data-edition='standalone'] .guest--spot .verdict__venue {
  grid-area: venue;
}

:root[data-edition='standalone'] .guest--spot .verdict > .thanks__code {
  grid-area: code;
}

:root[data-edition='standalone'] .guest--spot .verdict__pair {
  grid-area: said;
  display: block;
  margin-top: 26px;
  padding: 16px 22px;
  border-radius: 24px;
  background: var(--aura-glass);
  box-shadow:
    inset 0 1px 0 var(--aura-rim),
    inset 0 0 0 1px var(--aura-edge);
  -webkit-backdrop-filter: blur(24px) saturate(160%);
  backdrop-filter: blur(24px) saturate(160%);
}

:root[data-edition='standalone'] .guest--spot .verdict__side {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  height: 100%;
}

:root[data-edition='standalone'] .guest--spot .verdict__cap {
  font-size: 0.625rem;
  font-weight: 800;
  letter-spacing: 0.2em;
  color: var(--text-faint);
}

/* The number in the colour, lit from above; what it is out of, small and quiet beside it. */
:root[data-edition='standalone'] .guest--spot .verdict #verdict-yours {
  display: inline-flex;
  align-items: baseline;
  align-self: center;
  font-size: 4rem;
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.04em;
  color: var(--accent);
  background: var(--aura-fill-text);
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
}

:root[data-edition='standalone'] .guest--spot .verdict__of {
  margin-left: 2px;
  font-size: 1.25rem;
  font-weight: 700;
  letter-spacing: 0;
  color: var(--text-faint);
  -webkit-text-fill-color: currentColor;
}

:root[data-edition='standalone'] .guest--spot .verdict__note {
  grid-row: said;
  grid-column: 1;
  z-index: 1;
  align-self: start;
  margin: 26px 0 0;
  padding: 40px 0 18px 22px;
  font-size: 0.875rem;
  font-weight: 500;
  line-height: 1.45;
  color: var(--text-dim);
  text-align: left;
}

:root[data-edition='standalone'] .guest--spot .rate-nope__text {
  max-width: none;
  margin: 0;
  padding: 20px 22px;
  border-radius: 24px;
  background: var(--aura-glass);
  box-shadow:
    inset 0 1px 0 var(--aura-rim),
    inset 0 0 0 1px var(--aura-edge);
  color: var(--text-dim);
}

/* ------------------------------------------------------------ the privacy refusal */

:root[data-edition='standalone'] .guest--spot .modal--privacy {
  border: 0;
  border-radius: 24px;
  box-shadow:
    inset 0 1px 0 var(--aura-rim),
    inset 0 0 0 1px var(--aura-edge),
    var(--shadow-lift);
}

:root[data-edition='standalone'] .guest--spot .modal--privacy .btn--primary {
  min-height: 52px;
  border: 0;
  border-radius: 16px;
  background: var(--aura-fill);
  color: var(--aura-on-fill);
  font-weight: 800;
}

:root[data-edition='standalone'] .guest--spot .modal--privacy .btn--primary:hover:not(:disabled) {
  background: var(--aura-fill);
  color: var(--aura-on-fill);
  filter: brightness(1.06);
}

/* ------------------------------------------------------------ the icon picker, in Foyer
 *
 * The preview beside the choices is the tile as a guest will see it, so in Foyer it is Foyer's
 * tile: the drawing in its glowing circle beside the name, on the dark of the light's own page.
 */
html[data-edition='standalone'] .icon-pick__stage {
  background:
    radial-gradient(160px 120px at 70% -10%, color-mix(in srgb, var(--accent) 45%, transparent), transparent),
    #0e0e11;
}

html[data-edition='standalone'] .icon-pick__tile {
  width: auto;
  min-width: 150px;
  max-width: calc(100% - 24px);
  height: 66px;
  flex-direction: row;
  align-items: center;
  justify-content: flex-start;
  gap: 12px;
  padding: 0 16px 0 12px;
  border: 0;
  background: rgb(255 255 255 / 5.5%);
  box-shadow:
    inset 0 1px 0 rgb(255 255 255 / 8%),
    inset 0 0 0 1px rgb(255 255 255 / 6%);
  font-family: 'Manrope', var(--font);
  font-size: 0.9063rem;
  font-weight: 600;
  line-height: 1.2;
  --glyph: 20px;
}

html[data-edition='standalone'] .icon-pick__tile .glyph {
  box-sizing: content-box;
  padding: 9px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  color: color-mix(in srgb, var(--accent) 80%, white);
}

html[data-edition='standalone'] .icon-pick__tile .glyph--letter {
  box-sizing: border-box;
  width: 38px;
  height: 38px;
  padding: 0;
  border: 0;
  font-size: 1rem;
  font-stretch: 100%;
}

/* ------------------------------------------------------------ icons in Settings
 *
 * A hint that only applies to the CineScope edition says so in its markup, and a Foyer circuit
 * never sees it — the same rule the nav's cinema-only links follow.
 */
html[data-edition='standalone'] .field__hint[data-edition='cinema'] {
  display: none;
}

/* The icon, as the square at the start of an issue type's or a place's row. Pressing it is how
   one is chosen; dashed while nobody has, so what still shows the usual drawing is plain. */
.type__icon {
  width: 46px;
  height: 46px;
  flex: none;
  display: inline-grid;
  place-items: center;
  padding: 0;
  border: 1px solid var(--border-strong);
  border-radius: 10px;
  background: var(--surface);
  color: var(--text);
  cursor: pointer;
  --glyph: 22px;
  transition:
    border-color 140ms var(--ease),
    background-color 140ms var(--ease);
}

.type__icon--none {
  border-style: dashed;
  color: var(--text-dim);
}

.type__icon:hover {
  border-color: var(--accent);
  background: color-mix(in oklab, var(--accent) 10%, var(--surface));
}

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

/* ------------------------------------------------------------ the icon picker
 *
 * Choosing on the left, the tile as a guest will see it on the right — stacked on a phone, with
 * the preview first so the choice is seen as it is made.
 */
.modal--icons {
  width: min(760px, calc(100vw - 32px));
}

.icon-pick {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 200px;
  gap: 20px;
}

.icon-pick__choose {
  display: flex;
  flex-direction: column;
  gap: 14px;
  min-width: 0;
}

.icon-pick__intro {
  margin: 0;
}

.icon-pick__search {
  width: 100%;
  min-height: 44px;
}

.icon-pick__row,
.icon-pick__shelf,
.icon-pick__preview {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.icon-pick__shelves {
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.icon-pick__cap {
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-faint);
}

.icon-pick__quick,
.icon-pick__suggested {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}

.icon-pick__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(44px, 1fr));
  gap: 8px;
}

.icon-pick__choice,
.icon-pick__letter {
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--surface);
  color: var(--text);
  font: inherit;
  cursor: pointer;
  --glyph: 22px;
}

.icon-pick__choice {
  min-width: 44px;
}

.icon-pick__letter {
  padding: 0 14px 0 10px;
  border-style: dashed;
  color: var(--text-dim);
  font-size: 0.85rem;
  font-weight: 600;
}

.icon-pick__choice:hover,
.icon-pick__letter:hover {
  border-color: var(--border-strong);
  background: var(--surface-3);
}

.icon-pick__choice[aria-pressed='true'],
.icon-pick__letter[aria-pressed='true'] {
  border: 1px solid var(--accent);
  background: color-mix(in oklab, var(--accent) 18%, var(--surface));
  color: var(--accent);
}

.icon-pick__choice:focus-visible,
.icon-pick__letter:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.icon-pick__none {
  margin: 0;
}

/* The tile as a guest sees it: on the dark of the house, under the beam, at the tile's own size. */
.icon-pick__stage {
  position: relative;
  height: 170px;
  display: grid;
  place-items: center;
  overflow: hidden;
  border-radius: 14px;
  background:
    radial-gradient(90px 150px at 50% -10%, color-mix(in srgb, var(--accent) 45%, transparent), transparent),
    #06070a;
}

.icon-pick__tile {
  width: 104px;
  height: 104px;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: space-between;
  padding: 14px 12px 12px 14px;
  border-radius: 18px;
  border: 1px solid rgb(255 255 255 / 9%);
  background: rgb(255 255 255 / 5%);
  color: #f4f6fa;
  font-family: 'Archivo', var(--font);
  font-size: 0.8125rem;
  line-height: 1.22;
  font-weight: 500;
  --glyph: 26px;
}

.icon-pick__name {
  overflow-wrap: anywhere;
}

@media (max-width: 600px) {
  .icon-pick {
    grid-template-columns: minmax(0, 1fr);
  }

  .icon-pick__preview {
    order: -1;
  }

  .icon-pick__preview .field__hint {
    display: none;
  }

  .icon-pick__stage {
    height: 132px;
  }
}

/* ======================================================================= Home's search
 *
 * Where the round Refresh was, and the box it opens — see public/js/search.js. The button is
 * Refresh's disc with a glass in it. The box sits near the top of a desk screen rather than in
 * its middle, because the list under the field grows downward as it is typed into and a box
 * centred on the page would jump on every key; on a phone it is a bar that drops from the top of
 * the screen, with the answers dropping under it once something is typed (see 700px and under).
 *
 * Unscoped: every page this is on wears Studio, and each value here is a token.
 */
.search-open {
  display: inline-grid;
  place-items: center;
  flex: 0 0 auto;
  width: 36px;
  min-width: 36px;
  height: 36px;
  padding: 0;
}

/* On a phone the title row's buttons share the line out between them; the disc keeps its size,
   as Refresh's did. Studio's own rule for that row comes later in the file, so it is said again
   there, after it (see .page-head__actions .search-open under Studio). */
.page-head__actions .search-open {
  flex: 0 0 36px;
  width: 36px;
}

.search-open__glass {
  display: block;
}

/*
 * ON A MANAGER'S AND AN ENGINEER'S HOME the disc is in the range bar, where their Refresh was, at
 * the end of the chips' row. On a phone the range folds to one line of its own
 * (.rangebar__fold), and the buttons after it went to the next line — so Refresh was a disc alone
 * on a row, a row the installed app's pull had handed back to the page by hiding it (see
 * html[data-ptr] #refresh). Search is never hidden that way, so it takes the end of the fold's
 * line instead, and the page keeps the row: the fold gives up the disc's width and the gap before
 * it, and a Reset layout, when there is one, wraps under them as it did.
 *
 * Held at the top of the line, not its middle: the fold opens downward into its chips, and a disc
 * centred on the open fold drifted down beside them. The 2px sets it level with the fold's 40px
 * summary. Its padding is its own again — the bar gives its buttons room for a word, and this one
 * has none.
 */
@media (max-width: 560px) {
  .rangebar:has(> .search-open) > .rangebar__fold {
    flex: 1 1 calc(100% - 46px);
  }

  .rangebar > .search-open {
    order: 0;
    align-self: flex-start;
    margin-top: 2px;
    padding: 0;
  }
}

.search {
  width: min(680px, calc(100vw - 32px));
  max-height: min(78vh, 640px);
  max-height: min(78dvh, 640px);
  margin: 9vh auto auto;
}

.search__bar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 14px 8px 18px;
  border-bottom: 1px solid var(--border);
}

.search__glass {
  display: grid;
  flex: none;
  color: var(--text-dim);
}

/* Two classes, to outrank the shared text field (input[type='search'] and its :hover and :focus):
   this field is the box's whole top edge, not a field inside a form, so it has no box of its own. */
.search__bar .search__input {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 48px;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  /* Over 16px, which is also what keeps iOS from zooming the page when it is focused. */
  font-size: 1.12rem;
}

.search__bar .search__input:hover,
.search__bar .search__input:focus {
  background-color: transparent;
  box-shadow: none;
}

/* The field's own clear cross, drawn in the page's ink rather than the browser's blue. */
.search__input::-webkit-search-cancel-button {
  -webkit-appearance: none;
  appearance: none;
  width: 16px;
  height: 16px;
  background: var(--text-dim);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.6' stroke-linecap='round'%3E%3Cpath d='M6 6l12 12M18 6 6 18'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.6' stroke-linecap='round'%3E%3Cpath d='M6 6l12 12M18 6 6 18'/%3E%3C/svg%3E") center / contain no-repeat;
  cursor: pointer;
}

/* The completion, selected after the caret: the accent's own tint, so it reads as offered
   rather than as typed. */
.search__input::selection {
  background: var(--accent-soft);
  color: var(--text);
}

/* A phone's way out, since it has no Escape key and no page around the box to press. */
.search__cancel {
  display: none;
}

.search__body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 4px 8px 10px;
}

.search__group + .search__group {
  margin-top: 4px;
}

.search__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 10px 10px 4px;
  color: var(--text-dim);
  font-size: 0.76rem;
  font-weight: 650;
  letter-spacing: 0.02em;
}

/* Recent's Clear: a small pill, so it reads as a control rather than as a word in the heading. */
.search__forget {
  padding: 3px 10px;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--text-dim);
  font: inherit;
  font-size: 0.72rem;
  cursor: pointer;
}

.search__forget:hover {
  color: var(--text);
}

/* One row: the thing's drawing, its name over what and where it is, and — on the lit row, on a
   screen with a pointer — where Enter takes you. */
.search__item {
  display: grid;
  grid-template-columns: 36px minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 0 12px;
  padding: 8px 10px;
  border-radius: 14px;
  color: var(--text);
  text-decoration: none;
  cursor: pointer;
}

/* A row is a link in the markup and a row on screen: the link colours never reach it, hovered or
   visited — both of which outrank the plain class, hence the pair. */
.search__item:hover,
.search__item:visited {
  color: var(--text);
  text-decoration: none;
}

.search__item[aria-selected='true'] {
  background: var(--surface-3);
  box-shadow: inset 0 0 0 1px var(--accent-line);
}

.search__icon {
  display: grid;
  place-items: center;
  width: 36px;
  height: 36px;
  border-radius: 11px;
  background: var(--accent-soft);
  color: var(--accent);
}

.search__item > .face {
  width: 36px;
  height: 36px;
  font-size: 13px;
}

.search__text {
  display: grid;
  gap: 1px;
  min-width: 0;
}

.search__title,
.search__sub,
.search__snippet {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.search__title {
  font-size: 0.95rem;
  font-weight: 500;
}

/* What was typed, in the name: heavier, in the same ink — the eye finds why the row is here
   without the row turning into a coloured word. */
.search__mark {
  background: transparent;
  color: inherit;
  font-weight: 780;
}

.search__sub {
  color: var(--text-dim);
  font-size: 0.8rem;
}

/* The guest's own words under a case, in quotes' place: fainter, since they are how it is
   recognised and not what it is called. */
.search__snippet {
  color: var(--text-faint);
  font-size: 0.8rem;
  font-style: italic;
}

.search__pill {
  padding: 3px 9px;
  border-radius: 999px;
  background: var(--danger-soft);
  color: var(--danger);
  font-size: 0.72rem;
  font-weight: 700;
  white-space: nowrap;
}

.search__dest {
  color: var(--text-faint);
  font-size: 0.78rem;
  white-space: nowrap;
  visibility: hidden;
}

.search__item[aria-selected='true'] .search__dest {
  visibility: visible;
}

.search__dest::after {
  content: ' ↵';
}

.search__note {
  margin: 8px 12px 10px;
  color: var(--text-dim);
  font-size: 0.88rem;
}

.search__keys {
  margin: 0;
  padding: 9px 18px;
  border-top: 1px solid var(--border);
  color: var(--text-faint);
  font-size: 0.75rem;
}

.search__keys kbd {
  display: inline-block;
  min-width: 20px;
  margin-right: 3px;
  padding: 1px 6px;
  border: 1px solid var(--border-strong);
  border-radius: 6px;
  background: var(--surface-2);
  font: inherit;
  font-size: 0.72rem;
  text-align: center;
}

/* A finger has no Enter to press on a row and no keys to be told about. */
@media (pointer: coarse) {
  .search__keys,
  .search__dest {
    display: none;
  }
}

/* The phone's box arrives from the top edge it hangs from, rather than rising in the middle the
   way the other boxes do (dialog-in) — UNROLLED from that edge rather than slid down past it. Its
   field takes focus in the same tap that opens it, and a box that moved under a focused field left
   iOS drawing the caret where the field had been (see A BOX THAT OPENS STRAIGHT INTO TYPING, by
   dialog-in); a reveal leaves the field where it stays. The clip reaches 40px past the sides and
   the foot so the shadow the box casts is not cut off as it arrives. Same timings as the other
   boxes — dialogs.js keeps the clock. */
@keyframes search-drop-in {
  from {
    clip-path: inset(0 -40px 100% -40px);
  }

  to {
    clip-path: inset(0 -40px -40px -40px);
  }
}

@keyframes search-lift-out {
  from {
    clip-path: inset(0 -40px -40px -40px);
  }

  to {
    clip-path: inset(0 -40px 100% -40px);
  }
}

@media (max-width: 700px) {
  /*
   * A BAR THAT DROPS FROM THE TOP, not a page.
   *
   * The operator, on a phone: "it would be better if it made a search bar appear and then you
   * start typing and it shows the suggestions. Having an entire page appear is quite jarring." So
   * the box is the width of the screen and only as tall as what it holds: the field alone at
   * first, and the answers under it once something is typed (data-typed, set by search.js). The
   * page stays where it was behind the dimmed backdrop, and a press on it closes the box.
   *
   * Never taller than what can be seen above the keyboard: --search-vh is the visible strip's
   * height and --search-vtop where it starts, both from the visual viewport (search.js), so the
   * last answer can always be scrolled into view rather than sitting behind the keys.
   *
   * fit-content is the browser's own height for an open dialog, said again here because it is
   * the whole of "only as tall as what it holds": the browser also pins the box's top and bottom
   * to the screen's, and between those `auto` stretches it to the full height — which is what it
   * did, an empty box 820px tall, the page it was meant to stop being.
   */
  .search {
    width: 100vw;
    max-width: 100vw;
    height: fit-content;
    max-height: calc(var(--search-vh, 100dvh) - 24px);
    margin: var(--search-vtop, 0px) 0 auto;
    border: 0;
    border-radius: 0 0 22px 22px;
  }

  /* Rolled up into its top edge for the frame before it unrolls: search-drop-in's `from`, and none
     of the other boxes' rise. */
  .search.is-arming {
    opacity: 1;
    transform: none;
    clip-path: inset(0 -40px 100% -40px);
  }

  .search.is-opening {
    animation-name: search-drop-in;
  }

  .search.is-closing {
    animation-name: search-lift-out;
  }

  /* Nothing under the field until there is something to answer. */
  .search:not([data-typed]) .search__body {
    display: none;
  }

  .search:not([data-typed]) .search__bar {
    border-bottom: 0;
  }

  /* Below the status bar: the box hangs from the top of the screen, so its field is the top of it. */
  .search__bar {
    padding: calc(8px + env(safe-area-inset-top)) 10px 8px 16px;
  }

  .search__cancel {
    display: inline-flex;
  }

  .search__body {
    padding-bottom: calc(16px + env(safe-area-inset-bottom));
  }

  /* Room for the name: the lit row's destination is for a pointer, and a phone has none. */
  .search__item {
    grid-template-columns: 36px minmax(0, 1fr) auto;
  }

  .search__dest {
    display: none;
  }
}

/* Arriving from Home's search at one section of Settings, at one account on Active users, or at
   one of a manager's buildings on My cinemas: marked for a moment, the way the case monitor marks a
   case a link named (.complaint--spotlight), and let go after four seconds by the page that set it. */
.section--found,
.person--found,
.theatre--found {
  box-shadow: inset 0 0 0 2px var(--accent), var(--shadow);
}

/* Arriving from Home's search at one part of a page — a list, a chart, a tile, a tab, a tool (see
   js/find.js): ringed in the accent for a moment, closing in on it twice so the eye is taken there.
   An outline rather than the shadow above, because what it lands on is anything from a card to a
   button, half of which wear a shadow of their own, and an outline takes no room from the layout. */
.is-found {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  animation: found-ring 1100ms var(--ease-out) 2;
}

@keyframes found-ring {
  from {
    outline-color: transparent;
    outline-offset: 12px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .is-found {
    animation: none;
  }
}

/* ------------------------------------------------------------------- Submit a bug
 *
 * The box from the account menu (js/bug-report.js): what went wrong, a screenshot, what travels
 * with the report, and the sender's own reports with where each one stands. A form box like the
 * others — dialog > form > head, body, foot — so the rules above lay it out; these are its parts.
 */
.bugbox__lead {
  margin: 0 0 16px;
}

/* What travels with a report: folded, because it is there to be checked rather than read. */
.bugbox__sent {
  margin: 4px 0 0;
  padding: 12px 14px;
  border-radius: var(--radius);
  background: var(--surface-2);
}

.bugbox__summary {
  cursor: pointer;
  font-size: 0.88rem;
  font-weight: 600;
}

.bugbox__facts {
  margin-top: 10px;
}

/* The page's errors are one to a line, and a browser's name is long: both wrap where they must. */
.bugbox__facts dd {
  white-space: pre-line;
}

.bugbox__yours {
  margin-top: 22px;
}

.bugbox__heading {
  margin: 0 0 10px;
  font-size: 0.95rem;
  font-weight: 700;
}

.bugbox__list {
  display: grid;
  gap: 8px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.bugbox__item {
  padding: 10px 12px;
  border-radius: var(--radius-sm);
  background: var(--surface-2);
}

.bugbox__what {
  margin: 0;
  font-size: 0.9rem;
  font-weight: 600;
  overflow-wrap: anywhere;
}

.bugbox__meta {
  margin: 4px 0 0;
  color: var(--text-dim);
  font-size: 0.8rem;
}

/*
 * Where a bug report stands, said in words and coloured for a glance — never colour alone. Its own
 * block because two places draw it: the sender's list in the box, and the developers' in Settings.
 */
.bugstate {
  display: inline-block;
  padding: 1px 8px;
  border-radius: 999px;
  font-size: 0.75rem;
  font-weight: 700;
  white-space: nowrap;
}

.bugstate--new {
  background: var(--info-soft);
  color: var(--info);
}

.bugstate--looking {
  background: var(--warn-soft);
  color: var(--warn);
}

.bugstate--fixed {
  background: var(--ok-soft);
  color: var(--ok);
}

.bugstate--closed {
  background: var(--surface-3);
  color: var(--text-dim);
}

/* The developers' line back: somebody else's words, set apart from the sender's own. */
.bugbox__reply {
  margin: 8px 0 0;
  padding: 8px 10px;
  border-radius: var(--radius-sm);
  background: var(--surface-3);
  font-size: 0.85rem;
  white-space: pre-line;
  overflow-wrap: anywhere;
}

/*
 * The developers' list: Settings → Deployment → Bug reports (js/bug-list.js). One report a card,
 * its words first, what the page saw folded under them, and the two answers — where it stands and a
 * line back — in a row that wraps on a phone. Unanswered questions beside it (js/gap-list.js) is the
 * same list for the same readers, so it wears the same classes rather than a copy of them.
 */
.bugs {
  display: grid;
  gap: 12px;
  margin: 12px 0 0;
  padding: 0;
  list-style: none;
}

/*
 * The shared filter row, under a paragraph rather than in a bordered panel: its panel padding would
 * set Show in from the prose above it, as .audit__filters says of the audit log. One field, two
 * choices, so it keeps a field's width rather than stretching across the card.
 */
.bugs__filters {
  padding: 0;
  margin-top: 14px;
}

.bugs__filters .field {
  flex: 0 1 260px;
}

.bugs__item {
  padding: 14px 16px;
  border-radius: var(--radius);
  background: var(--surface-2);
}

.bugs__empty {
  color: var(--text-dim);
  font-size: 0.88rem;
}

.bugs__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 10px;
  margin: 0;
}

.bugs__meta {
  color: var(--text-dim);
  font-size: 0.8rem;
  overflow-wrap: anywhere;
}

/*
 * Which circuit it came from, at the deployment console, where every circuit's are one list
 * (js/console-inbox.js): a quiet pill beside where it stands, so the name reads at a glance without
 * competing with the state's colour.
 */
.bugs__where {
  padding: 1px 8px;
  border-radius: 999px;
  border: 1px solid var(--border-strong);
  font-size: 0.75rem;
  font-weight: 650;
  white-space: nowrap;
}

.bugs__what {
  margin: 10px 0 0;
  white-space: pre-line;
  overflow-wrap: anywhere;
}

/* The tools the assistant tried, under what it could not answer: by their own names, for the people who wrote them. */
.bugs__tools {
  margin: 8px 0 0;
  color: var(--text-dim);
  font-size: 0.82rem;
  overflow-wrap: anywhere;
}

.bugs__shot {
  display: inline-block;
  margin-top: 10px;
}

/* Big enough to see what they saw; the link opens it whole. */
.bugs__shot-img {
  display: block;
  max-width: 100%;
  max-height: 220px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border-strong);
}

.bugs__seen {
  margin-top: 10px;
}

.bugs__seen-summary {
  cursor: pointer;
  color: var(--text-dim);
  font-size: 0.85rem;
  font-weight: 600;
}

.bugs__facts dd {
  white-space: pre-line;
}

/*
 * Top-aligned, so the two labels share a line however tall the line back is drawn; the buttons sit
 * at the foot of the row, beside where the typing ends.
 */
.bugs__answer {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 10px 14px;
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
}

.bugs__field {
  flex: 0 1 200px;
  margin: 0;
}

/* The line back takes what the row has left, and the whole row on a phone. */
.bugs__field--wide {
  flex: 1 1 260px;
}

.bugs__reply {
  width: 100%;
  min-height: 2.6em;
  resize: vertical;
}

.bugs__buttons {
  display: flex;
  gap: 8px;
  align-self: flex-end;
}

/* ===================================================================== Studio
 *
 * THE STAFF PAGES' LOOK, on both editions — the direction the operator chose from three drawn
 * for the staff app, built here in dark and in daylight. The colours are the Studio palettes at
 * the top of this file; these are its shapes.
 *
 * Soft and tactile rather than outlined: solid surfaces layered on a flat ground, rounder
 * corners, Figtree for the words, and ONE mark for "you are here" wherever a page asks it — a
 * pill raised off a sunken track in the top bar, a range or a row of tabs, and the same pill
 * picked out on a surface in a rail or the dock. The accent keeps the jobs it already had: the
 * one action a page wants pressed, links, and a figure you can follow.
 *
 * EVERY RULE IS SCOPED WITH :where(:root[data-app='staff']), which weighs nothing. Each one weighs
 * exactly what the rule it restyles weighs and wins only by coming last, and that is the point:
 * a page's own more particular rule still beats it, and so does the accessibility block's high
 * contrast. The price is paid where it falls — a modifier of the same weight further up loses to
 * its base restated down here, so wherever a base is restyled, the modifiers it would flatten
 * are restated after it; and where there are too many of those to restate safely (the buttons,
 * the badges) the base rule reads a Studio token instead, with today's value as its fallback.
 *
 * SCREEN ONLY. The print block is the last word on paper and hides the chrome by the same
 * weight as these rules; a later screen-and-print rule setting `display` would put the tabs
 * and the top bar back on a printout.
 *
 * The five pages a guest sees never carry the stamp, so none of this can reach them.
 */

/* Figtree, self-hosted like Archivo and Manrope, since the CSP admits no font host but this
   one. Variable, 300 to 900, in the two subsets the app's words reach; the OFL licence is beside
   the files. `swap`, so words are never invisible while it arrives. */
@font-face {
  font-family: 'Figtree';
  font-style: normal;
  font-weight: 300 900;
  font-display: swap;
  src: url('/fonts/figtree-latin-v9.woff2') format('woff2');
  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: 'Figtree';
  font-style: normal;
  font-weight: 300 900;
  font-display: swap;
  src: url('/fonts/figtree-latin-ext-v9.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@media screen {
  /* The mark keeps the face it is drawn in everywhere else. A company's name does not change
     font with the page it is on, and the reticle is placed against that face's round letters
     (see .wordmark__reticle), not Figtree's. */
  :where(:root[data-app='staff']) .wordmark {
    font-family: var(--font-mark);
  }

  /* ------------------------------------------------------------------- the top bar
     The page's own ground, frosted, rather than a strip of another grey: the track of pages is
     what reads as the bar. */
  :where(:root[data-app='staff']) .topbar {
    background: color-mix(in oklab, var(--p-crust) 84%, transparent);
    border-bottom-color: transparent;
  }

  /* The pages, as a sunken track. No divider on its left: the track is its own edge. */
  :where(:root[data-app='staff']) .nav {
    margin-left: 0;
    padding: 4px;
    border-left: 0;
    /* Round at one row, a rounded rectangle at two — which is what the strip becomes on a
       laptop, where the links wrap rather than run off the bar (see .nav__rest). */
    border-radius: 21px;
    background: var(--track);
    box-shadow: inset 0 0 0 1px var(--border);
  }

  /* Column gap only: the row gap on a wrapped strip is set with the layout below. */
  :where(:root[data-app='staff']) .nav__rest {
    column-gap: 2px;
  }

  :where(:root[data-app='staff']) .nav__link {
    padding: 6px 14px;
    border-radius: 999px;
    font-size: 0.845rem;
    font-weight: 600;
  }

  :where(:root[data-app='staff']) .nav__link:hover {
    background: color-mix(in srgb, var(--p-text) 6%, transparent);
  }

  /* Home is a word on the track like the rest, as drawn: the house marked it out when the bar's
     only mark was colour, and the raised pill is the mark now. */
  :where(:root[data-app='staff']) .nav__icon {
    display: none;
  }

  /* The page you are on, raised. After the hover above, which weighs the same and would
     otherwise take the raise away under the pointer. The tablet's strip is buttons, which
     .nav--floor .nav__link strips of any fill — so its lit tab is named here as well. */
  :where(:root[data-app='staff']) .nav__link--current,
  :where(:root[data-app='staff']) .nav__link--current:hover,
  :where(:root[data-app='staff']) .nav--floor .nav__link--current {
    color: var(--text);
    background: var(--raised);
    border-color: transparent;
    box-shadow: var(--lift-raised);
    font-weight: 650;
  }

  :where(:root[data-app='staff']) .account__trigger {
    background: var(--surface);
    border-color: var(--border);
  }

  /* ------------------------------------------------------------------- the page head
     Left, like everything under it, with no tile beside the title and no rule beneath it: the
     name of the page, large, and then the page. */
  :where(:root[data-app='staff']) .page-head {
    justify-items: start;
    text-align: left;
  }

  :where(:root[data-app='staff']) .page-head::after,
  :where(:root[data-app='staff']) .page-head__icon {
    display: none;
  }

  :where(:root[data-app='staff']) .page-head__title {
    justify-content: flex-start;
  }

  :where(:root[data-app='staff']) .page-head__title h1 {
    font-size: clamp(1.75rem, 1.3rem + 1.4vw, 2.15rem);
    font-weight: 800;
    letter-spacing: -0.03em;
    line-height: 1.08;
  }

  :where(:root[data-app='staff']) .page-head__actions,
  :where(:root[data-app='staff']) .page-head > .btn {
    justify-content: flex-start;
  }

  /* A page with actions of its own sets them across from its title, as drawn, with the line
     under the title beneath it on the left and the page's description, when it is on, under
     both. */
  :where(:root[data-app='staff']) .page-head:has(> .page-head__actions) {
    grid-template-columns: minmax(0, 1fr) auto;
    column-gap: 16px;
    row-gap: 6px;
    align-items: center;
  }

  :where(:root[data-app='staff']) .page-head:has(> .page-head__actions) > .page-head__title {
    grid-row: 1;
    grid-column: 1;
  }

  :where(:root[data-app='staff']) .page-head:has(> .page-head__actions) > .page-head__sub {
    grid-row: 2;
    grid-column: 1;
  }

  :where(:root[data-app='staff']) .page-head:has(> .page-head__actions) > p {
    grid-row: 3;
    grid-column: 1 / -1;
  }

  :where(:root[data-app='staff']) .page-head > .page-head__actions {
    grid-row: 1 / span 2;
    grid-column: 2;
    margin-top: 0;
    justify-content: flex-end;
  }

  :where(:root[data-app='staff']) .page-head__sub {
    font-size: 0.95rem;
  }

  /* Refresh as the round button it is drawn as: the arrow alone, with the word kept for a
     screen reader. One rule for every page that has one, by its id, which is the same on all
     of them. */
  :where(:root[data-app='staff']) #refresh {
    display: inline-grid;
    place-items: center;
    flex: 0 0 auto;
    width: 36px;
    min-width: 36px;
    height: 36px;
    padding: 0;
    font-size: 0;
  }

  :where(:root[data-app='staff']) #refresh::before {
    content: '';
    width: 16px;
    height: 16px;
    background: currentColor;
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.1' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 12a8 8 0 1 1-2.34-5.66'/%3E%3Cpath d='M20 4.5v4.8h-4.8'/%3E%3C/svg%3E") center / contain no-repeat;
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.1' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 12a8 8 0 1 1-2.34-5.66'/%3E%3Cpath d='M20 4.5v4.8h-4.8'/%3E%3C/svg%3E") center / contain no-repeat;
  }

  /* A column narrower than the page, centred under a centred head, stays under the head now
     that the head sits left. */
  :where(:root[data-app='staff']) .announce-search,
  :where(:root[data-app='staff']) .announce-list {
    margin-inline: 0;
  }

  /* ------------------------------------------------------------------- the tiles
     Left-aligned and lifted, with no stripe along the top: the figure leads, and the tile is
     one of the surfaces rather than a panel with a coloured edge. */
  :where(:root[data-app='staff']) .stat {
    align-items: flex-start;
    justify-content: flex-start;
    text-align: left;
    border-radius: 20px;
    box-shadow: var(--shadow);
  }

  :where(:root[data-app='staff']) .stat::before {
    content: none;
  }

  /* ------------------------------------------------------------------- surfaces
     The cards and panels on the page, without the outline: set apart by their lift, as drawn.
     Each weighs what its own rule does, so the states that mark one out still win where they are
     heavier; the ones that weigh the same are restated after. */
  :where(:root[data-app='staff']) :is(.card, .announce, .stat, .chart, .pack, .skelcard, .sched__scroll,
    .writeup__steersum, .sales, .planbar, .rail, .kpi, .aihub__panel, .lockscreen__card, .tour__card,
    .market__rail, .cover, .consent__card, .chat) {
    border-color: var(--surface-edge);
  }

  :where(:root[data-app='staff']) .stat--edit {
    border-color: color-mix(in oklab, var(--accent) 40%, var(--border));
  }

  :where(:root[data-app='staff']) .card--dragging {
    border-color: var(--accent);
  }

  :where(:root[data-app='staff']) .planbar--plan {
    border-left-color: var(--accent);
  }

  /* The chevron of a tile that goes somewhere, across from its drawing at the top, as drawn:
     at the foot it read as the end of the note rather than as the tile's own way in. */
  :where(:root[data-app='staff']) .stat--link::after {
    top: 25px;
    right: 18px;
    bottom: auto;
  }

  /* A tile that is a <button> brought its own centring back, one step heavier than the tile. */
  :where(:root[data-app='staff']) button.stat--link {
    text-align: left;
  }

  :where(:root[data-app='staff']) .stat__value {
    font-weight: 800;
  }

  /* Sentence case, and large enough to read without it. No colour here: the warning label
     further up sets its own, at this weight, and must keep it. */
  :where(:root[data-app='staff']) .stat__label {
    margin-top: 6px;
    font-size: 0.82rem;
    letter-spacing: 0;
    text-transform: none;
  }

  /* ------------------------------------------------------------------- charts
     "Show the numbers" as the soft button it is, on no rule of its own: the card's foot is
     marked by the button, not by a line across the card. The bars keep their geometry. */
  :where(:root[data-app='staff']) .chart__data {
    padding-top: 12px;
    border-top: 0;
  }

  :where(:root[data-app='staff']) .chart__data > summary {
    display: inline-flex;
    align-items: center;
    width: fit-content;
    padding: 7px 14px;
    border-radius: 999px;
    background: var(--control);
    color: var(--text);
    font-size: 0.8rem;
    font-weight: 600;
  }

  :where(:root[data-app='staff']) .chart__data > summary:hover {
    background: var(--p-surface1);
  }

  :where(:root[data-app='staff']) .bars {
    gap: 2px;
  }

  /* Rows as drawn: 38px each, the name given more of the width and kept to one line — the
     whole name is in the tooltip, the row's spoken label and the table under the card. */
  :where(:root[data-app='staff']) .bars__row {
    grid-template-columns: minmax(84px, 42%) 1fr auto;
    min-height: 38px;
    padding: 0 8px;
    margin: 0 -8px;
    border-radius: 10px;
  }

  :where(:root[data-app='staff']) .bars__row--marked {
    grid-template-columns: 28px minmax(84px, 38%) 1fr auto;
  }

  :where(:root[data-app='staff']) .bars__label {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  /* The grip to move a card shows where it is wanted — under the pointer, or on the keyboard —
     and a page of charts at rest is charts, as drawn. On a touchscreen, with no hover to find
     it by, it stays as it was. */
  @media (hover: hover) {
    :where(:root[data-app='staff']) .chart__grip {
      opacity: 0;
    }

    :where(:root[data-app='staff']) .chart:hover .chart__grip,
    :where(:root[data-app='staff']) .chart:focus-within .chart__grip,
    :where(:root[data-app='staff']) .chart__grip:focus-visible {
      opacity: 1;
    }
  }

  :where(:root[data-app='staff']) .bars__row:hover,
  :where(:root[data-app='staff']) .bars__row:focus-visible {
    background: color-mix(in srgb, var(--p-text) 4%, transparent);
  }

  :where(:root[data-app='staff']) .bars__label {
    color: color-mix(in srgb, var(--p-text) 90%, var(--p-mantle));
    font-size: 0.875rem;
  }

  :where(:root[data-app='staff']) .bars__value {
    color: var(--text);
    font-size: 0.875rem;
    font-weight: 700;
  }

  /* A row about an issue type fills in the type's own hue, with its drawing on a tile of it. */
  :where(:root[data-app='staff']) .bars__row[data-type] .bars__fill {
    background: var(--type, var(--accent-fill));
  }

  :where(:root[data-app='staff']) .bars__icon {
    color: color-mix(in oklab, var(--type, var(--accent)) 80%, var(--p-text));
    background: color-mix(in srgb, var(--type, var(--accent)) 16%, transparent);
  }

  /* "Show all 10" in the accent, as the one line on a card that opens more of it. */
  :where(:root[data-app='staff']) .chart__more {
    border-top: 0;
    color: var(--accent);
    font-weight: 600;
  }

  :where(:root[data-app='staff']) .stack__key-label {
    color: color-mix(in srgb, var(--p-text) 90%, var(--p-mantle));
  }

  /* A list page's overview sets its tiles four across, as drawn, where there was room to take
     their drawings and their words without wrapping every label — and two across on a phone. */
  :where(:root[data-app='staff']) .market__glance .stats {
    grid-template-columns: repeat(auto-fit, minmax(min(180px, 45%), 1fr));
    gap: 12px;
  }

  /* ------------------------------------------------------------------- tracks
     The range, the tabs and the view switch: one sunken track each, with the choice showing
     raised out of it. */
  :where(:root[data-app='staff']) .rangebar {
    padding: 0;
    background: none;
    border-color: transparent;
    box-shadow: none;
  }

  :where(:root[data-app='staff']) .rangebar__options {
    gap: 2px;
    padding: 3px;
    border-radius: 14px;
    background: var(--track);
    box-shadow: inset 0 0 0 1px var(--border);
  }

  :where(:root[data-app='staff']) .rangebar__option {
    padding: 6px 12px;
    border-radius: 11px;
  }

  :where(:root[data-app='staff']) .rangebar__option--on {
    color: var(--text);
    background: var(--raised);
    border-color: transparent;
    box-shadow: var(--lift-raised);
  }

  :where(:root[data-app='staff']) .tabs {
    gap: 2px;
    width: fit-content;
    max-width: 100%;
    padding: 3px;
    border-bottom: 0;
    border-radius: 14px;
    background: var(--track);
    box-shadow: inset 0 0 0 1px var(--border);
  }

  :where(:root[data-app='staff']) .tab {
    padding: 7px 14px;
    border-bottom: 0;
    border-radius: 11px;
    font-size: 0.88rem;
  }

  /* The glow was the underline's light; there is no underline to rise off. */
  :where(:root[data-app='staff']) .tab::after {
    content: none;
  }

  :where(:root[data-app='staff']) .tab--on {
    background: var(--raised);
    box-shadow: var(--lift-raised);
  }

  :where(:root[data-app='staff']) .tab--on .tab__count {
    background: color-mix(in srgb, var(--p-text) 10%, transparent);
  }

  /* The week's arrows as the round buttons drawn at the ends of the track, rather than caps level
     with an underline that is no longer there. */
  :where(:root[data-app='staff']) .weeknav__arrow {
    align-self: center;
    width: 36px;
    height: 36px;
    margin-bottom: 0;
    border-bottom: 0;
    border-radius: 50%;
    background: var(--control);
  }

  :where(:root[data-app='staff']) .weeknav__arrow:hover {
    background: var(--p-surface1);
    color: var(--text);
  }

  :where(:root[data-app='staff']) .viewswitch {
    padding: 3px;
    border-color: transparent;
    border-radius: 12px;
    background: var(--track);
    box-shadow: inset 0 0 0 1px var(--border);
  }

  :where(:root[data-app='staff']) .viewswitch__option {
    border-radius: 9px;
  }

  :where(:root[data-app='staff']) .viewswitch__option--on {
    background: var(--raised);
    color: var(--text);
    box-shadow: var(--lift-raised);
  }

  /* 12h or 24h on the schedule: the same track, where the accent fill had it looking like the
     page's one action rather than a setting. */
  :where(:root[data-app='staff']) .seg {
    padding: 3px;
    border-color: transparent;
    border-radius: 14px;
    background: var(--track);
    box-shadow: inset 0 0 0 1px var(--border);
  }

  /* The same size as the day tabs beside it, so the row reads as one line of one kind of control. */
  :where(:root[data-app='staff']) .seg__opt {
    padding: 6px 13px;
    border-radius: 11px;
    font-size: 0.82rem;
  }

  :where(:root[data-app='staff']) .seg__opt--on {
    background: var(--raised);
    color: var(--text);
    box-shadow: var(--lift-raised);
  }

  /* Film colours beside it, on the same track at the same height, so the two read as one row of
     settings: the pair's padding and its option's, added up. */
  :where(:root[data-app='staff']) .switch {
    gap: 10px;
    padding: 9px 11px 9px 16px;
    border-color: transparent;
    border-radius: 14px;
    background: var(--track);
    box-shadow: inset 0 0 0 1px var(--border);
    font-size: 0.82rem;
  }

  :where(:root[data-app='staff']) .switch__track {
    width: 30px;
    height: 18px;
  }

  :where(:root[data-app='staff']) .switch__track::after {
    top: 3px;
    left: 3px;
    width: 12px;
    height: 12px;
  }

  /* ------------------------------------------------------------------- rails
     Picked out on the rail's own surface, where a pill raised in white would be white on white
     in daylight. */
  :where(:root[data-app='staff']) .rail__item,
  :where(:root[data-app='staff']) .market__railitem {
    border-radius: 12px;
  }

  :where(:root[data-app='staff']) .rail__item[aria-current='true'],
  :where(:root[data-app='staff']) .market__railitem--on {
    background: var(--picked);
    color: var(--text);
    box-shadow: var(--lift-picked);
  }

  :where(:root[data-app='staff']) .market__railhead {
    font-size: 0.78rem;
    font-weight: 650;
    letter-spacing: 0;
    text-transform: none;
  }

  /* ------------------------------------------------------------------- the case monitor
     As drawn: every case its own soft card on the page, under a line naming its cinema and a
     quieter one naming its room, with the issue type's drawing on a tile at the card's left and
     the impact as a pill in its colour. The glass rows and islands this restyles are the case
     pages' own (see "the glass rows"), and every selector here weighs what theirs does.

     The rail that hung each report off its severity gives way to the pill — still one colour per
     card, which is what the queue's calm-down asked for. */
  :where(:root[data-app='staff']) [data-caseview] .theatre > .theatre__head {
    padding: 8px 44px 6px 4px;
    background: none;
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    border: 0;
    border-radius: 12px;
    box-shadow: none;
  }

  :where(:root[data-app='staff']) [data-caseview] .theatre__name {
    font-size: 1.15rem;
    font-weight: 700;
  }

  :where(:root[data-app='staff']) [data-caseview] .theatre__head--toggle::before {
    top: 17px;
    right: 16px;
  }

  :where(:root[data-app='staff']) [data-caseview] .auditorium,
  :where(:root[data-app='staff']) [data-caseview] .auditorium:last-child {
    background: none;
    border: 0;
    border-radius: 0;
    box-shadow: none;
    overflow: visible;
  }

  :where(:root[data-app='staff']) .auditorium__head {
    padding-top: 8px;
    padding-bottom: 4px;
    background: none;
    border-bottom: 0;
  }

  :where(:root[data-app='staff']) .auditorium__body {
    gap: 12px;
    padding: 6px 0 14px;
  }

  /* The screen-down alert's Open button, a real tap target beside any name and at any width:
     Studio's small button round two short words came to 59px, under the 60 the alert has
     always kept. */
  :where(:root[data-app='staff']) .alarm__row .btn {
    min-width: 64px;
  }

  /* ------------------------------------------------------------------- questions
     A question waiting on the reader, as a card among the cards rather than an alarm: nothing
     is broken, and a blue frame round it made it the loudest thing on the page. The tag keeps
     the accent, and says what it is. */
  :where(:root[data-app='staff']) .alarm--asks,
  :where(:root[data-app='staff']) .alarm--asked,
  :where(:root[data-app='staff']) .alarm--ahead {
    border: 0;
    border-radius: 20px;
    background: var(--surface);
    box-shadow: var(--shadow);
  }

  :where(:root[data-app='staff']) .alarm--asks .alarm__tag,
  :where(:root[data-app='staff']) .alarm--ahead .alarm__tag {
    background: var(--accent-fill);
    color: var(--accent-ink);
    text-transform: none;
    letter-spacing: 0;
  }

  :where(:root[data-app='staff']) .alarm--asks .alarm__case,
  :where(:root[data-app='staff']) .alarm--ahead .alarm__case {
    border-top-color: var(--border);
  }

  /* ------------------------------------------------------------------- the assistant's button
     Round, frosted, and quiet: the page's own card with the hairline ring and the lift every
     Studio surface has, the reticle alone inside it. The glow and the gradient ring were the
     old tile's way of standing out on a page where everything had an edge; here the one round
     button in the corner is already the thing that stands out. */
  :where(:root[data-app='staff']) .aihub__launcher {
    border: 0;
    border-radius: 50%;
    background: color-mix(in srgb, var(--p-mantle) 80%, transparent);
    -webkit-backdrop-filter: blur(22px) saturate(160%);
    backdrop-filter: blur(22px) saturate(160%);
    box-shadow:
      inset 0 0 0 1px color-mix(in srgb, var(--p-text) 7%, transparent),
      0 18px 40px -6px var(--p-shadow);
  }

  :where(:root[data-app='staff']) .aihub__launcher::before {
    content: none;
  }

  /* ------------------------------------------------------------------- the queue's row
     The stages, and across from them the filters as a pill and the view switch as two icons. */
  :where(:root[data-app='staff']) .queuebar .filters__button {
    padding: 8px 16px;
    border: 0;
    border-radius: 999px;
    background: var(--control);
    font-size: 0.86rem;
    font-weight: 600;
  }

  :where(:root[data-app='staff']) .queuebar .filters__summary:hover .filters__button {
    background: var(--p-surface1);
  }

  :where(:root[data-app='staff']) .queuebar .filters--advanced[open] .filters__button,
  :where(:root[data-app='staff']) .queuebar .filters--advanced .filters__button:has(.filters__count:not([hidden])) {
    color: var(--accent);
    box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 55%, transparent);
  }

  :where(:root[data-app='staff']) .queuebar .filters--advanced .filters__body {
    border: 0;
    border-radius: 18px;
    background: var(--surface);
  }

  :where(:root[data-app='staff']) .queuebar .viewswitch__option {
    padding: 7px 9px;
  }

  /* The stages as drawn: smaller words, and each count a plain number after its name rather
     than a pill of its own — the raised tab is the mark, and five pills beside it were five
     more. */
  :where(:root[data-app='staff']) .queuebar .tab {
    padding: 6px 12px;
    font-size: 0.845rem;
  }

  :where(:root[data-app='staff']) .queuebar .tab__count,
  :where(:root[data-app='staff']) .queuebar .tab--on .tab__count {
    padding: 0;
    background: none;
    font-size: 0.76rem;
    font-weight: 700;
  }

  /* The issue types' chip on the line under the title: the filters' pill, smaller and quieter,
     and its panel on the filter panel's surface. */
  :where(:root[data-app='staff']) .typescope__chip {
    background: var(--control);
  }

  :where(:root[data-app='staff']) .typescope__chip:hover {
    background: var(--p-surface1);
  }

  :where(:root[data-app='staff']) .typescope__panel {
    border: 0;
    border-radius: 18px;
  }

  /* ------------------------------------------------------------------- a case's buttons
     Each with the drawing of what it does before its word, as drawn, in the page's ink — the
     accent is kept for the one decision a card in the queue is waiting on, filled. The drawing
     is a mask over the word's own colour, so it follows the button through hover, danger and
     high contrast without a colour of its own. */
  :where(:root[data-app='staff']) .complaint__actions [data-act]::before {
    content: '';
    flex: none;
    width: 15px;
    height: 15px;
    background: currentColor;
    -webkit-mask: var(--act-icon) center / contain no-repeat;
    mask: var(--act-icon) center / contain no-repeat;
  }

  :where(:root[data-app='staff']) .complaint__actions [data-act='note'] {
    --act-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7 3.5h7l4 4v13H7z M14 3.5v4h4 M9.5 12.5h6 M9.5 16h6'/%3E%3C/svg%3E");
  }

  :where(:root[data-app='staff']) .complaint__actions [data-act='ask'] {
    --act-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3.5a8.5 8.5 0 1 1-4.3 15.8L3.5 20.5l1.2-4.1A8.5 8.5 0 0 1 12 3.5z M9.8 9.6a2.3 2.3 0 1 1 3.2 2.1c-.7.3-1 .8-1 1.5v.4 M12 16.3v.2'/%3E%3C/svg%3E");
  }

  :where(:root[data-app='staff']) .complaint__actions [data-act='resolve'] {
    --act-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12.5l4.5 4.5L19 7.5'/%3E%3C/svg%3E");
  }

  :where(:root[data-app='staff']) .complaint__actions [data-act='reject'] {
    --act-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7 7l10 10 M17 7L7 17'/%3E%3C/svg%3E");
  }

  :where(:root[data-app='staff']) .complaint__actions [data-act='send'] {
    --act-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20.5 3.5L10.5 13.5 M20.5 3.5l-6.5 17-3.5-7-7-3.5z'/%3E%3C/svg%3E");
  }

  :where(:root[data-app='staff']) .complaint__actions [data-act='resolve'] {
    color: var(--text);
  }

  :where(:root[data-app='staff']) .complaint__actions [data-act='send'] {
    background: var(--accent-fill);
    color: var(--accent-ink);
    box-shadow: var(--btn-cta-lift);
  }

  :where(:root[data-app='staff']) .complaint__actions [data-act='send']:hover:not(:disabled) {
    background: color-mix(in srgb, var(--accent-fill) 88%, white);
  }

  /* Who it went to, after a tick, as drawn: a record of something done. */
  :where(:root[data-app='staff']) .complaint__sent {
    display: flex;
    align-items: center;
    gap: 8px;
  }

  :where(:root[data-app='staff']) .complaint__sent::before {
    content: '';
    flex: none;
    width: 14px;
    height: 14px;
    background: var(--ok);
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12.5l4.5 4.5L19 7.5'/%3E%3C/svg%3E") center / contain no-repeat;
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12.5l4.5 4.5L19 7.5'/%3E%3C/svg%3E") center / contain no-repeat;
  }

  :where(:root[data-app='staff']) .queuebar .viewswitch__option > span {
    display: none;
  }

  :where(:root[data-app='staff']) .complaint--report {
    position: relative;
    padding: 16px 18px;
    border: 0;
    border-radius: 22px;
    background: var(--surface);
    background-image: none;
    box-shadow: var(--shadow);
  }

  /* The states a card can be in, which weigh what the card does and were written before it —
     restated, or the card above would paint over the ring that marks an arrival from a link. */
  :where(:root[data-app='staff']) .complaint--spotlight {
    background: var(--accent-soft);
    box-shadow: inset 0 0 0 2px var(--accent), var(--shadow);
  }

  /* And the two Home's search lands on, restated for the same reason: a section and an account
     card each have a shadow of their own here, which would otherwise paint over the ring. */
  :where(:root[data-app='staff']) .section--found,
  :where(:root[data-app='staff']) .person--found {
    box-shadow: inset 0 0 0 2px var(--accent), var(--shadow);
  }

  :where(:root[data-app='staff']) .complaint--editing {
    background: var(--accent-soft);
  }

  :where(:root[data-app='staff']) .complaint--staged {
    background: var(--danger-soft);
  }

  /* The tile, at the card's left, with the words beside it rather than under it. */
  :where(:root[data-app='staff']) .complaint__type {
    position: absolute;
    top: 0;
    left: 0;
    display: inline-grid;
    place-items: center;
    width: 44px;
    height: 44px;
    border-radius: 13px;
    --glyph: 22px;
    color: color-mix(in oklab, var(--type, var(--accent)) 76%, var(--p-text));
    background: color-mix(in srgb, var(--type, var(--accent)) 16%, transparent);
  }

  :where(:root[data-app='staff']) .complaint__type .glyph--letter {
    border: 0;
    font-size: 1rem;
    font-stretch: 100%;
  }

  :where(:root[data-app='staff']) .complaint--fold > .complaint__face:not(.complaint__row) {
    min-height: 44px;
    padding-left: 58px;
  }

  :where(:root[data-app='staff']) article.complaint--report:not(.complaint__row) {
    padding-left: 76px;
  }

  :where(:root[data-app='staff']) article.complaint--report:not(.complaint__row) > .complaint__type {
    top: 16px;
    left: 18px;
  }

  :where(:root[data-app='staff']) .complaint--fold > .complaint__rest {
    margin-left: 58px;
  }

  /* The title: the place in the page's ink, the type after it, quieter, across a dot. */
  :where(:root[data-app='staff']) .complaint__head {
    margin-bottom: 8px;
  }

  :where(:root[data-app='staff']) .complaint__place {
    font-size: 1rem;
    color: var(--text);
  }

  :where(:root[data-app='staff']) .complaint__title > .meta__word {
    font-size: 0.9rem;
    color: var(--text-dim);
  }

  :where(:root[data-app='staff']) .complaint__place + .meta__word::before {
    content: '·' / '';
    margin-right: 8px;
    color: var(--text-faint);
  }

  /* Which and when, as one line in the page's own face: "R-4821 · 40 min ago". */
  :where(:root[data-app='staff']) .complaint__id {
    gap: 6px;
    font-size: 0.8rem;
  }

  :where(:root[data-app='staff']) .complaint__ref {
    font-family: inherit;
    font-size: 0.8rem;
    letter-spacing: 0;
  }

  :where(:root[data-app='staff']) .complaint__ref + .complaint__when::before {
    content: '·' / '';
    margin-right: 6px;
  }

  /* The fold as the round button drawn at the card's corner: a disc, with the chevron in it. */
  :where(:root[data-app='staff']) .complaint--fold > .complaint__face {
    padding-right: 46px;
  }

  :where(:root[data-app='staff']) .complaint--fold > .complaint__face::before {
    content: '';
    position: absolute;
    top: 0;
    right: 0;
    width: 32px;
    height: 32px;
    border-radius: 50%;
    background: var(--control);
    transition: background-color 140ms var(--ease);
  }

  :where(:root[data-app='staff']) .complaint--fold > .complaint__face:hover::before {
    background: var(--p-surface1);
  }

  :where(:root[data-app='staff']) .complaint--fold > .complaint__face::after {
    top: 11px;
    right: 13px;
    border-color: var(--text-dim);
  }

  :where(:root[data-app='staff']) .complaint--fold[open] > .complaint__face::after {
    top: 14px;
  }

  /* Same fault keeps the corner beside the disc rather than under it, and the title row stops
     short of it, so the time is never written underneath. 104px is the button with its words —
     94 measured — and a gap; it was 40 when the button was the glyph alone. */
  :where(:root[data-app='staff']) .complaint__meta > .complaint__fold-btn {
    top: 2px;
    right: 44px;
  }

  :where(:root[data-app='staff']) .complaint__face:has(.complaint__fold-btn) > .complaint__head {
    padding-right: 104px;
  }

  :where(:root[data-app='staff']) .complaint__body {
    font-size: 0.95rem;
    line-height: 1.5;
  }

  /* SHUT, the pills and the first line of the words share a row, as drawn: a folded card is a
     title and one line, which is what a queue of them is scanned by. */
  :where(:root[data-app='staff']) .complaint--fold:not([open]) > .complaint__face:not(.complaint__row) {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-items: center;
    column-gap: 10px;
  }

  :where(:root[data-app='staff']) .complaint--fold:not([open]) > .complaint__face > .complaint__head {
    grid-column: 1 / -1;
  }

  :where(:root[data-app='staff']) .complaint--fold:not([open]) > .complaint__face > .complaint__top {
    grid-column: 1;
    margin-bottom: 0;
    padding-right: 0;
  }

  :where(:root[data-app='staff']) .complaint--fold:not([open]) > .complaint__face > .complaint__body {
    grid-column: 2;
    -webkit-line-clamp: 1;
    line-clamp: 1;
    color: var(--p-subtext1);
    font-size: 0.9rem;
  }

  /* ------------------------------------------------------------------- notes
     The story of a case as a conversation, as drawn: each note a bubble with the writer's face
     beside its foot, who and when at its head, and the words under them. */
  :where(:root[data-app='staff']) .progress {
    display: grid;
    gap: 10px;
    border-left: 0;
    color: var(--text);
    font-style: normal;
  }

  :where(:root[data-app='staff']) .progress__label {
    display: none;
  }

  :where(:root[data-app='staff']) .progress__row {
    grid-template-columns: 30px minmax(0, 1fr);
    align-items: end;
    gap: 10px;
    padding: 0;
    border-top: 0;
  }

  :where(:root[data-app='staff']) .progress__line {
    width: fit-content;
    max-width: min(100%, 620px);
    padding: 10px 14px 11px;
    border-radius: 16px 16px 16px 6px;
    background: color-mix(in srgb, var(--p-text) 5%, var(--surface));
    gap: 3px 8px;
  }

  :where(:root[data-app='staff']) .progress__by {
    order: -1;
    font-size: 0.78rem;
    color: var(--text-dim);
  }

  :where(:root[data-app='staff']) .progress__tag {
    order: -2;
  }

  :where(:root[data-app='staff']) .progress__text {
    flex: 1 1 100%;
    font-size: 0.9rem;
    line-height: 1.45;
  }

  :where(:root[data-app='staff']) .progress__earlier > summary {
    color: var(--text-dim);
    font-size: 0.8rem;
  }

  /* Each type in a colour of its own, as drawn — identity, like a series in a chart, and never a
     status: how bad it is lives in the pill. The same hue on the case's tile and on the type's row
     in a chart, from the palette (see --type-sound and its neighbours), so each suits the ground
     it is on. A type the app does not ship takes the accent. */
  :where(:root[data-app='staff']) .complaint__type[data-type='sound'],
  :where(:root[data-app='staff']) .bars__row[data-type='sound'],
  :where(:root[data-app='staff']) .complaint__type[data-type='sound_system'],
  :where(:root[data-app='staff']) .bars__row[data-type='sound_system'] {
    --type: var(--type-sound);
  }

  :where(:root[data-app='staff']) .complaint__type[data-type='picture'],
  :where(:root[data-app='staff']) .bars__row[data-type='picture'],
  :where(:root[data-app='staff']) .complaint__type[data-type='projector'],
  :where(:root[data-app='staff']) .bars__row[data-type='projector'] {
    --type: var(--type-picture);
  }

  :where(:root[data-app='staff']) .complaint__type[data-type='seating'],
  :where(:root[data-app='staff']) .bars__row[data-type='seating'] {
    --type: var(--type-seating);
  }

  :where(:root[data-app='staff']) .complaint__type[data-type='cleanliness'],
  :where(:root[data-app='staff']) .bars__row[data-type='cleanliness'] {
    --type: var(--type-cleanliness);
  }

  :where(:root[data-app='staff']) .complaint__type[data-type='temperature'],
  :where(:root[data-app='staff']) .bars__row[data-type='temperature'],
  :where(:root[data-app='staff']) .complaint__type[data-type='hvac'],
  :where(:root[data-app='staff']) .bars__row[data-type='hvac'] {
    --type: var(--type-temperature);
  }

  :where(:root[data-app='staff']) .complaint__type[data-type='concessions'],
  :where(:root[data-app='staff']) .bars__row[data-type='concessions'] {
    --type: var(--type-concessions);
  }

  :where(:root[data-app='staff']) .complaint__type[data-type='disruption'],
  :where(:root[data-app='staff']) .bars__row[data-type='disruption'] {
    --type: var(--type-disruption);
  }

  :where(:root[data-app='staff']) .complaint__type[data-type='staff'],
  :where(:root[data-app='staff']) .bars__row[data-type='staff'] {
    --type: var(--type-staff);
  }

  :where(:root[data-app='staff']) .complaint__type[data-type='other'],
  :where(:root[data-app='staff']) .bars__row[data-type='other'] {
    --type: var(--p-subtext0);
  }

  /* The impact as a pill in its own colour with a dot, the status as a quiet one beside it, and
     the dots between words gone: two pills and a word need no punctuation to be told apart. */
  :where(:root[data-app='staff']) .complaint__meta .meta__word--severity,
  :where(:root[data-app='staff']) .complaint__meta .meta__word--status {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 3px 10px;
    border-radius: 999px;
    line-height: 1.3;
  }

  :where(:root[data-app='staff']) .complaint__meta .meta__word--severity {
    background: color-mix(in srgb, var(--card-edge, var(--text-dim)) 15%, transparent);
  }

  /* Medium in the caution colour, as it is in the impact ring: the one hue for the one level. It
     is also the slot the colour-blind palettes rewrite, which the warm one it replaces was not. */
  :where(:root[data-app='staff']) .complaint--report[data-severity='medium'] {
    --card-edge: var(--warn);
  }

  /* Low in the good colour, as in the ring, rather than grey on a grey pill — the word as well as
     the pill, since the rule that greys it weighs what this one does. */
  :where(:root[data-app='staff']) .complaint--report[data-severity='low'] {
    --card-edge: var(--ok);
  }

  :where(:root[data-app='staff']) [data-severity='low'] .meta__word--severity {
    color: var(--card-edge, var(--text-dim));
  }

  :where(:root[data-app='staff']) .complaint__meta .meta__word--severity::before {
    content: '';
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: currentColor;
  }

  :where(:root[data-app='staff']) .complaint__meta .meta__word--status {
    background: color-mix(in srgb, var(--p-text) 7%, transparent);
    color: var(--p-subtext1);
    font-size: 0.8rem;
    font-weight: 600;
  }

  :where(:root[data-app='staff']) .complaint__meta .meta__word--status::before {
    content: '';
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--accent-fill);
  }

  /* Jev's reading as the status's quiet neighbour: the same pill, the spark where the dot is. */
  :where(:root[data-app='staff']) .complaint__meta .meta__word--jev {
    padding: 3px 10px;
    border-radius: 999px;
    line-height: 1.3;
    background: color-mix(in srgb, var(--p-text) 7%, transparent);
    color: var(--p-subtext1);
    font-size: 0.8rem;
    font-weight: 600;
  }

  /* Where a case is stuck, as pills beside its stage, as drawn: waiting on somebody in a lighter
     step of the caution colour, and a fix from the floor waiting for sign-off in the good one. */
  :where(:root[data-app='staff']) .complaint__meta .meta__word--waiting,
  :where(:root[data-app='staff']) .complaint__meta .meta__word--fixed {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 3px 10px;
    border-radius: 999px;
    line-height: 1.3;
    font-size: 0.8rem;
    font-weight: 600;
  }

  :where(:root[data-app='staff']) .complaint__meta .meta__word--waiting {
    color: color-mix(in oklab, var(--warn) 70%, var(--p-text));
    background: color-mix(in srgb, var(--warn) 14%, transparent);
  }

  :where(:root[data-app='staff']) .complaint__meta .meta__word--fixed {
    color: var(--ok);
    background: color-mix(in srgb, var(--ok) 14%, transparent);
  }

  /* The guest's email, as the same pill in the accent: a fact about who reads the closure. */
  :where(:root[data-app='staff']) .complaint__meta .meta__word--emailing {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 3px 10px;
    border-radius: 999px;
    line-height: 1.3;
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--accent);
    background: color-mix(in srgb, var(--accent) 14%, transparent);
  }

  :where(:root[data-app='staff']) .complaint__meta .meta__word--waiting::before,
  :where(:root[data-app='staff']) .complaint__meta .meta__word--emailing::before,
  :where(:root[data-app='staff']) .complaint__meta .meta__word--fixed::before {
    content: '';
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: currentColor;
  }

  :where(:root[data-app='staff']) .complaint__meta {
    align-items: center;
  }

  :where(:root[data-app='staff']) .complaint__meta .meta__sep {
    display: none;
  }

  /* ------------------------------------------------------------------- the schedule
     The Showtime scheduler as drawn: the pickers as pills beside the week's tools, the days as a
     track between round arrows with the clock at its far end, the day on a card of its own —
     rooms down the side on rules rather than in boxes, faint hour lines, each film in a colour of
     its own and a red pill for now — and the week's figures on a card of tiles under it. */

  /* The cinema and the week as pills with their names inside, before the value, and a drawing
     of what each picks at its head. Still the browser's own select and date box, with their
     pickers and their keyboards: only the frame is the pill. */
  :where(:root[data-app='staff']) .sched__bar > .sched__toolbar {
    flex: 0 1 auto;
    align-items: center;
    gap: 10px;
  }

  :where(:root[data-app='staff']) .sched__pickers .field {
    display: flex;
    flex: 0 1 auto;
    align-items: center;
    gap: 8px;
    min-width: 0;
    padding-left: 14px;
    border-radius: 999px;
    background: var(--surface);
    box-shadow: inset 0 0 0 1px var(--border-strong);
  }

  :where(:root[data-app='staff']) .sched__pickers .field::before {
    content: '';
    flex: none;
    width: 16px;
    height: 16px;
    background: var(--text-dim);
    -webkit-mask: var(--pick-icon) center / contain no-repeat;
    mask: var(--pick-icon) center / contain no-repeat;
  }

  :where(:root[data-app='staff']) .sched__pickers .field:has(#cinema) {
    --pick-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 20.5V5a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v15.5 M3.5 20.5h17 M9 8h1 M14 8h1 M9 11.5h1 M14 11.5h1 M9 15h1 M14 15h1 M11 20.5v-3h2v3'/%3E%3C/svg%3E");
  }

  :where(:root[data-app='staff']) .sched__pickers .field:has(#week) {
    --pick-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4.5 6h15v13.5h-15z M4.5 10h15 M8.5 3.8v3.4 M15.5 3.8v3.4'/%3E%3C/svg%3E");
  }

  /* The ring the control would have drawn round itself, round the pill instead. */
  :where(:root[data-app='staff']) .sched__pickers .field:focus-within {
    box-shadow:
      inset 0 0 0 1px var(--accent),
      0 0 0 3px color-mix(in oklab, var(--accent) 22%, transparent);
  }

  :where(:root[data-app='staff']) .sched__pickers .field > label {
    margin: 0;
    color: var(--text-faint);
    font-size: 0.86rem;
    font-weight: 500;
    white-space: nowrap;
  }

  :where(:root[data-app='staff']) .sched__pickers :is(select, input[type='date']) {
    width: auto;
    min-width: 0;
    min-height: 40px;
    padding: 0 36px 0 0;
    border: 0;
    border-radius: 0 999px 999px 0;
    background-color: transparent;
    box-shadow: none;
    color: var(--text);
    font-weight: 600;
  }

  :where(:root[data-app='staff']) .sched__pickers input[type='date'] {
    padding-right: 12px;
  }

  :where(:root[data-app='staff']) .sched__pickers input[type='date']::-webkit-calendar-picker-indicator {
    width: 14px;
    height: 14px;
    margin-left: 10px;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238b8f99' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9.5l6 6 6-6'/%3E%3C/svg%3E") center / contain no-repeat;
    opacity: 1;
  }

  /* And the select's own chevron the same drawing, rather than the page's small triangle. */
  :where(:root[data-app='staff']) .sched__pickers select {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238b8f99' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9.5l6 6 6-6'/%3E%3C/svg%3E");
    background-position: right 14px center;
    background-size: 14px 14px;
  }

  /* The size drawn, where a mouse is doing the picking. A finger keeps the 16px every control has,
     under which iOS zooms the page on the tap. */
  @media (hover: hover) and (pointer: fine) {
    :where(:root[data-app='staff']) .sched__pickers :is(select, input[type='date']) {
      font-size: 0.9rem;
    }
  }

  /* The board's pickers take the editor's shape: the pills, and Print across from them. */
  :where(:root[data-app='staff']) .sched__pickers {
    align-items: center;
    gap: 10px;
  }

  :where(:root[data-app='staff']) .sched__pickers .field {
    flex: 0 1 auto;
  }

  :where(:root[data-app='staff']) .sched__pickers > #print {
    min-height: 38px;
    margin-left: auto;
    padding: 0 16px;
    border-radius: 999px;
  }

  /* The range is the line under the title now, where it is read first. */
  :where(:root[data-app='staff']) .sched__range {
    display: none;
  }

  /* The week's tools as the soft pills drawn, each with its picture. */
  :where(:root[data-app='staff']) .sched__actions {
    gap: 8px;
  }

  :where(:root[data-app='staff']) .sched__actions .btn {
    min-height: 38px;
    padding: 0 16px;
    border-radius: 999px;
    font-size: 0.84rem;
  }

  :where(:root[data-app='staff']) .pos-state {
    min-height: 38px;
    padding: 0 16px;
    border: 0;
    background: color-mix(in srgb, var(--accent) 10%, var(--surface));
  }

  :where(:root[data-app='staff']) .pos-state--warn {
    background: color-mix(in srgb, var(--warn) 12%, var(--surface));
  }

  /* A page head with nothing to show in its buttons' place leaves no row for them. */
  :where(:root[data-app='staff']) .page-head__actions:not(:has(> :not([hidden]))) {
    display: none;
  }

  /* The Optimizer's way in and the scheduling agent's button on one line under the takings, as
     two buttons rather than two rows. Inline, so the two bars sit side by side without a wrapper
     round them — each stays where the page's order puts it. The band the agent's button opens is
     a block, a row of its own under both, and the button stays on the line it was pressed on. */
  :where(:root[data-app='staff']) .optbar:not([hidden]),
  :where(:root[data-app='staff']) .agentbar:not([hidden]) {
    display: inline-flex;
    vertical-align: top;
    margin: 0 6px 16px 0;
  }

  :where(:root[data-app='staff']) .advicebar--on {
    border: 0;
    border-radius: 18px;
    background: color-mix(in srgb, var(--accent) 9%, var(--surface));
    box-shadow: var(--shadow);
  }

  /* The days as the track drawn, only as wide as its seven, with the clock at the far end. */
  :where(:root[data-app='staff']) .weeknav {
    align-items: center;
    gap: 10px;
    margin: 0 0 16px;
  }

  :where(:root[data-app='staff']) .weeknav > .tabs {
    flex: 0 1 auto;
    margin-bottom: 0;
  }

  :where(:root[data-app='staff']) .weeknav .tab {
    padding: 6px 14px;
    font-size: 0.845rem;
  }

  :where(:root[data-app='staff']) .weeknav .tab__count,
  :where(:root[data-app='staff']) .weeknav .tab--on .tab__count {
    padding: 0;
    background: none;
    font-size: 0.76rem;
    font-weight: 700;
  }

  /*
   * ALL SEVEN DAYS ON A COMPUTER. With a count and an advice dot on every day the strip can be
   * wider than its row, and a mouse cannot swipe a strip the way a finger can: the days past the
   * fade were simply out of reach. fitWeekNav (js/schedule-grid.js) measures the row and makes
   * room in two steps, only as far as it has to. First the settings move to a line of their own
   * under the days, right-aligned as a phone has them. Then the days go by their short names,
   * "Fri 25", a little closer together. A phone keeps its own row, where a finger scrolls.
   */
  :where(:root[data-app='staff']) .weeknav--stacked {
    flex-wrap: wrap;
    row-gap: 12px;
  }

  :where(:root[data-app='staff']) .weeknav--stacked > .weeknav__end {
    flex: 1 1 100%;
    justify-content: flex-end;
    margin-left: 0;
  }

  /* The days keep both arrows on their line. A row that wraps would otherwise put "next week" on
     a line of its own the moment the strip filled the first one, so the strip is held to the row
     less the two 36px arrows and their two 10px gaps (above), and the short names take over when
     that is not enough. */
  :where(:root[data-app='staff']) .weeknav--stacked > .tabs {
    max-width: calc(100% - 2 * 36px - 2 * 10px);
  }

  :where(:root[data-app='staff']) .tabs--short .tab {
    padding-inline: 11px;
  }

  /* The day on a card of its own. The room names stand on the card's own colour, as the column
     that keeps them in view while a phone scrolls the day under them has to. */
  :where(:root[data-app='staff']) .sched__scroll {
    padding: 12px 16px 4px 0;
    border-radius: 22px;
    background: var(--surface);
    box-shadow: var(--shadow);
  }

  :where(:root[data-app='staff']) .grid {
    --lane: 84px;
    gap: 0;
  }

  :where(:root[data-app='staff']) .grid__ruler {
    align-items: center;
    height: 30px;
  }

  :where(:root[data-app='staff']) .grid__corner {
    padding: 0 0 0 16px;
    font-size: 0.75rem;
    font-weight: 600;
    box-shadow: none;
  }

  :where(:root[data-app='staff']) .grid__hours {
    height: 30px;
  }

  :where(:root[data-app='staff']) .grid__hour {
    top: 7px;
  }

  /* A room is a rule across the card rather than a box of its own, as drawn. */
  :where(:root[data-app='staff']) .grid__lane {
    border-top: 1px solid var(--border);
  }

  :where(:root[data-app='staff']) .grid__room {
    justify-content: flex-start;
    padding: 12px 12px 0 16px;
    border-right: 0;
    box-shadow: none;
  }

  :where(:root[data-app='staff']) .grid__number {
    font-size: 0.875rem;
    font-weight: 700;
  }

  :where(:root[data-app='staff']) .grid__facts {
    font-size: 0.75rem;
  }

  :where(:root[data-app='staff']) .grid__add {
    display: inline-flex;
    align-items: center;
    height: 26px;
    margin-top: 6px;
    padding: 0 10px;
    border: 0;
    border-radius: 999px;
    background: var(--control);
    color: var(--text);
    font-size: 0.75rem;
    font-weight: 600;
  }

  :where(:root[data-app='staff']) .grid__add:hover {
    background: var(--p-surface1);
    color: var(--text);
  }

  /* The hour lines alone, faint, across a lane of the card's own colour: the half-hours were a
     second grid over the first, and the pills are what the lane is for. */
  :where(:root[data-app='staff']) .grid__track {
    border-radius: 0;
    background: repeating-linear-gradient(
      90deg,
      var(--border) 0 1px,
      transparent 1px calc(60 * var(--minute))
    );
  }

  /*
   * A showing as the tile drawn: its film's colour, soft, with the times in it — so the same
   * title reads as the same title down the whole day and across the week, and no two titles on a
   * day read as one. The colour is the film's for the week, never the showing's place (see
   * filmColours in js/schedule-grid.js): identity, from sixteen hues of its own (--film-1 on),
   * and never a status. The gauge that fills a pill with how sold it is fills in the film's colour
   * too.
   */
  :where(:root[data-app='staff']) .showing {
    --film: var(--accent);
    top: 10px;
    bottom: 10px;
    justify-content: flex-start;
    gap: 3px;
    padding: 8px 10px;
    border: 1px solid color-mix(in srgb, var(--film) 35%, transparent);
    border-radius: 14px;
    background-color: color-mix(in srgb, var(--film) 16%, var(--surface));
  }

  :where(:root[data-app='staff']) .showing:hover {
    border-color: color-mix(in srgb, var(--film) 70%, transparent);
  }

  :where(:root[data-app='staff']) .showing__open {
    justify-content: flex-start;
    gap: 3px;
    padding: 8px 10px;
  }

  :where(:root[data-app='staff']) .showing__time {
    color: var(--film);
    font-size: 0.75rem;
  }

  :where(:root[data-app='staff']) .showing__name {
    font-size: 0.8125rem;
  }

  :where(:root[data-app='staff']) .showing--gauge {
    background-image: linear-gradient(
      to top,
      color-mix(in srgb, var(--film) 22%, transparent) 0 calc(var(--fill) * 100%),
      transparent calc(var(--fill) * 100%)
    );
  }

  :where(:root[data-app='staff']) .showing[data-film='1'] {
    --film: var(--film-1);
  }

  :where(:root[data-app='staff']) .showing[data-film='2'] {
    --film: var(--film-2);
  }

  :where(:root[data-app='staff']) .showing[data-film='3'] {
    --film: var(--film-3);
  }

  :where(:root[data-app='staff']) .showing[data-film='4'] {
    --film: var(--film-4);
  }

  :where(:root[data-app='staff']) .showing[data-film='5'] {
    --film: var(--film-5);
  }

  :where(:root[data-app='staff']) .showing[data-film='6'] {
    --film: var(--film-6);
  }

  :where(:root[data-app='staff']) .showing[data-film='7'] {
    --film: var(--film-7);
  }

  :where(:root[data-app='staff']) .showing[data-film='8'] {
    --film: var(--film-8);
  }

  :where(:root[data-app='staff']) .showing[data-film='9'] {
    --film: var(--film-9);
  }

  :where(:root[data-app='staff']) .showing[data-film='10'] {
    --film: var(--film-10);
  }

  :where(:root[data-app='staff']) .showing[data-film='11'] {
    --film: var(--film-11);
  }

  :where(:root[data-app='staff']) .showing[data-film='12'] {
    --film: var(--film-12);
  }

  :where(:root[data-app='staff']) .showing[data-film='13'] {
    --film: var(--film-13);
  }

  :where(:root[data-app='staff']) .showing[data-film='14'] {
    --film: var(--film-14);
  }

  :where(:root[data-app='staff']) .showing[data-film='15'] {
    --film: var(--film-15);
  }

  :where(:root[data-app='staff']) .showing[data-film='16'] {
    --film: var(--film-16);
  }

  /* A day with more films than colours — a megaplex, not a room count this circuit has — deals the
     colours a second time, striped, so that pills of one look are still one film. On a layer of
     its own under the words: the pill's background is the sold gauge's (see .showing--gauge). */
  :where(:root[data-app='staff']) .showing[data-film-stripe] {
    isolation: isolate;
  }

  :where(:root[data-app='staff']) .showing[data-film-stripe]::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: -1;
    border-radius: inherit;
    background: repeating-linear-gradient(
      135deg,
      color-mix(in srgb, var(--film) 30%, transparent) 0 4px,
      transparent 4px 10px
    );
    pointer-events: none;
  }

  /* The quick way off the day shows where it is wanted — under the pointer, or on the keyboard —
     as the chart grips do, rather than an x in the corner of every tile over its times. On a
     touchscreen, with no pointer to find it by, it stays. */
  @media (hover: hover) {
    :where(:root[data-app='staff']) .showing__x {
      opacity: 0;
    }

    :where(:root[data-app='staff']) .showing:hover .showing__x,
    :where(:root[data-app='staff']) .showing__x:focus-visible {
      opacity: 1;
    }
  }

  /* A slot the scheduling agent would change, split as before, inside the new insets. */
  :where(:root[data-app='staff']) .showing.showing--halved {
    bottom: calc(50% + 2px);
  }

  :where(:root[data-app='staff']) .showing.showing--advised {
    top: calc(50% + 2px);
    bottom: 10px;
  }

  /* Now, as drawn: the line in the impact red with a glow, and its word on a pill centred on it
     in the ruler's band. The badge is a fixed width so that hideHourUnderNow, which reads its
     box off this stylesheet, measures where it really is. */
  :where(:root[data-app='staff']) .grid__now {
    top: 30px;
    box-shadow: 0 0 10px color-mix(in srgb, var(--danger) 60%, transparent);
  }

  :where(:root[data-app='staff']) .grid__now::before {
    top: -25px;
    left: -19px;
    width: 40px;
    height: 18px;
    padding: 0;
    border-radius: 9px;
    color: var(--p-crust);
    font-size: 0.66rem;
    font-weight: 800;
    line-height: 18px;
    text-align: center;
  }

  :where(:root[data-app='staff']) .grid__now--flip::before {
    left: auto;
    right: -1px;
  }

  /* The week's figures as drawn: a card of their own, the figures large, the days as tiles with
     the day on screen raised out of them. */
  :where(:root[data-app='staff']) .sales {
    padding: 16px 20px;
    border-radius: 22px;
    background: var(--surface);
    box-shadow: var(--shadow);
  }

  :where(:root[data-app='staff']) .sales__title {
    margin-bottom: 12px;
    color: var(--text-dim);
    font-size: 0.8rem;
  }

  :where(:root[data-app='staff']) .sales__facts {
    gap: 12px 28px;
  }

  :where(:root[data-app='staff']) .sales__label {
    font-size: 0.78rem;
  }

  :where(:root[data-app='staff']) .sales__figure {
    font-size: 1.25rem;
    font-weight: 800;
    letter-spacing: -0.01em;
  }

  :where(:root[data-app='staff']) .sales__hint {
    font-size: 0.75rem;
  }

  :where(:root[data-app='staff']) .maxes__row {
    gap: 12px 28px;
  }

  :where(:root[data-app='staff']) .maxes__days,
  :where(:root[data-app='staff']) .dock .maxes__days,
  :where(:root[data-app='staff']) .sales__days {
    flex: 1 1 auto;
    gap: 8px;
  }

  :where(:root[data-app='staff']) .sales__days {
    margin-top: 14px;
  }

  :where(:root[data-app='staff']) .maxes__day,
  :where(:root[data-app='staff']) .dock .maxes__day,
  :where(:root[data-app='staff']) .sales__day {
    flex: 1 0 64px;
    width: auto;
    padding: 10px 12px;
    border: 0;
    border-radius: 14px;
    background: color-mix(in srgb, var(--p-text) 4%, transparent);
  }

  :where(:root[data-app='staff']) .maxes__day--on,
  :where(:root[data-app='staff']) .sales__day--on {
    background: var(--raised);
    box-shadow: var(--lift-raised);
  }

  :where(:root[data-app='staff']) .maxes__dayname,
  :where(:root[data-app='staff']) .sales__dayname {
    font-size: 0.75rem;
  }

  :where(:root[data-app='staff']) .maxes__figure,
  :where(:root[data-app='staff']) .sales__dayfigure {
    font-size: 0.95rem;
    font-weight: 700;
  }

  /* The seat count floats over the day while a long one is scrolled, so it keeps the stronger
     lift that says it is above the page rather than on it — and its figures the size drawn, where
     the floating box had made them small to cost the schedule less. */
  :where(:root[data-app='staff']) .dock__panel {
    padding: 16px 20px;
    box-shadow: var(--shadow-lift);
  }

  :where(:root[data-app='staff']) .dock__panel .sales__figure {
    font-size: 1.25rem;
    line-height: 1.2;
  }

  /* The controls above a list or a board, on the page rather than in a panel of their own. */
  :where(:root[data-app='staff']) .toolbar {
    padding: 0;
    background: none;
    border-color: transparent;
    box-shadow: none;
  }

  /* ------------------------------------------------------------------- words
     Sentence case for the small labels, as for the badges: a label in capitals is a label raised
     above the voice of the page, and at these sizes the capitals were most of what made them
     hard to read. Every one of these is written in sentence case where it is made, so only the
     dressing comes off. Each selector weighs what its own rule weighs. The printed placards keep
     their capitals: they are paper, with a look of their own. */
  :where(:root[data-app='staff']) .meta__word--severity,
  :where(:root[data-app='staff']) .complaint__note strong,
  :where(:root[data-app='staff']) .table thead th,
  :where(:root[data-app='staff']) .sentiment__table th[scope='col'],
  :where(:root[data-app='staff']) .prose__table th,
  :where(:root[data-app='staff']) .toolbar .field > label,
  :where(:root[data-app='staff']) .toolbar--bare .field > label,
  :where(:root[data-app='staff']) .filters__body .field > label,
  :where(:root[data-app='staff']) .person--editing .field > label,
  :where(:root[data-app='staff']) .sales__title,
  :where(:root[data-app='staff']) .grid__corner,
  :where(:root[data-app='staff']) .grid__downbadge,
  :where(:root[data-app='staff']) .theme__default,
  :where(:root[data-app='staff']) .chart__peaklabel,
  :where(:root[data-app='staff']) .alarm__tag,
  :where(:root[data-app='staff']) .askreport__label,
  :where(:root[data-app='staff']) .announce__who-head,
  :where(:root[data-app='staff']) .dash-add__count,
  :where(:root[data-app='staff']) .rangebar__lead,
  :where(:root[data-app='staff']) .progress__tag,
  :where(:root[data-app='staff']) .progress__label,
  :where(:root[data-app='staff']) .compliance__head,
  :where(:root[data-app='staff']) .edit-more__label,
  :where(:root[data-app='staff']) .notify__label,
  :where(:root[data-app='staff']) .shots__label,
  :where(:root[data-app='staff']) .peekcase__tag,
  :where(:root[data-app='staff']) .attn__tag,
  :where(:root[data-app='staff']) .oblig__state,
  :where(:root[data-app='staff']) .borrowed__tag,
  :where(:root[data-app='staff']) .watched__tag,
  :where(:root[data-app='staff']) .tour__kicker,
  :where(:root[data-app='staff']) .onnow__label,
  :where(:root[data-app='staff']) .shopfloor__grouptitle {
    letter-spacing: 0;
    text-transform: none;
  }

  /* The impact word on a case, out of its capitals, at the size of the words beside it. */
  :where(:root[data-app='staff']) .meta__word--severity {
    font-size: 0.8rem;
  }

  /* The field labels in a toolbar, out of their capitals, take back the size the capitals were
     lending them. */
  :where(:root[data-app='staff']) .toolbar .field > label,
  :where(:root[data-app='staff']) .toolbar--bare .field > label,
  :where(:root[data-app='staff']) .filters__body .field > label,
  :where(:root[data-app='staff']) .person--editing .field > label {
    font-size: 0.82rem;
  }

  /* ------------------------------------------------------------------- the front door
     The accent hairline over the card gives way to a glow of the accent at the top of the page,
     on the tint layer every page already has. */
  :where(:root[data-app='staff']) .signin__card::before {
    content: none;
  }

  :where(:root[data-app='staff']) .signin__card {
    border-radius: 28px;
    box-shadow: 0 1px 0 var(--p-hairline) inset, 0 30px 80px -24px var(--p-shadow-strong);
  }

  :where(:root[data-app='staff']) .signin__title {
    font-size: 1.6rem;
    font-weight: 800;
    letter-spacing: -0.02em;
  }
}

/* The sign-in page's glow is the theme's own wash turned up, so it lives on the palette's slot
   rather than on body, which paints no background of its own (see html::before). Heavier than
   every Studio palette but Your brand in daylight, whose wash is already the circuit's colour. */
:root[data-app='staff']:has(> body.signin) {
  --p-tint: color-mix(in srgb, var(--p-accent-fill) 16%, var(--p-crust));
}

/*
 * INSTALLED ON A HOME SCREEN, THE BAR IS SOLID — its glass stays in the browser.
 *
 * iOS 26 and later lay a Liquid Glass blur of their own over the top of an installed web app,
 * reaching about 38 to 40 points below the status bar. It is not a layer of this page, so nothing in
 * the stacking reaches it, and nothing here turns it off. The wordmark and the account start 12px
 * under the status bar with the top of them in it, which the operator preferred to a bar pushed
 * down clear of it (THE TOP BAR CLEARS THE STATUS BAR, under "installed on a home screen").
 *
 * This rule was written first, to make WebKit take the bar for a solid top edge and leave the blur
 * off — a fixed or sticky box across the window with an opaque colour and no backdrop filter, read
 * from WebKit's LocalFrameView. On the operator's phone it did not: pulled down, the bar came out of
 * the blur sharp and the blur stayed where it was. It stays for what it does do. The band between
 * the status bar and the wordmark is the bar's, and the blur is drawn over it; over one flat colour
 * a blur has nothing to smear, where the frosted bar (84% of its colour) let the page scrolling
 * under it show through, blurred twice.
 *
 * Here and not with the rest of "installed on a home screen": every page with this bar is a Studio
 * page (the stamp is in src/app.js), and Studio's own bar rule above would win over one written
 * there. In a browser tab the browser's own bar is at the top of the screen, and this one keeps its
 * glass. Held in test/browser/installed-top.browser.js.
 */
@media screen and (display-mode: standalone),
  screen and (display-mode: minimal-ui),
  screen and (display-mode: fullscreen) {
  :where(:root[data-app='staff']) .topbar {
    background: var(--p-crust);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
}

/* A wider screen: the track of pages sits in the middle of the bar, with the mark at one end
   and the account at the other. A grid rather than the flex row, because a flex row can only
   centre it between two ends of different widths. The columns either side never narrow past
   what they hold, so the strip still wraps inside its own column exactly as it did before (see
   .nav__rest above 701px) rather than pushing anything off the bar. Everything is placed by
   name, because the tablet's bar has no account and the install page's has only Home. */
@media screen and (min-width: 701px) {
  :where(:root[data-app='staff']) .topbar__inner {
    display: grid;
    grid-template-columns: minmax(max-content, 1fr) minmax(0, auto) minmax(max-content, 1fr);
    align-items: center;
  }

  :where(:root[data-app='staff']) .brand {
    grid-column: 1;
    justify-self: start;
  }

  :where(:root[data-app='staff']) .nav {
    grid-column: 2;
  }

  :where(:root[data-app='staff']) .topbar__spacer {
    display: none;
  }

  :where(:root[data-app='staff']) .account,
  :where(:root[data-app='staff']) .topbar__inner > .topbar__who {
    grid-column: 3;
    justify-self: end;
  }

  :where(:root[data-app='staff']) .nav__rest {
    row-gap: 4px;
  }
}

/* The range's actions at the far end of its row, as drawn. The options push them there already
   where the fold dissolves, but Chrome keeps a box for the fold's contents regardless (see
   .rangebar__fold), and inside that box the push has nothing to push against — so the push is
   made by the box itself. A browser that does not know the pseudo-element drops this one rule
   and keeps the buttons beside the range, which is where they always were. */
@media screen and (min-width: 561px) {
  :where(:root[data-app='staff']) .rangebar__fold::details-content {
    margin-right: auto;
  }
}

/* A narrower screen, where the rail folds into a row of chips. They sit on the page itself
   rather than in a panel of their own — the ground is their track — so the pick is the raised
   pill, not the one for a surface. No lift: the row scrolls sideways, and its edge would cut
   the shadow off square. */
@media screen and (max-width: 860px) {
  /* Square, too: the panel's 22px corners are gone with the panel, but a scroller clips to its
     corners all the same, and they shaved the curve off whichever chip stood at the strip's end —
     a straight stub of border where its rounded edge should be. Unseen while that chip was the
     lit Overview, which draws no border; plain once a list page's Overview moved to the bar. */
  :where(:root[data-app='staff']) .market__rail {
    padding: 0;
    background: none;
    border-color: transparent;
    border-radius: 0;
    box-shadow: none;
  }

  :where(:root[data-app='staff']) .market__railitem {
    border-color: var(--border-strong);
  }

  :where(:root[data-app='staff']) .market__railitem--on {
    background: var(--raised);
    border-color: transparent;
    box-shadow: none;
  }

  /* A list page's Overview in the range bar is one of these chips, moved: drawn as they are. */
  :where(:root[data-app='staff']) .rangebar__overview {
    border-color: var(--border-strong);
    border-radius: 12px;
  }

  :where(:root[data-app='staff']) .rangebar__overview[aria-current='page'] {
    background: var(--raised);
    border-color: transparent;
  }
}

/* A PHONE'S CASE MONITOR, as drawn: less between the title and the first case, and every card
   the whole of its width for its words.

   The card's head is a row — the type's tile, then the place and type with which report and when
   under them, then the fold's disc — and everything else runs the card's full width under it,
   the pills, the words, the notes and the buttons. A column beside the tile, the old shape, left
   the words a third of a phone's width and every pill on a line of its own. */
@media screen and (max-width: 560px) {
  :where(:root[data-app='staff']) .complaint--fold > .complaint__face:not(.complaint__row),
  :where(:root[data-app='staff']) .complaint--fold:not([open]) > .complaint__face:not(.complaint__row) {
    display: grid;
    grid-template-columns: 36px minmax(0, 1fr);
    align-items: start;
    column-gap: 12px;
    min-height: 0;
    padding-left: 0;
    padding-right: 0;
  }

  :where(:root[data-app='staff']) .complaint--fold > .complaint__face > .complaint__type {
    position: static;
    grid-column: 1;
    grid-row: 1;
    width: 36px;
    height: 36px;
    border-radius: 11px;
    --glyph: 19px;
  }

  /* Beside the tile, shut as well as open — the shut card's own rule spans the head across the
     whole row, which on a desk is right (the tile stands in the face's padding there) and on a
     phone put the title over the tile, since here the tile has a column of its own. */
  :where(:root[data-app='staff']) .complaint--fold > .complaint__face > .complaint__head,
  :where(:root[data-app='staff']) .complaint--fold:not([open]) > .complaint__face > .complaint__head {
    grid-column: 2;
    grid-row: 1;
    flex-direction: column;
    align-items: flex-start;
    gap: 2px;
    min-height: 36px;
    margin-bottom: 10px;
    padding-right: 40px;
  }

  /* On a phone the corner is the title's line only — which report and when is the line under it,
     clear of the button — so the TITLE stops short of Same fault, words and all. The head keeps the
     disc's own 40px. And the place and its type stack, "Auditorium 2" over "Temperature": in the
     width left beside the button most titles wrapped anyway, and they wrapped at the dot, which
     opened the second line on "·". */
  :where(:root[data-app='staff']) .complaint--fold > .complaint__face:has(.complaint__fold-btn) > .complaint__head {
    padding-right: 40px;
  }

  :where(:root[data-app='staff']) .complaint--fold > .complaint__face:has(.complaint__fold-btn) > .complaint__head > .complaint__title {
    flex-direction: column;
    align-items: flex-start;
    padding-right: 110px;
  }

  :where(:root[data-app='staff']) .complaint--fold > .complaint__face:has(.complaint__fold-btn) .complaint__place + .meta__word::before {
    content: none;
  }

  :where(:root[data-app='staff']) .complaint--fold > .complaint__face > .complaint__head > .complaint__id {
    margin-left: 0;
  }

  :where(:root[data-app='staff']) .complaint--fold > .complaint__face > .complaint__top,
  :where(:root[data-app='staff']) .complaint--fold > .complaint__face > .complaint__body,
  :where(:root[data-app='staff']) .complaint--fold:not([open]) > .complaint__face > .complaint__top,
  :where(:root[data-app='staff']) .complaint--fold:not([open]) > .complaint__face > .complaint__body {
    grid-column: 1 / -1;
  }

  :where(:root[data-app='staff']) .complaint--fold:not([open]) > .complaint__face > .complaint__top {
    margin-bottom: 6px;
    padding-right: 0;
  }

  :where(:root[data-app='staff']) .complaint--fold:not([open]) > .complaint__face > .complaint__body {
    -webkit-line-clamp: 2;
    line-clamp: 2;
  }

  :where(:root[data-app='staff']) article.complaint--report:not(.complaint__row) {
    padding-left: 66px;
  }

  :where(:root[data-app='staff']) .complaint__type {
    width: 36px;
    height: 36px;
    border-radius: 11px;
    --glyph: 19px;
  }

  :where(:root[data-app='staff']) .complaint--fold > .complaint__rest {
    margin-left: 0;
  }

  /* The buttons a step smaller than the page's own: four to a card, they were most of it. Still
     a thumb's height. */
  :where(:root[data-app='staff']) .complaint__actions .btn {
    min-height: 40px;
    padding-block: 8px;
    font-size: 0.88rem;
  }

  /* A cinema's heading as one line and a note, as drawn: its name and how bad, then what it
     holds. The code and the town are the building's, not the queue's, and on a phone they were
     a line of their own above every cinema. */
  :where(:root[data-app='staff']) [data-caseview] .theatre__meta,
  :where(:root[data-app='staff']) .auditorium__meta {
    display: none;
  }

  :where(:root[data-app='staff']) [data-caseview] .theatre__count {
    flex-basis: 100%;
    margin-left: 0;
  }

  /* Folding every cinema at once is a desk's shortcut. A phone has the rail's chips to pick one
     cinema, and each heading folds its own; the button was a row of its own above the list. */
  :where(:root[data-app='staff']) #fold {
    display: none;
  }

  :where(:root[data-app='staff']) .tabs__foot:not(:has(> #case-history:not([hidden]))) {
    display: none;
  }

  /* The questions panel in three lines: what it is, which case, and who with the answer's
     buttons — the question itself cut to a line, whole on the case's own note. */
  :where(:root[data-app='staff']) .alarm--asks {
    padding: 12px 14px;
    margin-bottom: 16px;
  }

  :where(:root[data-app='staff']) .alarm--asks .alarm__title {
    font-size: 0.92rem;
  }

  :where(:root[data-app='staff']) .alarm--asks .alarm__case {
    padding-top: 8px;
  }

  :where(:root[data-app='staff']) .alarm--asks .alarm__where {
    flex-basis: 100%;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    font-size: 0.86rem;
  }

  /* The question itself under who asked it, and the buttons that answer it under the question —
     the line's parts laid out as one column rather than a row the question hangs below. */
  :where(:root[data-app='staff']) .alarm--asks .alarm__case {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px 8px;
  }

  :where(:root[data-app='staff']) .alarm--asks .alarm__row {
    display: contents;
  }

  :where(:root[data-app='staff']) .alarm--asks .alarm__where {
    order: 1;
  }

  :where(:root[data-app='staff']) .alarm--asks .alarm__age--who {
    order: 2;
    flex-basis: 100%;
  }

  :where(:root[data-app='staff']) .alarm--asks .alarm__what {
    order: 3;
    flex-basis: 100%;
    margin: 2px 0 4px;
  }

  :where(:root[data-app='staff']) .alarm--asks .alarm__actions {
    order: 4;
    margin-left: 0;
  }

  :where(:root[data-app='staff']) .alarm--asks .alarm__actions .btn {
    min-height: 32px;
    padding: 5px 12px;
  }

  /* A title with actions keeps them, but under it: a phone has no width to set them across. */
  :where(:root[data-app='staff']) .page-head:has(> .page-head__actions) {
    grid-template-columns: minmax(0, 1fr);
  }

  :where(:root[data-app='staff']) .page-head > .page-head__actions {
    grid-row: auto;
    grid-column: 1;
    justify-content: flex-start;
    margin-top: 6px;
  }

  /* On one line, as drawn — Edit dashboard, Detailed report and the round Refresh — rather than a
     stack of full-width buttons with Refresh a disc on a line of its own under them. */
  :where(:root[data-app='staff']) .page-head__actions {
    flex-direction: row;
    flex-wrap: nowrap;
  }

  :where(:root[data-app='staff']) .page-head__actions .btn {
    width: auto;
    flex: 1 1 auto;
  }

  :where(:root[data-app='staff']) .page-head__actions #refresh {
    flex: 0 0 36px;
  }

  /* After the row's own rule, which it ties with: the disc keeps its size on the phone's line. */
  :where(:root[data-app='staff']) .page-head__actions .search-open {
    flex: 0 0 36px;
    width: 36px;
  }

  /* The Case monitor's title row holds nothing drawn — Create new is on the filters' row here and
     the agent's row on a desk, and Refresh is kept hidden for the pull to press — so the row goes,
     and the counts under the title are the whole of it. See placeRaise in js/review.js. */
  :where(:root[data-app='staff']) .page-head__actions--queue {
    display: none;
  }

  /* The filters' row, under the stages: the filters and the view switch from its start, and
     Create new at its end, its own size. */
  :where(:root[data-app='staff']) .queuebar__tools {
    flex: 1 1 auto;
    margin-left: 0;
  }

  :where(:root[data-app='staff']) .queuebar__tools > #raise {
    flex: 0 0 auto;
    margin-left: auto;
  }

  /* The AI Reviewer Agent its own size, a pill like the others on the page, rather than a bar
     across the phone above the stages. */
  :where(:root[data-app='staff']) .agentrow__btn {
    flex: 0 0 auto;
  }
}

/* THE DOCK AS DRAWN: a capsule of the page's own card, frosted, floating where the dock always
   has — the same seven pixels off the floor the operator settled on — and taller, so the page you
   are on is a round pill raised inside it rather than a tile squeezed into a strip.

   Its measure is one number, --dock-h, because four other things stand on it: the page's own
   foot, the sticky bars that float above it, the More menu that opens off it, and the assistant's
   button that sits beside it at the same height. The icon's step up and the bloom under the lit
   tab were the old dock's mark for the same fact, and two marks for one fact is one too many. No
   transition on the items: the bar's own press spring owns that property (see .dockbar__item near
   the motion pass), and reduced motion turns it off there. */
@media screen and (max-width: 700px) {
  :where(:root[data-app='staff']) {
    --dock-h: 66px;
    --dock-inset: 14px;
    --dock-surface: color-mix(in srgb, var(--p-mantle) 80%, transparent);
    --dock-lift:
      inset 0 0 0 1px color-mix(in srgb, var(--p-text) 7%, transparent),
      0 18px 40px -6px var(--p-shadow);
  }

  :where(:root[data-app='staff']) .dockbar {
    left: var(--dock-inset);
    right: var(--dock-inset);
    height: var(--dock-h);
    gap: 4px;
    padding: 6px;
    border: 0;
    border-radius: calc(var(--dock-h) / 2);
    background: var(--dock-surface);
    -webkit-backdrop-filter: blur(22px) saturate(160%);
    backdrop-filter: blur(22px) saturate(160%);
    box-shadow: var(--dock-lift);
  }

  /* Beside the assistant's button, the capsule stops short of it by a gap of 8px. Written with
     the element name, to weigh what the rule it replaces does and win by coming after it. */
  html:where([data-app='staff'])[data-assistant] .dockbar {
    right: calc(var(--dock-inset) + var(--dock-h) + 8px);
  }

  :where(:root[data-app='staff']) .dockbar__item {
    flex: 1 1 0;
    align-self: stretch;
    justify-content: center;
    min-width: 0;
    gap: 3px;
    /* A capsule inside the capsule: the bar's height, less the 6px it is inset by each side. */
    border-radius: calc(var(--dock-h) / 2 - 6px);
    font-size: 11px;
  }

  :where(:root[data-app='staff']) .dockbar__item svg {
    width: 22px;
    height: 22px;
  }

  :where(:root[data-app='staff']) .dockbar__item--on {
    background: var(--picked);
    box-shadow: var(--lift-picked);
  }

  :where(:root[data-app='staff']) .dockbar__item--on svg {
    transform: none;
  }

  :where(:root[data-app='staff']) .dockbar__item--on::after {
    content: none;
  }

  /* The assistant's button as the capsule's round companion — the same height, the same frosted
     card, the same ring and shadow — rather than a glowing tile of its own: the reticle is the
     mark, and it needs no halo to be found at the end of the bar it belongs to. */
  html:where([data-app='staff']):has(.dockbar) .aihub__launcher {
    right: var(--dock-inset);
    width: var(--dock-h);
    height: var(--dock-h);
  }

  /* Everything that stood on the old 52px bar stands on this one: the page's foot clears it, the
     sticky bars float 16px above it, and the More menu opens 10px off it. */
  html:where([data-app='staff']):has(.dockbar) body {
    padding-bottom: calc(7px + var(--dock-h) + 20px + env(safe-area-inset-bottom, 0px));
  }

  html:where([data-app='staff']):has(.dockbar) .savebar--sticky,
  html:where([data-app='staff']):has(.dockbar) .dock {
    bottom: calc(7px + var(--dock-h) + 16px + env(safe-area-inset-bottom, 0px));
  }

  :where(:root[data-app='staff']) .dockbar__menu {
    right: var(--dock-inset);
    bottom: calc(7px + var(--dock-h) + 10px + env(safe-area-inset-bottom, 0px));
  }

  /* More's pages, each with its icon on a tile of the accent, as the drawings had them. The
     glyph is the <svg> itself, so the tile is its own padding and fill. */
  :where(:root[data-app='staff']) .dockbar__menu-link svg {
    box-sizing: content-box;
    padding: 7px;
    border-radius: 10px;
    background: color-mix(in srgb, var(--accent) 16%, transparent);
    color: var(--accent);
  }
}

/* THE SHOWTIME SCHEDULER ON A PHONE, as drawn: the cinema and the week side by side, the days as
   the track with Edit and the clock under them, the day on a narrower card with the rooms' names
   wrapping, and the figures as a strip that scrolls. */
@media screen and (max-width: 700px) {
  /*
   * The cinema and the week SIDE BY SIDE, and open. They were folded behind one line that had to
   * be pressed before either could be — the operator: "instead of the drop down to the two
   * selectors, can we make it so it's just two side by side to save a press". The same height as
   * that line was, a press shorter.
   *
   * Each pill wears its name over its value rather than beside it, which is what lets two fit
   * across a phone, and the drawing at its head goes: the name says what it picks. The whole pill
   * is the browser's own control — the label lies over it and lets the tap through — so a finger
   * anywhere on it opens the phone's own picker. The cinema is given the wider half: its value is
   * a name and a code, the week's a date.
   */
  :where(:root[data-app='staff']) .sched__bar > .sched__toolbar {
    flex: 1 1 100%;
  }

  :where(:root[data-app='staff']) .sched__pickers {
    display: grid;
    grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr);
    align-items: stretch;
    gap: 8px;
  }

  /* Anything else in the row — the board's Print, the note on whose clock — across both halves. */
  :where(:root[data-app='staff']) .sched__pickers > :not(.field) {
    grid-column: 1 / -1;
    margin-left: 0;
  }

  :where(:root[data-app='staff']) .sched__pickers .field {
    position: relative;
    display: block;
    min-width: 0;
    padding: 0;
    border-radius: 18px;
  }

  :where(:root[data-app='staff']) .sched__pickers .field::before {
    display: none;
  }

  :where(:root[data-app='staff']) .sched__pickers .field > label {
    position: absolute;
    top: 9px;
    left: 16px;
    z-index: 1;
    font-size: 0.72rem;
    line-height: 1;
    pointer-events: none;
  }

  /* One height for both, set rather than grown to: a date box brings a taller line of its own, and
     the two pills side by side came out two pixels apart. */
  :where(:root[data-app='staff']) .sched__pickers :is(select, input[type='date']) {
    display: block;
    width: 100%;
    height: 56px;
    min-height: 56px;
    padding: 22px 32px 8px 16px;
    border-radius: inherit;
    text-align: left;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  :where(:root[data-app='staff']) .sched__pickers select {
    background-position: right 12px center;
  }

  /* The date box as a box: iOS draws its own with the date centred in it, and ignores the padding
     that puts it under its name. */
  :where(:root[data-app='staff']) .sched__pickers input[type='date'] {
    -webkit-appearance: none;
    appearance: none;
    padding-right: 12px;
  }

  :where(:root[data-app='staff']) .sched__pickers input[type='date']::-webkit-date-and-time-value {
    text-align: left;
  }

  :where(:root[data-app='staff']) .sched__bar > .sched__actions {
    justify-content: flex-start;
  }

  /* The days between the arrows, and under them Edit and the clock, as drawn. */
  :where(:root[data-app='staff']) .weeknav {
    flex-wrap: wrap;
    row-gap: 12px;
  }

  :where(:root[data-app='staff']) .weeknav > .tabs {
    flex: 1 1 0;
  }

  /* Edit and the two settings share a row where they fit. Where they do not — a small phone, or
     the larger text sizes — the settings go under Edit together (see .weeknav__settings), and
     Edit takes the row it is left with. */
  :where(:root[data-app='staff']) .weeknav__end {
    flex: 1 1 100%;
    flex-wrap: wrap;
    row-gap: 10px;
    justify-content: flex-end;
    margin-left: 0;
  }

  :where(:root[data-app='staff']) .sched__edit {
    flex: 1 1 auto;
    justify-content: center;
    gap: 8px;
    min-height: 38px;
    border-radius: 999px;
  }

  :where(:root[data-app='staff']) .sched__edit::before {
    content: '';
    flex: none;
    width: 16px;
    height: 16px;
    background: currentColor;
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 7.2h5.2 M13.4 7.2H20 M4 12h9.4 M17.6 12H20 M4 16.8h3.1 M11.3 16.8H20 M11.3 5.1a2.1 2.1 0 1 1 0 4.2a2.1 2.1 0 1 1 0-4.2z M15.5 9.9a2.1 2.1 0 1 1 0 4.2a2.1 2.1 0 1 1 0-4.2z M9.2 14.7a2.1 2.1 0 1 1 0 4.2a2.1 2.1 0 1 1 0-4.2z'/%3E%3C/svg%3E") center / contain no-repeat;
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 7.2h5.2 M13.4 7.2H20 M4 12h9.4 M17.6 12H20 M4 16.8h3.1 M11.3 16.8H20 M11.3 5.1a2.1 2.1 0 1 1 0 4.2a2.1 2.1 0 1 1 0-4.2z M15.5 9.9a2.1 2.1 0 1 1 0 4.2a2.1 2.1 0 1 1 0-4.2z M9.2 14.7a2.1 2.1 0 1 1 0 4.2a2.1 2.1 0 1 1 0-4.2z'/%3E%3C/svg%3E") center / contain no-repeat;
  }

  /* The page's buttons keep their own width beside each other rather than sharing the row. */
  :where(:root[data-app='staff']) .page-head__actions:has(#library-toggle) .btn {
    flex: 0 0 auto;
  }

  /* The day's card, narrower: the names in a column of 104px that may wrap, as drawn, and the
     lanes a little lower. The column keeps its soft edge here, where the day always scrolls under
     it. */
  :where(:root[data-app='staff']) .grid {
    --room: 104px;
    --lane: 78px;
  }

  :where(:root[data-app='staff']) .grid__corner {
    padding-left: 12px;
  }

  :where(:root[data-app='staff']) .grid__room {
    padding: 10px 8px 0 12px;
  }

  :where(:root[data-app='staff']) .grid__corner,
  :where(:root[data-app='staff']) .grid__room {
    box-shadow: 8px 0 10px -8px var(--p-shadow);
  }

  :where(:root[data-app='staff']) .grid__number {
    font-size: 0.8125rem;
  }

  /* The figures side by side, and the days a strip that scrolls. */
  :where(:root[data-app='staff']) .sales,
  :where(:root[data-app='staff']) .dock__panel {
    padding: 14px 16px;
  }

  :where(:root[data-app='staff']) .sales__facts {
    gap: 10px 22px;
  }

  /* The takings' three figures are sentences rather than numbers — "351 of 1,620 seats · 22%" —
     and they stand above the day, so on a phone they take the size of the words round them
     rather than a screenful. The seat count's two short figures keep theirs. */
  :where(:root[data-app='staff']) .sales:not(.dock__panel) .sales__figure {
    font-size: 1.05rem;
  }

  :where(:root[data-app='staff']) .sales__title {
    margin-bottom: 8px;
  }

  :where(:root[data-app='staff']) .maxes__row {
    flex-wrap: wrap;
  }

  :where(:root[data-app='staff']) .maxes__days,
  :where(:root[data-app='staff']) .dock .maxes__days,
  :where(:root[data-app='staff']) .sales__days {
    flex: 1 1 100%;
    gap: 6px;
  }

  :where(:root[data-app='staff']) .maxes__day,
  :where(:root[data-app='staff']) .dock .maxes__day,
  :where(:root[data-app='staff']) .sales__day {
    flex: 0 0 auto;
    min-width: 58px;
    padding: 8px 10px;
  }
}
