/*
 * THE FACES. Nothing above this file in the cascade may declare a family that is not served here.
 *
 * ── THE DEFECT THIS FILE CLOSES ──────────────────────────────────────────────────────────────
 *
 * `tokens.css` has named Inter, Archivo and IBM Plex Mono since the site was drawn. The built
 * stylesheet contained ZERO `@font-face` rules and ZERO `url(` references, so not one byte of any
 * of them was ever requested. Measured on the served asset:
 *
 *     $ curl -s https://minirestaurants.online/assets/styles.css | grep -c '@font-face'   ->  0
 *     $ curl -s https://minirestaurants.online/assets/styles.css | grep -c 'url('         ->  0
 *
 * The families still RESOLVED — for the fraction of visitors with Inter or Archivo installed
 * locally, which on a restaurant owner's phone is approximately none. Everyone else silently got
 * `system-ui`. The typography was therefore a description of a design rather than the design, and
 * nothing failed: no 404, no console error, no gate. A stack whose first entry never loads reads
 * exactly like a stack that has no first entry.
 *
 * ── SELF-HOSTED, AND WHY THERE IS NO PACKAGE HERE ────────────────────────────────────────────
 *
 * An external font CDN is the wrong answer for this property regardless of host: a third-party
 * request on every page, for a site whose entire pitch is that it does not depend on someone
 * else's uptime. The `.woff2` files are committed under `public/fonts/` and copied verbatim by
 * `build.mjs`'s `copyDir`, so they are served by whichever path is live — the Cloudflare Pages
 * direct upload the site answers from today, or the `deploy/nginx.conf` image.
 *
 * ON CONTENT-TYPE, and the part of it that is NOT measured. `serve.mjs` — the dev server the
 * browser tests read — answered `application/octet-stream` for these four files and now answers
 * `font/woff2`; that one IS measured (`curl -w '%{content_type}'`, and the four faces load in
 * Chrome from it). What is NOT measured here is the response the `nginx:1.27-alpine` image sends:
 * stock `mime.types` is expected to carry `font/woff2`, no rule was added on that expectation, and
 * no Docker daemon exists on this machine to check it. If that image ever becomes the live path,
 * run `docker run --rm nginx:1.27-alpine grep woff /etc/nginx/mime.types` before trusting this
 * paragraph. It would not break rendering either way — browsers sniff woff2 from its signature and
 * ignore the declared type for `@font-face` — which is exactly why it would go unnoticed.
 *
 * They are NOT an npm dependency, deliberately: `build.mjs` is zero-dependency and this package is
 * not a pnpm workspace member, which is the property that keeps a copy edit here from re-running
 * the Rust, iOS and Android lanes. Adding `@fontsource/*` to a manifest would spend that.
 *
 * PROVENANCE — the files were extracted from these published tarballs and copied unmodified:
 *
 *   inter-latin-variable.woff2      @fontsource-variable/inter 5.3.0
 *                                   files/inter-latin-wght-normal.woff2         48,256 bytes
 *                                   sha256 3100e775e8616cd2611beecfa23a4263d7037586789b43f035236a2e6fbd4c62
 *   archivo-latin-variable.woff2    @fontsource-variable/archivo 5.3.0
 *                                   files/archivo-latin-wdth-normal.woff2       90,104 bytes
 *                                   sha256 e3a28eade21a900c7155a247757f4b2834c07bb7ef07ad7efa55cebaac1e8f5e
 *   ibm-plex-mono-latin-400.woff2   @fontsource/ibm-plex-mono 5.3.0
 *                                   files/ibm-plex-mono-latin-400-normal.woff2  14,708 bytes
 *                                   sha256 08949f728dc52d528e69b1667d15c89a5686a4ee9a296ff90983985f99c380f7
 *   ibm-plex-mono-latin-600.woff2   @fontsource/ibm-plex-mono 5.3.0
 *                                   files/ibm-plex-mono-latin-600-normal.woff2  15,620 bytes
 *                                   sha256 0d1f0b8d0722224e32e9f28261bdc86c79115be73444ae5eceb73976a1bcdf83
 *
 * All three families are SIL Open Font License 1.1. The OFL requires the licence to travel with
 * the files, so it does: `public/fonts/OFL-inter.txt`, `OFL-archivo.txt`, `OFL-ibm-plex-mono.txt`,
 * served next to the fonts they cover. Nothing here is renamed in a way the OFL reserves.
 *
 * ── WHAT IS DELIBERATELY NOT SERVED, stated rather than left to be discovered ────────────────
 *
 * 1. LATIN ONLY. `latin-ext`, `cyrillic` and `vietnamese` subsets exist upstream and are not
 *    shipped. The site is English and Spanish; every Spanish character (á é í ó ú ü ñ ¿ ¡) is
 *    inside the latin range below. `unicode-range` is declared on every face, so a character
 *    outside it falls to `system-ui` and RENDERS — it does not become a tofu box. That is a real
 *    fallback and it is the reason the ranges are declared rather than omitted.
 *
 * 2. NO ITALICS. Grepped: the stylesheet sets `font-style: italic` nowhere, and no page markup
 *    uses `<em>` or `<i>`. Shipping four more files for a style the site does not use would be
 *    ~90 KB spent on nothing. If an italic ever lands, `font-synthesis-style` is left at its
 *    default so the browser slants — visible, not silent.
 *
 * 3. NO "Inter Fallback" / "Archivo Fallback". Those two names sat in the token stacks and were
 *    never defined anywhere, in any file — a metric-matched fallback that does not exist is the
 *    same silent nothing this whole file is about. They are removed from the stacks rather than
 *    left in place, per the brief: a family that cannot be served does not stay in the stack.
 *
 * 4. NO "Archivo Expanded" EITHER, and this one is an upgrade rather than a removal. The expanded
 *    display face is not a separate family — it is the `wdth` axis of the one Archivo file below,
 *    which is why the `wdth` variable subset is shipped (90 KB) rather than the `wght`-only one
 *    (35 KB). `font-stretch: 62% 125%` on the face is what makes base.css's EXISTING
 *    `font-stretch: expanded` on `.display-1` at >=1024px do something. Until now it did nothing,
 *    twice over: no font, and no width axis to move.
 *
 * `font-display: swap` on every face. `--size-*` are clamped and the fallback is `system-ui`, so
 * the swap moves text; it does not blank it. `font-synthesis-weight: none` is already set on
 * `body`, which is why real 400/600 mono files ship instead of one file and a faux bold.
 */

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

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

/* ── IBM Plex Mono — data. Static, because upstream publishes no variable build. ─────────────── */
@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-display: swap;
  font-weight: 400;
  src: url("/fonts/ibm-plex-mono-latin-400.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: "IBM Plex Mono";
  font-style: normal;
  font-display: swap;
  font-weight: 600;
  src: url("/fonts/ibm-plex-mono-latin-600.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;
}

/*
 * Design tokens — WEB-DESIGN-SPEC.md §3. One source, and the console (doc 17 §11) is the second
 * consumer, so nothing here names a page or a component.
 *
 * Direction: THE TICKET RAIL — stainless and heat. Exactly three hues: steel (dominant), paper
 * (neutral), ember (accent, capped at ~10% of any viewport).
 */

:root {
  /* ── Colour ramp (light) ─────────────────────────────────────────────── */
  --steel-950: #0f1720;
  --steel-900: #16202b;
  --steel-850: #1e2a37;
  --steel-800: #1b2733;
  --steel-600: #4a5d71;
  --steel-500: #64798f;
  --steel-300: #c3cedaff;
  --steel-200: #dde4eb;
  --steel-100: #edf1f5;
  --paper-base: #f4f6f8;
  --paper-card: #fbfcfd;
  --ember-600: #a8420a;
  --ember-500: #c85107;
  --ember-400: #e4780b;
  --ember-300: #f59b45;
  --ember-050: #fdf1e7;

  /* ── Semantic roles. Themes reassign these and nothing else. ──────────── */
  --surface-page: var(--paper-base);
  --surface-raised: var(--steel-100);
  --surface-sunken: var(--steel-200);
  --surface-card: var(--paper-card);
  --surface-dark: var(--steel-950);
  --surface-accent-wash: var(--ember-050);

  --text-primary: var(--steel-950); /* 16.65:1 on --paper-base */
  --text-secondary: var(--steel-600); /* 6.26:1 — passes 1.4.3 for normal text */
  --text-nontext: var(--steel-500); /* 4.15:1 — NON-TEXT ONLY. Never body copy. */
  --text-on-dark: #e8eef4; /* 15.44:1 on --steel-950 */
  --text-on-dark-secondary: #9fb0c2; /* 8.14:1 on --steel-950 */
  --text-accent: var(--ember-600); /* 5.62:1 on --paper-base */

  --rule-decorative: var(--steel-300); /* 1.42:1 — decoration only, never a control boundary */
  --rule-structural: var(--steel-500); /* 4.15:1 — passes 1.4.11 at 3:1 */

  --action-fill: var(--ember-600); /* resting fill; label reads at 5.62:1 */
  --action-fill-hover: var(--ember-500); /* hover only — 4.18:1, fails 1.4.3 as a resting state */
  --action-label: var(--paper-base);

  /*
   * THEME-DEPENDENT FOCUS RING. WEB-DESIGN-SPEC.md §3.2 and §9.1: a single ember ring across both
   * themes fails 1.4.11 in one of them (ember/400 on light measures 2.77:1). This variable is
   * reassigned in the dark block below and must never be flattened to one value.
   */
  --focus-ring: var(--ember-600);

  /*
   * ...AND THE SAME ARGUMENT ONE LEVEL DOWN, WHICH THE §3.2 NOTE ABOVE MISSED.
   *
   * "Both themes" is not the whole set of backgrounds. The LIGHT theme paints two of them: the
   * page (`--paper-base`) and the dark surfaces cut into it (`--surface-dark`, i.e. `--steel-950`)
   * — the site footer on all 26 routes, and `.section--bleed.section--dark`. `--focus-ring` above
   * is chosen for the first and was never measured against the second.
   *
   * Measured by Cisco on the served page via `getComputedStyle`, 2026-08-21, and reproduced from
   * these two token values: `--ember-600` #a8420a on `--steel-950` #0f1720 is **2.96:1**, under
   * 1.4.11's 3:1 floor for a focus indicator. WCAG 2.2 AA FAIL, every published route. On `/404`
   * the footer IS the entire navigation, so the worst case is the page where the ring matters most.
   *
   * The fix reuses the value the dark theme already vetted rather than minting a third ember:
   * `--ember-400` #e4780b on #0f1720 is **6.01:1**. Scoped to the surface, not to the theme, so it
   * holds in light AND dark — in dark the block below has already set this exact value globally
   * and this re-declaration is a no-op there.
   *
   * `.section--dark` is included with the footer because it is the SAME background painted by the
   * same token; excluding it would leave the identical failure waiting on the first focusable
   * control placed in a dark band. One selector, one defect class.
   */
  --focus-ring-on-dark: var(--ember-400); /* 6.01:1 on --steel-950 */

  /* ── Type ─────────────────────────────────────────────────────────────── */
  /*
   * Every family named here is SERVED by `fonts.css`, which is the first file in the cascade. It
   * was not: these four stacks named Inter, Archivo and IBM Plex Mono against zero `@font-face`
   * rules, so the first entry silently resolved to `system-ui` for anyone without them installed.
   *
   * Three names are GONE rather than left as decoration. "Inter Fallback" and "Archivo Fallback"
   * were never defined in any file — a metric-matched fallback that does not exist is a comment
   * with a comma in it. "Archivo Expanded" is not a family either: expanded is the `wdth` axis of
   * Archivo itself, and `.display-1`'s existing `font-stretch: expanded` reaches it now that the
   * face declares `font-stretch: 62% 125%`. `--font-display-expanded` is kept — base.css uses it,
   * and it stays as the name for "the display face, at its wide end".
   */
  --font-display: "Archivo", system-ui, sans-serif;
  --font-display-expanded: "Archivo", system-ui, sans-serif;
  --font-body: "Inter", system-ui, sans-serif;
  --font-data: "IBM Plex Mono", ui-monospace, "SFMono-Regular", monospace;

  --size-display-1: clamp(2.5rem, 1.6rem + 4.5vw, 4.5rem);
  --size-display-2: clamp(2rem, 1.4rem + 3vw, 3.25rem);
  --size-h2: clamp(1.75rem, 1.4rem + 1.75vw, 2.5rem);
  --size-h3: clamp(1.375rem, 1.2rem + 0.9vw, 1.75rem);
  --size-h4: clamp(1.125rem, 1.05rem + 0.25vw, 1.25rem);
  --size-lede: clamp(1.1875rem, 1.14rem + 0.19vw, 1.3125rem);
  --size-body: clamp(1.0625rem, 1.04rem + 0.1vw, 1.125rem);
  --size-small: 0.9375rem; /* 15px floor. Nothing on this site is smaller, legal pages included. */
  --size-label: 0.875rem;
  --size-data: clamp(0.9375rem, 0.92rem + 0.1vw, 1rem);
  --size-data-lg: clamp(1.5rem, 1.15rem + 1.5vw, 2rem);

  /* ── Spacing — 8pt, and nothing off the scale ─────────────────────────── */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-6: 24px;
  --space-8: 32px;
  --space-10: 40px;
  --space-12: 48px;
  --space-16: 64px;
  --space-20: 80px;
  --space-24: 96px;
  --space-32: 128px;
  --space-40: 160px;

  /* ── Radii — one vocabulary, nothing else gets a radius ───────────────── */
  --radius-sm: 2px;
  --radius-md: 6px;
  --radius-lg: 12px;
  --radius-pill: 999px;

  /* ── Elevation ────────────────────────────────────────────────────────── */
  --elev-1: 0 1px 2px rgb(15 23 32 / 6%), 0 2px 8px rgb(15 23 32 / 6%);
  --elev-2: 0 8px 24px rgb(15 23 32 / 10%);
  --elev-3: 0 -6px 20px rgb(15 23 32 / 12%);

  /* ── Motion ───────────────────────────────────────────────────────────── */
  --dur-instant: 90ms;
  --dur-fast: 140ms;
  --dur-base: 200ms;
  --dur-slow: 320ms;
  --dur-rail: 520ms;
  --stagger: 60ms;
  --ease-out: cubic-bezier(0.25, 1, 0.35, 1);
  --ease-standard: cubic-bezier(0.2, 0, 0, 1);
  --ease-in: cubic-bezier(0.4, 0, 1, 1);

  /* ── Layout ───────────────────────────────────────────────────────────── */
  --container-md: 720px;
  --container-lg: 960px;
  --container-xl: 1160px;
  --container-2xl: 1240px;
  --gutter: var(--space-4);
  --header-height: 64px;
}

@media (min-width: 768px) {
  :root {
    --gutter: var(--space-8);
  }
}

@media (min-width: 1024px) {
  :root {
    --gutter: var(--space-10);
    --header-height: 72px;
  }
}

@media (min-width: 1280px) {
  :root {
    --gutter: var(--space-12);
  }
}

/*
 * Dark theme. OPT-IN via the OS preference only (WEB-DESIGN-SPEC.md §3.2) — never forced, because
 * unrequested permanent dark mode is a banned default. The product itself ships dark because
 * kitchens are dim, so honouring the preference is consistency rather than decoration.
 */
@media (prefers-color-scheme: dark) {
  :root {
    --surface-page: var(--steel-950);
    --surface-raised: var(--steel-900);
    --surface-sunken: var(--steel-850);
    --surface-card: var(--steel-900);
    --surface-dark: var(--steel-950);
    --surface-accent-wash: #2a1a10;

    --text-primary: #e8eef4;
    --text-secondary: #9fb0c2;
    --text-nontext: #6e8298;
    --text-accent: var(--ember-400);

    --rule-decorative: #2a3846;
    --rule-structural: #6e8298;

    --action-fill: var(--ember-300);
    --action-fill-hover: var(--ember-400);
    --action-label: var(--steel-950); /* ink on ember, 9.2:1 — not white on ember */

    --focus-ring: var(--ember-400); /* 6.01:1 on steel/950 */

    --elev-1: 0 1px 0 rgb(255 255 255 / 4%) inset;
    --elev-2: 0 1px 0 rgb(255 255 255 / 4%) inset;
    --elev-3: 0 -6px 20px rgb(0 0 0 / 40%);
  }
}

/*
 * Base: reset, type scale, layout primitives, motion policy.
 * WEB-DESIGN-SPEC.md §3.3 (type), §4.2 (the three layout primitives), §4.4 (vertical rhythm),
 * §7.4 (reduced motion).
 */

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

html {
  -webkit-text-size-adjust: 100%;
  /* 2.4.11 Focus Not Obscured: the sticky header and the sticky mobile CTA bar can both cover a
   * focused element, so anchored jumps and focus scrolling clear both. */
  scroll-padding-top: 88px;
  scroll-padding-bottom: 80px;
}

body {
  margin: 0;
  background: var(--surface-page);
  color: var(--text-primary);
  font-family: var(--font-body);
  font-size: var(--size-body);
  line-height: 1.6;
  font-synthesis-weight: none;
  text-rendering: optimizeLegibility;
}

img,
svg,
video {
  max-width: 100%;
  height: auto;
  display: block;
}

/* ── Type scale ─────────────────────────────────────────────────────────── */

.display-1,
.display-2,
.h2,
.h3,
.h4 {
  font-family: var(--font-display);
  color: var(--text-primary);
  margin: 0;
  text-wrap: balance;
}

.display-1 {
  font-size: var(--size-display-1);
  font-weight: 800;
  line-height: 1.03;
  letter-spacing: -0.02em;
  max-width: 22ch; /* 14–22 characters per line, enforced by measure not by manual <br> */
}

@media (min-width: 1024px) {
  .display-1 {
    font-family: var(--font-display-expanded);
    font-stretch: expanded;
  }
}

.display-2 {
  font-size: var(--size-display-2);
  font-weight: 800;
  line-height: 1.08;
  letter-spacing: -0.015em;
  max-width: 26ch;
}

.h2 {
  font-size: var(--size-h2);
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: -0.01em;
  max-width: 30ch;
}

.h3 {
  font-size: var(--size-h3);
  font-weight: 700;
  line-height: 1.25;
  max-width: 40ch;
}

.h4 {
  font-size: var(--size-h4);
  font-weight: 600;
  line-height: 1.35;
}

.lede {
  font-size: var(--size-lede);
  line-height: 1.55;
  max-width: 70ch;
  margin: 0;
  color: var(--text-secondary);
}

.body {
  font-size: var(--size-body);
  line-height: 1.6;
  max-width: 75ch;
  margin: 0;
}

.body-strong {
  font-weight: 600;
}

.small {
  font-size: var(--size-small);
  line-height: 1.5;
  max-width: 75ch;
  color: var(--text-secondary);
}

.label {
  font-size: var(--size-label);
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: 0.02em;
  color: var(--text-secondary);
  margin: 0;
}

/* The mono is for numbers and machine-emitted strings ONLY. If it appears in body copy that is
 * drift (WEB-DESIGN-SPEC.md §2). */
.data {
  font-family: var(--font-data);
  font-size: var(--size-data);
  line-height: 1.4;
  font-variant-numeric: tabular-nums;
}

.data-lg {
  font-family: var(--font-data);
  font-size: var(--size-data-lg);
  font-weight: 600;
  line-height: 1.1;
  letter-spacing: -0.01em;
  font-variant-numeric: tabular-nums;
}

/* ── Machine-emitted strings must wrap, or they take the page off the screen ──────────────────
 *
 * MEASURED 2026-08-21, on the published subset artifact, at a 390px viewport (iPhone-class, the
 * width most restaurant owners will open this site at):
 *
 *   /help/reading-ticket-status-without-colour            929px of content in a 390px viewport
 *   /help/which-printers-and-connections-are-supported    868px
 *   /help/holding-an-order-and-when-it-comes-back         868px
 *   /help/how-a-screen-arranges-its-tickets               837px
 *   /help/sounds-tones-and-the-repeat-alert               827px
 *   /hardware                                             750px
 *   /help/language-and-text-size-on-a-screen              449px
 *
 * Seven of the twenty-six published pages, all from one cause: `citedSources()` in
 * src/pages/help.mjs and the "Read from …" line in src/pages/hardware.mjs print real repository
 * paths, and a path like
 * `android/app/src/main/kotlin/online/minirestaurants/kds/audio/SoundPolicy.kt` is 74 characters
 * with no space in it. There was no `code` rule in this stylesheet at all, so it stayed one
 * unbreakable word and pushed the whole document wider than the screen.
 *
 * ⚠️ IT MUST BE `anywhere`, NOT `break-word`. They are NOT interchangeable here, and the wrong
 * one is a SILENT NO-OP — it looks like a fix and changes nothing. Measured on the 929px page,
 * changing only this value:
 *
 *   overflow-wrap: normal       -> scrollWidth 929px
 *   overflow-wrap: break-word   -> scrollWidth 929px   <- fixes NOTHING
 *   overflow-wrap: anywhere     -> scrollWidth 390px   <- fixes it
 *
 * The reason is the container. `.plain-list` is `display: grid`, so its track sizes to the item's
 * MIN-CONTENT width. `break-word` breaks glyphs but does NOT reduce min-content, so the track
 * stays as wide as the longest unbroken path and the page still overflows. `anywhere` does reduce
 * min-content, so the track can shrink. Anyone "simplifying" this to `break-word` reintroduces all
 * seven pages while the CSS still reads as correct.
 *
 * Deliberately NOT `word-break: break-all`: that breaks ordinary words too, and `/hardware`
 * renders `<code>` INSIDE a sentence ("Read from …"), where break-all would shred the prose around
 * it. `overflow-wrap: anywhere` breaks a word only when it cannot otherwise fit.
 *
 * This rule sets WRAPPING ONLY. `<code>` still has no typography of its own — it renders in the
 * browser's default monospace rather than `--font-data`. That is a separate design question and is
 * flagged for Felicity rather than decided here.
 */
code {
  overflow-wrap: anywhere;
}

/* Vertical rhythm inside a flow of blocks. Intra-group gap is always strictly smaller than
 * inter-group gap (§3.4 grouping rule). */
.stack > * + * {
  margin-top: var(--space-4);
}
.stack-tight > * + * {
  margin-top: var(--space-3);
}
.stack-loose > * + * {
  margin-top: var(--space-8);
}
.stack-block > * + * {
  margin-top: var(--space-12);
}

/* ── Layout primitives (§4.2) ───────────────────────────────────────────── */

.container {
  width: 100%;
  max-width: var(--container-2xl);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

@media (min-width: 768px) {
  .container {
    max-width: calc(var(--container-md) + 2 * var(--gutter));
  }
}
@media (min-width: 1024px) {
  .container {
    max-width: calc(var(--container-lg) + 2 * var(--gutter));
  }
}
@media (min-width: 1280px) {
  .container {
    max-width: calc(var(--container-xl) + 2 * var(--gutter));
  }
}
@media (min-width: 1536px) {
  .container {
    max-width: calc(var(--container-2xl) + 2 * var(--gutter));
  }
}

/*
 * A. Inset column — content in columns 1–8 of 12, right margin deliberately empty, left-aligned.
 * Never centred.
 */
.section--inset .section__body {
  max-width: 68ch;
}

@media (min-width: 1024px) {
  .section--inset .section__inner {
    display: grid;
    grid-template-columns: repeat(12, 1fr);
    column-gap: var(--gutter);
  }
  .section--inset .section__label {
    grid-column: 1 / span 2;
  }
  .section--inset .section__body {
    grid-column: 3 / span 6;
  }
  /* A hung label needs its own column; without one the body takes the inset span itself. */
  .section--inset .section__inner:not(:has(.section__label)) .section__body {
    grid-column: 1 / span 8;
  }
}

/* B. Full-bleed instrument — a table breaking the container to the viewport edge. */
.section--bleed {
  background: var(--surface-raised);
}
.section--bleed.section--dark {
  background: var(--surface-dark);
  color: var(--text-on-dark);
  /* W2 / SC 1.4.11. This band repaints the background out from under the page-level focus ring,
   * so it re-points the token at the value measured against the background it actually paints.
   * See the --focus-ring-on-dark note in tokens.css for the two ratios. */
  --focus-ring: var(--focus-ring-on-dark);
}
.section--dark .display-1,
.section--dark .display-2,
.section--dark .h2,
.section--dark .h3,
.section--dark .h4 {
  color: var(--text-on-dark);
}
.section--dark .lede,
.section--dark .small,
.section--dark .label {
  color: var(--text-on-dark-secondary);
}

/* C. The rail — see components.css. */

/* Vertical rhythm MUST vary (§4.4). These are the only section paddings on the site. */
.section {
  padding-block: var(--space-16);
}
.section--rhythm-hero {
  padding-block: var(--space-12) var(--space-16);
}
.section--rhythm-argument {
  padding-block: var(--space-16);
}
.section--rhythm-instrument {
  padding-block: var(--space-12);
}
.section--rhythm-aside {
  padding-block: var(--space-10);
}
.section--rhythm-closing {
  padding-block: var(--space-20);
}

@media (min-width: 1024px) {
  .section--rhythm-hero {
    padding-block: var(--space-20) var(--space-24);
  }
  .section--rhythm-argument {
    padding-block: var(--space-24);
  }
  .section--rhythm-instrument {
    padding-block: var(--space-16);
  }
  .section--rhythm-aside {
    padding-block: var(--space-12);
  }
  .section--rhythm-closing {
    padding-block: var(--space-32);
  }
}

.section--wash {
  background: var(--surface-accent-wash);
}

/* Hung label: hangs in the left margin at lg+, sits above the heading below that (§4.3). */
.section__label {
  display: block;
  margin-bottom: var(--space-4);
}

@media (min-width: 1024px) {
  .section__label {
    margin-bottom: 0;
    padding-top: 0.6em;
  }
}

/* Asymmetric proof blocks: 5/4/3 of 12, never 4/4/4, widest carries the load-bearing claim. */
.proof-blocks {
  display: grid;
  gap: var(--space-12);
}

@media (min-width: 768px) {
  .proof-blocks {
    grid-template-columns: 1fr 1fr;
  }
  .proof-blocks > :first-child {
    grid-column: 1 / -1;
  }
}

@media (min-width: 1024px) {
  .proof-blocks {
    grid-template-columns: 5fr 4fr 3fr;
  }
  .proof-blocks > :first-child {
    grid-column: auto;
  }
}

.two-col {
  display: grid;
  gap: var(--space-6) var(--space-12);
}

@media (min-width: 768px) {
  .two-col {
    grid-template-columns: 1fr 1fr;
  }
}

/* ── String expansion (§10) ─────────────────────────────────────────────── */
/*
 * Spanish runs 25–35% longer. Display type steps down ONE notch so a headline that grew 25% still
 * holds the 14–22 characters-per-line target instead of running to five lines on a phone. Body
 * sizes do not change — the floor is a legibility decision, not a fitting one.
 */
:lang(es) .display-1 {
  font-size: clamp(2.375rem, 1.6rem + 3.6vw, 3.75rem);
}

:lang(es) .display-2 {
  font-size: clamp(1.875rem, 1.4rem + 2.4vw, 2.875rem);
}

:lang(es) .h2 {
  font-size: clamp(1.625rem, 1.35rem + 1.4vw, 2.25rem);
}

/* ── Focus, links, skip link ────────────────────────────────────────────── */

:where(a, button, input, textarea, select, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

.skip-link {
  position: absolute;
  left: var(--space-4);
  top: var(--space-2);
  z-index: 100;
  transform: translateY(-200%);
  background: var(--action-fill);
  color: var(--action-label);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
  text-decoration: none;
  transition: transform var(--dur-fast) var(--ease-standard);
}

.skip-link:focus {
  transform: translateY(0);
}

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

/* ── Reduced motion (§7.4) ──────────────────────────────────────────────── */
/*
 * Collapse the movement, KEEP the state change perceptible. A global-only override silently kills
 * state feedback, which is worse than the animation was — so the exceptions below are explicit.
 */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }

  /* Exception 1 — the hero rail renders in final position, no stagger, no fade. */
  .ticket-rail__card {
    opacity: 1 !important;
    transform: none !important;
    animation: none !important;
  }

  /* Exception 2 — the bump undo affordance still shows for its full 4s. */
  /* Exception 3 — the connection chip still changes COLOUR AND TEXT: state is information. */
  /* Exception 4 — the accordion chevron keeps its static rotation, it just does not tween. */
  /* Exception 5 — the sticky CTA bar appears with no transform. */
  .sticky-cta {
    transform: none !important;
  }
  /* Exception 6 — the mobile header stops hiding on scroll rather than snapping. */
  .site-header[data-hidden="true"] {
    transform: none !important;
  }
}

/*
 * Components. The split mirrors WEB-DESIGN-SPEC.md §6: everything above the SITE-LOCAL divider is
 * site-agnostic and is the `ba-ui` candidate set; everything below it is MiniRestaurants-specific.
 */

/* ══ ba-ui: Button ═══════════════════════════════════════════════════════ */

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  font-family: var(--font-body);
  font-weight: 600;
  font-size: var(--size-body);
  line-height: 1.25;
  /* Spanish runs 25–35% longer: buttons are padding-sized with a min-width, never width-sized,
   * and a two-line label produces a taller button rather than a clipped one (§10 rule 3). */
  min-width: 8rem;
  min-height: 48px;
  padding: var(--space-3) var(--space-6);
  border: 0;
  border-radius: var(--radius-md);
  text-decoration: none;
  text-align: center;
  cursor: pointer;
  transition:
    background-color var(--dur-fast) var(--ease-standard),
    transform var(--dur-fast) var(--ease-standard);
}

.button--primary {
  background: var(--action-fill);
  color: var(--action-label);
}

.button--secondary {
  background: transparent;
  color: var(--text-primary);
  box-shadow: inset 0 0 0 1px var(--rule-structural);
}

.button--ghost {
  background: transparent;
  color: var(--text-accent);
  min-width: 0;
  padding-inline: var(--space-2);
}

.button--sm {
  min-height: 40px;
  padding: var(--space-2) var(--space-4);
}
.button--lg {
  min-height: 56px;
  padding: var(--space-4) var(--space-8);
}

@media (hover: hover) {
  .button--primary:hover {
    background: var(--action-fill-hover);
    transform: translateY(-1px);
  }
  .button--secondary:hover {
    background: var(--surface-raised);
    transform: translateY(-1px);
  }
}

.button:active {
  transform: translateY(0) scale(0.985);
  transition-duration: var(--dur-instant);
}

.button-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  align-items: center;
}

/* At 390px the primary is full-width and the secondary is a text link beneath it: two stacked
 * full-width buttons read as indecision (§5.1). */
@media (max-width: 479px) {
  .button-row .button--primary {
    width: 100%;
  }
  .button-row .button--secondary {
    background: none;
    box-shadow: none;
    color: var(--text-accent);
    text-decoration: underline;
    text-underline-offset: 3px;
    min-width: 0;
    padding-inline: 0;
  }
}

/* ══ ba-ui: TextLink ═════════════════════════════════════════════════════ */

.text-link {
  color: var(--text-accent);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
  transition: text-decoration-thickness var(--dur-fast) var(--ease-standard);
}

.text-link:hover,
.text-link:focus-visible {
  text-decoration-thickness: 2px;
}

.section--dark .text-link {
  color: var(--ember-400);
}

/* ══ ba-ui: SiteHeader + NavDrawer ═══════════════════════════════════════ */

.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--surface-page);
  border-bottom: 1px solid var(--rule-decorative);
  transition: transform var(--dur-base) var(--ease-standard);
}

.site-header[data-hidden="true"] {
  transform: translateY(-100%);
}

.site-header__inner {
  display: flex;
  align-items: center;
  /* Tighter than the section rhythm on purpose: the header is the one fixed-height element on the
   * site, so it is where a 25–35% longer language runs out of room first. */
  gap: var(--space-3);
  min-height: var(--header-height);
}

@media (min-width: 768px) {
  .site-header__inner {
    gap: var(--space-4);
  }
}

/*
 * Header controls are padding-sized, never width-sized, and the CTA drops the Button component's
 * 128px min-width here. Measured at 390px on /es, the default min-width pushed the menu button
 * 42px past the viewport — English fitted and Spanish did not, which is exactly why the layout is
 * signed off in Spanish at 390px first.
 */
.site-header__cta .button {
  min-width: 0;
}

/* 2.5.8 Target Size: 44px from padding, not from a larger font. */
.wordmark,
.language-switcher {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}

.site-header__cta .button--sm {
  min-height: 44px;
}

.wordmark {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 1.125rem;
  letter-spacing: -0.01em;
  text-decoration: none;
  white-space: nowrap;
  color: var(--text-primary);
}

/* "Mini" carries the accent, matching the wordmark asset in both themes: --text-accent resolves
   to ember-600 on light and ember-400 on dark, the same two values the SVG fills use. Previously
   --text-secondary, which made the live header text disagree with the mark it stands in for.
   docs/BRAND-MARK-STANDARD.md */
.wordmark__mini {
  color: var(--text-accent);
}

.site-header__nav {
  display: none;
  align-items: center;
  gap: var(--space-4);
  margin-inline-start: auto;
}

.site-header__nav a {
  color: var(--text-primary);
  text-decoration: none;
  font-size: var(--size-small);
  padding: var(--space-3) var(--space-2);
  min-height: 44px;
  display: inline-flex;
  align-items: center;
}

.site-header__nav a:hover {
  text-decoration: underline;
  text-underline-offset: 3px;
}

.site-header__cta {
  margin-inline-start: auto;
}

.nav-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 44px;
  min-height: 44px;
  background: none;
  border: 0;
  color: var(--text-primary);
  font: inherit;
  font-size: var(--size-small);
  cursor: pointer;
  gap: var(--space-2);
}

/*
 * The collapse point is CONTENT-MEASURED, not a device breakpoint (§4.6). `data-nav="inline"` is
 * set by the header's ResizeObserver when the link row fits; the no-JS fallback below keeps the
 * inline nav from 1180px, which is the measured English fit. /es collapses later on the same
 * device and that is correct, not a bug.
 */
@media (min-width: 1180px) {
  .site-header__nav {
    display: flex;
  }
  .site-header__cta {
    margin-inline-start: 0;
  }
  .nav-toggle {
    display: none;
  }
}

.site-header[data-nav="inline"] .site-header__nav {
  display: flex;
}
.site-header[data-nav="inline"] .nav-toggle {
  display: none;
}
.site-header[data-nav="drawer"] .site-header__nav {
  display: none;
}
.site-header[data-nav="drawer"] .nav-toggle {
  display: inline-flex;
}

.nav-drawer {
  position: fixed;
  inset: 0 0 0 auto;
  width: min(88vw, 380px);
  background: var(--surface-page);
  border-inline-start: 1px solid var(--rule-decorative);
  padding: var(--space-6) var(--space-6) var(--space-12);
  z-index: 60;
  overflow-y: auto;
  transform: translateX(100%);
  transition: transform var(--dur-base) var(--ease-out);
}

.nav-drawer[data-open="true"] {
  transform: translateX(0);
}

.nav-drawer[hidden] {
  display: none;
}

.nav-drawer__backdrop {
  position: fixed;
  inset: 0;
  background: rgb(15 23 32 / 50%);
  z-index: 55;
  border: 0;
}

.nav-drawer a {
  display: block;
  padding: var(--space-3) 0;
  min-height: 44px;
  color: var(--text-primary);
  text-decoration: none;
  font-size: var(--size-lede);
}

/* ══ ba-ui: SiteFooter ═══════════════════════════════════════════════════ */

.site-footer {
  background: var(--surface-dark);
  color: var(--text-on-dark);
  padding-block: var(--space-16) var(--space-12);
  /*
   * W2 / SC 1.4.11 Non-text Contrast — Cisco, all 26 published routes, measured 2026-08-21.
   *
   * The four footer links are the only focusable controls on the site that sit on `--surface-dark`,
   * and on `/404` they are the ENTIRE navigation. The page-level ring measured 2.96:1 here against
   * a 3:1 floor. Re-pointing the token — not the outline rule — keeps ONE focus declaration on the
   * site (base.css) and lets the surface that changed the background own the correction.
   */
  --focus-ring: var(--focus-ring-on-dark);
}

.site-footer__columns {
  display: grid;
  gap: var(--space-10);
}

@media (min-width: 768px) {
  .site-footer__columns {
    grid-template-columns: repeat(4, 1fr);
  }
}

.site-footer h2 {
  font-family: var(--font-display);
  font-size: var(--size-h4);
  font-weight: 600;
  margin: 0 0 var(--space-4);
  color: var(--text-on-dark);
}

.site-footer ul {
  list-style: none;
  margin: 0;
  padding: 0;
}

.site-footer a {
  display: inline-flex;
  align-items: center;
  /* 44px tap height from PADDING, never from a bigger font (2.5.8). */
  min-height: 44px;
  padding-block: var(--space-3);
  color: var(--text-on-dark-secondary);
  text-decoration: none;
  font-size: var(--size-small);
}

.site-footer a:hover {
  color: var(--text-on-dark);
  text-decoration: underline;
  text-underline-offset: 3px;
}

/*
 * The site's only contact channel, above the columns on every route.
 *
 * ⚠️ THIS BLOCK MUST STAY BELOW `.site-footer a`, AND THAT IS NOT TIDINESS. `.site-footer a` and
 * `.site-footer__contact a` have IDENTICAL specificity (0,1,1), so the later rule wins and nothing
 * warns you which that is. Written above it, this block lost its colour and its underline in
 * silence: measured at 8.13:1 in Chrome instead of the 15.44:1 it asks for — a real value, a
 * passing value, and the wrong one. Found by measuring the rendered page, not by reading the CSS.
 *
 * Underlined, in `--text-on-dark`, at body size — NOT `--size-small` like the column links, and
 * not the accent colour. It is the highest-intent element in the footer and it is the only one a
 * visitor may need to READ and copy by hand rather than click, so it is set at reading size and at
 * the footer's strongest contrast (15.44:1). The underline is the affordance; there is no icon, no
 * envelope glyph and no coloured pill, because a single sentence does not need chrome to be found.
 *
 * 44px tap height comes from PADDING, exactly like the column links (2.5.8) — never from a bigger
 * font, which would make it shout at a reader who is not looking for it.
 */
.site-footer__contact {
  margin: 0 0 var(--space-10);
  color: var(--text-on-dark-secondary);
  font-size: var(--size-body);
  line-height: 1.6;
}

.site-footer__contact a {
  display: inline-block;
  min-height: 44px;
  padding-block: var(--space-3);
  color: var(--text-on-dark);
  text-decoration: underline;
  text-underline-offset: 3px;
  /* An address wraps mid-string on a 320px viewport rather than taking the page off the screen —
   * the same failure class as W1/B4, arriving one element later. */
  overflow-wrap: anywhere;
}

.site-footer__contact a:hover {
  text-decoration-thickness: 2px;
}

.site-footer__legal {
  margin-top: var(--space-12);
  padding-top: var(--space-6);
  border-top: 1px solid rgb(255 255 255 / 10%);
  color: var(--text-on-dark-secondary);
  font-size: var(--size-small);
}

/* ══ ba-ui: DataTable / ComparisonMatrix / PricingTable ══════════════════ */

.data-table {
  width: 100%;
  border-collapse: collapse;
  font-variant-numeric: tabular-nums;
}

.data-table caption {
  text-align: start;
  font-size: var(--size-small);
  color: var(--text-secondary);
  margin-bottom: var(--space-4);
  max-width: 75ch;
}

.section--dark .data-table caption {
  color: var(--text-on-dark-secondary);
}

.data-table th,
.data-table td {
  text-align: start;
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--rule-decorative);
  /* Columns are auto-width with a min on the label column, never percentage-locked to English
   * headers (§10 rule 1). */
  min-width: 8ch;
}

.data-table th[scope="row"] {
  font-weight: 600;
  min-width: 14ch;
}

.data-table thead th {
  font-family: var(--font-body);
  font-size: var(--size-label);
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--text-secondary);
}

.section--dark .data-table thead th {
  color: var(--text-on-dark-secondary);
}

.data-table .num {
  text-align: end;
}

@media (hover: hover) {
  .data-table tbody tr:hover {
    background: var(--surface-raised);
  }
  .section--dark .data-table tbody tr:hover {
    background: var(--steel-900);
  }
}

/*
 * Mobile strategy is a DECLARED PROP, never an accidental horizontal scroll. Every table on this
 * site uses `transpose` or `stack`; a horizontal scroll on the most important proof on the site
 * would be a design failure (§5.2), and it would break 1.4.10 Reflow at 320px.
 */
.table-transposed {
  display: block;
}

@media (min-width: 768px) {
  .table-transposed {
    display: none;
  }
}

.table-wide {
  display: none;
}

@media (min-width: 768px) {
  .table-wide {
    display: block;
    overflow-x: visible;
  }
}

.transposed-group {
  padding: var(--space-6) 0;
  border-bottom: 1px solid var(--rule-decorative);
}

.transposed-group dl {
  margin: var(--space-4) 0 0;
  display: grid;
  gap: var(--space-3);
}

.transposed-group dt {
  font-weight: 600;
}

.transposed-group dd {
  margin: 0 0 var(--space-3);
  color: var(--text-secondary);
}

.section--dark .transposed-group dd {
  color: var(--text-on-dark-secondary);
}

/*
 * 1.4.1 Use of Colour: a matrix cell is NEVER colour-only. Word + glyph, always. The glyph is a
 * meaning-bearing graphic, so it carries 1.4.11's 3:1 in its own right (steel/600 = 6.26:1).
 */
.state-cell {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  white-space: nowrap;
}

.state-glyph {
  width: 12px;
  height: 12px;
  flex: none;
  border: 2px solid currentColor;
  border-radius: var(--radius-sm);
}

.state-glyph--yes {
  background: currentColor;
}

.state-glyph--partial {
  background: linear-gradient(to right, currentColor 50%, transparent 50%);
}

.state-glyph--no {
  background: none;
}

/* Do NOT soften the "No" cells — the negative cells are what make the table believable. */
.state-cell--no {
  color: var(--text-primary);
  font-weight: 600;
}

.section--dark .state-cell--no {
  color: var(--text-on-dark);
}

/* ══ ba-ui: PlanCard ═════════════════════════════════════════════════════ */

.plan-cards {
  display: grid;
  gap: var(--space-6);
}

@media (min-width: 768px) {
  .plan-cards {
    display: none;
  }
}

/* No tier is visually promoted. Mutually exclusive alternatives get equal weight — and a
 * "most popular" ribbon is a lie until somebody has bought something (§5.5). */
.plan-card {
  background: var(--surface-raised);
  border-radius: var(--radius-md);
  padding: var(--space-8);
}

.plan-card__price {
  margin: var(--space-4) 0;
}

.plan-card ul {
  list-style: none;
  margin: var(--space-4) 0 0;
  padding: 0;
  display: grid;
  gap: var(--space-3);
}

/* ══ ba-ui: Accordion / FaqList ══════════════════════════════════════════ */

.accordion {
  border-top: 1px solid var(--rule-decorative);
}

.accordion__item {
  border-bottom: 1px solid var(--rule-decorative);
}

/* <details>/<summary> so every disclosure works with JavaScript switched off. */
.accordion__item > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  min-height: 56px;
  padding: var(--space-4) 0;
  cursor: pointer;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--size-h4);
  list-style: none;
}

.accordion__item > summary::-webkit-details-marker {
  display: none;
}

.accordion__chevron {
  flex: none;
  width: 16px;
  height: 16px;
  border-inline-end: 2px solid var(--text-nontext);
  border-block-end: 2px solid var(--text-nontext);
  transform: rotate(45deg);
  transition: transform var(--dur-base) var(--ease-standard);
}

.accordion__item[open] .accordion__chevron {
  transform: rotate(225deg);
}

.accordion__panel {
  padding-bottom: var(--space-6);
}

/* ══ ba-ui: DefinitionList ═══════════════════════════════════════════════ */

.definition-list {
  margin: 0;
  display: grid;
  gap: var(--space-10);
}

@media (min-width: 768px) {
  .definition-list--two-col {
    grid-template-columns: 1fr 1fr;
    gap: var(--space-10) var(--space-12);
  }
}

.definition-list dt {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--size-h4);
  margin-bottom: var(--space-3);
}

.definition-list dd {
  margin: 0;
  color: var(--text-secondary);
}

/* Label ABOVE value below md, always. This is where +25% Spanish breaks bilingual layouts first
 * (§10 rule 6) — so there is no side-by-side label/value row anywhere on this site. */

/* ══ ba-ui: FormField / Input ════════════════════════════════════════════ */

.form-field {
  display: grid;
  gap: var(--space-2);
  margin-bottom: var(--space-6);
}

.form-field label {
  font-weight: 600;
  font-size: var(--size-small);
  color: var(--text-primary);
}

.form-field .form-field__description {
  font-size: var(--size-small);
  color: var(--text-secondary);
  margin: 0;
}

.form-field input,
.form-field textarea,
.form-field select {
  font: inherit;
  font-size: var(--size-body);
  color: var(--text-primary);
  background: var(--surface-card);
  min-height: 48px;
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--rule-structural);
  border-radius: var(--radius-md);
  transition:
    border-color var(--dur-fast) var(--ease-standard),
    box-shadow var(--dur-fast) var(--ease-standard);
}

.form-field textarea {
  min-height: 140px;
  resize: vertical;
}

.form-field input:focus,
.form-field textarea:focus {
  border-color: var(--focus-ring);
  box-shadow: 0 0 0 2px var(--focus-ring);
  outline: none;
}

/*
 * VALIDATION (S-223). Colour is never the carrier and never the only signal. An invalid field gets
 * WORDS under it, `aria-invalid` on the control, and a link to it from the summary at the top of
 * the form; the ember border is the fourth signal, for people who can see it. Removing all colour
 * from this stylesheet would not remove a single piece of information.
 *
 * The message node is always in the DOM and starts empty, so it is associated with the field from
 * the first paint rather than at the moment it fills. `:empty` keeps an empty one out of the flow
 * without hiding it from the accessibility tree the way `display:none` would once it has words.
 */
.form-field .form-field__error {
  margin: 0;
  font-size: var(--size-small);
  font-weight: 600;
  color: var(--text-accent);
}

.form-field .form-field__error:empty {
  display: none;
}

.form-field input[aria-invalid="true"],
.form-field textarea[aria-invalid="true"] {
  border-color: var(--text-accent);
  border-width: 2px;
}

/* The error summary. Focus lands here, so it is a focus target with a visible ring of its own. */
.form-errors {
  margin-bottom: var(--space-6);
  padding: var(--space-4) var(--space-6);
  border-radius: var(--radius-md);
  border: 2px solid var(--text-accent);
  background: var(--surface-accent-wash);
  color: var(--text-primary);
}

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

.form-errors[hidden] {
  display: none;
}

/* A file input is not a text input; it keeps the 48px target and drops the text padding. */
.form-field input[type="file"] {
  padding: var(--space-2) var(--space-3);
}

/* Honeypot: spam control with no cognitive function test, per 3.3.8. */
.form-honeypot {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* ══ ba-ui: Banner ═══════════════════════════════════════════════════════ */

.banner {
  padding: var(--space-4) var(--space-6);
  border-radius: var(--radius-md);
  background: var(--surface-raised);
  border-inline-start: 0;
  font-size: var(--size-small);
}

.banner--accent {
  background: var(--surface-accent-wash);
  color: var(--text-primary);
}

/*
 * `warn` is what the importer uses when it read nothing at all. It is the accent wash with a real
 * border, because a refusal has to be distinguishable from a result at a glance — and the words
 * inside it say what happened, so the border is confirmation rather than information.
 */
.banner--warn {
  background: var(--surface-accent-wash);
  color: var(--text-primary);
  border: 2px solid var(--text-accent);
}

/* ══ Import report (S-226) ═══════════════════════════════════════════════ */

.import-report {
  margin-top: var(--space-8);
  display: grid;
  gap: var(--space-6);
}

.import-report[hidden] {
  display: none;
}

.import-report .data-table {
  width: 100%;
}

/* ══ ba-ui: StickyCtaBar ═════════════════════════════════════════════════ */

.sticky-cta {
  position: fixed;
  inset-inline: 0;
  bottom: 0;
  z-index: 40;
  min-height: 64px;
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-2) var(--gutter);
  background: var(--surface-page);
  box-shadow: var(--elev-3);
  transform: translateY(100%);
  transition: transform var(--dur-slow) var(--ease-out);
}

.sticky-cta[data-visible="true"] {
  transform: translateY(0);
}

/* 2.5.8: the mobile CTA is the largest target on the site, not the smallest. */
.sticky-cta .button {
  min-height: 56px;
}

/* Inert until the hero has left the viewport, so a focused control can never sit under it. */
.sticky-cta[inert] {
  pointer-events: none;
}

@media (min-width: 768px) {
  .sticky-cta {
    display: none;
  }
}

/* ══ ba-ui: Prose + TableOfContents ══════════════════════════════════════ */

.prose {
  max-width: 68ch;
}

.prose h2 {
  font-family: var(--font-display);
  font-size: var(--size-h2);
  font-weight: 700;
  line-height: 1.15;
  margin: var(--space-12) 0 var(--space-3);
  scroll-margin-top: 88px;
}

.prose h3 {
  font-family: var(--font-display);
  font-size: var(--size-h3);
  font-weight: 700;
  margin: var(--space-8) 0 var(--space-3);
}

.prose p,
.prose li {
  font-size: var(--size-body);
  line-height: 1.6;
}

.prose ul,
.prose ol {
  padding-inline-start: var(--space-6);
  display: grid;
  gap: var(--space-3);
}

.legal-layout {
  display: grid;
  gap: var(--space-12);
}

@media (min-width: 1024px) {
  .legal-layout {
    grid-template-columns: minmax(0, 1fr) 260px;
    gap: var(--space-16);
    align-items: start;
  }
  .toc {
    position: sticky;
    top: calc(var(--header-height) + var(--space-6));
    order: 2;
  }
}

.toc ol {
  list-style: none;
  margin: var(--space-3) 0 0;
  padding: 0;
  display: grid;
  gap: var(--space-2);
}

.toc a {
  color: var(--text-secondary);
  text-decoration: none;
  font-size: var(--size-small);
  display: inline-flex;
  min-height: 44px;
  align-items: center;
}

.toc a[aria-current="true"] {
  color: var(--text-accent);
  font-weight: 600;
}

/* ══ ba-ui: LanguageSwitcher ═════════════════════════════════════════════ */

.language-switcher {
  white-space: nowrap;
  /* 1.4.3 Contrast: this rule and the target-size rule above both style the switcher for LAYOUT
   * and neither set a colour, and there is no global `a { color }` anywhere in src/styles — so the
   * label fell through to the UA default link blue, #0000ee. On the light page that measures
   * 8.67:1 and looks deliberate; in dark mode it is #0000ee on --steel-950 and measures 1.92:1,
   * against a 4.5:1 floor. /es is the only page that renders the switcher, so this was the whole
   * Spanish surface. Matches .site-header__nav a, its neighbour in the header. */
  color: var(--text-primary);
}

/* ══════════════════════════════════════════════════════════════════════════
 * SITE-LOCAL — MiniRestaurants only. Nothing below this line generalizes.
 * ══════════════════════════════════════════════════════════════════════ */

/* ── TicketRail / TicketCard / BumpableCard ─────────────────────────────── */

.ticket-rail {
  display: flex;
  gap: var(--space-4);
  /* Runs off the right edge deliberately: the visual signal that there is more than fits, which
   * is the truth about a kitchen at 7pm (§4.2 primitive C). */
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  padding-block: var(--space-4);
  padding-inline: var(--gutter) 25vw;
  margin-inline: calc(-1 * var(--gutter));
  scrollbar-width: thin;
}

.ticket-rail:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 4px;
}

.ticket-rail__card {
  flex: none;
  width: 280px;
  scroll-snap-align: start;
  background: var(--paper-card);
  color: var(--steel-950);
  border-radius: var(--radius-lg);
  box-shadow: var(--elev-1);
  overflow: hidden;
  text-align: start;
}

@media (min-width: 768px) {
  .ticket-rail__card {
    width: 320px;
  }
}

.ticket-card__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  /* 88pt-equivalent bump header, faithful to the product's OrderCard. */
  min-height: 88px;
  padding: var(--space-4);
  background: var(--steel-100);
  font-family: var(--font-data);
  font-weight: 600;
}

.ticket-card__body {
  padding: var(--space-4);
  display: grid;
  gap: var(--space-2);
}

.ticket-card__item {
  display: flex;
  gap: var(--space-3);
  font-size: var(--size-small);
  color: var(--steel-950);
}

.ticket-card__qty {
  font-family: var(--font-data);
  font-weight: 600;
  min-width: 2ch;
}

.ticket-card__modifier {
  padding-inline-start: var(--space-6);
  color: var(--steel-600);
  font-size: var(--size-small);
}

.ticket-card__allergen {
  margin-top: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  background: #fdecdf;
  color: #7a2d05;
  font-size: var(--size-small);
  font-weight: 600;
}

.ticket-card__timer {
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
}

button.ticket-card__header {
  width: 100%;
  border: 0;
  cursor: pointer;
  font-size: var(--size-body);
  transition: background-color var(--dur-fast) var(--ease-standard);
}

button.ticket-card__header:hover {
  background: var(--steel-200);
}

.ticket-rail__card[data-bumped="true"] {
  animation: bump-out var(--dur-rail) var(--ease-in) forwards;
}

@keyframes bump-out {
  to {
    opacity: 0;
    transform: translateX(40%);
  }
}

.rail-undo {
  margin-top: var(--space-4);
  min-height: 44px;
}

/* ── ConnectionChip ─────────────────────────────────────────────────────── */

.connection-chip {
  display: none;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-pill);
  background: var(--surface-raised);
  color: var(--text-secondary);
  font-size: var(--size-small);
  text-decoration: none;
  white-space: nowrap;
}

@media (min-width: 768px) {
  .connection-chip {
    display: inline-flex;
  }
}

/* Colour + dot SHAPE + text. Three signals, never colour alone. */
.connection-chip__dot {
  width: 10px;
  height: 10px;
  flex: none;
  border: 2px solid currentColor;
  border-radius: var(--radius-pill);
  background: currentColor;
}

.connection-chip[data-state="local"] {
  color: #7a4a05;
}
.connection-chip[data-state="local"] .connection-chip__dot {
  border-radius: var(--radius-sm);
  background: transparent;
}
.connection-chip[data-state="down"] {
  color: var(--ember-600);
}
.connection-chip[data-state="down"] .connection-chip__dot {
  border-radius: var(--radius-sm);
  background: linear-gradient(to right, currentColor 50%, transparent 50%);
}

/* ── NetworkStateControl ────────────────────────────────────────────────── */

.network-control {
  border: 0;
  margin: 0 0 var(--space-6);
  padding: 0;
}

.network-control legend {
  padding: 0;
  margin-bottom: var(--space-3);
}

.network-control__options {
  display: grid;
  gap: var(--space-3);
}

@media (min-width: 768px) {
  .network-control__options {
    grid-auto-flow: column;
    justify-content: start;
  }
}

.network-control__option {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: 48px;
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
  background: var(--surface-raised);
  cursor: pointer;
  font-size: var(--size-small);
}

.network-control__option:has(input:checked) {
  box-shadow: inset 0 0 0 2px var(--focus-ring);
  font-weight: 600;
}

.network-control__option:has(input:focus-visible) {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

/* ── SetupStepRail ──────────────────────────────────────────────────────── */

.setup-steps {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-12);
}

.setup-step {
  display: grid;
  gap: var(--space-3);
  position: relative;
  padding-inline-start: var(--space-10);
}

/* A real connector for a real sequence. This is the only numbered content on the site. */
.setup-step::before {
  content: "";
  position: absolute;
  inset-block: 0;
  inset-inline-start: var(--space-2);
  width: 2px;
  background: var(--rule-decorative);
}

.setup-step:last-child::before {
  bottom: auto;
  height: 2.5rem;
}

.setup-step__number {
  color: var(--text-nontext);
}

@media (min-width: 1024px) {
  .setup-step {
    grid-template-columns: 5rem minmax(0, 1fr);
    padding-inline-start: 0;
    column-gap: var(--space-6);
  }
  .setup-step::before {
    inset-inline-start: 5.75rem;
  }
  .setup-step__number {
    grid-row: 1 / span 3;
  }
}

/* ── PosConnectionTable wordmarks ───────────────────────────────────────── */

/* Functional identification, not social proof: monochrome, uniform cap height, always paired with
 * the vendor name in text, so the mark itself is decorative and aria-hidden. */
.pos-wordmark {
  color: var(--text-secondary);
  font-weight: 700;
  font-family: var(--font-display);
  font-size: 20px;
  line-height: 1;
}

/* ── PauseProof ─────────────────────────────────────────────────────────── */

.pause-proof {
  display: grid;
  gap: var(--space-6);
}

@media (min-width: 768px) {
  .pause-proof {
    grid-template-columns: 1fr 1fr;
  }
}

.pause-proof figure {
  margin: 0;
}

.pause-proof figcaption {
  margin-top: var(--space-3);
  font-size: var(--size-small);
  color: var(--text-secondary);
}

/* ── Press boilerplate ──────────────────────────────────────────────────── */

/*
 * A block of copy whose only job is to be selected and pasted, so it is set apart by a left rule
 * and a sunken surface rather than by a card. This is the one place the Anti-Slop Rulebook's
 * borderless default gives way and says why: a paste target needs a visible edge, or a journalist
 * cannot tell where the quote stops. No gradient, no icon, no rounded card, no colour strip as
 * decoration — the rule here is the boundary of the quote.
 */
.boilerplate {
  background: var(--surface-sunken);
  border-left: 3px solid var(--rule-structural);
  padding: var(--space-6);
  border-radius: var(--radius-sm);
}

.boilerplate .label {
  margin-bottom: var(--space-3);
  color: var(--text-secondary);
}

/* ── SlotPlaceholder ────────────────────────────────────────────────────── */

/*
 * An unfilled CEO-input slot renders VISIBLY and could never be mistaken for finished content.
 * `build.mjs --production` refuses to emit a page that still contains one, so this can only ever
 * be seen in a draft build. This is Harper's "leave it visible and let it fail review", made
 * mechanical instead of hopeful.
 */
.slot-placeholder {
  display: block;
  margin-block: var(--space-6);
  padding: var(--space-6);
  border: 3px dashed #8a5a00;
  border-radius: var(--radius-md);
  background: repeating-linear-gradient(135deg, #fff8e6, #fff8e6 12px, #ffefc4 12px, #ffefc4 24px);
  color: #4a3000;
}

.slot-placeholder__tag {
  display: inline-block;
  margin-bottom: var(--space-2);
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-sm);
  background: #4a3000;
  color: #fff8e6;
  font-family: var(--font-data);
  font-size: var(--size-small);
  font-weight: 600;
}

.slot-placeholder__question {
  margin: 0;
  font-size: var(--size-small);
  line-height: 1.5;
  max-width: 75ch;
  color: #4a3000;
}

.slot-placeholder--gate {
  border-color: #0d4f6c;
  background: repeating-linear-gradient(135deg, #eaf6fb, #eaf6fb 12px, #d3ecf7 12px, #d3ecf7 24px);
  color: #062b3c;
}

.slot-placeholder--gate .slot-placeholder__tag {
  background: #062b3c;
  color: #eaf6fb;
}

.slot-placeholder--gate .slot-placeholder__question {
  color: #062b3c;
}

/* ── PlaceholderPerson ─────────────────────────────────────────────────── */

/*
 * A quote from an operator WHO DOES NOT EXIST. It reuses `.slot-placeholder` for its banner
 * rather than inventing a second warning treatment, so every "this is not real" block on the site
 * looks the same to a reader who has already learned what that hatch means.
 *
 * The quote itself is deliberately NOT styled to look finished — no large display type, no
 * decorative quotation mark, no card elevation. A placeholder testimonial that looks like a
 * designed testimonial is the thing this component exists to prevent.
 */
.placeholder-person {
  margin: 0;
  padding: 0;
  border-left: none;
}

.placeholder-person__quote {
  margin: var(--space-6) 0 0;
  padding: 0;
  max-width: 62ch;
}

.placeholder-person__quote p {
  margin: 0;
}

.placeholder-person__who {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  margin-top: var(--space-6);
}

.placeholder-person__avatar {
  flex: 0 0 auto;
  width: 56px;
  height: 56px;
}

.placeholder-person__avatar-disc {
  fill: #4a3000;
}

.placeholder-person__avatar-initials {
  fill: #fff8e6;
  font-family: var(--font-data);
  font-size: 24px;
  font-weight: 600;
}

.placeholder-person__attribution {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.placeholder-person__name {
  font-weight: 600;
}

/* ── /404 ───────────────────────────────────────────────────────────────── */

.error-links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4) var(--space-6);
  padding: 0;
  margin: 0;
  list-style: none;
}

.error-links a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}

.search-form {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  align-items: end;
}

.search-form .form-field {
  margin-bottom: 0;
  flex: 1 1 240px;
}

@media (min-width: 768px) {
  .search-form .form-field {
    flex: 0 1 380px;
  }
}

/* ── Misc site lists ────────────────────────────────────────────────────── */

.marker-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-6);
}

.marker-list li {
  position: relative;
  padding-inline-start: var(--space-8);
  font-size: var(--size-h4);
  font-family: var(--font-display);
  font-weight: 600;
}

.marker-list li::before {
  content: "";
  position: absolute;
  inset-inline-start: 0;
  top: 0.7em;
  width: var(--space-6);
  height: 2px;
  background: var(--text-nontext);
  /*
   * The rule draws in with scaleX from its start edge, not by animating `width`.
   * `transition: width` was here and the performance guard added in this pass rejected it — the
   * first thing the guard caught, and it was already shipped. Identical picture, no layout pass
   * per frame, no CLS contribution.
   */
  transform: scaleX(0.35);
  transform-origin: left center;
  transition: transform var(--dur-fast) var(--ease-standard);
}

.feature-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-6);
}

@media (min-width: 768px) {
  .feature-list {
    grid-template-columns: 1fr 1fr;
    gap: var(--space-6) var(--space-12);
  }
}

.feature-list dt,
.feature-list__term {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--size-h4);
  margin-bottom: var(--space-2);
}

.plain-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-6);
}

/*
 * B3. The restaurant directory (`/restaurants`).
 *
 * The name sits on its own line above its summary in EVERY row, linked or not, so the row's shape
 * does not depend on whether the page it names is in this build. The name keeps the display font
 * and weight in both states; only the underline and the link colour differ, which is what a link
 * is supposed to differ by. Before this, an unlinked name ran straight into its summary as one
 * grey sentence beside five underlined siblings, and read as a link that had failed to render.
 */
.segment-index__item {
  min-width: 0;
}

.segment-index__name {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--size-h4);
  line-height: 1.3;
  margin: 0 0 var(--space-2);
}

.segment-index__summary {
  margin: 0;
  color: var(--text-secondary);
}

.breadcrumbs {
  font-size: var(--size-small);
  color: var(--text-secondary);
  margin-bottom: var(--space-6);
}

.breadcrumbs ol {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
}

/* ── S-220: competitor citation cells and the chooser ───────────────────── */

/*
 * A price, its qualification, its source and the day it was read are FOUR LINES IN ONE CELL, and
 * they stack rather than wrap into each other. `display: block` on each is the whole layout: the
 * moment a source line is allowed to run inline after a number it starts reading as part of the
 * price, and at 320px it wraps into it.
 */
.competitor-price {
  display: block;
  font-size: var(--size-h4);
}

.competitor-note,
.competitor-checked {
  display: block;
  margin-top: var(--space-2);
  color: var(--text-secondary);
}

/* The date is the half of a citation people skip. It is not allowed to be the quietest thing. */
.competitor-checked {
  font-weight: 600;
  color: var(--text-primary);
}

.chooser {
  border: 0;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-8);
}

.chooser__question {
  border: 0;
  margin: 0;
  padding: 0;
  min-inline-size: 0; /* a fieldset defaults to min-content and blows out the grid at 320px */
}

.chooser__question legend {
  padding: 0;
}

.chooser__help {
  color: var(--text-secondary);
  margin: var(--space-2) 0 var(--space-4);
}

.chooser__options {
  display: grid;
  gap: var(--space-3);
}

.chooser__option {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: 48px;
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
  background: var(--surface-raised);
  cursor: pointer;
}

.chooser__option:has(input:checked) {
  box-shadow: inset 0 0 0 2px var(--focus-ring);
  font-weight: 600;
}

.chooser__option:has(input:focus-visible) {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

.chooser__result {
  margin-top: var(--space-8);
  padding: var(--space-6);
  border-radius: var(--radius-md);
  background: var(--surface-card);
}

.chooser__outcomes {
  margin-top: var(--space-8);
}

.chooser__outcome {
  padding: var(--space-6);
  border-radius: var(--radius-md);
  background: var(--surface-card);
}

/*
 * The verdict is a WORD. There is deliberately no colour rule keyed off `data-verdict` here: the
 * day "Not us" is drawn in red and "Us" in green is the day the meaning starts living in the
 * colour, and a comparison surface on a public page must survive being read without it.
 */
.chooser__verdict {
  margin-bottom: var(--space-2);
}

.chooser__when {
  margin-top: var(--space-6);
  color: var(--text-secondary);
}

/* ── RadioGroup (ui/radio-group.mjs) ────────────────────────────────────── */

.radio-group {
  border: 0;
  margin: 0 0 var(--space-6);
  padding: 0;
}

.radio-group legend {
  padding: 0;
  margin-bottom: var(--space-3);
}

.radio-group__options {
  display: grid;
  gap: var(--space-3);
}

@media (min-width: 768px) {
  .radio-group__options {
    grid-auto-flow: column;
    justify-content: start;
  }
}

.radio-group__option {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: 48px;
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
  background: var(--surface-raised);
  cursor: pointer;
  font-size: var(--size-small);
}

.radio-group__option:has(input:checked) {
  box-shadow: inset 0 0 0 2px var(--focus-ring);
  font-weight: 600;
}

.radio-group__option:has(input:focus-visible) {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

.radio-group__note {
  display: block;
  color: var(--text-secondary);
  font-weight: 400;
}

/*
 * The cost comparison's answer (/roi). There is deliberately NO colour rule keyed off
 * `data-roi-state`, exactly as the chooser above carries none keyed off `data-verdict`. The day
 * "you spend less" is drawn green and "we cost more" is drawn red is the day the verdict lives in
 * a colour, and the unflattering answer is the one that would be quietly easier to miss. The
 * sentence says which way it went, in words, in the first line of the region.
 */
.roi-output {
  padding: var(--space-6);
  border-radius: var(--radius-md);
  background: var(--surface-card);
}

.roi-output dl {
  margin-bottom: 0;
}

/*
 * A FORM THAT CANNOT BE SENT.
 *
 * The refusal reads as text a person can read, not as a warning triangle: the border and wash are
 * the third signal, after the words and the disabled state of the controls themselves. It sits
 * ABOVE the fields in document order, so it is met before the first label rather than after the
 * last one.
 */
.form-unavailable {
  max-width: 36rem;
  margin-top: var(--space-8);
  padding: var(--space-6);
  border-radius: var(--radius-md);
  border: 2px solid var(--text-accent);
  background: var(--surface-accent-wash);
  color: var(--text-primary);
}

.form-unavailable p + p {
  margin-top: var(--space-4);
}

/*
 * The inert fields. Browsers dim a disabled control by default and the dimming is what drops it
 * below 4.5:1, so the colours are restated here rather than inherited: the text stays readable and
 * `not-allowed` plus the announced disabled state carry the meaning. Showing the real fields is
 * the point — this is what we will ask for, and we cannot ask for it yet.
 */
.inert-fieldset {
  max-width: 36rem;
  margin: var(--space-8) 0 0;
  padding: 0;
  border: 0;
}

.inert-fieldset :disabled {
  opacity: 1;
  color: var(--text-primary);
  background: var(--surface-sunken);
  cursor: not-allowed;
}

/* ── Class C blog header schematics: the label-size ladder ─────────────────
 *
 * An SVG `font-size` is in the drawing's own units and is multiplied by the viewBox scale, so one
 * constant in `diagram.mjs` is one legible size at one width and an illegible one everywhere else.
 * Measured in Chrome on /blog/what-a-missed-ticket-costs/, the old 22 units arrived at 13.88px on
 * a 1280 viewport, 12.91 at 768, 11.15 at 640 and 6.29 at 375 — every rung under `--size-small`,
 * which `tokens.css:70` declares as this site's absolute 15px floor with no exemption.
 *
 * The rungs are keyed to the FRAME'S OWN WIDTH, not the viewport, because the frame's width is the
 * SVG's rendered width (the root is `width:100%`) and that is the only quantity that decides the
 * label's real size. A viewport media query would be a proxy for it and would be wrong the moment
 * the column changes. Every rung below is `18000 / container-width` rounded up, i.e. the unit size
 * that puts a 1200-unit canvas's label at 15 CSS px or better:
 *
 *   container >= 750px  ->  25 units  (757.33px measured at 1280 viewport -> 15.78px)
 *   container >= 690px  ->  27 units  (704px    measured at  768 viewport -> 15.84px)
 *   container >= 600px  ->  31 units  (608px    measured at  640 viewport -> 15.71px)
 *   container <  600px  ->  hidden, and `[data-post-schematic-legend]` carries the same words
 *
 * The ladder STOPS at 600px rather than continuing, and that is a geometric limit rather than a
 * choice: the longest label on this canvas is 37 characters, and 37 characters of Plex Mono at the
 * ~52 units a 343px render would need overflows the 1200-unit canvas outright. Below the last rung
 * the words are re-presented as HTML at `--size-small` instead of being shrunk past legibility or
 * dropped — dropping them would take the non-colour cue off the two headers whose accent is an
 * outline rectangle, and cost SC 1.4.1.
 *
 * NOTHING HERE ANIMATES OR TRANSITIONS: `src/checks/perf-guard.mjs` rejects animating a layout
 * property, and `font-size` is one. These are static rungs, not a transition. */
[data-post-schematic-frame] {
  container-type: inline-size;
  container-name: post-schematic;
}

/* Default state is the SAFE one: labels off, legend on. A browser without container query support
 * therefore degrades to the version that keeps every word on the page at 15px, rather than to the
 * version with 6px labels. */
[data-post-schematic] text {
  display: none;
}

[data-post-schematic-legend] {
  margin: var(--space-3) 0 0;
  font-size: var(--size-small);
  line-height: 1.5;
  color: var(--text-secondary);
}

@container post-schematic (min-width: 600px) {
  [data-post-schematic] text {
    display: initial;
    font-size: 31px;
  }

  [data-post-schematic-legend] {
    display: none;
  }
}

@container post-schematic (min-width: 690px) {
  [data-post-schematic] text {
    font-size: 27px;
  }
}

@container post-schematic (min-width: 750px) {
  [data-post-schematic] text {
    font-size: 25px;
  }
}
/*
 * Photo — a real, approved photograph. Lives here rather than in its own stylesheet because
 * `buildStyles()` in build.mjs keeps an explicit cascade list, and one more component does not
 * justify one more file in it.
 *
 * The block is deliberately thin. The component already renders explicit `width`/`height`
 * attributes, so the browser reserves the correct box from the aspect ratio with no CSS at all;
 * everything below is about NOT undoing that. `height: auto` is the one line that matters — set a
 * fixed CSS height on a responsive image and the attributes stop protecting against layout shift.
 */
.photo {
  display: block;
}

.photo__img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius-md);
  background: var(--surface-sunken);
}

/*
 * Photo band — the same photograph whole on a phone, cropped to a letterbox on a desktop.
 *
 * WHY THE CROP IS CSS AND NOT A SECOND SET OF FILES
 * A 16/9 derivative of every photograph would be eight more names on disk, eight more chances for
 * a name to point at a picture the CEO did not approve, and an art-direction `<picture>` with a
 * media query in it. `aspect-ratio` + `object-fit` gets the same rendered result from the same
 * approved file, and `scripts/encode-photos.mjs` stays a resize-and-re-encode step with no crop
 * decision in it at all.
 *
 * ZERO LAYOUT SHIFT ON BOTH SIDES OF THE BREAKPOINT. Below 768px there is no rule here at all, so
 * the box comes from the `width`/`height` attributes (1264x848, the native 3/2). At and above
 * 768px `aspect-ratio` replaces that ratio in the SAME cascade pass that lays the page out, before
 * the image loads — a ratio swap costs nothing at load because nothing was ever painted at the
 * other ratio. Never animate or transition `aspect-ratio` here; that would be a layout property in
 * motion and `src/checks/perf-guard.mjs` would reject it, correctly.
 */
@media (min-width: 768px) {
  .photo--band .photo__img {
    aspect-ratio: 16 / 9;
    object-fit: cover;
  }
}

/*
 * Focal points. WHICH pixels a crop discards is a design decision per photograph, so it arrives as
 * data (`photo.focal` in the content module) and lands here as one class per named position. The
 * vocabulary is closed and is validated in `src/site/photo-band.mjs` — an unknown focal throws at
 * build time rather than falling back to a centre crop that nobody would notice in review.
 *
 * Below 768px these are inert: nothing is cropped, so there is nothing to position.
 */
.photo--focal-center .photo__img {
  object-position: 50% 50%;
}
.photo--focal-center-left .photo__img {
  object-position: 25% 50%;
}
.photo--focal-center-right .photo__img {
  object-position: 75% 50%;
}
.photo--focal-lower-left .photo__img {
  object-position: 25% 75%;
}

/*
 * Interactive widget styling and the motion layer — CEO directive 2026-08-17, and
 * WEB-DESIGN-SPEC.md §7.
 *
 * EVERY transition and keyframe in this file is transform, opacity, or paint-only. That is not a
 * preference; `node build.mjs` runs src/checks/perf-guard.mjs over this file and refuses to emit
 * if it finds anything that can move a box. If you are here to add `transition: height`, the build
 * will stop you before the reviewer does.
 *
 * Reduced-motion handling lives at the bottom of this file, and it follows §7.4: collapse the
 * movement, KEEP the state change perceptible. A blanket `transition-duration: 0` is not compliance,
 * it is a silently degraded product.
 */

/* ── Placeholder image ────────────────────────────────────────────────────
 * It must never be mistakeable for a screenshot. The diagonal hatch is the load-bearing part: no
 * software looks like this, so nobody can read it as a picture of the product.
 */
.placeholder-image {
  margin: var(--space-8) 0;
}

.placeholder-image__frame {
  aspect-ratio: var(--placeholder-ratio, 16 / 9);
  display: grid;
  place-items: center;
  border: 1px dashed var(--rule-structural);
  border-radius: var(--radius-lg);
  background-color: var(--surface-sunken);
  background-image: repeating-linear-gradient(
    135deg,
    transparent 0,
    transparent 10px,
    rgb(100 121 143 / 12%) 10px,
    rgb(100 121 143 / 12%) 20px
  );
}

.placeholder-image__tag {
  font-family: var(--font-data);
  font-size: var(--size-label);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-secondary);
  background: var(--surface-card);
  border: 1px solid var(--rule-structural);
  border-radius: var(--radius-pill);
  padding: var(--space-2) var(--space-4);
}

.placeholder-image__caption {
  margin-top: var(--space-3);
  font-size: var(--size-small);
  color: var(--text-secondary);
  max-width: 60ch;
}

/* ── Live demo wrapper ────────────────────────────────────────────────────
 * The alternative to a placeholder: a real working thing, labelled as operable so a visitor knows
 * to touch it.
 */
.live-demo {
  border: 1px solid var(--rule-structural);
  border-radius: var(--radius-lg);
  padding: var(--space-4);
  background: var(--surface-raised);
  margin: var(--space-8) 0;
}

.live-demo__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}

.live-demo__badge {
  font-family: var(--font-data);
  font-size: var(--size-label);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--action-label);
  background: var(--action-fill);
  border-radius: var(--radius-pill);
  padding: var(--space-1) var(--space-3);
}

.live-demo__title {
  font-family: var(--font-display);
  font-weight: 700;
  color: var(--text-primary);
}

.live-demo__hint {
  margin: var(--space-4) 0 0;
  font-size: var(--size-small);
  color: var(--text-secondary);
  max-width: 62ch;
}

.live-demo__keys {
  display: inline-block;
  font-family: var(--font-data);
  font-size: var(--size-label);
  color: var(--text-secondary);
  border: 1px solid var(--rule-structural);
  border-radius: var(--radius-sm);
  padding: 0 var(--space-2);
}

/* ── Strike-out ───────────────────────────────────────────────────────────
 * The rule is drawn with scaleX on a pseudo-element. Animating `width` here is the obvious
 * implementation and it is rejected by the build.
 */
button.ticket-card__item {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  width: 100%;
  min-height: 44px;
  text-align: start;
  background: none;
  border: 0;
  border-radius: var(--radius-sm);
  padding: var(--space-2) var(--space-2);
  cursor: pointer;
  font: inherit;
  color: inherit;
  position: relative;
  transition: background-color var(--dur-fast) var(--ease-standard);
}

button.ticket-card__item:hover {
  background-color: var(--steel-200); /* raw: the card is always light. See the note below. */
}

.ticket-card__item-text {
  position: relative;
}

/*
 * ⚠ EVERYTHING INSIDE A TICKET CARD USES RAW RAMP TOKENS, NEVER SEMANTIC ONES.
 *
 * `.ticket-rail__card` is pinned to `--paper-card` with `--steel-950` ink in BOTH themes
 * (components.css:828-838). That is deliberate: the card is a replica of the product's OrderCard,
 * so it looks like the product's card whatever theme the surrounding site is in.
 *
 * A semantic token inside it therefore breaks in dark mode — `--text-secondary` resolves to the
 * dark theme's #9fb0c2 while the card underneath stays light #fbfcfd. Measured by axe-core at
 * 1.95:1 across 20 nodes on `/`, 9 on `/reliability`, 8 on `/es`. Light mode was clean, which is
 * why reading the CSS would not have found it.
 *
 * Rule for anyone adding to this block: inside the card use `--steel-*` / `--ember-600` directly;
 * outside the card use the semantic roles. The card does not follow the theme, so its contents
 * must not either.
 */
.ticket-card__item-text::after {
  content: "";
  position: absolute;
  inset-inline-start: 0;
  top: 50%;
  width: 100%;
  height: 2px;
  background: var(--ember-600); /* 5.62:1 on the card. Non-text, 1.4.11 needs 3:1. */
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--dur-base) var(--ease-out);
}

button.ticket-card__item[data-struck="true"] .ticket-card__item-text::after {
  transform: scaleX(1);
}

/* Never colour alone (1.4.1): the row also dims and gains a made marker. */
button.ticket-card__item[data-struck="true"] {
  color: var(--steel-600); /* 6.26:1 on the card */
}

button.ticket-card__item[data-struck="true"] .ticket-card__made {
  opacity: 1;
}

.ticket-card__made {
  margin-inline-start: auto;
  font-family: var(--font-data);
  font-size: var(--size-label);
  color: var(--ember-600); /* 5.62:1 on the card */
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease-standard);
}

.ticket-card__progress {
  font-family: var(--font-data);
  font-size: var(--size-label);
  color: var(--steel-600); /* 6.26:1 on the card */
}

.ticket-rail__card[data-all-struck="true"] {
  box-shadow: inset 0 0 0 2px var(--ember-600);
  transition: box-shadow var(--dur-base) var(--ease-standard);
}

/* ── The timer turning ────────────────────────────────────────────────────
 * Three states, each carrying a WORD as well as a hue (1.4.1). The dot is a second, non-colour
 * signal: filled, half, hollow.
 */
.ticket-card__timer {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
  transition: color var(--dur-base) var(--ease-standard);
}

/*
 * Raw ramp values, and NO dark-theme override. The card never goes dark, so an override for dark
 * mode would have applied a dark-surface colour to a light card — which is exactly the defect this
 * block used to have. All three measured against `--paper-card` #fbfcfd:
 *   fresh #4a5d71 → 6.26:1 · warn #a8420a → 5.62:1 · late #9a1c12 → 7.06:1
 */
.ticket-card__timer[data-timer-state="fresh"] {
  color: var(--steel-600);
}

.ticket-card__timer[data-timer-state="warn"] {
  color: var(--ember-600);
}

.ticket-card__timer[data-timer-state="late"] {
  color: #9a1c12;
}

.timer-dot {
  width: 10px;
  height: 10px;
  border-radius: var(--radius-pill);
  border: 2px solid currentcolor;
  transition: background-color var(--dur-base) var(--ease-standard);
}

.ticket-card__timer[data-timer-state="fresh"] .timer-dot {
  background-color: currentcolor;
}

.ticket-card__timer[data-timer-state="warn"] .timer-dot {
  background-color: transparent;
}

.ticket-card__timer[data-timer-state="late"] .timer-dot {
  background-color: transparent;
  border-style: dashed;
}

.timer-word {
  font-size: var(--size-label);
  letter-spacing: 0.04em;
  text-transform: uppercase;
}

/* The attention pulse, and the only part of the timer removed under reduced motion. */
.ticket-card__timer[data-timer-pulse="true"] {
  animation: timer-pulse 600ms var(--ease-out) 1;
}

@keyframes timer-pulse {
  0% {
    transform: scale(1);
  }

  40% {
    transform: scale(1.12);
  }

  100% {
    transform: scale(1);
  }
}

/* ── Routing board ────────────────────────────────────────────────────────
 * A tap moves an item to the next station. Drag is additional, never required (WCAG 2.5.7).
 */
.routing-board {
  display: grid;
  gap: var(--space-4);
  margin-top: var(--space-4);
}

@media (min-width: 640px) {
  .routing-board {
    grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  }
}

.routing-station {
  border: 1px solid var(--rule-structural);
  border-radius: var(--radius-md);
  background: var(--surface-card);
  padding: var(--space-3);
  min-height: 132px;
  transition:
    background-color var(--dur-base) var(--ease-standard),
    box-shadow var(--dur-base) var(--ease-standard);
}

.routing-station[data-drop-target="true"],
.routing-station[data-received="true"] {
  background-color: var(--surface-accent-wash);
  box-shadow: inset 0 0 0 2px var(--action-fill);
}

.routing-station__name {
  font-family: var(--font-data);
  font-size: var(--size-label);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-secondary);
  margin: 0 0 var(--space-2);
}

.routing-station__items {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-2);
}

.routing-item {
  display: block;
  width: 100%;
  min-height: 44px;
  text-align: start;
  font: inherit;
  color: var(--text-primary);
  background: var(--surface-raised);
  border: 1px solid var(--rule-structural);
  border-radius: var(--radius-sm);
  padding: var(--space-2) var(--space-3);
  cursor: pointer;
  transition:
    background-color var(--dur-fast) var(--ease-standard),
    transform var(--dur-fast) var(--ease-standard);
}

.routing-item:hover {
  background-color: var(--surface-accent-wash);
}

.routing-item:active {
  transform: scale(0.985);
}

.routing-item[data-dragging="true"] {
  opacity: 0.55;
}

.routing-status {
  font-family: var(--font-data);
  font-size: var(--size-label);
  color: var(--text-secondary);
  min-height: 22px;
}

/* ── The offline moment ───────────────────────────────────────────────────── */
.offline-demo__controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}

.offline-demo[data-network="offline"] {
  box-shadow: inset 0 0 0 2px var(--rule-structural);
  transition: box-shadow var(--dur-base) var(--ease-standard);
}

/*
 * 44x44 floor on the controls added in this pass.
 *
 * `button--sm` renders at 40px high. WCAG 2.5.8 AA only asks for 24x24, so 40px is not a criterion
 * failure — but WEB-DESIGN-SPEC.md §9.2 sets the house floor at 44px on every target, and the undo
 * has to be hittable with a wet thumb inside a four-second window. The proof harness measured 128x40
 * and failed it.
 *
 * Scoped to the controls this pass introduced rather than raised on `button--sm` globally: that
 * class is used across pages other lanes own, and a site-wide 4px height change is not this
 * branch's to make. Reported as a finding instead.
 */
.rail-undo [data-rail-undo-button],
.offline-demo__controls [data-network-toggle] {
  min-height: 44px;
}

/*
 * `[hidden]` must actually hide.
 *
 * The UA sheet's `[hidden] { display: none }` is a type-selector-strength rule, so ANY class that
 * sets `display` beats it — and `.button` sets `display: inline-flex`. The undo button therefore
 * rendered visible on page load with its `hidden` attribute present and correct, which is worse
 * than a styling slip: it advertised an undo for a bump that had not happened.
 *
 * Caught by looking at a 390px screenshot, not by any assertion. The harness now asserts it too.
 */
[hidden] {
  display: none !important;
}

.rail-undo {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  min-height: 44px;
}

/* The card header holds a number, a channel and a three-part timer. At 390px it must wrap rather
 * than squeeze the timer, because the timer is the thing the section is about. */
.ticket-card__header {
  flex-wrap: wrap;
  gap: var(--space-2);
}

.rail-undo__status {
  font-family: var(--font-data);
  font-size: var(--size-label);
  color: var(--text-secondary);
}

/* ── M1: focus indicators must survive forced-colors ──────────────────────
 * `/contact`'s four fields use `outline: none` plus a `box-shadow` ring
 * (components.css:653-657). In normal mode that ring is fine — 5.93:1, comfortably over 1.4.11.
 * In Windows High Contrast / forced-colors, `box-shadow` IS NOT RENDERED AT ALL, so all four
 * fields lose every focus indicator and a keyboard user loses their place entirely.
 *
 * The fix is additive and cannot regress the normal-mode ring: it only applies where the shadow
 * has already stopped existing. `Highlight` is a system colour, so it is correct by definition in
 * whatever theme the user has chosen.
 */
@media (forced-colors: active) {
  .form-field input:focus,
  .form-field textarea:focus,
  .form-field select:focus {
    outline: 2px solid Highlight;
    outline-offset: 2px;
  }

  /* Same class of defect anywhere else this pass introduced a shadow-only state. */
  .ticket-rail__card[data-all-struck="true"],
  .routing-station[data-drop-target="true"],
  .routing-station[data-received="true"] {
    outline: 2px solid Highlight;
    outline-offset: -2px;
  }
}

/* ── M2: the header row must fit at 390px under 1.4.12 text-spacing ───────
 * Measured on `/es` at 390px with word-spacing .16em and letter-spacing .12em applied: the header
 * row was 424px wide in a 390px viewport — wordmark 176 + switcher 69 + CTA 82 + toggle 44 + gaps.
 * The nav toggle was the visible offender only because it is last in the row.
 *
 * Two independent fixes, because there were two independent causes:
 *   1. The toggle sized itself from its own translated label. It is now a fixed 44x44 square.
 *   2. The row itself did not fit. The accessory moves into the drawer at drawer width.
 */
.site-header[data-nav="drawer"] .nav-toggle {
  flex-shrink: 0;
  width: 44px;
  min-width: 44px;
  height: 44px;
  padding: 0;
  gap: 0;
  overflow: hidden;
}

.site-header[data-nav="drawer"] .nav-toggle__label {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/*
 * At drawer width the header carries the wordmark, the CTA and the toggle, and nothing else. The
 * accessory moves into the drawer, where it has room and where the nav links already live.
 *
 * This is what actually fixes M2. Squaring the toggle stopped it growing with its own label, but
 * the row was still 424px wide because every sibling kept its full width.
 */
.site-header[data-nav="drawer"] .site-header__accessory {
  display: none;
}

.nav-drawer__accessory {
  margin-top: var(--space-6);
  padding-top: var(--space-6);
  border-top: 1px solid var(--rule-decorative);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
}

.site-header[data-nav="drawer"] .wordmark {
  min-width: 0;
}

/*
 * The hamburger. Three 2px rules, drawn in CSS, never an emoji and never an icon font.
 *
 * The two outer bars are pseudo-elements with a `background`, NOT `box-shadow`. The first version
 * of this rule used `box-shadow: 0 -6px 0, 0 6px 0` — which is the exact property the M1 fix above
 * exists because of. `box-shadow` is not rendered under `forced-colors: active`, so the icon would
 * have degraded to a single bar: a fix for a forced-colors defect shipping a new forced-colors
 * defect in the same file.
 */
.nav-toggle__icon {
  display: block;
  position: relative;
  width: 18px;
  height: 2px;
  background: currentcolor;
}

.nav-toggle__icon::before,
.nav-toggle__icon::after {
  content: "";
  position: absolute;
  left: 0;
  width: 18px;
  height: 2px;
  background: currentcolor;
}

.nav-toggle__icon::before {
  top: -6px;
}

.nav-toggle__icon::after {
  top: 6px;
}

.site-header[data-nav="inline"] .nav-toggle__icon {
  display: none;
}

/* ── B1: the pause control (WCAG 2.2.2) ───────────────────────────────────── */
.timer-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-4);
}

.timer-controls__note {
  font-family: var(--font-data);
  font-size: var(--size-label);
  color: var(--text-secondary);
}

.timer-controls [data-timer-pause-button] {
  min-height: 44px;
}

/* ── Strikeable checklist (W3 / F4 / S1) ──────────────────────────────────
 * The same gesture as a ticket line, applied to the visitor's own list.
 */
.strike-list {
  margin-top: var(--space-8);
}

.strike-list__items {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-2);
}

.strike-list__row {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  width: 100%;
  min-height: 44px;
  text-align: start;
  font: inherit;
  font-size: var(--size-body);
  color: var(--text-primary);
  background: none;
  border: 0;
  border-radius: var(--radius-sm);
  padding: var(--space-2) var(--space-3);
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease-standard);
}

.strike-list__row:hover {
  background-color: var(--surface-raised);
}

/* The checkbox mark. A square that fills — never colour alone, and never an emoji. */
.strike-list__mark {
  flex-shrink: 0;
  width: 20px;
  height: 20px;
  margin-top: 3px;
  border: 2px solid var(--rule-structural);
  border-radius: var(--radius-sm);
  background: transparent;
  transition: background-color var(--dur-fast) var(--ease-standard);
}

.strike-list__row[data-struck="true"] .strike-list__mark {
  background-color: var(--action-fill);
  border-color: var(--action-fill);
}

.strike-list__row[data-struck="true"] {
  color: var(--text-secondary);
}

.strike-list__progress {
  margin-top: var(--space-4);
  font-family: var(--font-data);
  font-size: var(--size-label);
  color: var(--text-secondary);
}

.queue-count {
  font-family: var(--font-data);
  font-size: var(--size-small);
  color: var(--text-secondary);
}

.queue-list {
  list-style: none;
  margin: var(--space-3) 0 0;
  padding: 0;
  display: grid;
  gap: var(--space-2);
}

.queue-list__item {
  font-family: var(--font-data);
  font-size: var(--size-small);
  color: var(--text-secondary);
  border-inline-start: 2px solid var(--rule-structural);
  padding-inline-start: var(--space-3);
  transition:
    opacity var(--dur-base) var(--ease-in),
    transform var(--dur-base) var(--ease-in);
}

.queue-list__item[data-syncing="true"] {
  opacity: 0;
  transform: translateX(24px);
}

/* ── Replacement ticket arriving, and the bump leaving ────────────────────── */
.ticket-rail__card[data-replacement="true"] {
  will-change: transform;
}

/* ── Disclosure panels: contents lift, the box does not tween its height ──── */
[data-disclosure-panel][data-entered="true"] {
  animation: disclosure-in var(--dur-base) var(--ease-out) 1;
}

@keyframes disclosure-in {
  from {
    opacity: 0;
    transform: translateY(-6px);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* ── The /about "will not do" markers, wired to hover and focus-within ────── */
.marker-list li:hover::before,
.marker-list li:focus-within::before {
  transform: scaleX(1);
}

/* ── Scroll-reveal ─────────────────────────────────────────────────────────
 * There is NO generic `[data-reveal] { opacity: 0 }` rule here, on purpose. The hidden state is
 * applied by JavaScript, only after it has confirmed it can remove it again. A CSS-hidden reveal
 * is how a page ships blank to anyone whose observer never fires.
 */

/* ── Reduced motion (§7.4) ─────────────────────────────────────────────────
 * base.css already collapses durations globally. These are the widget-level exceptions: the state
 * changes that must SURVIVE the collapse, because they are information rather than decoration.
 */
@media (prefers-reduced-motion: reduce) {
  /* The strike-through still appears — it just does not draw. */
  button.ticket-card__item[data-struck="true"] .ticket-card__item-text::after {
    transform: scaleX(1) !important;
  }

  /* The made marker still appears. */
  button.ticket-card__item[data-struck="true"] .ticket-card__made {
    opacity: 1 !important;
  }

  /* The timer still changes COLOUR AND WORD. Only the pulse is removed. */
  .ticket-card__timer[data-timer-pulse="true"] {
    animation: none !important;
  }

  /* The routed item still lands in its new station; it simply does not travel. */
  .routing-station[data-received="true"] {
    box-shadow: inset 0 0 0 2px var(--action-fill) !important;
  }

  /* The queue still drains; the items simply vanish rather than sliding out. */
  .queue-list__item[data-syncing="true"] {
    opacity: 0 !important;
  }

  /* The disclosure panel still opens instantly with its content in place. */
  [data-disclosure-panel][data-entered="true"] {
    animation: none !important;
    opacity: 1 !important;
  }

  /* The "will not do" marker still marks. */
  .marker-list li:hover::before,
  .marker-list li:focus-within::before {
    transform: scaleX(1) !important;
  }
}

/*
 * THE COST PROJECTOR AND INLINE VALIDATION — CEO brief 2026-08-19.
 *
 * Its own file rather than another 200 lines on the end of motion.css, because it is a component
 * with a chart in it rather than a site-wide motion rule, and motion.css is already the longest
 * sheet here. Added to the cascade list in `build.mjs` LAST, so it can override `components.css`
 * for the one rule that needs to (`.form-field__error`).
 *
 * The reasoning behind the widget — why it is not an ROI calculator, why it never animates on
 * scroll, and why the axis is shared — lives in `src/site/cost-projector.mjs`. What matters HERE:
 *
 *   NOTHING IN THIS FILE TRANSITIONS A PROPERTY THAT CAN MOVE A BOX. The plot is a fixed-height
 *   grid, every bar is a full-height element scaled with `transform`, and the digits are
 *   tabular so a counting number does not resize its own container. `src/checks/perf-guard.mjs`
 *   rejects the build if that ever stops being true, so this is enforced rather than promised.
 *
 *   NO NEW COLOUR AND NO NEW FONT. Two series, two of the palette's existing three hues. There is
 *   no red in this design system, so the validation error uses `--text-accent` — the existing
 *   accent role — rather than a `--color-error` invented in a stylesheet.
 */

.projector {
  margin-top: var(--space-8);
  border: 1px solid var(--rule-structural);
  border-radius: var(--radius-lg);
  background: var(--surface-card);
  padding: var(--space-6);
}

.projector__controls {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-8);
  align-items: flex-start;
}

.projector__field {
  border: 0;
  margin: 0;
  padding: 0;
  min-width: 0;
}

.projector__label {
  display: block;
  font-family: var(--font-data);
  font-size: var(--size-label);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-secondary);
  padding: 0;
  margin-bottom: var(--space-3);
}

.projector__stepper {
  display: flex;
  align-items: stretch;
  gap: var(--space-2);
}

/* 44x44 floor (WEB-DESIGN-SPEC.md §9.2). The stepper is the most-tapped control on the page. */
.projector__step {
  min-width: 44px;
  min-height: 44px;
  font: inherit;
  font-size: var(--size-h4);
  line-height: 1;
  color: var(--text-primary);
  background: var(--surface-raised);
  border: 1px solid var(--rule-structural);
  border-radius: var(--radius-md);
  cursor: pointer;
  transition:
    background-color var(--dur-fast) var(--ease-standard),
    transform var(--dur-instant) var(--ease-standard);
}

.projector__step:hover {
  background-color: var(--surface-accent-wash);
}

/*
 * Press feedback, and it is the only thing on this widget that is purely tactile. It is `:active`
 * rather than `:hover` on purpose — the brief forbids relying on hover for anything critical, and
 * a phone has no hover at all, so the confirmation that a tap registered must come from the press.
 */
.projector__step:active {
  transform: scale(0.94);
}

.projector__input {
  width: 5rem;
  min-height: 44px;
  font-family: var(--font-data);
  font-size: var(--size-data-lg);
  font-variant-numeric: tabular-nums;
  text-align: center;
  color: var(--text-primary);
  background: var(--surface-page);
  border: 1px solid var(--rule-structural);
  border-radius: var(--radius-md);
}

.projector__radios {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
}

.projector__radio {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 44px;
}

.projector__radio label {
  font-size: var(--size-body);
  color: var(--text-primary);
}

.projector__figures {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-8);
  margin: var(--space-8) 0 0;
}

.projector__figure {
  min-width: 0;
}

.projector__term {
  font-family: var(--font-data);
  font-size: var(--size-label);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-secondary);
}

/*
 * `tabular-nums` is load-bearing, not typographic taste. The figure counts through intermediate
 * values, and proportional digits change the string's width on almost every frame — a box that
 * resizes sixty times a second inside a flex row. Fixed-width digits make the count-up inert.
 */
.projector__value {
  margin: var(--space-1) 0 0;
  font-variant-numeric: tabular-nums;
  color: var(--text-primary);
}

.projector__note {
  margin: var(--space-1) 0 0;
  font-size: var(--size-small);
  color: var(--text-secondary);
  max-width: 32ch;
}

.projector__chart-figure {
  margin: var(--space-8) 0 0;
}

/*
 * FIXED HEIGHT. This is the declaration that guarantees zero layout shift: whatever the numbers
 * do, the plot occupies exactly this much space, so nothing below the chart ever moves.
 */
.projector-chart {
  height: 180px;
  padding-bottom: var(--space-6);
}

.projector-chart__plot {
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: var(--space-1);
  height: 100%;
  align-items: end;
}

.projector-chart__col {
  display: grid;
  grid-template-rows: 1fr auto;
  height: 100%;
  min-width: 0;
}

.projector-chart__stack {
  position: relative;
  height: 100%;
  display: flex;
  align-items: flex-end;
  gap: 1px;
}

/*
 * Each bar is a FULL-HEIGHT box scaled down from its base. Animating `height` is the obvious
 * implementation and the build rejects it.
 */
.projector-chart__bar {
  display: block;
  flex: 1 1 0;
  min-width: 0;
  height: 100%;
  transform-origin: bottom center;
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
}

/*
 * Two series, two of the palette's three hues, nothing invented. Steel is the neutral majority
 * hue and ember the accent — the right pairing for "the ordinary case" against "the one you are
 * being asked to choose". They are distinguishable without colour too: the annual series is the
 * flat one, and both are named in the legend and spelled out in the caption.
 */
.projector-chart__bar--monthly {
  background-color: var(--steel-500);
}

.projector-chart__bar--annual {
  background-color: var(--ember-500);
}

.projector-chart__tick {
  font-family: var(--font-data);
  font-size: var(--size-label);
  color: var(--text-secondary);
  text-align: center;
  padding-top: var(--space-2);
}

.projector__legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-6);
  margin-top: var(--space-4);
}

/* The key is a swatch AND a word. Never colour alone (1.4.1). */
.projector__key {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-family: var(--font-data);
  font-size: var(--size-label);
  color: var(--text-secondary);
}

.projector__key::before {
  content: "";
  width: 14px;
  height: 14px;
  border-radius: var(--radius-sm);
}

.projector__key--monthly::before {
  background-color: var(--steel-500);
}

.projector__key--annual::before {
  background-color: var(--ember-500);
}

.projector__caption {
  margin-top: var(--space-4);
  font-size: var(--size-small);
  color: var(--text-secondary);
  max-width: 68ch;
}

/*
 * TRANSITIONS ARE OPT-IN, and the widget only opts in after its first render and never under
 * reduced motion. Without this attribute the bars have no transition at all, which is what stops
 * twenty-four of them animating up from zero on page load — motion `/pricing` is not allowed to
 * have, and a count-up nobody asked for.
 */
.projector[data-animate="true"] .projector-chart__bar {
  transition: transform var(--dur-base) var(--ease-out);
}

/* ── Inline validation (`/contact`) ─────────────────────────────────────── */

/*
 * The message node is rendered EMPTY and always present so `aria-describedby` is associated before
 * anything is wrong. An empty paragraph would still occupy a line box and push the next field
 * down when it filled, so it is collapsed until it has something to say — and because it collapses
 * on `:empty` rather than being inserted by script, the reflow happens at the moment the visitor
 * caused it and never during page load.
 */
.form-field__error:empty {
  display: none;
}

.form-field__error {
  margin: var(--space-2) 0 0;
  font-size: var(--size-small);
  color: var(--text-accent);
}

/* The border is a SECOND signal behind the sentence, never the only one (1.4.1). */
.form-field input[aria-invalid="true"],
.form-field textarea[aria-invalid="true"] {
  border-color: var(--text-accent);
  transition: border-color var(--dur-fast) var(--ease-standard);
}

@media (prefers-reduced-motion: reduce) {
  /*
   * ONE rule here, and the absence of the other two is deliberate.
   *
   * `base.css` already collapses `transition-duration` and `animation-duration` to 0.01ms on
   * `*, *::before, *::after` with `!important`. Every TRANSITION in this file is therefore already
   * dead under reduced motion — the bar tween and the invalid-border fade both measured 0.01ms in
   * a real reduced-motion browser context (`MR-R-30271`). Re-declaring `transition: none` for them
   * here would be a rule that cannot be observed doing anything: deleting it changes no measurable
   * behaviour, which is the definition of code that has not earned its place. It was written, it
   * was measured, and it was removed.
   *
   * The press scale is a DIFFERENT KIND OF THING and the global collapse does not touch it.
   * `:active { transform: scale(0.94) }` is a state, not a transition — collapsing its duration
   * only makes it snap to 0.94 instantly, which is a jolt rather than a fade. It has to be
   * cancelled by value, so it is, and it is the only thing in this block.
   *
   * WHAT SURVIVES under reduced motion, in full: every number, the caption rewrite, the
   * live-region announcement, all twenty-four bar heights, the error text and the invalid border.
   * §7.4 — collapse the movement, keep the state change perceptible.
   */
  .projector__step:active {
    transform: none;
  }
}
