/*! ==========================================================================
 *  Deck v0.1.1 — the CSS framework for Keel
 *  Mobile first. One file. No build step. No config. No dependencies.
 *
 *  <link rel="stylesheet" href="/assets/deck.css">
 *
 *  Retheme the entire app from one line:
 *    :root { --hue-brand: 265; }
 *
 *  Layer order (so your app CSS always wins without !important):
 *    deck.reset, deck.tokens, deck.type, deck.layout, deck.components,
 *    deck.mobile, deck.motion, deck.effects, deck.utilities, deck.rtl,
 *    deck.print, then app.base, app.components, app.pages, app.overrides
 *
 *  MIT licence. https://get-deck.dev
 *  ======================================================================== */

/* ==========================================================================
   Deck — 00 Layers
   The whole cascade contract in one place, declared before any rule exists.
   Order is decided here, not by file order, not by specificity, and never by
   !important.

   Later layers win. Anything you write OUTSIDE a layer beats every layer, so
   a plain rule in a page template always wins with no escalation.

     deck.*    the framework
     app.*     your slots, in the order you almost certainly want them

   In a Keel view you can write:

     @layer app.pages {
       .page-header { padding-block: var(--space-8); }
     }

   and it beats every deck rule without a single !important, while still being
   overridable by app.overrides and by unlayered CSS.
   ========================================================================== */

@layer
  deck.reset,      /* normalize and element defaults        */
  deck.tokens,     /* custom properties only                */
  deck.type,       /* headings, prose, links                */
  deck.layout,     /* containers, stack, grid, containers   */
  deck.components, /* buttons, forms, cards, grid, charts   */
  deck.mobile,     /* sheets, tab bar, icons, emoji         */
  deck.motion,     /* transitions, entrances, view trans.   */
  deck.effects,    /* gradients, 3D, surface treatments     */
  deck.utilities,  /* single-purpose classes                */
  deck.rtl,        /* direction fixes, must beat utilities  */
  deck.print,      /* paper                                 */

  app.base,        /* your resets and element styles        */
  app.components,  /* your components and deck overrides    */
  app.pages,       /* per-page and per-view tweaks          */
  app.overrides;   /* the escape hatch, still not !important */

/* --------------------------------------------------------------------------
   Registered custom properties.
   @property is not affected by layers. Registering a property gives it a type,
   which is what lets the browser interpolate it — an angle or a color in a
   custom property can now animate, which plain custom properties cannot do.
   -------------------------------------------------------------------------- */

@property --g-angle {
  syntax: "<angle>";
  initial-value: 180deg;
  inherits: false;
}
@property --g-from {
  syntax: "<color>";
  initial-value: transparent;
  inherits: false;
}
@property --g-to {
  syntax: "<color>";
  initial-value: transparent;
  inherits: false;
}
@property --g-stop {
  syntax: "<percentage>";
  initial-value: 50%;
  inherits: false;
}
@property --sheen {
  syntax: "<percentage>";
  initial-value: -40%;
  inherits: false;
}
@property --tilt-x {
  syntax: "<angle>";
  initial-value: 0deg;
  inherits: false;
}
@property --tilt-y {
  syntax: "<angle>";
  initial-value: 0deg;
  inherits: false;
}
@property --depth {
  syntax: "<length>";
  initial-value: 0px;
  inherits: false;
}

/* ==========================================================================
   Deck — 01 Tokens
   Everything is a custom property. Change a hue, retheme the whole app.
   No build step. No purge. No config file.
   ========================================================================== */

/* Layer order is declared in 00-layers.css. */

@layer deck.tokens {

  :root {
    color-scheme: light dark;

    /* --- Hue control -------------------------------------------------------
       These six numbers are the whole palette. Set --hue-brand to 12 and the
       framework becomes rust. Set it to 265 and it becomes violet. Buttons,
       focus rings, links, charts, and shadows all follow.
       ---------------------------------------------------------------------- */
    --hue-brand: 196;   /* harbor teal   */
    --hue-accent: 42;   /* signal amber  */
    --hue-good: 152;    /* starboard     */
    --hue-warn: 74;     /* caution       */
    --hue-bad: 24;      /* port light    */
    --hue-neutral: 232; /* chart ink     */

    --chroma-brand: .118;
    --chroma-accent: .142;
    --chroma-status: .132;
    --chroma-neutral: .012; /* neutrals carry a trace of the ink hue */

    /* --- Brand ramp -------------------------------------------------------- */
    --brand-50:  oklch(97.4% calc(var(--chroma-brand) * .16) var(--hue-brand));
    --brand-100: oklch(94.2% calc(var(--chroma-brand) * .30) var(--hue-brand));
    --brand-200: oklch(88.6% calc(var(--chroma-brand) * .48) var(--hue-brand));
    --brand-300: oklch(80.4% calc(var(--chroma-brand) * .70) var(--hue-brand));
    --brand-400: oklch(70.2% calc(var(--chroma-brand) * .90) var(--hue-brand));
    --brand-500: oklch(60.8% var(--chroma-brand) var(--hue-brand));
    --brand-600: oklch(51.6% var(--chroma-brand) var(--hue-brand));
    --brand-700: oklch(43.4% calc(var(--chroma-brand) * .92) var(--hue-brand));
    --brand-800: oklch(35.6% calc(var(--chroma-brand) * .78) var(--hue-brand));
    --brand-900: oklch(28.2% calc(var(--chroma-brand) * .62) var(--hue-brand));
    --brand-950: oklch(20.4% calc(var(--chroma-brand) * .46) var(--hue-brand));

    /* --- Neutral ramp ------------------------------------------------------ */
    --ink-0:   oklch(100% 0 0);
    --ink-25:  oklch(99.1% calc(var(--chroma-neutral) * .5) var(--hue-neutral));
    --ink-50:  oklch(97.8% var(--chroma-neutral) var(--hue-neutral));
    --ink-100: oklch(95.4% calc(var(--chroma-neutral) * 1.4) var(--hue-neutral));
    --ink-200: oklch(91.2% calc(var(--chroma-neutral) * 1.8) var(--hue-neutral));
    --ink-300: oklch(84.6% calc(var(--chroma-neutral) * 2.2) var(--hue-neutral));
    --ink-400: oklch(70.8% calc(var(--chroma-neutral) * 2.6) var(--hue-neutral));
    --ink-500: oklch(58.4% calc(var(--chroma-neutral) * 2.8) var(--hue-neutral));
    --ink-600: oklch(47.6% calc(var(--chroma-neutral) * 2.8) var(--hue-neutral));
    --ink-700: oklch(38.2% calc(var(--chroma-neutral) * 2.6) var(--hue-neutral));
    --ink-800: oklch(29.4% calc(var(--chroma-neutral) * 2.4) var(--hue-neutral));
    --ink-900: oklch(22.6% calc(var(--chroma-neutral) * 2.2) var(--hue-neutral));
    --ink-950: oklch(16.2% calc(var(--chroma-neutral) * 2.0) var(--hue-neutral));
    --ink-1000: oklch(11.4% calc(var(--chroma-neutral) * 1.8) var(--hue-neutral));

    /* --- Status ------------------------------------------------------------ */
    --good-100: oklch(94% calc(var(--chroma-status) * .3) var(--hue-good));
    --good-500: oklch(62% var(--chroma-status) var(--hue-good));
    --good-600: oklch(52% var(--chroma-status) var(--hue-good));
    --good-700: oklch(44% var(--chroma-status) var(--hue-good));
    --warn-100: oklch(95% calc(var(--chroma-status) * .32) var(--hue-warn));
    --warn-500: oklch(76% var(--chroma-status) var(--hue-warn));
    --warn-700: oklch(50% var(--chroma-status) var(--hue-warn));
    --bad-100:  oklch(94.5% calc(var(--chroma-status) * .3) var(--hue-bad));
    --bad-500:  oklch(58% calc(var(--chroma-status) * 1.25) var(--hue-bad));
    --bad-600:  oklch(52% calc(var(--chroma-status) * 1.25) var(--hue-bad));
    --bad-700:  oklch(44% calc(var(--chroma-status) * 1.1) var(--hue-bad));
    --accent-100: oklch(95% calc(var(--chroma-accent) * .3) var(--hue-accent));
    --accent-500: oklch(74% var(--chroma-accent) var(--hue-accent));
    --accent-700: oklch(52% calc(var(--chroma-accent) * .9) var(--hue-accent));

    /* --- Semantic surfaces (auto light/dark) -------------------------------- */
    --bg:          light-dark(var(--ink-50),  var(--ink-1000));
    --bg-sunken:   light-dark(var(--ink-100), oklch(8.6% .012 var(--hue-neutral)));
    --surface:     light-dark(var(--ink-0),   var(--ink-950));
    --surface-2:   light-dark(var(--ink-25),  var(--ink-900));
    --surface-hover:light-dark(var(--ink-50), var(--ink-900));
    --line:        light-dark(var(--ink-200), oklch(28% .016 var(--hue-neutral)));
    --line-strong: light-dark(var(--ink-300), oklch(36% .018 var(--hue-neutral)));

    --text:        light-dark(var(--ink-900), var(--ink-100));
    --text-muted:  light-dark(var(--ink-600), var(--ink-400));
    --text-faint:  light-dark(var(--ink-500), var(--ink-500));
    /* --- Ink on a coloured surface ----------------------------------------
       One per saturated surface. These are the static values: they are correct
       for the shipped hues but they are guesses about any hue you set, which
       is what the @supports block below fixes.
       -------------------------------------------------------------------- */
    --text-on-brand:  oklch(99% .01 var(--hue-brand));
    --text-on-accent: oklch(24% .06 var(--hue-accent));
    --text-on-good:   oklch(99% .01 var(--hue-good));
    --text-on-warn:   oklch(24% .06 var(--hue-warn));
    --text-on-bad:    oklch(99% .01 var(--hue-bad));

    --brand:       light-dark(var(--brand-600), var(--brand-400));
    --brand-hover: light-dark(var(--brand-700), var(--brand-300));
    --brand-soft:  light-dark(var(--brand-50),  oklch(26% .05 var(--hue-brand)));
    --brand-soft-text: light-dark(var(--brand-800), var(--brand-200));

    --focus: light-dark(var(--brand-500), var(--brand-300));

    /* --- Space: 4px base, plus fluid steps for page rhythm ------------------ */
    --space-0: 0;
    --space-px: 1px;
    --space-1: .25rem;
    --space-2: .5rem;
    --space-3: .75rem;
    --space-4: 1rem;
    --space-5: 1.25rem;
    --space-6: 1.5rem;
    --space-8: 2rem;
    --space-10: 2.5rem;
    --space-12: 3rem;
    --space-16: 4rem;
    --space-20: 5rem;
    --space-24: 6rem;
    --space-section: clamp(2.5rem, 1.4rem + 5.5vw, 6rem);
    --space-gutter: clamp(1rem, .55rem + 2.2vw, 2rem);

    /* --- Radius: intentionally not one value ------------------------------- */
    --r-xs: 4px;    /* chips, tags, kbd            */
    --r-sm: 7px;    /* inputs, buttons             */
    --r-md: 12px;   /* cards, menus                */
    --r-lg: 18px;   /* sheets, modals, hero panels */
    --r-full: 999px;

    /* --- Elevation: tinted with the ink hue, never flat black -------------- */
    --shadow-color: var(--hue-neutral);
    --shadow-1: 0 1px 2px oklch(20% .03 var(--shadow-color) / .06),
                0 1px 1px oklch(20% .03 var(--shadow-color) / .04);
    --shadow-2: 0 2px 4px oklch(20% .03 var(--shadow-color) / .05),
                0 4px 12px oklch(20% .03 var(--shadow-color) / .07);
    --shadow-3: 0 4px 8px oklch(20% .03 var(--shadow-color) / .06),
                0 12px 28px oklch(20% .03 var(--shadow-color) / .10);
    --shadow-4: 0 8px 16px oklch(20% .03 var(--shadow-color) / .08),
                0 24px 56px oklch(20% .03 var(--shadow-color) / .14);
    --shadow-inset: inset 0 1px 0 oklch(100% 0 0 / .6);

    /* A legible floor for text over a photo. Eased rather than a flat wash: a
       50% black overlay dulls the whole image, while this only darkens where
       the text actually sits. Shared by .g-scrim and .carousel-caption so the
       two cannot drift apart. */
    --scrim: linear-gradient(to top,
             oklch(12% .02 var(--hue-neutral) / .82) 0%,
             oklch(12% .02 var(--hue-neutral) / .62) 22%,
             oklch(12% .02 var(--hue-neutral) / .28) 48%,
             transparent 78%);

    /* --- Type -------------------------------------------------------------- */
    --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI Variable Text",
                 "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
    --font-mono: ui-monospace, "SF Mono", "Cascadia Mono", "Roboto Mono",
                 Menlo, Consolas, monospace;
    --font-display: var(--font-sans);

    --text-2xs: .6875rem;
    --text-xs:  .75rem;
    --text-sm:  .8125rem;
    --text-base: .9375rem;
    --text-md:  1.0625rem;
    --text-lg:  clamp(1.125rem, 1.06rem + .32vw, 1.3125rem);
    --text-xl:  clamp(1.3125rem, 1.19rem + .62vw, 1.625rem);
    --text-2xl: clamp(1.5625rem, 1.33rem + 1.16vw, 2.125rem);
    --text-3xl: clamp(1.875rem, 1.44rem + 2.16vw, 2.875rem);
    --text-4xl: clamp(2.25rem, 1.5rem + 3.75vw, 3.75rem);

    --leading-tight: 1.12;
    --leading-snug: 1.3;
    --leading-normal: 1.55;
    --leading-loose: 1.72;
    --measure: 68ch;

    /* --- Motion ------------------------------------------------------------ */
    --ease-out: cubic-bezier(.22, 1, .36, 1);
    --ease-in-out: cubic-bezier(.65, 0, .35, 1);
    --ease-spring: linear(0, .18 6%, .53 14%, .89 24%, 1.04 32%, 1.06 40%, 1);
    --dur-1: 110ms;
    --dur-2: 190ms;
    --dur-3: 300ms;

    /* --- Controls ---------------------------------------------------------- */
    --tap: 44px;             /* minimum touch target, everywhere */
    --control-h-sm: 34px;
    --control-h: 42px;
    --control-h-lg: 50px;
    --ring: 0 0 0 3px light-dark(oklch(from var(--focus) l c h / .32),
                                 oklch(from var(--focus) l c h / .42));

    --z-sticky: 100;
    --z-nav: 200;
    --z-sheet: 300;
    --z-modal: 400;
    --z-toast: 500;
  }

  /* contrast-color() resolves against the colour as it actually computes, so
     these follow --hue-brand and friends wherever they are dragged. It returns
     only black or white and it aims at AA (4.5:1), which is why every surface
     that carries text is kept out of the 55-65% lightness band -- see
     --good-600 and --bad-600 above. Baseline Newly Available, so the static
     values in :root stand in where it is missing. */
  @supports (color: contrast-color(red)) {
    :root {
      --text-on-brand:  contrast-color(var(--brand-600));
      --text-on-accent: contrast-color(var(--accent-500));
      --text-on-good:   contrast-color(var(--good-600));
      --text-on-warn:   contrast-color(var(--warn-500));
      --text-on-bad:    contrast-color(var(--bad-600));
    }
  }

  /* Explicit theme override beats the OS setting */
  [data-theme="light"] { color-scheme: light; }
  [data-theme="dark"]  { color-scheme: dark; }

  /* Dark mode wants slightly less shadow and slightly more border */
  @media (prefers-color-scheme: dark) {
    :root {
      --shadow-2: 0 2px 4px oklch(0% 0 0 / .3), 0 4px 12px oklch(0% 0 0 / .34);
      --shadow-3: 0 4px 10px oklch(0% 0 0 / .34), 0 14px 30px oklch(0% 0 0 / .4);
      --shadow-4: 0 10px 20px oklch(0% 0 0 / .4), 0 28px 60px oklch(0% 0 0 / .5);
      --shadow-inset: inset 0 1px 0 oklch(100% 0 0 / .06);
    }
  }
}

/* ==========================================================================
   Deck — 02 Reset
   ========================================================================== */

@layer deck.reset {

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

  * { margin: 0; }

  html {
    -webkit-text-size-adjust: 100%;
    -moz-text-size-adjust: 100%;
    text-size-adjust: 100%;
    tab-size: 2;
    scroll-behavior: smooth;
    scroll-padding-block-start: calc(var(--control-h-lg) + var(--space-4));
  }

  body {
    min-block-size: 100dvh;
    background: var(--bg);
    color: var(--text);
    font-family: var(--font-sans);
    font-size: var(--text-base);
    line-height: var(--leading-normal);
    font-synthesis-weight: none;
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
    overflow-wrap: break-word;
  }

  img, svg, video, canvas, audio, iframe, embed, object {
    display: block;
    max-inline-size: 100%;
  }
  img, video { block-size: auto; }

  input, button, textarea, select { font: inherit; color: inherit; }

  button { background: none; border: 0; }

  p, li, figcaption { text-wrap: pretty; }
  h1, h2, h3, h4, h5, h6 { text-wrap: balance; }

  ul[class], ol[class] { list-style: none; padding: 0; }

  table { border-collapse: collapse; border-spacing: 0; }

  hr {
    border: 0;
    block-size: 1px;
    background: var(--line);
  }

  :target { scroll-margin-block-start: var(--space-16); }

  ::selection {
    background: var(--brand-200);
    color: var(--ink-950);
  }

  /* One focus treatment for the whole framework */
  :focus-visible {
    outline: 2px solid var(--focus);
    outline-offset: 2px;
    border-radius: var(--r-xs);
  }
  :focus:not(:focus-visible) { outline: none; }

  /* Firefox needs help with the scrollbar; WebKit gets the thin version */
  * { scrollbar-color: var(--ink-300) transparent; scrollbar-width: thin; }
  ::-webkit-scrollbar { inline-size: 10px; block-size: 10px; }
  ::-webkit-scrollbar-thumb {
    background: var(--ink-300);
    border-radius: var(--r-full);
    border: 3px solid transparent;
    background-clip: content-box;
  }
  ::-webkit-scrollbar-thumb:hover { background: var(--ink-400); background-clip: content-box; }

  @media (prefers-reduced-motion: reduce) {
    html { scroll-behavior: auto; }
    *, *::before, *::after {
      animation-duration: .01ms !important;
      animation-iteration-count: 1 !important;
      transition-duration: .01ms !important;
      scroll-behavior: auto !important;
    }
    /* Loading indicators keep moving, slowly. A frozen spinner reads as broken,
       and progress feedback is information, not decoration. */
    .spinner, .icon-spin, .skeleton, .marquee-track, .btn.is-loading::after,
    .dg-wrap.is-loading::after, .ping::after {
      animation-duration: 2.4s !important;
      animation-iteration-count: infinite !important;
    }
  }

  /* Opt in per-element where a smooth auto height matters */
  @supports (interpolate-size: allow-keywords) {
    :root { interpolate-size: allow-keywords; }
  }
}

/* ==========================================================================
   Deck — 03 Type
   ========================================================================== */

@layer deck.type {

  h1, .h1, h2, .h2, h3, .h3, h4, .h4, h5, .h5, h6, .h6 {
    font-family: var(--font-display);
    font-weight: 640;
    line-height: var(--leading-tight);
    letter-spacing: -.021em;
    color: var(--text);
  }

  h1, .h1 { font-size: var(--text-3xl); letter-spacing: -.031em; }
  h2, .h2 { font-size: var(--text-2xl); letter-spacing: -.026em; }
  h3, .h3 { font-size: var(--text-xl); }
  h4, .h4 { font-size: var(--text-lg); letter-spacing: -.014em; }
  h5, .h5 { font-size: var(--text-md); letter-spacing: -.008em; font-weight: 620; }
  h6, .h6 { font-size: var(--text-base); letter-spacing: 0; font-weight: 640; }

  .display {
    font-size: var(--text-4xl);
    font-weight: 700;
    line-height: 1.04;
    letter-spacing: -.036em;
  }

  .lede {
    font-size: var(--text-md);
    line-height: var(--leading-loose);
    color: var(--text-muted);
    max-inline-size: var(--measure);
  }

  small { font-size: var(--text-sm); }

  strong, b { font-weight: 640; }

  a {
    color: var(--brand);
    text-decoration-thickness: 1px;
    text-underline-offset: .18em;
    text-decoration-color: color-mix(in oklab, currentColor 40%, transparent);
    transition: color var(--dur-1) var(--ease-out),
                text-decoration-color var(--dur-1) var(--ease-out);
  }
  a:hover { color: var(--brand-hover); text-decoration-color: currentColor; }

  .link-quiet { color: var(--text); text-decoration: none; }
  .link-quiet:hover { color: var(--brand); text-decoration: underline; }

  code, kbd, samp, pre { font-family: var(--font-mono); font-size: .92em; }

  :not(pre) > code {
    background: var(--surface-2);
    border: 1px solid var(--line);
    border-radius: var(--r-xs);
    padding: .12em .38em;
    color: light-dark(var(--brand-800), var(--brand-200));
  }

  pre {
    background: var(--ink-950);
    color: var(--ink-100);
    padding: var(--space-4);
    border-radius: var(--r-md);
    overflow-x: auto;
    line-height: 1.6;
    font-size: var(--text-sm);
    -webkit-overflow-scrolling: touch;
  }
  pre code { background: none; border: 0; padding: 0; color: inherit; }

  kbd {
    display: inline-block;
    background: var(--surface);
    border: 1px solid var(--line-strong);
    border-block-end-width: 2px;
    border-radius: var(--r-xs);
    padding: .1em .42em;
    font-size: var(--text-xs);
    line-height: 1.5;
    color: var(--text-muted);
  }

  blockquote {
    border-inline-start: 3px solid var(--brand-300);
    padding-inline-start: var(--space-4);
    color: var(--text-muted);
    font-size: var(--text-md);
    line-height: var(--leading-loose);
  }

  mark {
    background: var(--accent-100);
    color: light-dark(var(--ink-900), var(--ink-900));
    padding: .05em .2em;
    border-radius: var(--r-xs);
  }

  abbr[title] { text-decoration: underline dotted; cursor: help; }

  /* Prose: for anything that came out of a WYSIWYG or markdown */
  .prose { max-inline-size: var(--measure); }
  .prose > * + * { margin-block-start: var(--space-4); }
  .prose > * + :is(h2, h3, h4) { margin-block-start: var(--space-8); }
  .prose :is(ul, ol) { padding-inline-start: var(--space-6); display: grid; gap: var(--space-2); }
  .prose ul { list-style: disc; }
  .prose ol { list-style: decimal; }
  .prose li::marker { color: var(--text-faint); }
  .prose img { border-radius: var(--r-md); }
  .prose hr { margin-block: var(--space-8); }
}

/* ==========================================================================
   Deck — 04 Layout
   Composable primitives. You lay out with about eight classes, not forty.
   ========================================================================== */

@layer deck.layout {

  /* Centres content and caps it at --container, with the responsive page
     gutter already applied. Nest one inside another and the inner one adds
     a second gutter, so use .container-full instead. */
  .container {
    inline-size: 100%;
    max-inline-size: var(--container, 76rem);
    margin-inline: auto;
    padding-inline: var(--space-gutter);
  }
  /* A single column of prose: about 70 characters at the body size,
     which is inside the maximum line length WCAG recommends. */
  .container-sm { --container: 40rem; }
  /* An article with a rail, or a form. */
  .container-md { --container: 56rem; }
  /* The same 76rem as the bare .container, named. Use it when a page
     should say which width it means rather than rely on the default. */
  .container-lg { --container: 76rem; }
  /* A wide dashboard or a data grid. */
  .container-xl { --container: 90rem; }
  /* Keeps the page gutter, drops the max width. This is what to nest
     inside another container — a second .container would apply the
     gutter twice. */
  .container-full { --container: none; }

  /* --- The spacing scale ----------------------------------------------------
     Every spacing utility in Deck uses the same seven steps:

       0    1        2       3       4      6        8
       0    .25rem   .5rem   .75rem  1rem   1.5rem   2rem

     .stack, .cluster, .gap-*, the padding utilities and the margin utilities
     all implement exactly these, so a step that exists in one family exists in
     all of them. There is deliberately no 5: at 1.25rem it is indistinguishable
     from 4 and 6 in a real layout, and a half step between two neighbours is
     the step people reach for when they are not sure, which is how .stack-5
     came to be used in the demo when no such class existed.

     The --space-* tokens keep their full range (0 through 24). Tokens are the
     palette a component composes from; the utilities are the smaller set you
     author markup with. Those are different jobs, so they are different sizes.
     ---------------------------------------------------------------------- */

  /* Vertical rhythm without margin collapse surprises */
  .stack {
    display: flex;
    flex-direction: column;
    gap: var(--gap, var(--space-4));
  }
  /* No gap. For two elements that must touch. */
  .stack-0 { --gap: 0; }
  /* A hairline. A label and the control it names, when the two read as
     one thing. */
  .stack-1 { --gap: var(--space-1); }
  /* Tight vertical rhythm, for a label and its control or a title and its
     subtitle. */
  .stack-2 { --gap: var(--space-2); }
  /* The default rhythm for related blocks inside a card or a section. */
  .stack-3 { --gap: var(--space-3); }
  /* The default, named. Same as .stack with no step class. */
  .stack-4 { --gap: var(--space-4); }
  /* Loose vertical rhythm, for separating whole sections of a page. */
  .stack-6 { --gap: var(--space-6); }
  /* Very loose, for whole sections of a page where .section is too much. */
  .stack-8 { --gap: var(--space-8); }

  /* Horizontal group that wraps instead of overflowing on a phone */
  .cluster {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--gap, var(--space-3));
  }
  /* Kept as the readable name for step 2, which is what most clusters want */
  .cluster-tight { --gap: var(--space-2); }
  /* Packs the row against the end edge, which follows the writing
     direction rather than always being the right. */
  .cluster-end { justify-content: flex-end; }
  /* Centres the row. */
  .cluster-center { justify-content: center; }
  /* First item to one end, last to the other. Beware of combining it
     with wrapping: each line is justified separately, so a lone item on
     the second line jumps to the far end and looks stranded. .bar with
     .push is the predictable version. */
  .cluster-between { justify-content: space-between; }

  /* Push the last child away from the rest */
  .bar {
    display: flex;
    align-items: center;
    gap: var(--space-3);
  }
  .bar > .push { margin-inline-start: auto; }

  /* Auto-fitting grid: no breakpoints needed, it responds to its own width */
  .grid {
    display: grid;
    gap: var(--gap, var(--space-4));
    grid-template-columns: repeat(auto-fit, minmax(min(var(--min, 17rem), 100%), 1fr));
  }
  /* Two-ish columns: an auto-fit grid with a 24rem floor. Identical in
     effect to .grid-wide, but it writes grid-template-columns directly
     rather than setting --min, so --min cannot adjust it afterwards.
     Prefer .grid-wide. */
  .grid-2 { grid-template-columns: repeat(auto-fit, minmax(min(24rem, 100%), 1fr)); }
  /* Narrows the auto-fit column floor to 12rem, so the grid packs more
     columns in before it wraps. */
  .grid-tight { --min: 12rem; }
  /* Widens the auto-fit column floor to 24rem, so the grid packs fewer,
     wider columns before it wraps. For cards with a paragraph in them. */
  .grid-wide { --min: 24rem; }
  /* Exactly two equal columns at every width, including a phone. Only
     for a count that is part of the meaning — a 2x2 comparison. It does
     not reflow, so it does not satisfy the reflow criterion on its own. */
  .grid-fixed-2 { grid-template-columns: 1fr 1fr; }
  /* Exactly three equal columns at every width. See .grid-fixed-2. */
  .grid-fixed-3 { grid-template-columns: repeat(3, 1fr); }
  /* Exactly four equal columns at every width. On a phone that is four
     70px columns; reach for .grid with a --min instead. */
  .grid-fixed-4 { grid-template-columns: repeat(4, 1fr); }

  /* Content + rail. Collapses to one column below the width it needs. */
  .split {
    display: grid;
    gap: var(--gap, var(--space-6));
    grid-template-columns: 1fr;
  }
  /* A grid item's min-inline-size is auto, so a child that cannot shrink — a
     form row, a wide table, a long code block — makes the whole column wider
     than its track and pushes the page sideways. This is the single most
     common grid bug there is; the two-column form in the demo overflowed a
     375px viewport by 16px until this line existed. */
  .split > * { min-inline-size: 0; }
  @media (min-width: 64rem) {
    .split { grid-template-columns: minmax(0, 1fr) var(--rail, 20rem); }
    /* Puts the rail on the starting edge without touching markup order, so
       the main column stays first for a screen reader and for the keyboard.
       Use it for supplementary navigation; not for a rail holding the first
       thing a reader needs. */
    .split-rail-start { grid-template-columns: var(--rail, 20rem) minmax(0, 1fr); }
  }

  /* Center a thing with a max width and no side effects */
  .center {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: var(--gap, var(--space-4));
  }

  /* Vertical page rhythm: a block of --space-section, which is a clamp that
     grows with the viewport. For spacing inside a component reach for
     .stack instead. */
  .section { padding-block: var(--space-section); }

  /* Everything stays on one line and scrolls sideways — cards, chips, dates */
  .scroller {
    display: flex;
    gap: var(--gap, var(--space-3));
    overflow-x: auto;
    overscroll-behavior-inline: contain;
    scroll-snap-type: x proximity;
    scrollbar-width: none;
    padding-block-end: var(--space-2);
    margin-inline: calc(var(--space-gutter) * -1);
    padding-inline: var(--space-gutter);
  }
  .scroller::-webkit-scrollbar { display: none; }
  .scroller > * { scroll-snap-align: start; flex: 0 0 auto; }

  /* A sticky header. Where scroll-state container queries are supported
     the blur and shadow live on an ::after that is transparent until the
     header actually sticks, so in flow it looks like part of the page.
     The treatment rides on a pseudo-element because a scroll-state
     container can style its descendants but not itself. */
  .sticky-top {
    position: sticky;
    inset-block-start: 0;
    z-index: var(--z-sticky);
    background: color-mix(in oklab, var(--bg) 82%, transparent);
    backdrop-filter: saturate(1.6) blur(12px);
    -webkit-backdrop-filter: saturate(1.6) blur(12px);
  }

  /* Sitting in flow it needs no treatment at all; it earns the blur and the
     shadow at the moment it actually sticks. A scroll-state container cannot
     style itself, so the treatment rides on ::after, which is inside it. */
  @supports (container-type: scroll-state) {
    .sticky-top {
      container-type: scroll-state;
      background: none;
      backdrop-filter: none;
      -webkit-backdrop-filter: none;
    }
    .sticky-top::after {
      content: "";
      position: absolute;
      inset: 0;
      z-index: -1;
      opacity: 0;
      background: color-mix(in oklab, var(--bg) 82%, transparent);
      backdrop-filter: saturate(1.6) blur(12px);
      -webkit-backdrop-filter: saturate(1.6) blur(12px);
      transition: opacity var(--dur-2) var(--ease-out);
    }
    @container scroll-state(stuck: block-start) {
      .sticky-top::after { opacity: 1; box-shadow: var(--shadow-2); }
    }
  }

  /* Header, content, footer in a 100dvh grid whose middle row takes the
     remaining height — the sticky footer, without the hacks. dvh rather
     than vh so it tracks a phone browser hiding its chrome. It expects
     exactly three children. */
  .app-shell {
    display: grid;
    min-block-size: 100dvh;
    grid-template-rows: auto 1fr auto;
  }

  /* Aspect boxes for media */
  .ratio-16x9 { aspect-ratio: 16 / 9; object-fit: cover; inline-size: 100%; }
  .ratio-4x3  { aspect-ratio: 4 / 3;  object-fit: cover; inline-size: 100%; }
  .ratio-1x1  { aspect-ratio: 1;      object-fit: cover; inline-size: 100%; }

  /* Container queries: components respond to their slot, not the viewport */
  .cq { container-type: inline-size; }
}

/* ==========================================================================
   Deck — 05 Buttons
   ========================================================================== */

@layer deck.components {

  .btn {
    --btn-bg: var(--surface);
    --btn-text: var(--text);
    --btn-border: var(--line-strong);
    --btn-shadow: var(--shadow-1);

    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    block-size: var(--control-h);
    min-inline-size: var(--control-h);
    padding-inline: var(--space-4);
    border: 1px solid var(--btn-border);
    border-radius: var(--r-sm);
    background: var(--btn-bg);
    color: var(--btn-text);
    box-shadow: var(--btn-shadow);
    font-size: var(--text-base);
    font-weight: 560;
    line-height: 1;
    letter-spacing: -.008em;
    white-space: nowrap;
    text-decoration: none;
    cursor: pointer;
    user-select: none;
    -webkit-tap-highlight-color: transparent;
    touch-action: manipulation;
    transition: background-color var(--dur-1) var(--ease-out),
                border-color var(--dur-1) var(--ease-out),
                color var(--dur-1) var(--ease-out),
                box-shadow var(--dur-1) var(--ease-out),
                translate var(--dur-1) var(--ease-out);
  }

  /* Every button clears the 44px touch target even when it looks smaller */
  .btn { position: relative; }
  .btn::before {
    content: "";
    position: absolute;
    inset-block-start: 50%;
    inset-inline-start: 50%;
    translate: -50% -50%;
    inline-size: 100%;
    block-size: 100%;
    min-inline-size: var(--tap);
    min-block-size: var(--tap);
  }

  .btn:hover { background: var(--surface-hover); border-color: var(--ink-400); }
  .btn:active { translate: 0 1px; box-shadow: none; }
  .btn:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

  /* Styles a link or non-form element as disabled. It cannot stop a click
     the way the disabled attribute does, so add aria-disabled="true" and
     remove the href if it must not be followed. */
  .btn[disabled], .btn[aria-disabled="true"], .btn.is-disabled {
    opacity: .5;
    cursor: not-allowed;
    pointer-events: none;
  }

  /* --- Variants ----------------------------------------------------------- */
  /* The one important action in a view. Solid brand fill; use exactly one
     per screen so it keeps meaning something. */
  .btn-primary {
    --btn-bg: var(--brand-600);
    --btn-text: var(--text-on-brand);
    --btn-border: transparent;
    --btn-shadow: var(--shadow-2), var(--shadow-inset);
  }
  .btn-primary:hover { --btn-bg: var(--brand-700); border-color: transparent; background: var(--brand-700); }

  /* A secondary emphasis colour, for upsells and highlights. */
  .btn-accent {
    --btn-bg: var(--accent-500);
    --btn-text: var(--text-on-accent);
    --btn-border: transparent;
    --btn-shadow: var(--shadow-2), var(--shadow-inset);
  }
  .btn-accent:hover { background: oklch(from var(--accent-500) calc(l - .05) c h); border-color: transparent; }

  /* Destructive and irreversible. Pair it with a confirmation. */
  .btn-danger {
    --btn-bg: var(--bad-600);
    --btn-text: var(--text-on-bad);
    --btn-border: transparent;
    --btn-shadow: var(--shadow-2), var(--shadow-inset);
  }
  .btn-danger:hover { background: var(--bad-700); border-color: transparent; }

  /* Tinted, low weight. Good for an action repeated down a list. */
  .btn-soft {
    --btn-bg: var(--brand-soft);
    --btn-text: var(--brand-soft-text);
    --btn-border: transparent;
    --btn-shadow: none;
  }
  .btn-soft:hover { background: color-mix(in oklab, var(--brand-soft) 80%, var(--brand-300)); border-color: transparent; }

  /* No border or background until hover. For toolbars and dense rows where
     seven bordered buttons would be noise. */
  .btn-ghost {
    --btn-bg: transparent;
    --btn-border: transparent;
    --btn-shadow: none;
  }
  .btn-ghost:hover { background: var(--surface-hover); border-color: transparent; }

  /* Brand-coloured border, transparent fill. */
  .btn-outline {
    --btn-bg: transparent;
    --btn-text: var(--brand);
    --btn-border: color-mix(in oklab, var(--brand) 45%, transparent);
    --btn-shadow: none;
  }
  .btn-outline:hover { background: var(--brand-soft); border-color: var(--brand); }

  /* Looks like a link, behaves like a button. Only for a real <button>
     that has to sit in a row of links — on an anchor it adds a touch
     target, a ring offset and nowrap to something that wanted none of them. */
  .btn-link {
    --btn-bg: transparent;
    --btn-border: transparent;
    --btn-shadow: none;
    --btn-text: var(--brand);
    padding-inline: var(--space-1);
    block-size: auto;
  }
  .btn-link:hover { background: none; border-color: transparent; text-decoration: underline; }

  /* --- Sizes -------------------------------------------------------------- */
  /* The 34px size. Still clears the 44px touch target, because the target
     is a pseudo-element rather than the box. */
  .btn-sm {
    block-size: var(--control-h-sm);
    padding-inline: var(--space-3);
    font-size: var(--text-sm);
    border-radius: var(--r-xs);
    gap: var(--space-1);
  }
  /* The large size, from --control-h-lg. */
  .btn-lg {
    block-size: var(--control-h-lg);
    padding-inline: var(--space-6);
    font-size: var(--text-md);
    font-weight: 600;
  }
  /* Full width. For a phone, or the primary action at the end of a form. */
  .btn-block { inline-size: 100%; }

  /* Square, icon-only button. It has no text, so it needs an aria-label. */
  .btn-icon { padding-inline: 0; inline-size: var(--control-h); }
  .btn-icon.btn-sm { inline-size: var(--control-h-sm); }
  .btn-icon.btn-lg { inline-size: var(--control-h-lg); }
  /* A fully rounded button, usually paired with .btn-icon. */
  .btn-round { border-radius: var(--r-full); }

  /* --- Group -------------------------------------------------------------- */
  /* Buttons joined into one control, with the inner radii and the doubled
     border between them removed. Logical radii, so it mirrors under RTL. */
  .btn-group { display: inline-flex; }
  .btn-group > .btn { border-radius: 0; box-shadow: none; }
  .btn-group > .btn:first-child { border-start-start-radius: var(--r-sm); border-end-start-radius: var(--r-sm); }
  .btn-group > .btn:last-child  { border-start-end-radius: var(--r-sm); border-end-end-radius: var(--r-sm); }
  .btn-group > .btn + .btn { margin-inline-start: -1px; }
  .btn-group > .btn:hover, .btn-group > .btn:focus-visible { z-index: 1; }

  /* --- Loading ------------------------------------------------------------ */
  .btn.is-loading { color: transparent; pointer-events: none; }
  .btn.is-loading::after {
    content: "";
    position: absolute;
    inline-size: 1em;
    block-size: 1em;
    border: 2px solid color-mix(in oklab, var(--btn-text) 30%, transparent);
    border-block-start-color: var(--btn-text);
    border-radius: var(--r-full);
    animation: dk-spin .62s linear infinite;
  }

  @keyframes dk-spin { to { rotate: 360deg; } }

  /* Floating action button, parked above the tab bar and the notch */
  .fab {
    position: fixed;
    inset-block-end: calc(var(--space-5) + env(safe-area-inset-bottom));
    inset-inline-end: var(--space-5);
    z-index: var(--z-nav);
    inline-size: 56px;
    block-size: 56px;
    border-radius: var(--r-full);
    background: var(--brand-600);
    color: var(--text-on-brand);
    box-shadow: var(--shadow-4);
    display: grid;
    place-items: center;
    cursor: pointer;
    transition: scale var(--dur-2) var(--ease-spring), background-color var(--dur-1) var(--ease-out);
  }
  .fab:hover { background: var(--brand-700); scale: 1.04; }
  .fab:active { scale: .96; }
  body:has(.tabbar) .fab { inset-block-end: calc(var(--space-4) + var(--tap) + var(--space-3) + env(safe-area-inset-bottom)); }
}

/* ==========================================================================
   Deck — 06 Forms
   Inputs are 16px on touch devices so iOS never zooms on focus.
   ========================================================================== */

@layer deck.components {

  /* One form control and everything attached to it: label, input, help
     text, error. The vertical gap between them comes from here, so a field
     never needs margins. */
  .field {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    min-inline-size: 0;
  }

  /* The label for a control. A flex row, so a "required" or "optional"
     marker sits beside the text without extra markup. */
  .label {
    font-size: var(--text-sm);
    font-weight: 580;
    color: var(--text);
    display: flex;
    align-items: center;
    gap: var(--space-2);
  }
  /* The marker for an optional field. Where most fields are required this
     reads better than an asterisk on nearly every label. */
  .label .optional { color: var(--text-faint); font-weight: 400; }
  /* The marker beside a required field's label. Visual only — a screen
     reader announces the required attribute, not the asterisk. */
  .label .required { color: var(--bad-500); }

  /* A hint below a control. Needs aria-describedby on the control to reach
     a screen reader — sitting next to it is not an association. */
  .help { font-size: var(--text-sm); color: var(--text-muted); }
  /* A validation message below a control. Lightened for dark mode with
     light-dark(), and aligned to flex-start so an icon stays on the first
     line of a two-line message. */
  .error {
    font-size: var(--text-sm);
    color: light-dark(var(--bad-700), oklch(76% .13 var(--hue-bad)));
    display: flex;
    gap: var(--space-2);
    align-items: flex-start;
  }

  .input, .textarea, .select {
    inline-size: 100%;
    min-block-size: var(--control-h);
    padding: var(--space-2) var(--space-3);
    background: var(--surface);
    color: var(--text);
    border: 1px solid var(--line-strong);
    border-radius: var(--r-sm);
    box-shadow: var(--shadow-1);
    font-size: var(--text-base);
    line-height: 1.45;
    transition: border-color var(--dur-1) var(--ease-out),
                box-shadow var(--dur-1) var(--ease-out),
                background-color var(--dur-1) var(--ease-out);
  }

  @media (pointer: coarse) {
    .input, .textarea, .select { font-size: 16px; }
  }

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

  .input:hover, .textarea:hover, .select:hover { border-color: var(--ink-400); }

  .input:focus, .textarea:focus, .select:focus {
    outline: none;
    border-color: var(--focus);
    box-shadow: var(--ring);
  }

  .input[disabled], .textarea[disabled], .select[disabled] {
    background: var(--bg-sunken);
    color: var(--text-faint);
    cursor: not-allowed;
    box-shadow: none;
  }
  .input[readonly] { background: var(--surface-2); }

  /* Grows with its content instead of scrolling in a 4-row box */
  .textarea {
    min-block-size: calc(var(--control-h) * 2);
    resize: vertical;
    field-sizing: content;
    max-block-size: 22rem;
  }

  .select {
    appearance: none;
    padding-inline-end: var(--space-10);
    background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                      linear-gradient(135deg, currentColor 50%, transparent 50%);
    background-position: calc(100% - 19px) calc(50% + 1px), calc(100% - 14px) calc(50% + 1px);
    background-size: 5px 5px, 5px 5px;
    background-repeat: no-repeat;
    cursor: pointer;
  }

  /* Validation only after the user has actually interacted */
  /* Marks a field invalid from your own validation, producing the same
     border and ring as :user-invalid without needing the browser to agree.
     One of the few states an author writes by hand. */
  .input:user-invalid, .textarea:user-invalid, .select:user-invalid,
  .field.is-invalid :is(.input, .textarea, .select) {
    border-color: var(--bad-500);
  }
  .input:user-invalid:focus, .field.is-invalid :is(.input, .textarea, .select):focus {
    box-shadow: 0 0 0 3px color-mix(in oklab, var(--bad-500) 26%, transparent);
  }

  /* --- Input group: prefix / suffix / attached button --------------------- */
  /* A prefix, suffix or attached button joined to a control. The group
     takes over the border, radius and shadow, and the input inside strips
     its own, so the seam does not double up. :focus-within puts the ring
     around the whole assembly. */
  .input-group {
    display: flex;
    align-items: stretch;
    inline-size: 100%;
    border: 1px solid var(--line-strong);
    border-radius: var(--r-sm);
    background: var(--surface);
    box-shadow: var(--shadow-1);
    overflow: hidden;
  }
  .input-group:focus-within { border-color: var(--focus); box-shadow: var(--ring); }
  .input-group > .input {
    border: 0;
    border-radius: 0;
    box-shadow: none;
    background: transparent;
  }
  .input-group > .input:focus { box-shadow: none; }
  /* A prefix or suffix attached to a control inside .input-group. Its
     divider is border-inline-end normally and border-inline-start on
     :last-child, so a leading and a trailing addon each get their rule on
     the correct side in both writing directions. */
  .input-group > .addon {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding-inline: var(--space-3);
    background: var(--surface-2);
    color: var(--text-muted);
    font-size: var(--text-sm);
    white-space: nowrap;
    border-inline-end: 1px solid var(--line);
  }
  .input-group > .addon:last-child { border-inline-start: 1px solid var(--line); border-inline-end: 0; }
  .input-group > .btn { border-radius: 0; border-block: 0; border-inline-end: 0; box-shadow: none; }

  /* --- Search field ------------------------------------------------------- */
  /* A control with an icon inside its starting edge. The icon is
     pointer-events: none, so clicking it focuses the field underneath
     rather than doing nothing. */
  .search { position: relative; display: flex; align-items: center; }
  .search > .icon {
    position: absolute;
    inset-inline-start: var(--space-3);
    color: var(--text-faint);
    pointer-events: none;
  }
  .search > .input { padding-inline-start: var(--space-10); border-radius: var(--r-full); }

  /* --- Checkbox and radio ------------------------------------------------- */
  /* A checkbox or radio and its text, laid out as a row that clears the
     44px touch target. Wrap the text in .check-text to add a second line. */
  .check {
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
    min-block-size: var(--tap);
    padding-block: var(--space-2);
    cursor: pointer;
    line-height: 1.4;
  }
  .check input[type="checkbox"], .check input[type="radio"] {
    appearance: none;
    flex: 0 0 auto;
    inline-size: 20px;
    block-size: 20px;
    margin-block-start: 1px;
    border: 1.5px solid var(--line-strong);
    background: var(--surface);
    border-radius: var(--r-xs);
    display: grid;
    place-content: center;
    cursor: pointer;
    transition: background-color var(--dur-1) var(--ease-out),
                border-color var(--dur-1) var(--ease-out);
  }
  .check input[type="radio"] { border-radius: var(--r-full); }
  .check input:hover { border-color: var(--brand-400); }
  .check input:checked { background: var(--brand-600); border-color: var(--brand-600); }
  .check input[type="checkbox"]::before {
    content: "";
    inline-size: 11px;
    block-size: 11px;
    scale: 0;
    transition: scale var(--dur-2) var(--ease-spring);
    box-shadow: inset 1em 1em var(--text-on-brand);
    clip-path: polygon(14% 44%, 0 65%, 41% 100%, 100% 16%, 84% 0, 39% 68%);
  }
  .check input[type="radio"]::before {
    content: "";
    inline-size: 9px;
    block-size: 9px;
    border-radius: var(--r-full);
    background: var(--text-on-brand);
    scale: 0;
    transition: scale var(--dur-2) var(--ease-spring);
  }
  .check input:checked::before { scale: 1; }
  .check input:indeterminate { background: var(--brand-600); border-color: var(--brand-600); }
  .check input:indeterminate::before {
    scale: 1;
    clip-path: polygon(10% 42%, 90% 42%, 90% 58%, 10% 58%);
  }
  .check input:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
  /* The text beside a checkbox or radio. A column, so a .check-note can sit
     under the label as a second line. */
  /* The text beside a checkbox, as a column, so a .check-note can sit
     beneath the label as a second line. */
  .check .check-text { display: flex; flex-direction: column; gap: 2px; }
  /* A muted second line inside .check-text. It is inside the label, so it
     becomes part of the accessible name — move it out and use
     aria-describedby if that name gets too long. */
  .check .check-note { font-size: var(--text-sm); color: var(--text-muted); }

  /* Card-style choice, good for pricing and plan pickers */
  .check-card {
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    padding: var(--space-4);
    background: var(--surface);
    transition: border-color var(--dur-1) var(--ease-out), background-color var(--dur-1) var(--ease-out);
  }
  .check-card:has(input:checked) {
    border-color: var(--brand-500);
    background: var(--brand-soft);
  }

  /* --- Switch ------------------------------------------------------------- */
  /* An on/off control that takes effect immediately, drawn over a real
     checkbox. If the reader has to press Save afterwards, use .check
     instead. Add role="switch" when "on"/"off" reads better than
     "checked"/"unchecked". */
  .switch { display: flex; align-items: center; gap: var(--space-3); min-block-size: var(--tap); cursor: pointer; }
  .switch input {
    appearance: none;
    flex: 0 0 auto;
    inline-size: 46px;
    block-size: 28px;
    border-radius: var(--r-full);
    background: var(--ink-300);
    position: relative;
    cursor: pointer;
    transition: background-color var(--dur-2) var(--ease-out);
  }
  .switch input::after {
    content: "";
    position: absolute;
    inset-block-start: 3px;
    inset-inline-start: 3px;
    inline-size: 22px;
    block-size: 22px;
    border-radius: var(--r-full);
    background: var(--ink-0);
    box-shadow: var(--shadow-2);
    transition: translate var(--dur-2) var(--ease-spring);
  }
  .switch input:checked { background: var(--brand-600); }
  .switch input:checked::after { translate: 18px 0; }
  .switch input:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

  /* --- Range -------------------------------------------------------------- */
  /* A slider. The track and thumb are vendor pseudo-elements with
     different names and different box models per engine, so every
     declaration is written twice — WebKit needs a negative block-start
     margin to centre the thumb and Firefox does not. */
  .range {
    inline-size: 100%;
    appearance: none;
    background: transparent;
    block-size: var(--tap);
  }
  .range::-webkit-slider-runnable-track {
    block-size: 6px;
    border-radius: var(--r-full);
    background: var(--ink-200);
  }
  .range::-moz-range-track {
    block-size: 6px;
    border-radius: var(--r-full);
    background: var(--ink-200);
  }
  .range::-webkit-slider-thumb {
    appearance: none;
    margin-block-start: -8px;
    inline-size: 22px;
    block-size: 22px;
    border-radius: var(--r-full);
    background: var(--brand-600);
    border: 3px solid var(--ink-0);
    box-shadow: var(--shadow-2);
    cursor: grab;
  }
  .range::-moz-range-thumb {
    inline-size: 22px;
    block-size: 22px;
    border-radius: var(--r-full);
    background: var(--brand-600);
    border: 3px solid var(--ink-0);
    box-shadow: var(--shadow-2);
    cursor: grab;
  }
  .range:active::-webkit-slider-thumb { cursor: grabbing; scale: 1.08; }

  /* --- File --------------------------------------------------------------- */
  /* A dashed drop zone wrapping a real file input. The input is 1px and
     transparent rather than display: none, so it keeps its place in the
     tab order, and :focus-within is what makes that focus visible. Deck
     styles the zone; dropping needs a handler of your own. */
  .file {
    display: grid;
    place-items: center;
    gap: var(--space-2);
    padding: var(--space-6);
    border: 1.5px dashed var(--line-strong);
    border-radius: var(--r-md);
    background: var(--surface-2);
    text-align: center;
    color: var(--text-muted);
    cursor: pointer;
    transition: border-color var(--dur-1) var(--ease-out), background-color var(--dur-1) var(--ease-out);
  }
  .file:hover, .file:focus-within { border-color: var(--brand-500); background: var(--brand-soft); }
  .file input[type="file"] { position: absolute; opacity: 0; inline-size: 1px; block-size: 1px; }

  /* --- Fieldset ----------------------------------------------------------- */
  /* A bordered group of controls that answer one question. Its legend is
     the group's accessible name, which is what a radio group needs and
     what a .label above one does not provide. */
  .fieldset {
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    padding: var(--space-5);
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    min-inline-size: 0;
  }
  .fieldset > legend {
    padding-inline: var(--space-2);
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--text-muted);
  }

  /* Two fields side by side on anything wider than a phone */
  .field-row {
    display: grid;
    gap: var(--space-4);
    grid-template-columns: 1fr;
  }
  @media (min-width: 40rem) {
    .field-row { grid-template-columns: repeat(auto-fit, minmax(min(14rem, 100%), 1fr)); }
  }

  /* Sticky save bar for long forms on mobile */
  .form-actions {
    display: flex;
    gap: var(--space-3);
    flex-wrap: wrap;
  }
  /* Pins the action row to the bottom of the viewport with a blur behind
     it and a safe-area inset for the home indicator. Add matching bottom
     padding to the form, or the last field sits underneath it. */
  .form-actions-sticky {
    position: sticky;
    inset-block-end: 0;
    z-index: var(--z-sticky);
    background: color-mix(in oklab, var(--bg) 88%, transparent);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    border-block-start: 1px solid var(--line);
    padding-block: var(--space-3);
    padding-block-end: calc(var(--space-3) + env(safe-area-inset-bottom));
  }
  .form-actions-sticky > .btn { flex: 1 1 auto; }
  @media (min-width: 48rem) {
    .form-actions-sticky > .btn { flex: 0 0 auto; }
  }
}

/* ==========================================================================
   Deck — 07 Components
   ========================================================================== */

@layer deck.components {

  /* --- Card --------------------------------------------------------------- */
  /* A surface with a border, radius and shadow. It brings no padding of its
     own — put .card-body inside so the header, body and footer can each own
     their spacing. */
  .card {
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    box-shadow: var(--shadow-1);
    display: flex;
    flex-direction: column;
    min-inline-size: 0;
    overflow: clip;
  }
  /* The bottom region. margin-block-start: auto pins it to the bottom of
     the card, so a row of cards of unequal length still has its footers
     level without anything being measured. */
  /* The top region of a card. A flex row, so a title and a control sit
     on one line; the divider below it belongs to the header, so a card
     with no header has no stray line. */
  .card-header, .card-footer {
    padding: var(--space-4) var(--space-5);
    display: flex;
    align-items: center;
    gap: var(--space-3);
  }
  .card-header { border-block-end: 1px solid var(--line); }
  .card-footer { border-block-start: 1px solid var(--line); background: var(--surface-2); margin-block-start: auto; }
  /* The padded region of a card. A column with a gap, so children stack
     without margins. */
  .card-body { padding: var(--space-5); display: flex; flex-direction: column; gap: var(--space-3); }
  /* The heading inside a card. Sized independently of h1-h6 so the same
     card works at any heading level. */
  .card-title { font-size: var(--text-md); font-weight: 620; letter-spacing: -.012em; }
  /* An image region that reaches the card's edges. The card's own
     overflow: clip does the rounding, so the image needs no radius.
     Put it beside .card-body, never inside it. */
  .card-media { margin: 0; }
  .card-media img { inline-size: 100%; object-fit: cover; }

  /* Whole card is the link, but nested links still work */
  .card-link { position: relative; transition: box-shadow var(--dur-2) var(--ease-out), border-color var(--dur-2) var(--ease-out); }
  .card-link:hover { box-shadow: var(--shadow-3); border-color: var(--line-strong); }
  .card-link .stretch::after { content: ""; position: absolute; inset: 0; }

  /* A card with no border, shadow or background. Grouping without a box
     drawn around it. */
  .card-flush { border: 0; box-shadow: none; background: none; }
  /* A deeper shadow and no visible border, for a surface that should read
     as floating above the page. */
  .card-raised { box-shadow: var(--shadow-3); border-color: transparent; }
  /* Brand-tinted fill and border, for the one card on a page that is an
     offer rather than a record. */
  .card-brand { border-color: var(--brand-200); background: var(--brand-soft); }

  /* --- Panel: a card that goes edge to edge on a phone --------------------- */
  .panel {
    background: var(--surface);
    border-block: 1px solid var(--line);
    padding: var(--space-5) var(--space-gutter);
    margin-inline: calc(var(--space-gutter) * -1);
  }
  @media (min-width: 40rem) {
    .panel {
      border: 1px solid var(--line);
      border-radius: var(--r-md);
      margin-inline: 0;
      padding-inline: var(--space-5);
    }
  }

  /* --- Badge and chip ----------------------------------------------------- */
  /* A small inline status pill. Sized in em, so it tracks whatever text it
     sits next to rather than needing a size variant per context. */
  .badge {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    padding: .18em .55em;
    border-radius: var(--r-xs);
    background: var(--surface-2);
    color: var(--text-muted);
    border: 1px solid var(--line);
    font-size: var(--text-xs);
    font-weight: 580;
    line-height: 1.45;
    white-space: nowrap;
  }
  /* Brand tint. For a label that names a category rather than a status. */
  .badge-brand { background: var(--brand-soft); color: var(--brand-soft-text); border-color: transparent; }
  /* Paid, shipped, passing, active. */
  .badge-good  { background: var(--good-100); color: var(--good-700); border-color: transparent; }
  /* Pending, expiring: needs attention, nothing is broken. */
  .badge-warn  { background: var(--warn-100); color: var(--warn-700); border-color: transparent; }
  /* Failed, overdue, rejected. */
  .badge-bad   { background: var(--bad-100);  color: var(--bad-700);  border-color: transparent; }
  /* Filled with the brand colour. The highest-emphasis badge; use one per
     view or it stops meaning anything. */
  .badge-solid { background: var(--brand-600); color: var(--text-on-brand); border-color: transparent; }
  /* Adds a leading dot in currentColor, so it takes the tone's text colour
     without a second class. The dot is decoration — never ship one with no
     text beside it, because colour alone is not a status. */
  .badge-dot::before {
    content: "";
    inline-size: 6px; block-size: 6px;
    border-radius: var(--r-full);
    background: currentColor;
  }

  .chip {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    min-block-size: 34px;
    padding-inline: var(--space-3);
    border-radius: var(--r-full);
    border: 1px solid var(--line-strong);
    background: var(--surface);
    font-size: var(--text-sm);
    font-weight: 520;
    cursor: pointer;
    white-space: nowrap;
    transition: background-color var(--dur-1) var(--ease-out), border-color var(--dur-1) var(--ease-out);
  }
  .chip:hover { background: var(--surface-hover); }
  .chip[aria-pressed="true"], .chip.is-active {
    background: var(--brand-600);
    border-color: var(--brand-600);
    color: var(--text-on-brand);
  }
  .chip-remove { display: grid; place-items: center; opacity: .6; }
  .chip-remove:hover { opacity: 1; }

  /* --- Alert -------------------------------------------------------------- */
  /* A block of prose the reader is meant to stop and read. The tone is a
     three-pixel border-inline-start, so it moves to the right edge under
     dir="rtl" with no second rule. Deck sets no ARIA role: role="alert" on
     a message that is present at page load interrupts a screen reader
     before it has read anything. */
  .alert {
    display: flex;
    gap: var(--space-3);
    padding: var(--space-4);
    border-radius: var(--r-md);
    border: 1px solid var(--line);
    background: var(--surface-2);
    border-inline-start: 3px solid var(--ink-400);
  }
  /* The first line of an alert. A weight change, not a heading — if it
     belongs in the document outline, put this class on a real h2-h4. */
  .alert-title { font-weight: 620; margin-block-end: 2px; }
  /* The explanatory line. min-inline-size: 0 so a long unbroken string
     cannot push the alert wider than its container. */
  .alert-body { font-size: var(--text-sm); color: var(--text-muted); min-inline-size: 0; }
  .alert > .icon { flex: 0 0 auto; margin-block-start: 1px; }
  /* Something the reader should know before continuing. */
  .alert-info { background: var(--brand-soft); border-color: transparent; border-inline-start-color: var(--brand-500); color: var(--brand-soft-text); }
  /* Confirmation that something worked. */
  .alert-good { background: var(--good-100); border-color: transparent; border-inline-start-color: var(--good-500); color: var(--good-700); }
  /* A consequence the reader should understand first. */
  .alert-warn { background: var(--warn-100); border-color: transparent; border-inline-start-color: var(--warn-500); color: var(--warn-700); }
  /* Something failed, or is about to. */
  .alert-bad  { background: var(--bad-100);  border-color: transparent; border-inline-start-color: var(--bad-500);  color: var(--bad-700); }
  .alert :is(.alert-body, p) { color: inherit; opacity: .9; }

  /* --- Avatar ------------------------------------------------------------- */
  /* A round box holding an image or initials. --size drives the width, the
     height and the font size together, so a size variant is one
     declaration and the initials can never overflow the circle. */
  .avatar {
    --size: 40px;
    inline-size: var(--size);
    block-size: var(--size);
    border-radius: var(--r-full);
    background: var(--brand-soft);
    color: var(--brand-soft-text);
    display: grid;
    place-items: center;
    font-size: calc(var(--size) * .38);
    font-weight: 620;
    overflow: hidden;
    flex: 0 0 auto;
    user-select: none;
  }
  .avatar img { inline-size: 100%; block-size: 100%; object-fit: cover; }
  /* 24px. */
  .avatar-xs { --size: 24px; }
  /* 32px. */
  .avatar-sm { --size: 32px; }
  /* 56px. */
  .avatar-lg { --size: 56px; }
  /* 80px. */
  .avatar-xl { --size: 80px; }
  /* A square avatar, for anything that is not a person — an organisation,
     a project, a repository. */
  .avatar-square { border-radius: var(--r-sm); }

  /* Overlapping avatars. The offset is a fraction of --size, so it stays
     proportional at every size, and the ring is the surface colour, which
     is wrong on any other background. */
  .avatar-stack { display: flex; }
  .avatar-stack > .avatar + .avatar { margin-inline-start: calc(var(--size, 40px) * -.32); }
  .avatar-stack > .avatar { box-shadow: 0 0 0 2px var(--surface); }

  .presence { position: relative; }
  .presence::after {
    content: "";
    position: absolute;
    inset-block-end: 0;
    inset-inline-end: 0;
    inline-size: 30%;
    block-size: 30%;
    border-radius: var(--r-full);
    background: var(--good-500);
    box-shadow: 0 0 0 2px var(--surface);
  }
  .presence-away::after { background: var(--warn-500); }
  .presence-off::after { background: var(--ink-400); }

  /* --- Table -------------------------------------------------------------- */
  /* The scroll container for a table, and not optional. It carries the
     border, radius, background and overflow-x, without which a wide table
     pushes the whole page sideways. Add tabindex="0" when the table
     overflows, or a keyboard user cannot scroll it. */
  .table-wrap {
    inline-size: 100%;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    background: var(--surface);
  }
  /* A real table, kept a table at every width, because values in a column
     are only comparable while the columns exist. */
  .table { inline-size: 100%; font-size: var(--text-sm); }
  .table :is(th, td) {
    padding: var(--space-3) var(--space-4);
    text-align: start;
    vertical-align: middle;
  }
  .table thead th {
    background: var(--surface-2);
    font-weight: 600;
    color: var(--text-muted);
    font-size: var(--text-xs);
    letter-spacing: .01em;
    white-space: nowrap;
    position: sticky;
    inset-block-start: 0;
    z-index: 1;
  }
  .table tbody tr + tr { border-block-start: 1px solid var(--line); }
  .table tbody tr:hover { background: var(--surface-hover); }
  /* A numeric table cell: aligned to the end edge with tabular figures, so
     digits line up down the column. */
  .table .num { text-align: end; font-variant-numeric: tabular-nums; }
  /* Tighter cell padding, for a table the reader scans rather than reads. */
  .table-compact :is(th, td) { padding: var(--space-2) var(--space-3); }

  /* Below 40rem the table restacks into labelled rows.
     Put the column name in data-label on each cell. */
  @media (max-width: 39.999rem) {
    .table-stack thead { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); }
    /* Below 40rem, restacks each row into labelled lines, taking the label
       from data-label on the cell. Every cell needs one. The thead is
       clipped rather than removed, so header association survives; print
       undoes the whole thing, because paper is never narrow. */
    .table-stack, .table-stack tbody, .table-stack tr, .table-stack td { display: block; inline-size: 100%; }
    .table-stack tr {
      border: 1px solid var(--line);
      border-radius: var(--r-sm);
      padding: var(--space-2);
      margin-block-end: var(--space-3);
    }
    .table-stack tbody tr + tr { border-block-start: 1px solid var(--line); }
    .table-stack td {
      display: flex;
      justify-content: space-between;
      gap: var(--space-4);
      padding: var(--space-2) var(--space-3);
      text-align: end;
    }
    .table-stack td::before {
      content: attr(data-label);
      font-weight: 580;
      color: var(--text-muted);
      text-align: start;
    }
  }

  /* --- List rows ---------------------------------------------------------- */
  /* A bordered, clipped column of rows. Use it when the reader reads one
     row at a time; if they compare values down a column, that is a table. */
  .list {
    display: flex;
    flex-direction: column;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    overflow: clip;
  }
  /* One row. min-block-size: var(--tap) puts every row at the touch
     target, and the colour and text-decoration resets mean the row can be
     an <a> directly — one element, one tab stop, the whole row clickable. */
  .list-row {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    min-block-size: var(--tap);
    color: inherit;
    text-decoration: none;
    transition: background-color var(--dur-1) var(--ease-out);
  }
  .list-row + .list-row { border-block-start: 1px solid var(--line); }
  .list-row:hover { background: var(--surface-hover); }
  /* The text column of a row. min-inline-size: 0 is the important half: a
     flex item's minimum is its content by default, so without it a long
     value pushes the trailing content off the end of the row. */
  .list-row .list-main { flex: 1 1 auto; min-inline-size: 0; display: flex; flex-direction: column; gap: 1px; }
  /* The first line of a row. */
  .list-row .list-title { font-weight: 560; }
  /* The second, muted line of a row. */
  .list-row .list-sub { font-size: var(--text-sm); color: var(--text-muted); }
  /* What sits at the end of a row — a time, a count, a chevron. Faint and
     never shrinking. Below 4.5:1 by design, so nothing load-bearing
     belongs here. */
  .list-row .list-trail { color: var(--text-faint); flex: 0 0 auto; font-size: var(--text-sm); }
  /* A group heading inside a list. Styling only: a screen reader hears
     ordinary text, so if the grouping is meaningful use a real heading. */
  .list-header {
    padding: var(--space-2) var(--space-4);
    background: var(--surface-2);
    font-size: var(--text-xs);
    font-weight: 620;
    color: var(--text-muted);
    position: sticky;
    inset-block-start: 0;
    z-index: 1;
  }

  /* --- Tabs --------------------------------------------------------------- */
  /* The strip. Scrolls sideways rather than wrapping, because tabs that wrap
     stop reading as a strip. Deck styles the tabs and ships no keyboard
     behaviour: if you use role="tablist" you must add the roving tabindex and
     the arrow keys yourself, or the role promises what the page does not do. */
  .tabs {
    display: flex;
    gap: var(--space-1);
    border-block-end: 1px solid var(--line);
    overflow-x: auto;
    scrollbar-width: none;
  }
  .tabs::-webkit-scrollbar { display: none; }
  /* One trigger. Styled from [aria-selected="true"], so the visual state and
     the accessible state are the same attribute and cannot disagree. The
     unselected state carries the same transparent border, so selecting one
     changes a colour and never moves anything. */
  .tab {
    padding: var(--space-3) var(--space-4);
    min-block-size: var(--tap);
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    color: var(--text-muted);
    font-weight: 560;
    font-size: var(--text-sm);
    white-space: nowrap;
    cursor: pointer;
    border-block-end: 2px solid transparent;
    margin-block-end: -1px;
    text-decoration: none;
    transition: color var(--dur-1) var(--ease-out), border-color var(--dur-1) var(--ease-out);
  }
  .tab:hover { color: var(--text); }
  .tab[aria-selected="true"], .tab.is-active {
    color: var(--brand);
    border-block-end-color: var(--brand);
  }

  /* --- Segmented control (iOS-style, good on a phone) --------------------- */
  /* A one-of-N control: equal-width children in a sunken track with the
     selected one raised. Styled from [aria-selected="true"] or .is-active.
     Built from radio inputs it needs no JavaScript and gets arrow keys free —
     but Deck does not style :has(:checked), so that version needs one rule of
     your own. */
  .segmented {
    display: inline-flex;
    padding: 3px;
    gap: 2px;
    background: var(--bg-sunken);
    border-radius: var(--r-sm);
    border: 1px solid var(--line);
    inline-size: 100%;
  }
  .segmented > * {
    flex: 1 1 0;
    min-block-size: 36px;
    padding-inline: var(--space-3);
    border-radius: calc(var(--r-sm) - 2px);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    font-size: var(--text-sm);
    font-weight: 560;
    color: var(--text-muted);
    cursor: pointer;
    white-space: nowrap;
    transition: background-color var(--dur-2) var(--ease-out), color var(--dur-2) var(--ease-out);
  }
  .segmented > [aria-selected="true"], .segmented > .is-active {
    background: var(--surface);
    color: var(--text);
    box-shadow: var(--shadow-1);
  }

  /* --- Accordion (native details, animates open) -------------------------- */
  /* A bordered container for native <details>. The open state, the keyboard
     and the accessible name are the element's. Deck restyles the summary,
     replaces the marker with a chevron, and animates the height using
     ::details-content plus the interpolate-size opt-in in 02-reset.css — the
     first time a disclosure could animate to auto height without JavaScript.
     Give the details elements a shared name attribute for one-at-a-time. */
  .accordion { border: 1px solid var(--line); border-radius: var(--r-md); overflow: clip; background: var(--surface); }
  .accordion > details + details { border-block-start: 1px solid var(--line); }
  .accordion summary, .disclosure summary {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-4);
    min-block-size: var(--tap);
    font-weight: 560;
    cursor: pointer;
    list-style: none;
    transition: background-color var(--dur-1) var(--ease-out);
  }
  .accordion summary::-webkit-details-marker, .disclosure summary::-webkit-details-marker { display: none; }
  .accordion summary:hover, .disclosure summary:hover { background: var(--surface-hover); }
  .accordion summary::after, .disclosure summary::after {
    content: "";
    margin-inline-start: auto;
    inline-size: 8px;
    block-size: 8px;
    border-inline-end: 2px solid var(--text-faint);
    border-block-end: 2px solid var(--text-faint);
    rotate: 45deg;
    translate: 0 -2px;
    transition: rotate var(--dur-2) var(--ease-out);
  }
  .accordion details[open] summary::after, .disclosure[open] summary::after { rotate: 225deg; translate: 0 2px; }
  /* The padded region below a summary. Indented to line up with the summary
     text rather than the chevron. */
  .accordion .accordion-body, .disclosure .disclosure-body {
    padding: 0 var(--space-4) var(--space-4);
    color: var(--text-muted);
    font-size: var(--text-sm);
  }
  .accordion details::details-content, .disclosure::details-content {
    block-size: 0;
    overflow: hidden;
    transition: block-size var(--dur-3) var(--ease-out), content-visibility var(--dur-3) allow-discrete;
  }
  .accordion details[open]::details-content, .disclosure[open]::details-content { block-size: auto; }

  /* --- Breadcrumb --------------------------------------------------------- */
  .breadcrumb { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); font-size: var(--text-sm); }
  .breadcrumb a { color: var(--text-muted); text-decoration: none; }
  .breadcrumb a:hover { color: var(--brand); }
  .breadcrumb li + li::before { content: "/"; color: var(--text-faint); margin-inline-end: var(--space-2); }
  .breadcrumb li { display: flex; align-items: center; }
  .breadcrumb [aria-current] { color: var(--text); font-weight: 560; }

  /* --- Pagination --------------------------------------------------------- */
  .pagination { display: flex; gap: var(--space-1); align-items: center; flex-wrap: wrap; }
  .pagination > * {
    min-inline-size: var(--control-h-sm);
    min-block-size: var(--control-h-sm);
    padding-inline: var(--space-2);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--r-xs);
    color: var(--text-muted);
    font-size: var(--text-sm);
    font-variant-numeric: tabular-nums;
    text-decoration: none;
  }
  .pagination > a:hover { background: var(--surface-hover); color: var(--text); }
  .pagination > [aria-current="page"] { background: var(--brand-600); color: var(--text-on-brand); font-weight: 600; }

  /* --- Progress, ring, spinner, skeleton ---------------------------------- */

  /* Three ways to say "something is happening", and the choice is only ever
     about one thing: whether you know how much is left. .progress and .ring
     do, .spinner does not. There is no standalone meter -- a bar drawn from a
     value rather than from an element is .chart-meter, in 14-charts.css.

     .progress goes on a real <progress>, so the element carries the value, the
     maximum and the role. The two vendor pseudo-elements have to be written as
     separate rules: a vendor pseudo-element in a shared selector invalidates
     the whole rule for the other engine. Same constraint as .range. */
  .progress {
    inline-size: 100%;
    block-size: 8px;
    border-radius: var(--r-full);
    background: var(--ink-200);
    overflow: hidden;
    appearance: none;
    border: 0;
  }
  .progress::-webkit-progress-bar { background: var(--ink-200); }
  .progress::-webkit-progress-value { background: var(--brand-600); border-radius: var(--r-full); transition: inline-size var(--dur-3) var(--ease-out); }
  .progress::-moz-progress-bar { background: var(--brand-600); border-radius: var(--r-full); }

  /* A conic gradient for the fill, a radial mask for the hole. Three custom
     properties shape it: --value, --size, --thickness.

     The mask is the thing to understand. It applies to this element AND its
     whole subtree, so a label centred inside a .ring is punched out along with
     the middle -- it renders, it takes up space, it is in the accessibility
     tree, and it is invisible. That is what .ring-wrap and .ring-label below
     exist for: an unmasked wrapper that stacks the label over the ring as a
     sibling rather than a child.

     FINDINGS 60: place-items and the three font declarations here are the
     fossil of the approach that does not work. They style a child nobody can
     see -- .ring-label repeats all three on the sibling, which is the tell.
     Due for removal; kept for now because the API is frozen. */
  .ring {
    --value: 0;
    --thickness: 6px;
    --size: 56px;
    inline-size: var(--size);
    block-size: var(--size);
    display: grid;
    place-items: center;
    border-radius: var(--r-full);
    font-size: var(--text-xs);
    font-weight: 620;
    font-variant-numeric: tabular-nums;
    background: conic-gradient(var(--brand-600) calc(var(--value) * 1%), var(--ink-200) 0);
    mask: radial-gradient(farthest-side, transparent calc(100% - var(--thickness)), #000 calc(100% - var(--thickness) + 1px));
    -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - var(--thickness)), #000 calc(100% - var(--thickness) + 1px));
  }
  .ring-wrap { position: relative; display: inline-grid; place-items: center; }
  .ring-wrap > .ring-label { position: absolute; font-size: var(--text-xs); font-weight: 620; font-variant-numeric: tabular-nums; }

  /* Exempt from the reduced-motion reset in 02-reset.css: instead of freezing
     it slows to 2.4s. A frozen spinner reads as a hung page, which is worse
     for everyone than a slow one. Note border-block-start-color rather than
     border-top-color -- the lit segment starts at the block start in any
     writing mode. */
  .spinner {
    inline-size: 20px;
    block-size: 20px;
    border: 2px solid var(--ink-200);
    border-block-start-color: var(--brand-600);
    border-radius: var(--r-full);
    animation: dk-spin .62s linear infinite;
  }
  .spinner-lg { inline-size: 34px; block-size: 34px; border-width: 3px; }

  .skeleton {
    background: linear-gradient(100deg,
      var(--ink-100) 30%, var(--ink-200) 48%, var(--ink-100) 66%);
    background-size: 300% 100%;
    animation: dk-shimmer 1.4s var(--ease-in-out) infinite;
    border-radius: var(--r-xs);
    block-size: 1em;
    color: transparent;
    user-select: none;
  }
  .skeleton-text { block-size: .8em; margin-block: .35em; }
  .skeleton-circle { border-radius: var(--r-full); }
  @keyframes dk-shimmer { to { background-position: -300% 0; } }

  /* --- Tooltip ------------------------------------------------------------ */
  /* A CSS-only tooltip: the text comes from data-tip through ::after, so there
     is no second element. It cannot flip near a window edge, cannot escape an
     overflow:hidden ancestor, and is hidden entirely on a coarse pointer. Never
     put anything essential in it, and pair it with aria-label — generated
     content is not a reliable accessible name. */
  .tooltip { position: relative; }
  .tooltip::after {
    content: attr(data-tip);
    position: absolute;
    inset-block-end: calc(100% + 8px);
    inset-inline-start: 50%;
    translate: -50% 4px;
    background: var(--ink-900);
    color: var(--ink-50);
    font-size: var(--text-xs);
    font-weight: 500;
    line-height: 1.4;
    padding: var(--space-2) var(--space-3);
    border-radius: var(--r-xs);
    inline-size: max-content;
    max-inline-size: 16rem;
    box-shadow: var(--shadow-3);
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--dur-2) var(--ease-out), translate var(--dur-2) var(--ease-out);
    z-index: var(--z-toast);
  }
  .tooltip:is(:hover, :focus-visible)::after { opacity: 1; translate: -50% 0; }
  @media (pointer: coarse) { .tooltip::after { display: none; } }

  /* --- Menu / dropdown, built on the native popover ----------------------- */
  /* A dropdown built on the native popover attribute: light dismiss, Escape,
     the top layer and one-at-a-time behaviour all come from the platform.
     25-anchor.css positions it against its trigger with anchor positioning and
     a fallback chain, so it flips when there is no room below. */
  .menu {
    position: absolute;
    margin: 0;
    padding: var(--space-1);
    inline-size: max-content;
    min-inline-size: 12rem;
    max-inline-size: min(20rem, calc(100vw - var(--space-8)));
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    box-shadow: var(--shadow-4);
    inset: auto;
    opacity: 0;
    translate: 0 -6px;
    transition: opacity var(--dur-2) var(--ease-out),
                translate var(--dur-2) var(--ease-out),
                overlay var(--dur-2) allow-discrete,
                display var(--dur-2) allow-discrete;
  }
  .menu:popover-open { opacity: 1; translate: 0; }
  @starting-style { .menu:popover-open { opacity: 0; translate: 0 -6px; } }
  .menu::backdrop { background: transparent; }

  /* One row in a menu. Works on a button or an anchor — colour and
     text-decoration are reset so a link needs no help. Hover and focus share a
     style, so a keyboard user sees what a mouse user sees. 40px tall, which is
     under the 44px target: a deliberate density trade for a list of eight. */
  .menu-item {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    inline-size: 100%;
    min-block-size: 40px;
    padding: var(--space-2) var(--space-3);
    border-radius: var(--r-xs);
    color: var(--text);
    font-size: var(--text-sm);
    text-align: start;
    text-decoration: none;
    cursor: pointer;
    transition: background-color var(--dur-1) var(--ease-out);
  }
  .menu-item:hover, .menu-item:focus-visible { background: var(--surface-hover); }
  /* A destructive item. Red is not announced, so the label has to say what it
     does and a confirmation should stand behind it. */
  .menu-item-danger { color: var(--bad-700); }
  .menu-item-danger:hover { background: var(--bad-100); }
  .menu-item .push { margin-inline-start: auto; color: var(--text-faint); }
  /* A one-pixel divider between groups of items. */
  .menu-sep { block-size: 1px; background: var(--line); margin: var(--space-1) 0; }
  /* A small heading above a group of items. Styling only — a screen reader
     hears ordinary text. */
  .menu-label { padding: var(--space-2) var(--space-3) var(--space-1); font-size: var(--text-xs); font-weight: 620; color: var(--text-faint); }

  /* --- Modal, built on native dialog -------------------------------------- */
  /* A native <dialog>. Open it with showModal(), never show() and never the
     open attribute: only showModal() gives the top layer, the backdrop, the
     focus trap and an inert background. The open and close animate because
     display and overlay are in the transition with allow-discrete, which is
     what keeps the element rendered long enough to fade out. */
  .modal {
    inline-size: min(34rem, calc(100vw - var(--space-8)));
    max-block-size: min(85dvh, 48rem);
    padding: 0;
    border: 1px solid var(--line);
    border-radius: var(--r-lg);
    background: var(--surface);
    color: var(--text);
    box-shadow: var(--shadow-4);
    overflow: clip;
    opacity: 0;
    scale: .97;
    translate: 0 8px;
    transition: opacity var(--dur-2) var(--ease-out),
                scale var(--dur-2) var(--ease-spring),
                translate var(--dur-2) var(--ease-spring),
                overlay var(--dur-2) allow-discrete,
                display var(--dur-2) allow-discrete;
  }
  .modal[open] { opacity: 1; scale: 1; translate: 0; }
  @starting-style { .modal[open] { opacity: 0; scale: .97; translate: 0 8px; } }
  .modal::backdrop {
    background: oklch(18% .02 var(--hue-neutral) / .48);
    backdrop-filter: blur(3px);
    -webkit-backdrop-filter: blur(3px);
    opacity: 0;
    transition: opacity var(--dur-2) var(--ease-out), display var(--dur-2) allow-discrete, overlay var(--dur-2) allow-discrete;
  }
  .modal[open]::backdrop { opacity: 1; }
  @starting-style { .modal[open]::backdrop { opacity: 0; } }

  /* The title row. A flex row aligned to flex-start, so a long title and a
     close button line up on the first line. */
  .modal-header {
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
    padding: var(--space-5);
    border-block-end: 1px solid var(--line);
  }
  /* The dialog's heading. Give it an id and point aria-labelledby at it, or
     the dialog is announced with no name. */
  .modal-title { font-size: var(--text-lg); font-weight: 640; letter-spacing: -.018em; }
  /* The scrolling region. The modal caps at min(85dvh, 48rem) and clips, so a
     long body scrolls while the header and footer stay put — which is what
     keeps the actions above the fold. */
  .modal-body { padding: var(--space-5); overflow-y: auto; display: flex; flex-direction: column; gap: var(--space-4); }
  /* The action row, end-aligned. Below 32rem it becomes column-reverse with
     full-width buttons, so the primary action — last in the markup — is on top
     where a thumb reaches it, without changing DOM order. */
  .modal-footer {
    display: flex;
    gap: var(--space-3);
    justify-content: flex-end;
    padding: var(--space-4) var(--space-5);
    border-block-start: 1px solid var(--line);
    background: var(--surface-2);
    flex-wrap: wrap;
  }
  @media (max-width: 32rem) {
    .modal-footer { flex-direction: column-reverse; }
    .modal-footer > .btn { inline-size: 100%; }
  }

  /* --- Empty state -------------------------------------------------------- */
  .empty {
    display: grid;
    place-items: center;
    gap: var(--space-3);
    padding: var(--space-12) var(--space-5);
    text-align: center;
    color: var(--text-muted);
  }
  .empty-art {
    inline-size: 56px;
    block-size: 56px;
    display: grid;
    place-items: center;
    border-radius: var(--r-full);
    background: var(--brand-soft);
    color: var(--brand);
    font-size: 26px;
  }
  .empty-title { font-size: var(--text-md); font-weight: 620; color: var(--text); }
  .empty p { max-inline-size: 34ch; }

  /* --- Stat --------------------------------------------------------------- */
  .stat { display: flex; flex-direction: column; gap: var(--space-1); }
  .stat-value {
    font-size: var(--text-2xl);
    font-weight: 660;
    letter-spacing: -.03em;
    font-variant-numeric: tabular-nums;
    line-height: 1.05;
  }
  .stat-label { font-size: var(--text-sm); color: var(--text-muted); }
  .stat-delta { display: inline-flex; align-items: center; gap: var(--space-1); font-size: var(--text-sm); font-weight: 560; }
  .stat-delta-up { color: var(--good-700); }
  .stat-delta-down { color: var(--bad-700); }

  /* --- Timeline ----------------------------------------------------------- */
  .timeline { display: flex; flex-direction: column; }
  .timeline-item {
    display: grid;
    grid-template-columns: 22px 1fr;
    gap: var(--space-3);
    padding-block-end: var(--space-5);
    position: relative;
  }
  .timeline-item:not(:last-child)::before {
    content: "";
    position: absolute;
    inset-block: 22px 0;
    inset-inline-start: 10px;
    inline-size: 2px;
    background: var(--line);
  }
  .timeline-dot {
    inline-size: 22px;
    block-size: 22px;
    border-radius: var(--r-full);
    background: var(--surface);
    border: 2px solid var(--line-strong);
    display: grid;
    place-items: center;
    z-index: 1;
  }
  .timeline-item.is-done .timeline-dot { background: var(--brand-600); border-color: var(--brand-600); color: var(--text-on-brand); }

  /* --- Nav bar ------------------------------------------------------------ */
  .navbar {
    display: flex;
    align-items: center;
    gap: var(--space-4);
    min-block-size: var(--control-h-lg);
    padding-block: var(--space-2);
    border-block-end: 1px solid var(--line);
  }
  .navbar-brand {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    font-weight: 680;
    font-size: var(--text-md);
    letter-spacing: -.024em;
    color: var(--text);
    text-decoration: none;
  }
  .navbar-links { display: none; gap: var(--space-1); align-items: center; }
  @media (min-width: 48rem) { .navbar-links { display: flex; } }
  /* One destination in a .navbar. Carries the hover and current-page
     states; use .btn only for actions, never for navigation. */
  .nav-link {
    padding: var(--space-2) var(--space-3);
    border-radius: var(--r-sm);
    color: var(--text-muted);
    font-size: var(--text-sm);
    font-weight: 540;
    text-decoration: none;
    transition: background-color var(--dur-1) var(--ease-out), color var(--dur-1) var(--ease-out);
  }
  .nav-link:hover { background: var(--surface-hover); color: var(--text); }
  .nav-link[aria-current] { color: var(--brand); background: var(--brand-soft); }

  /* --- Sidebar ------------------------------------------------------------ */
  .sidebar {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    padding: var(--space-3);
  }
  .sidebar-group { font-size: var(--text-xs); font-weight: 620; color: var(--text-faint); padding: var(--space-3) var(--space-3) var(--space-1); }
  .sidebar-link {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    min-block-size: 40px;
    padding: var(--space-2) var(--space-3);
    border-radius: var(--r-sm);
    color: var(--text-muted);
    font-size: var(--text-sm);
    font-weight: 540;
    text-decoration: none;
    transition: background-color var(--dur-1) var(--ease-out), color var(--dur-1) var(--ease-out);
  }
  .sidebar-link:hover { background: var(--surface-hover); color: var(--text); }
  .sidebar-link[aria-current] { background: var(--brand-soft); color: var(--brand-soft-text); font-weight: 600; }
  .sidebar-link .push { margin-inline-start: auto; }
}

/* ==========================================================================
   Deck — 08 Mobile
   The parts Tailwind makes you build yourself every time.
   ========================================================================== */

@layer deck.mobile {

  /* --- Bottom sheet -------------------------------------------------------
     <dialog class="sheet"> on a phone, a centred modal from 40rem up.
     ---------------------------------------------------------------------- */
  .sheet {
    inline-size: 100%;
    max-inline-size: 100%;
    max-block-size: 88dvh;
    margin: 0;
    margin-block-start: auto;
    padding: 0;
    border: 0;
    border-start-start-radius: var(--r-lg);
    border-start-end-radius: var(--r-lg);
    background: var(--surface);
    color: var(--text);
    box-shadow: var(--shadow-4);
    overflow: clip;
    translate: 0 100%;
    transition: translate var(--dur-3) var(--ease-out),
                display var(--dur-3) allow-discrete,
                overlay var(--dur-3) allow-discrete;
  }
  .sheet[open] { translate: 0; }
  @starting-style { .sheet[open] { translate: 0 100%; } }

  .sheet::backdrop {
    background: oklch(18% .02 var(--hue-neutral) / .5);
    opacity: 0;
    transition: opacity var(--dur-3) var(--ease-out),
                display var(--dur-3) allow-discrete,
                overlay var(--dur-3) allow-discrete;
  }
  .sheet[open]::backdrop { opacity: 1; }
  @starting-style { .sheet[open]::backdrop { opacity: 0; } }

  /* The drag handle at the top of a bottom sheet. Deck draws it and does not
     implement the drag — swiping down does nothing — so always ship a real
     close button as well. */
  .sheet-grip {
    inline-size: 38px;
    block-size: 4px;
    border-radius: var(--r-full);
    background: var(--ink-300);
    margin: var(--space-3) auto var(--space-1);
  }
  /* The title row of a sheet. */
  .sheet-header { padding: var(--space-3) var(--space-5); display: flex; align-items: center; gap: var(--space-3); }
  /* The sheet's heading. Point aria-labelledby at it. */
  .sheet-title { font-size: var(--text-md); font-weight: 640; }
  /* The scrolling region. Pads for the home indicator, and contains its
     overscroll so reaching the end does not scroll the page behind. */
  .sheet-body {
    padding: var(--space-2) var(--space-5) calc(var(--space-6) + env(safe-area-inset-bottom));
    overflow-y: auto;
    overscroll-behavior: contain;
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
  }

  @media (min-width: 40rem) {
    .sheet {
      inline-size: min(30rem, calc(100vw - var(--space-8)));
      margin: auto;
      border-radius: var(--r-lg);
      translate: 0;
      opacity: 0;
      scale: .97;
      transition: opacity var(--dur-2) var(--ease-out), scale var(--dur-2) var(--ease-spring),
                  display var(--dur-2) allow-discrete, overlay var(--dur-2) allow-discrete;
    }
    .sheet[open] { opacity: 1; scale: 1; }
    @starting-style { .sheet[open] { opacity: 0; scale: .97; } }
  }

  /* --- Bottom tab bar ----------------------------------------------------- */
  .tabbar {
    position: fixed;
    inset-inline: 0;
    inset-block-end: 0;
    z-index: var(--z-nav);
    display: flex;
    background: color-mix(in oklab, var(--surface) 88%, transparent);
    backdrop-filter: saturate(1.8) blur(16px);
    -webkit-backdrop-filter: saturate(1.8) blur(16px);
    border-block-start: 1px solid var(--line);
    padding-block-end: env(safe-area-inset-bottom);
  }
  @media (min-width: 48rem) { .tabbar { display: none; } }

  .tabbar-item {
    flex: 1 1 0;
    min-block-size: var(--tap);
    padding-block: var(--space-2);
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 3px;
    color: var(--text-faint);
    font-size: var(--text-2xs);
    font-weight: 560;
    text-decoration: none;
    -webkit-tap-highlight-color: transparent;
    transition: color var(--dur-1) var(--ease-out);
  }
  .tabbar-item[aria-current] { color: var(--brand); }
  .tabbar-item[aria-current] .icon { scale: 1.06; }
  .tabbar-item .icon { transition: scale var(--dur-2) var(--ease-spring); }

  /* Give the page room so the tab bar never covers the last row */
  body:has(.tabbar) { padding-block-end: calc(var(--tap) + var(--space-4) + env(safe-area-inset-bottom)); }
  @media (min-width: 48rem) { body:has(.tabbar) { padding-block-end: 0; } }

  /* --- Safe area helpers --------------------------------------------------- */
  .safe-top { padding-block-start: env(safe-area-inset-top); }
  .safe-bottom { padding-block-end: env(safe-area-inset-bottom); }
  .safe-x { padding-inline: max(var(--space-gutter), env(safe-area-inset-left)) max(var(--space-gutter), env(safe-area-inset-right)); }

  /* --- Icons --------------------------------------------------------------
     <svg class="icon"><use href="deck-icons.svg#check"></use></svg>
     Icons inherit color and font-size, so they line up with text by default.
     ---------------------------------------------------------------------- */
  /* An SVG icon sized in em, so it scales with the text it sits beside and
     lands on the baseline. Icons are filled with currentColor and inherit
     the surrounding colour. */
  .icon {
    inline-size: 1.25em;
    block-size: 1.25em;
    flex: 0 0 auto;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.75;
    stroke-linecap: round;
    stroke-linejoin: round;
    vertical-align: -.22em;
  }
  /* Small icons reference the heavier cut of the sprite, not a thicker stroke:
     the symbols are filled outlines, so stroke-width does nothing to them.
     Every icon ships twice, #name at wght 400 and #name-sm at wght 500, and
     .icon-sm goes with the -sm symbol. CSS cannot rewrite a <use href>, so the
     pairing lives in the markup:
       <svg class="icon icon-sm"><use href="deck-icons.svg#check-sm"></use></svg> */
  .icon-sm { inline-size: 1em; block-size: 1em; }
  /* A 1.6em icon, for icon tiles and empty states where the default would
     look undersized. */
  .icon-lg { inline-size: 1.6em; block-size: 1.6em; stroke-width: 1.6; }
  .icon-xl { inline-size: 2.25em; block-size: 2.25em; stroke-width: 1.4; }
  .icon-fill { fill: currentColor; stroke: none; }
  .icon-muted { color: var(--text-faint); }
  .icon-spin { animation: dk-spin 1.1s linear infinite; }

  /* Icon in a tinted tile — for feature rows and empty states */
  .icon-tile {
    inline-size: 40px;
    block-size: 40px;
    border-radius: var(--r-sm);
    display: grid;
    place-items: center;
    background: var(--brand-soft);
    color: var(--brand);
    flex: 0 0 auto;
  }
  .icon-tile-good { background: var(--good-100); color: var(--good-700); }
  .icon-tile-warn { background: var(--warn-100); color: var(--warn-700); }
  .icon-tile-bad  { background: var(--bad-100);  color: var(--bad-700); }
  .icon-tile-lg { inline-size: 52px; block-size: 52px; border-radius: var(--r-md); }

  /* --- Emoji --------------------------------------------------------------
     Emoji render at wildly different sizes and baselines across platforms.
     .emoji pins the font stack and the metrics so they sit on the line.
     ---------------------------------------------------------------------- */
  /* Pins the emoji font stack and metrics so an emoji renders at the same
     size and baseline on Windows, iOS and Android instead of jumping
     between them. */
  .emoji {
    font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji",
                 "Twemoji Mozilla", "Android Emoji", sans-serif;
    font-size: 1.05em;
    line-height: 1;
    font-style: normal;
    font-weight: 400;
    vertical-align: -.12em;
    display: inline-block;
    text-rendering: auto;
    -webkit-font-smoothing: auto;
  }
  .emoji-lg { font-size: 1.6em; vertical-align: -.2em; }
  .emoji-xl { font-size: 2.5em; vertical-align: -.24em; }
  .emoji-hero { font-size: 3.5rem; line-height: 1; }

  /* Emoji as an avatar or a status marker */
  .emoji-tile {
    inline-size: 40px;
    block-size: 40px;
    border-radius: var(--r-sm);
    background: var(--surface-2);
    border: 1px solid var(--line);
    display: grid;
    place-items: center;
    font-size: 20px;
    flex: 0 0 auto;
  }
  .emoji-tile-round { border-radius: var(--r-full); }

  /* Emoji picker grid — drop your own set in */
  .emoji-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(40px, 1fr));
    gap: 2px;
  }
  .emoji-grid > button {
    aspect-ratio: 1;
    border-radius: var(--r-xs);
    font-size: 21px;
    line-height: 1;
    cursor: pointer;
    transition: background-color var(--dur-1) var(--ease-out), scale var(--dur-1) var(--ease-spring);
  }
  .emoji-grid > button:hover { background: var(--surface-hover); scale: 1.16; }

  /* Reaction pill, like a Slack or Teams reaction */
  .reaction {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    min-block-size: 28px;
    padding-inline: var(--space-2);
    border-radius: var(--r-full);
    border: 1px solid var(--line);
    background: var(--surface);
    font-size: var(--text-xs);
    font-variant-numeric: tabular-nums;
    color: var(--text-muted);
    cursor: pointer;
  }
  .reaction[aria-pressed="true"] { background: var(--brand-soft); border-color: var(--brand-300); color: var(--brand-soft-text); }
}

/* ==========================================================================
   Deck — 09 Utilities
   A short list on purpose. Utilities are for the last 10 percent of a layout,
   not for rebuilding a button out of fourteen class names.
   ========================================================================== */

@layer deck.utilities {

  /* --- Display ------------------------------------------------------------ */
  .hidden { display: none; }
  .block { display: block; }
  .inline-block { display: inline-block; }
  .flex { display: flex; }
  .inline-flex { display: inline-flex; }
  .contents { display: contents; }

  /* --- Flex and grid ------------------------------------------------------ */
  .flex-row { flex-direction: row; }
  .flex-col { flex-direction: column; }
  .wrap { flex-wrap: wrap; }
  .nowrap { flex-wrap: nowrap; white-space: nowrap; }
  .items-start { align-items: flex-start; }
  .items-center { align-items: center; }
  .items-end { align-items: flex-end; }
  .items-stretch { align-items: stretch; }
  .items-baseline { align-items: baseline; }
  .justify-start { justify-content: flex-start; }
  .justify-center { justify-content: center; }
  .justify-end { justify-content: flex-end; }
  .justify-between { justify-content: space-between; }
  .place-center { place-items: center; }
  .grow { flex: 1 1 auto; min-inline-size: 0; }
  .shrink-0 { flex: 0 0 auto; }
  /* Pushes this element and everything after it to the far end of a flex
     row. The one-class replacement for a spacer div. */
  .push { margin-inline-start: auto; }

  .gap-0 { gap: 0; }
  .gap-1 { gap: var(--space-1); }
  .gap-2 { gap: var(--space-2); }
  .gap-3 { gap: var(--space-3); }
  .gap-4 { gap: var(--space-4); }
  .gap-6 { gap: var(--space-6); }
  .gap-8 { gap: var(--space-8); }

  /* --- Spacing ------------------------------------------------------------ */
  /* The scale is 0 1 2 3 4 6 8, declared in 04-layout.css. Every family below
     implements all seven, so a step that exists anywhere exists everywhere. */
  .p-0 { padding: 0; }                       .p-1 { padding: var(--space-1); }
  .p-2 { padding: var(--space-2); }          .p-3 { padding: var(--space-3); }
  .p-4 { padding: var(--space-4); }          .p-6 { padding: var(--space-6); }
  .p-8 { padding: var(--space-8); }

  .px-0 { padding-inline: 0; }               .px-1 { padding-inline: var(--space-1); }
  .px-2 { padding-inline: var(--space-2); }  .px-3 { padding-inline: var(--space-3); }
  .px-4 { padding-inline: var(--space-4); }  .px-6 { padding-inline: var(--space-6); }
  .px-8 { padding-inline: var(--space-8); }

  .py-0 { padding-block: 0; }                .py-1 { padding-block: var(--space-1); }
  .py-2 { padding-block: var(--space-2); }   .py-3 { padding-block: var(--space-3); }
  .py-4 { padding-block: var(--space-4); }   .py-6 { padding-block: var(--space-6); }
  .py-8 { padding-block: var(--space-8); }

  .m-0 { margin: 0; }
  .mt-0 { margin-block-start: 0; }              .mt-1 { margin-block-start: var(--space-1); }
  .mt-2 { margin-block-start: var(--space-2); } .mt-3 { margin-block-start: var(--space-3); }
  .mt-4 { margin-block-start: var(--space-4); } .mt-6 { margin-block-start: var(--space-6); }
  .mt-8 { margin-block-start: var(--space-8); }
  .mb-0 { margin-block-end: 0; }                .mb-1 { margin-block-end: var(--space-1); }
  .mb-2 { margin-block-end: var(--space-2); }   .mb-3 { margin-block-end: var(--space-3); }
  .mb-4 { margin-block-end: var(--space-4); }   .mb-6 { margin-block-end: var(--space-6); }
  .mb-8 { margin-block-end: var(--space-8); }
  .mx-auto { margin-inline: auto; }

  /* --- Sizing ------------------------------------------------------------- */
  .w-full { inline-size: 100%; }
  .w-auto { inline-size: auto; }
  .h-full { block-size: 100%; }
  .min-h-screen { min-block-size: 100dvh; }
  .max-w-prose { max-inline-size: var(--measure); }
  .max-w-sm { max-inline-size: 24rem; }
  .max-w-md { max-inline-size: 34rem; }
  .max-w-lg { max-inline-size: 48rem; }
  .square { aspect-ratio: 1; }

  /* --- Type --------------------------------------------------------------- */
  .text-2xs { font-size: var(--text-2xs); }
  .text-xs { font-size: var(--text-xs); }
  /* One step down the type scale, for secondary text and dense rows. */
  .text-sm { font-size: var(--text-sm); }
  .text-base { font-size: var(--text-base); }
  .text-md { font-size: var(--text-md); }
  .text-lg { font-size: var(--text-lg); }
  .text-xl { font-size: var(--text-xl); }
  .text-2xl { font-size: var(--text-2xl); }

  .fw-normal { font-weight: 400; }
  .fw-medium { font-weight: 540; }
  /* Semibold. The weight for a label or a name that needs to lead without
     becoming a heading. */
  .fw-semi { font-weight: 620; }
  .fw-bold { font-weight: 700; }

  .text-start { text-align: start; }
  .text-center { text-align: center; }
  .text-end { text-align: end; }

  /* Secondary text colour. Still meets contrast against every surface Deck
     defines, so it is safe for body copy rather than decoration only. */
  .text-muted { color: var(--text-muted); }
  .text-faint { color: var(--text-faint); }
  .text-brand { color: var(--brand); }
  .text-good { color: var(--good-700); }
  .text-warn { color: var(--warn-700); }
  .text-bad { color: var(--bad-700); }
  .text-inherit { color: inherit; }

  /* Tabular figures, so numbers line up in a column and do not jitter when
     they change. */
  .nums { font-variant-numeric: tabular-nums; }
  .mono { font-family: var(--font-mono); }
  .uppercase { text-transform: uppercase; letter-spacing: .04em; }
  .capitalize { text-transform: capitalize; }
  .no-underline { text-decoration: none; }

  .truncate {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-inline-size: 0;
  }
  .clamp-2, .clamp-3 {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }
  .clamp-2 { -webkit-line-clamp: 2; line-clamp: 2; }
  .clamp-3 { -webkit-line-clamp: 3; line-clamp: 3; }

  /* --- Surfaces ----------------------------------------------------------- */
  .bg-surface { background: var(--surface); }
  .bg-sunken { background: var(--bg-sunken); }
  .bg-brand { background: var(--brand-600); color: var(--text-on-brand); }
  .bg-soft { background: var(--brand-soft); color: var(--brand-soft-text); }
  .bg-none { background: none; }

  .border { border: 1px solid var(--line); }
  .border-t { border-block-start: 1px solid var(--line); }
  .border-b { border-block-end: 1px solid var(--line); }
  .border-brand { border-color: var(--brand-400); }
  .border-0 { border: 0; }

  .r-xs { border-radius: var(--r-xs); }
  .r-sm { border-radius: var(--r-sm); }
  .r-md { border-radius: var(--r-md); }
  .r-lg { border-radius: var(--r-lg); }
  .r-full { border-radius: var(--r-full); }

  .shadow-0 { box-shadow: none; }
  .shadow-1 { box-shadow: var(--shadow-1); }
  .shadow-2 { box-shadow: var(--shadow-2); }
  .shadow-3 { box-shadow: var(--shadow-3); }
  .shadow-4 { box-shadow: var(--shadow-4); }

  /* --- Position and overflow ---------------------------------------------- */
  .relative { position: relative; }
  .absolute { position: absolute; }
  .fixed { position: fixed; }
  .sticky { position: sticky; }
  .inset-0 { inset: 0; }
  .z-1 { z-index: 1; }
  .overflow-hidden { overflow: hidden; }
  .overflow-auto { overflow: auto; }
  .overflow-clip { overflow: clip; }

  .opacity-60 { opacity: .6; }
  .pointer-none { pointer-events: none; }
  .no-select { user-select: none; }
  .cursor-pointer { cursor: pointer; }

  /* --- Accessibility ------------------------------------------------------ */
  .sr-only {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }
  .skip-link {
    position: absolute;
    inset-block-start: -100%;
    inset-inline-start: var(--space-4);
    z-index: var(--z-toast);
    background: var(--brand-600);
    color: var(--text-on-brand);
    padding: var(--space-3) var(--space-4);
    border-radius: var(--r-sm);
  }
  .skip-link:focus { inset-block-start: var(--space-4); }

  /* --- Responsive variants (mobile first: these turn ON at the breakpoint) - */
  @media (min-width: 40rem) {
    .sm\:hidden { display: none; }
    .sm\:block { display: block; }
    .sm\:flex { display: flex; }
    .sm\:row { flex-direction: row; }
    .sm\:w-auto { inline-size: auto; }
    .sm\:text-start { text-align: start; }
  }
  @media (min-width: 48rem) {
    .md\:hidden { display: none; }
    .md\:block { display: block; }
    .md\:flex { display: flex; }
    .md\:grid-d { display: grid; }
    .md\:row { flex-direction: row; }
    .md\:col { flex-direction: column; }
    .md\:items-center { align-items: center; }
    .md\:justify-between { justify-content: space-between; }
    .md\:w-auto { inline-size: auto; }
    .md\:text-center { text-align: center; }
    .md\:p-8 { padding: var(--space-8); }
    .md\:gap-6 { gap: var(--space-6); }
  }
  @media (min-width: 64rem) {
    .lg\:hidden { display: none; }
    .lg\:block { display: block; }
    .lg\:flex { display: flex; }
    .lg\:grid-d { display: grid; }
    .lg\:row { flex-direction: row; }
    .lg\:gap-8 { gap: var(--space-8); }
  }

  /* Show only to one input type */
  @media (pointer: fine) { .touch-only { display: none; } }
  @media (pointer: coarse) { .mouse-only { display: none; } }
  @media print { .no-print { display: none !important; } }
}

/* ==========================================================================
   Deck — 10 Date picker
   Popover calendar on desktop, bottom sheet on a phone. Single date, range,
   and month/year jump. Behaviour lives in deck.js; this file is the skin.
   ========================================================================== */

@layer deck.components {

  /* --- Trigger field ------------------------------------------------------ */
  /* The trigger for a date picker: an input with a calendar icon inside its
     starting edge and a clear button at the end. The input is readonly and
     cursor: pointer, because the value comes from the calendar — which also
     means a reader who knows the date cannot type it. */
  .datefield { position: relative; display: flex; align-items: center; }
  .datefield > .icon {
    position: absolute;
    inset-inline-start: var(--space-3);
    color: var(--text-faint);
    pointer-events: none;
  }
  .datefield > .input { padding-inline-start: var(--space-10); cursor: pointer; }
  /* Clears the field. Hidden until .has-value is set by deck.js. */
  .datefield > .datefield-clear {
    position: absolute;
    inset-inline-end: var(--space-2);
    inline-size: 28px;
    block-size: 28px;
    display: grid;
    place-items: center;
    border-radius: var(--r-full);
    color: var(--text-faint);
    cursor: pointer;
  }
  .datefield > .datefield-clear:hover { background: var(--surface-hover); color: var(--text); }
  .datefield:not(.has-value) .datefield-clear { display: none; }

  /* --- Panel -------------------------------------------------------------- */
  /* The calendar panel: a native popover, anchored to its field by
     25-anchor.css with the same fallback chain as a menu. Where there is no
     room below it opens upward, and position-try-order: most-block-size picks
     the direction with the most room rather than the first that fits — which
     matters more for a calendar than for a menu. */
  .datepicker {
    position: absolute;
    margin: 0;
    inset: auto;
    padding: 0;
    inline-size: max-content;
    max-inline-size: calc(100vw - var(--space-6));
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    box-shadow: var(--shadow-4);
    color: var(--text);
    opacity: 0;
    translate: 0 -6px;
    transition: opacity var(--dur-2) var(--ease-out),
                translate var(--dur-2) var(--ease-out),
                overlay var(--dur-2) allow-discrete,
                display var(--dur-2) allow-discrete;
  }
  .datepicker:popover-open { opacity: 1; translate: 0; }
  @starting-style { .datepicker:popover-open { opacity: 0; translate: 0 -6px; } }
  .datepicker::backdrop { background: transparent; }

  .datepicker-layout { display: flex; flex-direction: column; }
  @media (min-width: 40rem) {
    .datepicker-layout:has(.datepicker-presets) { flex-direction: row; }
  }

  /* --- Preset rail --------------------------------------------------------- */
  /* The shortcut rail — "Last 7 days", "This month". A horizontal scroller
     below 40rem and a column beside the calendar above it. */
  .datepicker-presets {
    display: flex;
    gap: var(--space-1);
    padding: var(--space-2);
    overflow-x: auto;
    scrollbar-width: none;
    border-block-end: 1px solid var(--line);
  }
  .datepicker-presets::-webkit-scrollbar { display: none; }
  @media (min-width: 40rem) {
    .datepicker-presets {
      flex-direction: column;
      inline-size: 10.5rem;
      overflow-x: visible;
      border-block-end: 0;
      border-inline-end: 1px solid var(--line);
      background: var(--surface-2);
    }
  }
  .datepicker-preset {
    padding: var(--space-2) var(--space-3);
    min-block-size: 36px;
    border-radius: var(--r-xs);
    font-size: var(--text-sm);
    font-weight: 520;
    color: var(--text-muted);
    text-align: start;
    white-space: nowrap;
    cursor: pointer;
    transition: background-color var(--dur-1) var(--ease-out), color var(--dur-1) var(--ease-out);
  }
  .datepicker-preset:hover { background: var(--surface-hover); color: var(--text); }
  .datepicker-preset[aria-pressed="true"] { background: var(--brand-soft); color: var(--brand-soft-text); font-weight: 600; }

  /* --- Calendar ----------------------------------------------------------- */
  .datepicker-main { padding: var(--space-3); }

  .datepicker-head {
    display: flex;
    align-items: center;
    gap: var(--space-1);
    padding-block-end: var(--space-2);
  }
  .datepicker-title {
    flex: 1 1 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    min-block-size: 36px;
    padding-inline: var(--space-3);
    border-radius: var(--r-sm);
    font-weight: 620;
    font-size: var(--text-sm);
    cursor: pointer;
    letter-spacing: -.01em;
  }
  .datepicker-title:hover { background: var(--surface-hover); }
  .datepicker-nav {
    inline-size: 36px;
    block-size: 36px;
    display: grid;
    place-items: center;
    border-radius: var(--r-sm);
    color: var(--text-muted);
    cursor: pointer;
  }
  .datepicker-nav:hover { background: var(--surface-hover); color: var(--text); }
  .datepicker-nav[disabled] { opacity: .35; pointer-events: none; }

  .datepicker-months { display: flex; gap: var(--space-5); }
  .datepicker-month { display: flex; flex-direction: column; }

  .datepicker-grid {
    display: grid;
    grid-template-columns: repeat(7, 1fr);
    gap: 1px;
  }
  .datepicker-dow {
    text-align: center;
    font-size: var(--text-2xs);
    font-weight: 620;
    color: var(--text-faint);
    padding-block: var(--space-2);
  }

  /* One day cell. 38px, raised to 42px on a coarse pointer so it approaches
     the touch target. Eight is- states compose on it, and the range band is
     built from is-in-range plus logical corner radii on the two ends. */
  .datepicker-day {
    position: relative;
    inline-size: 38px;
    block-size: 38px;
    display: grid;
    place-items: center;
    border-radius: var(--r-sm);
    font-size: var(--text-sm);
    font-variant-numeric: tabular-nums;
    color: var(--text);
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: background-color var(--dur-1) var(--ease-out), color var(--dur-1) var(--ease-out);
  }
  @media (pointer: coarse) { .datepicker-day { inline-size: 42px; block-size: 42px; } }

  .datepicker-day:hover { background: var(--surface-hover); }
  .datepicker-day.is-outside { color: var(--text-faint); opacity: .55; }
  .datepicker-day[disabled], .datepicker-day.is-blocked {
    color: var(--text-faint);
    opacity: .4;
    pointer-events: none;
    text-decoration: line-through;
  }

  /* Today gets a marker, not a fill, so it never competes with the selection */
  .datepicker-day.is-today::after {
    content: "";
    position: absolute;
    inset-block-end: 5px;
    inline-size: 4px;
    block-size: 4px;
    border-radius: var(--r-full);
    background: var(--brand);
  }

  .datepicker-day.is-selected {
    background: var(--brand-600);
    color: var(--text-on-brand);
    font-weight: 620;
  }
  .datepicker-day.is-selected::after { background: var(--text-on-brand); }

  /* Range: the middle days get a continuous band, the ends get the caps */
  .datepicker-day.is-in-range {
    background: var(--brand-soft);
    color: var(--brand-soft-text);
    border-radius: 0;
  }
  .datepicker-day.is-range-start {
    border-start-end-radius: 0;
    border-end-end-radius: 0;
  }
  .datepicker-day.is-range-end {
    border-start-start-radius: 0;
    border-end-start-radius: 0;
  }
  .datepicker-day.is-range-start.is-range-end { border-radius: var(--r-sm); }
  .datepicker-grid:has(.is-in-range) .datepicker-day.is-in-range:first-child {
    border-start-start-radius: var(--r-sm); border-end-start-radius: var(--r-sm);
  }
  .datepicker-grid:has(.is-in-range) .datepicker-day.is-in-range:nth-child(7n) {
    border-start-end-radius: var(--r-sm); border-end-end-radius: var(--r-sm);
  }
  .datepicker-day.is-preview { background: color-mix(in oklab, var(--brand-soft) 60%, transparent); }
  .datepicker-day:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; z-index: 1; }

  /* --- Month and year jump grid ------------------------------------------- */
  /* Opens the month and year grid. A picker without one makes a reader press
     the next-month arrow twenty-four times to reach a date two years out. */
  .datepicker-jump {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: var(--space-1);
    padding-block: var(--space-2);
  }
  .datepicker-jump > button {
    min-block-size: 40px;
    padding-inline: var(--space-2);
    border-radius: var(--r-sm);
    font-size: var(--text-sm);
    font-variant-numeric: tabular-nums;
    color: var(--text-muted);
    cursor: pointer;
  }
  .datepicker-jump > button:hover { background: var(--surface-hover); color: var(--text); }
  .datepicker-jump > button[aria-selected="true"] {
    background: var(--brand-600);
    color: var(--text-on-brand);
    font-weight: 620;
  }

  /* --- Time row and footer ------------------------------------------------- */
  .datepicker-time {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-3);
    border-block-start: 1px solid var(--line);
  }
  .datepicker-time .input { inline-size: auto; min-inline-size: 7.5rem; }

  .datepicker-foot {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-3);
    border-block-start: 1px solid var(--line);
    background: var(--surface-2);
  }
  .datepicker-readout {
    flex: 1 1 auto;
    font-size: var(--text-sm);
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
    min-inline-size: 0;
  }

  /* --- Phone: the picker becomes a sheet ---------------------------------- */
  @media (max-width: 30rem) {
    .datepicker {
      position: fixed;
      inset: auto 0 0 0;
      inline-size: 100%;
      max-inline-size: 100%;
      border-radius: var(--r-lg) var(--r-lg) 0 0;
      border-inline: 0;
      border-block-end: 0;
      translate: 0 100%;
      padding-block-end: env(safe-area-inset-bottom);
      transition: translate var(--dur-3) var(--ease-out),
                  overlay var(--dur-3) allow-discrete,
                  display var(--dur-3) allow-discrete;
      opacity: 1;
    }
    .datepicker:popover-open { translate: 0; }
    @starting-style { .datepicker:popover-open { translate: 0 100%; } }
    .datepicker::backdrop { background: oklch(18% .02 var(--hue-neutral) / .45); }
    .datepicker-months { flex-direction: column; }
    .datepicker-day { inline-size: 100%; }
    .datepicker-main { padding-inline: var(--space-4); }
  }
}

/* ==========================================================================
   Deck — 11 Combobox / autocomplete
   Type to filter, arrow keys to move, enter to pick. Single and multi select.
   ========================================================================== */

@layer deck.components {

  /* A filtering select. It wraps a real <select> that stays in the DOM and
     stays in sync, so an ordinary form post works with no server-side
     handling, server-side rendering is just marking options selected, and
     without JavaScript the reader still gets a working control. */
  .combo { position: relative; display: flex; flex-direction: column; }

  /* --- Control ------------------------------------------------------------ */
  /* The box, styled to match .input so a combobox in a form does not look
     like a different species. Focus is handled twice — :has(.combo-input:focus)
     and :focus-within — so a token's remove button lights the box too. */
  .combo-control {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    inline-size: 100%;
    min-block-size: var(--control-h);
    padding: var(--space-1) var(--space-2) var(--space-1) var(--space-3);
    background: var(--surface);
    border: 1px solid var(--line-strong);
    border-radius: var(--r-sm);
    box-shadow: var(--shadow-1);
    cursor: text;
    transition: border-color var(--dur-1) var(--ease-out), box-shadow var(--dur-1) var(--ease-out);
  }
  .combo-control:hover { border-color: var(--ink-400); }
  .combo:has(.combo-input:focus) .combo-control,
  .combo-control:focus-within {
    border-color: var(--focus);
    box-shadow: var(--ring);
  }
  .combo.is-invalid .combo-control { border-color: var(--bad-500); }
  .combo.is-disabled .combo-control {
    background: var(--bg-sunken);
    color: var(--text-faint);
    pointer-events: none;
  }

  /* The text field. deck.js gives it role="combobox" and keeps aria-expanded
     in step; arrow keys move .is-active rather than focus, so the reader can
     keep typing while the highlight moves. */
  .combo-input {
    flex: 1 1 6rem;
    min-inline-size: 4rem;
    border: 0;
    outline: none;
    background: none;
    padding: var(--space-2) 0;
    font-size: var(--text-base);
    color: inherit;
  }
  @media (pointer: coarse) { .combo-input { font-size: 16px; } }
  .combo-input::placeholder { color: var(--text-faint); }

  .combo-toggle {
    inline-size: 30px;
    block-size: 30px;
    display: grid;
    place-items: center;
    border-radius: var(--r-xs);
    color: var(--text-faint);
    cursor: pointer;
    flex: 0 0 auto;
  }
  .combo-toggle:hover { background: var(--surface-hover); color: var(--text); }
  .combo-toggle .icon { transition: rotate var(--dur-2) var(--ease-out); }
  .combo.is-open .combo-toggle .icon { rotate: 180deg; }
  .combo-clear { color: var(--text-faint); }

  /* --- Selected tokens (multi select) -------------------------------------- */
  .combo-tokens { display: contents; }
  .combo-token {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    min-block-size: 26px;
    padding-inline: var(--space-1) var(--space-2);
    padding-inline-start: var(--space-2);
    border-radius: var(--r-xs);
    background: var(--brand-soft);
    color: var(--brand-soft-text);
    font-size: var(--text-sm);
    font-weight: 540;
    max-inline-size: 14rem;
    flex: 0 0 auto;
  }
  .combo-token > span {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .combo-token > button {
    display: grid;
    place-items: center;
    inline-size: 18px;
    block-size: 18px;
    border-radius: var(--r-full);
    opacity: .65;
    cursor: pointer;
    flex: 0 0 auto;
  }
  .combo-token > button:hover { opacity: 1; background: color-mix(in oklab, currentColor 16%, transparent); }
  .combo-count {
    font-size: var(--text-xs);
    font-weight: 620;
    color: var(--text-muted);
    flex: 0 0 auto;
  }

  /* --- Listbox ------------------------------------------------------------- */
  /* The option list. A positioned sibling by default, which a clipping
     ancestor can cut off; 25-anchor.css moves it to the top layer where
     anchor positioning exists, which is the fix for the combobox-inside-a-modal
     bug. */
  .combo-list {
    position: absolute;
    inset-block-start: calc(100% + 4px);
    inset-inline: 0;
    z-index: var(--z-modal);
    margin: 0;
    padding: var(--space-1);
    max-block-size: min(20rem, 50dvh);
    overflow-y: auto;
    overscroll-behavior: contain;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    box-shadow: var(--shadow-4);
    display: none;
  }
  .combo.is-open .combo-list { display: block; animation: dk-combo-in var(--dur-2) var(--ease-out); }
  @keyframes dk-combo-in { from { opacity: 0; translate: 0 -4px; } }

  /* Open upward when there is no room below */
  .combo-list.is-above { inset-block: auto calc(100% + 4px); }

  .combo-option {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    inline-size: 100%;
    min-block-size: 40px;
    padding: var(--space-2) var(--space-3);
    border-radius: var(--r-xs);
    font-size: var(--text-sm);
    color: var(--text);
    text-align: start;
    cursor: pointer;
    scroll-margin-block: var(--space-1);
  }
  /* The text column of an option. min-inline-size: 0 so a long value cannot
     push the tick off the end. */
  .combo-option .combo-option-main { flex: 1 1 auto; min-inline-size: 0; display: flex; flex-direction: column; gap: 1px; }
  .combo-option .combo-option-sub { font-size: var(--text-xs); color: var(--text-muted); }
  .combo-option .combo-option-trail { color: var(--text-faint); font-size: var(--text-xs); flex: 0 0 auto; }

  /* Keyboard highlight and mouse hover use the same state, set by deck.js */
  .combo-option.is-active { background: var(--surface-hover); }
  .combo-option[aria-selected="true"] {
    background: var(--brand-soft);
    color: var(--brand-soft-text);
    font-weight: 560;
  }
  .combo-option[aria-selected="true"].is-active { background: color-mix(in oklab, var(--brand-soft) 78%, var(--brand-300)); }
  .combo-option[aria-disabled="true"] { opacity: .45; pointer-events: none; }
  /* The tick on a selected option, revealed by [aria-selected="true"]. */
  .combo-option .combo-check { margin-inline-start: auto; opacity: 0; flex: 0 0 auto; }
  .combo-option[aria-selected="true"] .combo-check { opacity: 1; }

  /* The matched substring, wrapped by deck.js */
  .combo-option mark {
    background: none;
    color: inherit;
    font-weight: 700;
    padding: 0;
    text-decoration: underline;
    text-decoration-color: color-mix(in oklab, var(--brand) 55%, transparent);
    text-underline-offset: .16em;
  }

  /* A sticky heading above a group of options. */
  .combo-group {
    padding: var(--space-3) var(--space-3) var(--space-1);
    font-size: var(--text-2xs);
    font-weight: 700;
    color: var(--text-faint);
    position: sticky;
    inset-block-start: calc(var(--space-1) * -1);
    background: var(--surface);
  }
  .combo-sep { block-size: 1px; background: var(--line); margin: var(--space-1) 0; }

  /* The no-matches row. Name what was searched for — "No repositories match
     'deckk'" tells a reader who mistyped what they actually typed. */
  .combo-empty, .combo-loading {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    padding: var(--space-6) var(--space-4);
    font-size: var(--text-sm);
    color: var(--text-muted);
    text-align: center;
  }

  /* Sticky "create new" row for tag-style inputs */
  .combo-create {
    position: sticky;
    inset-block-end: calc(var(--space-1) * -1);
    background: var(--surface);
    border-block-start: 1px solid var(--line);
    margin: var(--space-1) calc(var(--space-1) * -1) calc(var(--space-1) * -1);
    padding: var(--space-1);
  }
  .combo-create .combo-option { color: var(--brand); font-weight: 560; }

  .combo-hint {
    display: none;
    gap: var(--space-3);
    padding: var(--space-2) var(--space-3);
    border-block-start: 1px solid var(--line);
    font-size: var(--text-2xs);
    color: var(--text-faint);
  }
  @media (pointer: fine) { .combo.is-open .combo-hint { display: flex; } }

  /* --- Phone: the list takes over the screen ------------------------------- */
  @media (max-width: 30rem) {
    .combo.is-open .combo-list {
      position: fixed;
      inset: auto 0 0 0;
      max-block-size: 70dvh;
      border-radius: var(--r-lg) var(--r-lg) 0 0;
      border-inline: 0;
      border-block-end: 0;
      padding-block-end: calc(var(--space-2) + env(safe-area-inset-bottom));
      animation: dk-combo-up var(--dur-3) var(--ease-out);
    }
    .combo-option { min-block-size: var(--tap); }
    @keyframes dk-combo-up { from { translate: 0 100%; } }
  }
}

/* ==========================================================================
   Deck — 12 Data grid
   A real table with a frozen header and pinned columns. Add .dg-pin-start to
   the cells of a column you want frozen on the left, .dg-pin-end for the
   right. Set --dg-pin-start to the pinned column's width.
   ========================================================================== */

@layer deck.components {

  .dg-wrap {
    --dg-pin-start: 0px;
    --dg-pin-end: 0px;
    --dg-row-h: 44px;
    position: relative;
    inline-size: 100%;
    max-block-size: var(--dg-height, 70dvh);
    overflow: auto;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
  }

  /* A real table with a frozen header and pinned columns. Use it when the
     reader operates on data — sorts, selects, scans — rather than reads it;
     .table is lighter for presentation. Must sit inside .dg-wrap, which
     carries the scroll container and every custom property these rules read. */
  .dg {
    inline-size: 100%;
    border-collapse: separate;
    border-spacing: 0;
    font-size: var(--text-sm);
    font-variant-numeric: tabular-nums;
  }

  .dg :is(th, td) {
    padding: 0 var(--space-4);
    block-size: var(--dg-row-h);
    text-align: start;
    white-space: nowrap;
    background: var(--surface);
    border-block-end: 1px solid var(--line);
  }

  /* --- Frozen header ------------------------------------------------------- */
  .dg thead th {
    position: sticky;
    inset-block-start: 0;
    z-index: 3;
    background: var(--surface-2);
    font-size: var(--text-xs);
    font-weight: 620;
    color: var(--text-muted);
    border-block-end: 1px solid var(--line-strong);
  }

  /* --- Pinned columns ------------------------------------------------------ */
  .dg :is(th, td).dg-pin-start {
    position: sticky;
    inset-inline-start: 0;
    z-index: 2;
  }
  .dg thead th.dg-pin-start { z-index: 4; }

  .dg :is(th, td).dg-pin-start-2 {
    position: sticky;
    inset-inline-start: var(--dg-pin-start);
    z-index: 2;
  }
  .dg thead th.dg-pin-start-2 { z-index: 4; }

  .dg :is(th, td).dg-pin-end {
    position: sticky;
    inset-inline-end: 0;
    z-index: 2;
  }
  .dg thead th.dg-pin-end { z-index: 4; }

  /* The shadow only appears once the grid is actually scrolled sideways.
     A box-shadow offset is physical, so the geometry lives here and 19-logical
     mirrors it for RTL once, rather than once per branch below. */
  .dg-wrap {
    --dg-pin-shadow-start: 6px 0 8px -6px oklch(20% .03 var(--hue-neutral) / .28);
    --dg-pin-shadow-end: -6px 0 8px -6px oklch(20% .03 var(--hue-neutral) / .28);
    --dg-pin-clip-start: inset(0 -12px 0 0);
    --dg-pin-clip-end: inset(0 0 0 -12px);
  }

  /* Ask the scroll container directly. No scroll handler, no class to keep in
     sync, and the query is already logical so RTL needs nothing here. */
  @supports (container-type: scroll-state) {
    .dg-wrap { container-type: scroll-state; }
    @container scroll-state(scrollable: inline-start) {
      .dg :is(th, td).dg-pin-start:last-of-type,
      .dg :is(th, td).dg-pin-start-2,
      .dg :is(th, td).dg-pin-start:not(:has(~ .dg-pin-start-2)) {
        box-shadow: var(--dg-pin-shadow-start);
        clip-path: var(--dg-pin-clip-start);
      }
    }
    @container scroll-state(scrollable: inline-end) {
      .dg :is(th, td).dg-pin-end {
        box-shadow: var(--dg-pin-shadow-end);
        clip-path: var(--dg-pin-clip-end);
      }
    }
  }

  /* Where it is missing, Grid.shadows() in deck.js toggles these two classes. */
  @supports not (container-type: scroll-state) {
    .dg-wrap.is-scrolled-x .dg :is(th, td).dg-pin-start:last-of-type,
    .dg-wrap.is-scrolled-x .dg :is(th, td).dg-pin-start-2,
    .dg-wrap.is-scrolled-x .dg :is(th, td).dg-pin-start:not(:has(~ .dg-pin-start-2)) {
      box-shadow: var(--dg-pin-shadow-start);
      clip-path: var(--dg-pin-clip-start);
    }
    .dg-wrap.is-scrolled-end .dg :is(th, td).dg-pin-end {
      box-shadow: var(--dg-pin-shadow-end);
      clip-path: var(--dg-pin-clip-end);
    }
  }

  /* --- Rows ---------------------------------------------------------------- */
  .dg tbody tr:hover :is(td) { background: var(--surface-hover); }
  .dg tbody tr:last-child td { border-block-end: 0; }

  .dg tbody tr[aria-selected="true"] td {
    background: var(--brand-soft);
    color: var(--brand-soft-text);
  }
  .dg tbody tr[aria-selected="true"]:hover td {
    background: color-mix(in oklab, var(--brand-soft) 80%, var(--brand-200));
  }

  /* Zebra is opt-in, and it never fights the pinned column background */
  .dg-zebra tbody tr:nth-child(even) td { background: var(--surface-2); }
  .dg-zebra tbody tr:nth-child(even):hover td { background: var(--surface-hover); }

  .dg tbody tr.is-new td { animation: dg-flash 1.4s var(--ease-out); }
  @keyframes dg-flash { from { background: var(--good-100); } }

  /* --- Cell variants ------------------------------------------------------- */
  /* A numeric column in the data grid: aligned to the end edge, and sorted
     numerically when the header carries data-sort="num". */
  /* A numeric column: aligned to the end edge, and sorted numerically when
     the header carries data-sort="num". */
  .dg .dg-num { text-align: end; }
  /* A centred column. */
  .dg .dg-center { text-align: center; }
  .dg .dg-wrap-text { white-space: normal; min-inline-size: 16rem; }
  .dg .dg-tight { padding-inline: var(--space-2); }
  /* The selection column. 44px wide, and the one place Deck strips .check of
     its min-block-size — a dense grid of 34px rows cannot also have 44px
     checkboxes. The checkbox still needs its own accessible name. */
  .dg .dg-check { inline-size: 44px; padding-inline: 0; text-align: center; }
  .dg .dg-check .check { justify-content: center; min-block-size: auto; padding: 0; }
  /* A column shrunk to its content, for a row menu or a pair of buttons. */
  .dg .dg-actions { inline-size: 1%; padding-inline: var(--space-2); }

  /* --- Density ------------------------------------------------------------- */
  /* 34px rows and smaller text, for scanning many rows at once. */
  .dg-compact { --dg-row-h: 34px; font-size: var(--text-xs); }
  .dg-compact :is(th, td) { padding-inline: var(--space-3); }
  /* 56px rows, for a grid with two lines of content per cell. */
  .dg-comfy { --dg-row-h: 56px; }

  /* --- Sortable headers ---------------------------------------------------- */
  /* The button deck.js builds inside a th[data-sort]. The styling keys off
     th[aria-sort], so the arrow direction and the announced sort state are
     the same value and cannot drift apart. */
  .dg-sort {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    inline-size: 100%;
    block-size: 100%;
    color: inherit;
    font: inherit;
    cursor: pointer;
    user-select: none;
  }
  .dg-num .dg-sort { justify-content: flex-end; }
  .dg-sort:hover { color: var(--text); }
  .dg-sort .dg-sort-icon {
    inline-size: 14px;
    block-size: 14px;
    opacity: 0;
    flex: 0 0 auto;
    transition: opacity var(--dur-1) var(--ease-out), rotate var(--dur-2) var(--ease-out);
  }
  .dg-sort:hover .dg-sort-icon { opacity: .4; }
  th[aria-sort] .dg-sort { color: var(--brand); }
  th[aria-sort] .dg-sort-icon { opacity: 1; }
  th[aria-sort="descending"] .dg-sort-icon { rotate: 180deg; }

  /* Column resize grip. deck.js writes the width to the th. */
  .dg-resize {
    position: absolute;
    inset-block: 0;
    inset-inline-end: -3px;
    inline-size: 7px;
    cursor: col-resize;
    z-index: 1;
    touch-action: none;
  }
  .dg-resize::after {
    content: "";
    position: absolute;
    inset-block: 8px;
    inset-inline-start: 3px;
    inline-size: 1px;
    background: var(--line-strong);
    opacity: 0;
    transition: opacity var(--dur-1) var(--ease-out);
  }
  .dg-resize:hover::after, .dg-wrap.is-resizing .dg-resize::after { opacity: 1; background: var(--brand); }
  .dg thead th { position: sticky; }
  .dg thead th:has(.dg-resize) { position: sticky; }

  /* --- Footer totals ------------------------------------------------------- */
  .dg tfoot td {
    position: sticky;
    inset-block-end: 0;
    z-index: 3;
    background: var(--surface-2);
    border-block-start: 1px solid var(--line-strong);
    border-block-end: 0;
    font-weight: 640;
    color: var(--text);
  }
  .dg tfoot td.dg-pin-start { z-index: 4; }

  /* --- Toolbar and status bar ---------------------------------------------- */
  .dg-toolbar {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex-wrap: wrap;
    padding-block-end: var(--space-3);
  }
  .dg-toolbar .search { flex: 1 1 14rem; }

  .dg-statusbar {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex-wrap: wrap;
    padding-block-start: var(--space-3);
    font-size: var(--text-sm);
    color: var(--text-muted);
  }

  /* Selection bar that slides in once rows are checked */
  .dg-selection {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-2) var(--space-3);
    border-radius: var(--r-sm);
    background: var(--brand-soft);
    color: var(--brand-soft-text);
    font-size: var(--text-sm);
    font-weight: 560;
    animation: dk-combo-in var(--dur-2) var(--ease-out);
  }

  /* --- Loading and empty --------------------------------------------------- */
  .dg-wrap.is-loading { pointer-events: none; }
  .dg-wrap.is-loading .dg tbody { opacity: .45; }
  .dg-wrap.is-loading::after {
    content: "";
    position: absolute;
    inset-block-start: 0;
    inset-inline: 0;
    block-size: 2px;
    background: linear-gradient(90deg, transparent, var(--brand-500), transparent);
    background-size: 40% 100%;
    background-repeat: no-repeat;
    animation: dg-bar 1.1s var(--ease-in-out) infinite;
    z-index: 5;
  }
  @keyframes dg-bar {
    from { background-position: -40% 0; }
    to   { background-position: 140% 0; }
  }

  /* The row that holds an empty state, with its height and padding released
     so the state can be any size. */
  .dg-empty td { block-size: auto; padding: 0; }

  /* --- Phone: swap to cards ------------------------------------------------
     Add .dg-cards and put the column name in data-label on each td.
     ---------------------------------------------------------------------- */
  @media (max-width: 44rem) {
    .dg-cards-wrap { max-block-size: none; overflow: visible; border: 0; background: none; }
    .dg-cards thead, .dg-cards tfoot { display: none; }
    /* Below a breakpoint, restacks each row into a labelled card, taking the
       column name from data-label on the cell. Costs the column alignment that
       makes a grid comparable; prefer .dg-cq, which asks the container rather
       than the viewport. */
    .dg-cards, .dg-cards tbody, .dg-cards tr, .dg-cards td { display: block; inline-size: 100%; }
    .dg-cards tr {
      background: var(--surface);
      border: 1px solid var(--line);
      border-radius: var(--r-md);
      margin-block-end: var(--space-3);
      overflow: hidden;
    }
    .dg-cards td {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: var(--space-4);
      block-size: auto;
      min-block-size: var(--tap);
      padding: var(--space-2) var(--space-4);
      white-space: normal;
      text-align: end;
      position: static !important;
      box-shadow: none !important;
      clip-path: none !important;
    }
    .dg-cards td::before {
      content: attr(data-label);
      font-size: var(--text-xs);
      font-weight: 620;
      color: var(--text-muted);
      text-align: start;
      flex: 0 0 auto;
    }
    .dg-cards td:empty { display: none; }
    .dg-cards td.dg-num { text-align: end; }
  }
}

/* ==========================================================================
   Deck — 13 Toasts
   A stack, not a list. Collapsed toasts sit behind the newest one and fan out
   on hover or focus. Queue logic is in deck.js (Deck.toast).
   ========================================================================== */

@layer deck.components {

  /* The fixed container. pointer-events: none on the region and auto on each
     toast, so the empty area does not swallow clicks on the page beneath. Sits
     above a .tabbar when the page has one, and clears the home indicator. */
  .toast-region {
    position: fixed;
    z-index: var(--z-toast);
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    inline-size: min(23rem, calc(100vw - var(--space-6)));
    pointer-events: none;
    /* the stack grows upward from the bottom by default */
    inset-block-end: calc(var(--space-4) + env(safe-area-inset-bottom));
    inset-inline-end: var(--space-4);
  }
  body:has(.tabbar) .toast-region {
    inset-block-end: calc(var(--space-4) + var(--tap) + env(safe-area-inset-bottom));
  }
  @media (min-width: 48rem) {
    body:has(.tabbar) .toast-region { inset-block-end: calc(var(--space-4) + env(safe-area-inset-bottom)); }
  }

  .toast-region-start { inset-inline: var(--space-4) auto; }
  .toast-region-center { inset-inline: 0; margin-inline: auto; }
  .toast-region-top {
    inset-block: calc(var(--space-4) + env(safe-area-inset-top)) auto;
    flex-direction: column-reverse;
  }

  /* On a phone the stack spans the width and sits above the thumb */
  @media (max-width: 30rem) {
    .toast-region { inset-inline: var(--space-3); inline-size: auto; }
  }

  /* --- The toast ----------------------------------------------------------- */
  /* One notification. The accent colour is a single custom property,
     --toast-accent, which paints the edge, the icon and the timer together, so
     a kind is one declaration. Built by Deck.toast() rather than written by
     hand; touch-action: pan-y lets a horizontal swipe dismiss it while a
     vertical drag still scrolls the page. */
  .toast {
    --toast-accent: var(--ink-400);
    position: relative;
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    background: var(--surface);
    color: var(--text);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    box-shadow: var(--shadow-4);
    font-size: var(--text-sm);
    pointer-events: auto;
    overflow: hidden;
    touch-action: pan-y;
    transform-origin: bottom center;
    transition: translate var(--dur-3) var(--ease-out),
                scale var(--dur-3) var(--ease-out),
                opacity var(--dur-2) var(--ease-out),
                margin var(--dur-3) var(--ease-out);
  }
  .toast::before {
    content: "";
    position: absolute;
    inset-block: 0;
    inset-inline-start: 0;
    inline-size: 3px;
    background: var(--toast-accent);
  }

  /* The leading glyph, coloured by --toast-accent so it matches the kind
     without a rule of its own. */
  .toast-icon { color: var(--toast-accent); margin-block-start: 1px; flex: 0 0 auto; }
  /* The text column of a toast. min-inline-size: 0 so a long unbroken string
     cannot push the close button off the end. */
  .toast-main { flex: 1 1 auto; min-inline-size: 0; display: flex; flex-direction: column; gap: 2px; }
  .toast-title { font-weight: 620; }
  .toast-text { color: var(--text-muted); }
  .toast-close {
    flex: 0 0 auto;
    inline-size: 26px;
    block-size: 26px;
    display: grid;
    place-items: center;
    border-radius: var(--r-full);
    color: var(--text-faint);
    cursor: pointer;
    margin-block-start: -2px;
    margin-inline-end: -4px;
  }
  .toast-close:hover { background: var(--surface-hover); color: var(--text); }
  .toast-actions { display: flex; gap: var(--space-2); margin-block-start: var(--space-2); }
  .toast-action {
    font-size: var(--text-sm);
    font-weight: 620;
    color: var(--brand);
    cursor: pointer;
    padding: var(--space-1) 0;
  }
  .toast-action:hover { text-decoration: underline; }

  /* --- Kinds --------------------------------------------------------------- */
  .toast-good { --toast-accent: var(--good-500); }
  .toast-warn { --toast-accent: var(--warn-500); }
  .toast-bad  { --toast-accent: var(--bad-500); }
  .toast-info { --toast-accent: var(--brand-500); }
  .toast-loading { --toast-accent: var(--brand-500); }

  /* --- Auto-dismiss timer -------------------------------------------------- */
  /* The auto-dismiss bar. A CSS animation rather than a JavaScript countdown,
     which is what allows hovering or focusing the region to pause every timer
     in it with one declaration — so a reader moving toward an Undo button does
     not have it vanish as they arrive. */
  .toast-timer {
    position: absolute;
    inset-block-end: 0;
    inset-inline-start: 0;
    block-size: 2px;
    inline-size: 100%;
    background: var(--toast-accent);
    opacity: .5;
    transform-origin: left center;
    animation: dk-toast-timer linear forwards;
    animation-duration: var(--toast-duration, 5s);
  }
  @keyframes dk-toast-timer { to { scale: 0 1; } }
  /* Hovering the stack pauses every timer in it */
  .toast-region:is(:hover, :focus-within) .toast-timer { animation-play-state: paused; }

  /* --- Enter and exit ------------------------------------------------------ */
  .toast { animation: dk-toast-in var(--dur-3) var(--ease-spring); }
  @keyframes dk-toast-in {
    from { opacity: 0; translate: 0 16px; scale: .94; }
  }
  .toast-region-top .toast { animation-name: dk-toast-in-top; }
  @keyframes dk-toast-in-top {
    from { opacity: 0; translate: 0 -16px; scale: .94; }
  }
  .toast.is-leaving {
    opacity: 0;
    scale: .94;
    translate: var(--toast-exit-x, 0) 8px;
    margin-block-end: calc((var(--toast-h, 60px) + var(--space-2)) * -1);
    pointer-events: none;
  }

  /* --- Collapsed stack -----------------------------------------------------
     --i counts from the newest, which is the first child because push()
     prepends. Beyond the third toast they hide behind.
     ---------------------------------------------------------------------- */
  @supports (animation-delay: calc(sibling-index() * 1ms)) {
    .toast-region-stacked .toast { --i: calc(sibling-index() - 1); }
  }
  @supports not (animation-delay: calc(sibling-index() * 1ms)) {
    .toast-region-stacked .toast:nth-child(1)   { --i: 0; }
    .toast-region-stacked .toast:nth-child(2)   { --i: 1; }
    .toast-region-stacked .toast:nth-child(3)   { --i: 2; }
    .toast-region-stacked .toast:nth-child(4)   { --i: 3; }
    .toast-region-stacked .toast:nth-child(5)   { --i: 4; }
    .toast-region-stacked .toast:nth-child(6)   { --i: 5; }
    .toast-region-stacked .toast:nth-child(7)   { --i: 6; }
    .toast-region-stacked .toast:nth-child(8)   { --i: 7; }
  }
  .toast-region-stacked .toast {
    position: absolute;
    inset-inline: 0;
    inset-block-end: 0;
    translate: 0 calc(var(--i) * -8px);
    scale: calc(1 - var(--i) * .04);
    opacity: calc(1 - var(--i) * .18);
    z-index: calc(100 - var(--i));
  }
  .toast-region-stacked .toast:nth-child(n+4) { opacity: 0; pointer-events: none; }

  .toast-region-stacked:is(:hover, :focus-within) .toast {
    translate: 0 calc(var(--i) * (var(--toast-h, 68px) + var(--space-2)) * -1);
    scale: 1;
    opacity: 1;
  }

  /* --- Swipe to dismiss ---------------------------------------------------- */
  .toast.is-dragging { transition: none; cursor: grabbing; user-select: none; }

  /* --- Undo pattern -------------------------------------------------------- */
  .toast-undo { --toast-accent: var(--ink-500); }
  .toast-undo .toast-action { color: var(--text); }

  @media (prefers-reduced-motion: reduce) {
    .toast, .toast-region-stacked .toast { animation: none; transition: opacity var(--dur-1) linear; }
    .toast-timer { animation: none; display: none; }
  }
}

/* ==========================================================================
   Deck — 14 Charts
   No charting library. Bars and donuts are plain CSS driven by a --value
   custom property. Lines and areas are inline SVG that you style with these
   classes. Everything reads the brand palette, so charts retheme with the
   rest of the app.
   ========================================================================== */

@layer deck.components {

  /* The frame, and the palette. Six series colours are computed by rotating
     --hue-brand, so charts retheme with the rest of the app and stay in one
     family. That also makes them hard to tell apart for a colour-blind
     reader — label the marks directly rather than relying on hue. */
  .chart {
    --c1: var(--brand-500);
    --c2: oklch(64% .13 calc(var(--hue-brand) + 48));
    --c3: oklch(70% .14 calc(var(--hue-brand) + 96));
    --c4: oklch(62% .13 calc(var(--hue-brand) - 52));
    --c5: oklch(74% .12 calc(var(--hue-brand) + 148));
    --c6: oklch(56% .10 calc(var(--hue-brand) + 200));
    --chart-h: 240px;
    --chart-grid: color-mix(in oklab, var(--line) 90%, transparent);
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    min-inline-size: 0;
  }
  .chart-sm { --chart-h: 150px; }
  .chart-lg { --chart-h: 340px; }

  .chart-head { display: flex; align-items: baseline; gap: var(--space-3); flex-wrap: wrap; }
  .chart-title { font-size: var(--text-sm); font-weight: 620; }
  .chart-note { font-size: var(--text-xs); color: var(--text-muted); }

  /* --- Legend -------------------------------------------------------------- */
  /* The key. Built from real buttons it gets a working toggle state for free,
     because the faded style keys off [aria-pressed="false"]. */
  .chart-legend { display: flex; flex-wrap: wrap; gap: var(--space-3); font-size: var(--text-xs); color: var(--text-muted); }
  .chart-legend > * { display: inline-flex; align-items: center; gap: var(--space-2); }
  .chart-legend > *::before {
    content: "";
    inline-size: 10px;
    block-size: 10px;
    border-radius: 3px;
    background: var(--series, var(--c1));
    flex: 0 0 auto;
  }
  .chart-legend [aria-pressed="false"] { opacity: .4; }
  .chart-legend button { cursor: pointer; }

  /* --- Columns (vertical bars) --------------------------------------------
     <div class="chart-columns">
       <div class="chart-col" style="--value:68" data-label="Mar" data-value="68"></div>
     ---------------------------------------------------------------------- */
  .chart-columns {
    display: flex;
    align-items: flex-end;
    gap: var(--gap, 6px);
    block-size: var(--chart-h);
    padding-block-end: 22px;
    position: relative;
    background:
      repeating-linear-gradient(to top,
        var(--chart-grid) 0 1px,
        transparent 1px calc((100% - 22px) / 4));
    background-position: 0 -22px;
    background-size: 100% calc(100% - 22px);
    background-repeat: no-repeat;
  }
  /* A column. Its height is calc(var(--value) * 1%), so rendering a chart is
     writing numbers into style attributes. data-label and data-value are drawn
     as generated content, which means they are for sighted readers only. */
  .chart-col {
    flex: 1 1 0;
    min-inline-size: 6px;
    block-size: calc(var(--value) * 1%);
    background: var(--series, var(--c1));
    border-radius: var(--r-xs) var(--r-xs) 2px 2px;
    position: relative;
    transform-origin: bottom;
    animation: chart-rise var(--dur-3) var(--ease-out);
    transition: filter var(--dur-1) var(--ease-out);
  }
  .chart-col:hover { filter: brightness(1.08) saturate(1.1); }

  /* A label placed inside a bar gets ink chosen for that series rather than
     the page's --text. contrast-color() is written directly on the elements
     that carry the fill, not hoisted into a custom property: var() is
     substituted where a property is declared, so a shared --text-on-series
     token would read --series from .chart and get it wrong for the usual
     markup, which puts .s1 .. .s6 on the bar itself.

     Only inside @supports: with six hue-derived series there is no single
     static answer, and guessing one would be worse than the inheritance it
     replaced. Series colours are chosen for discriminability and some sit in
     the 55-65% band, so this raises the floor rather than guaranteeing AA. */
  @supports (color: contrast-color(red)) {
    .chart-col, .chart-col-stack > span, .chart-bar-fill, .chart-meter > span {
      color: contrast-color(var(--series, var(--c1)));
    }
  }
  .chart-col[data-label]::after {
    content: attr(data-label);
    position: absolute;
    inset-block-start: 100%;
    inset-inline: 0;
    padding-block-start: 6px;
    text-align: center;
    font-size: var(--text-2xs);
    color: var(--text-faint);
    white-space: nowrap;
  }
  .chart-col[data-value]::before {
    content: attr(data-value);
    position: absolute;
    inset-block-end: calc(100% + 6px);
    inset-inline-start: 50%;
    translate: -50% 0;
    padding: 3px 7px;
    background: var(--ink-900);
    color: var(--ink-50);
    border-radius: var(--r-xs);
    font-size: var(--text-2xs);
    font-weight: 620;
    white-space: nowrap;
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--dur-1) var(--ease-out);
    z-index: 2;
  }
  .chart-col:hover::before { opacity: 1; }
  @keyframes chart-rise { from { scale: 1 0; } }

  /* Stacked columns: nest segments inside a .chart-col-stack */
  .chart-col-stack {
    flex: 1 1 0;
    display: flex;
    flex-direction: column-reverse;
    block-size: calc(var(--value) * 1%);
    border-radius: var(--r-xs) var(--r-xs) 2px 2px;
    overflow: hidden;
    position: relative;
    animation: chart-rise var(--dur-3) var(--ease-out);
  }
  .chart-col-stack > span { block-size: calc(var(--part) * 1%); background: var(--series, var(--c1)); }

  /* Grouped columns */
  .chart-group { flex: 1 1 0; display: flex; align-items: flex-end; gap: 2px; block-size: 100%; position: relative; }
  .chart-group > .chart-col { border-radius: 2px 2px 1px 1px; }

  /* --- Bars (horizontal) --------------------------------------------------- */
  .chart-bars { display: flex; flex-direction: column; gap: var(--space-3); }
  .chart-bar {
    display: grid;
    grid-template-columns: minmax(4.5rem, auto) 1fr auto;
    align-items: center;
    gap: var(--space-3);
    font-size: var(--text-sm);
  }
  .chart-bar-label { color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .chart-bar-track {
    block-size: 10px;
    border-radius: var(--r-full);
    background: var(--bg-sunken);
    overflow: hidden;
  }
  /* The filled part of a horizontal bar, sized from --value. It grows from
     the starting edge, which needs transform-origin — a physical property —
     flipped for RTL in 19-logical.css. */
  .chart-bar-fill {
    display: block;
    block-size: 100%;
    inline-size: calc(var(--value) * 1%);
    border-radius: var(--r-full);
    background: var(--series, var(--c1));
    transform-origin: left;
    animation: chart-grow var(--dur-3) var(--ease-out);
  }
  /* The value printed beside a bar rather than inside it. Prefer this over
     text on the fill: contrast-color() raises the floor for text on a series
     colour but does not guarantee AA. */
  .chart-bar-value { font-variant-numeric: tabular-nums; font-weight: 600; color: var(--text); }
  @keyframes chart-grow { from { scale: 0 1; } }

  /* --- Line and area (inline SVG) ------------------------------------------
     <svg class="chart-svg" viewBox="0 0 300 100" preserveAspectRatio="none">
       <path class="chart-area" d="..."/>
       <path class="chart-line" d="..."/>
     ---------------------------------------------------------------------- */
  .chart-svg {
    inline-size: 100%;
    block-size: var(--chart-h);
    overflow: visible;
  }
  .chart-line {
    fill: none;
    stroke: var(--series, var(--c1));
    stroke-width: var(--stroke, 2);
    stroke-linecap: round;
    stroke-linejoin: round;
    vector-effect: non-scaling-stroke;
    stroke-dasharray: 1200;
    stroke-dashoffset: 1200;
    animation: chart-draw 1s var(--ease-out) forwards;
  }
  @keyframes chart-draw { to { stroke-dashoffset: 0; } }
  .chart-line-dashed { stroke-dasharray: 5 4; animation: none; stroke-dashoffset: 0; opacity: .6; }

  .chart-area {
    fill: var(--series, var(--c1));
    opacity: .14;
    stroke: none;
    animation: chart-fade var(--dur-3) var(--ease-out);
  }
  @keyframes chart-fade { from { opacity: 0; } }

  .chart-dot {
    fill: var(--surface);
    stroke: var(--series, var(--c1));
    stroke-width: 2;
    vector-effect: non-scaling-stroke;
  }
  .chart-dot-active { fill: var(--series, var(--c1)); }

  .chart-gridline {
    stroke: var(--chart-grid);
    stroke-width: 1;
    vector-effect: non-scaling-stroke;
  }
  .chart-baseline { stroke: var(--line-strong); stroke-width: 1; vector-effect: non-scaling-stroke; }

  /* --- Axis labels --------------------------------------------------------- */
  .chart-x {
    display: flex;
    justify-content: space-between;
    font-size: var(--text-2xs);
    color: var(--text-faint);
    padding-block-start: var(--space-2);
  }
  .chart-y {
    display: flex;
    flex-direction: column-reverse;
    justify-content: space-between;
    font-size: var(--text-2xs);
    color: var(--text-faint);
    font-variant-numeric: tabular-nums;
    text-align: end;
    padding-inline-end: var(--space-2);
    min-inline-size: 2.5rem;
  }
  .chart-plot { display: grid; grid-template-columns: auto 1fr; }

  /* --- Sparkline ----------------------------------------------------------- */
  /* A tiny inline chart for a table cell or a stat block. */
  .sparkline {
    inline-size: var(--spark-w, 88px);
    block-size: var(--spark-h, 28px);
    overflow: visible;
    vertical-align: middle;
  }
  .sparkline .chart-line { stroke-width: 1.75; animation: none; stroke-dashoffset: 0; }
  .sparkline-good .chart-line { stroke: var(--good-500); }
  .sparkline-bad .chart-line { stroke: var(--bad-500); }

  /* --- Donut --------------------------------------------------------------- */
  /* A conic gradient driven by --value, with .donut-center holding a figure
     in the hole. One element, no SVG. Give it role="img" and a label — the
     shape carries the meaning and nothing else conveys it. */
  .donut {
    --size: 128px;
    --thickness: 18px;
    inline-size: var(--size);
    block-size: var(--size);
    border-radius: var(--r-full);
    background: conic-gradient(var(--stops, var(--c1) 0 100%));
    mask: radial-gradient(farthest-side, transparent calc(100% - var(--thickness)), #000 calc(100% - var(--thickness) + 1px));
    -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - var(--thickness)), #000 calc(100% - var(--thickness) + 1px));
    animation: dk-spin-in var(--dur-3) var(--ease-out);
  }
  @keyframes dk-spin-in { from { rotate: -40deg; opacity: 0; } }
  .donut-wrap { position: relative; display: inline-grid; place-items: center; }
  .donut-center {
    position: absolute;
    display: grid;
    place-items: center;
    text-align: center;
    line-height: 1.15;
  }
  .donut-center .donut-value { font-size: var(--text-xl); font-weight: 660; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
  .donut-center .donut-label { font-size: var(--text-2xs); color: var(--text-muted); }

  /* --- Progress-style comparison ------------------------------------------- */
  .chart-meter {
    display: flex;
    block-size: 12px;
    border-radius: var(--r-full);
    overflow: hidden;
    background: var(--bg-sunken);
  }
  .chart-meter > span {
    inline-size: calc(var(--value) * 1%);
    background: var(--series, var(--c1));
    transition: inline-size var(--dur-3) var(--ease-out);
  }
  .chart-meter > span + span { border-inline-start: 1px solid var(--surface); }

  /* --- Heat cells ---------------------------------------------------------- */
  /* A grid of --cols columns whose cells mix the series colour with the
     sunken background by --value, interpolated in oklab. */
  .chart-heat { display: grid; grid-template-columns: repeat(var(--cols, 7), 1fr); gap: 3px; }
  .chart-heat > * {
    aspect-ratio: 1;
    border-radius: 3px;
    background: color-mix(in oklab, var(--c1) calc(var(--value) * 1%), var(--bg-sunken));
  }
  .chart-heat > *:hover { outline: 2px solid var(--brand); outline-offset: 1px; }

  /* --- Series color helpers ------------------------------------------------ */
  /* Series 1. Every chart mark reads --series, so a colour is set once on
     the element and the fill, stroke and legend swatch all follow. */
  .s1 { --series: var(--c1); }
  .s2 { --series: var(--c2); }
  .s3 { --series: var(--c3); }
  .s4 { --series: var(--c4); }
  .s5 { --series: var(--c5); }
  .s6 { --series: var(--c6); }
  .s-good { --series: var(--good-500); }
  .s-warn { --series: var(--warn-500); }
  .s-bad  { --series: var(--bad-500); }
  .s-muted { --series: var(--ink-300); }

  @media (prefers-reduced-motion: reduce) {
    .chart-col, .chart-col-stack, .chart-bar-fill, .chart-line, .chart-area, .donut { animation: none; }
    .chart-line { stroke-dashoffset: 0; }
  }
}

/* ==========================================================================
   Deck — 16 Motion
   Motion answers an action or shows what changed. It is not decoration.
   Nothing in this file runs unless you ask for it by class, except the
   view-transition setup, which you opt into with one at-rule in your app CSS.
   ========================================================================== */

@layer deck.motion {

  /* FINDINGS 68: these are real tokens and the documentation cannot see them.
     tools/docs/extract.mjs takes tokens from 01-tokens.css only, so all
     thirteen declared below are absent from dist/api.json, API.md and every
     docs_token_table() -- including --dur-4 and --dur-5, which are used nine
     and four times across the stylesheet. Passing one to docs_token_table()
     renders an empty row rather than failing, which is why it went unnoticed.
     Fix belongs in the extractor, not here. */

  /* --- Extended motion tokens ---------------------------------------------
     The base set lives in tokens. These are the rest of the vocabulary.
     ---------------------------------------------------------------------- */
  :root {
    --ease-in: cubic-bezier(.55, 0, 1, .45);
    --ease-emphasized: cubic-bezier(.2, 0, 0, 1);
    --ease-decelerate: cubic-bezier(0, 0, .2, 1);
    --ease-accelerate: cubic-bezier(.4, 0, 1, 1);

    /* linear() springs — real spring feel with no JS */
    --ease-bounce: linear(0, .004 2%, .016 4%, .062 8%, .137 12%, .5 24%, .68 28%,
                          .824 32%, .93 36%, .995 40%, 1.02 44%, 1.01 52%, 1 60%);
    --ease-overshoot: linear(0, .22 8%, .58 16%, .87 24%, 1.06 32%, 1.11 38%,
                             1.08 46%, 1.02 58%, 1);
    --ease-snap: linear(0, .48 12%, .82 22%, .97 32%, 1.02 42%, 1);

    --dur-0: 0ms;
    --dur-4: 420ms;
    --dur-5: 620ms;
    --dur-slow: 900ms;

    --stagger-step: 45ms;
    --travel: 12px;   /* how far entrances move */
  }

  /* --- Reduced motion ------------------------------------------------------
     Everything below is wrapped in no-preference. A person who asked their OS
     to calm things down gets the end state instantly, with one exception:
     loading indicators keep moving, because a frozen spinner reads as broken.
     ---------------------------------------------------------------------- */
  /* --- Transition utilities ------------------------------------------------ */
  .transition        { transition: all var(--dur-2) var(--ease-out); }
  .transition-colors { transition: background-color var(--dur-1) var(--ease-out),
                                   border-color var(--dur-1) var(--ease-out),
                                   color var(--dur-1) var(--ease-out),
                                   fill var(--dur-1) var(--ease-out),
                                   stroke var(--dur-1) var(--ease-out); }
  .transition-move   { transition: translate var(--dur-2) var(--ease-out),
                                   scale var(--dur-2) var(--ease-out),
                                   rotate var(--dur-2) var(--ease-out); }
  .transition-size   { transition: inline-size var(--dur-3) var(--ease-out),
                                   block-size var(--dur-3) var(--ease-out); }
  .transition-opacity{ transition: opacity var(--dur-2) var(--ease-out); }

  .dur-1 { transition-duration: var(--dur-1); animation-duration: var(--dur-1); }
  .dur-2 { transition-duration: var(--dur-2); animation-duration: var(--dur-2); }
  .dur-3 { transition-duration: var(--dur-3); animation-duration: var(--dur-3); }
  .dur-4 { transition-duration: var(--dur-4); animation-duration: var(--dur-4); }
  .dur-5 { transition-duration: var(--dur-5); animation-duration: var(--dur-5); }

  .ease-out { transition-timing-function: var(--ease-out); animation-timing-function: var(--ease-out); }
  .ease-in { transition-timing-function: var(--ease-in); animation-timing-function: var(--ease-in); }
  .ease-spring { transition-timing-function: var(--ease-spring); animation-timing-function: var(--ease-spring); }
  .ease-bounce { transition-timing-function: var(--ease-bounce); animation-timing-function: var(--ease-bounce); }
  .ease-overshoot { transition-timing-function: var(--ease-overshoot); animation-timing-function: var(--ease-overshoot); }
  .ease-linear { transition-timing-function: linear; animation-timing-function: linear; }

  .delay-1 { transition-delay: 60ms; animation-delay: 60ms; }
  .delay-2 { transition-delay: 140ms; animation-delay: 140ms; }
  .delay-3 { transition-delay: 260ms; animation-delay: 260ms; }
  .no-motion { transition: none !important; animation: none !important; }

  /* Tell the compositor what is about to move, then let go. Never leave
     will-change on permanently — it costs memory on every element. */
  .will-move:is(:hover, :focus-within, .is-animating) { will-change: translate, scale, opacity; }

  @media (prefers-reduced-motion: no-preference) {

    /* --- Entrances --------------------------------------------------------
       One-shot, on load. Use for content that appears, not for every section.
       -------------------------------------------------------------------- */
    .enter        { animation: dk-fade var(--dur-3) var(--ease-out) both; }
    .enter-rise   { animation: dk-rise var(--dur-3) var(--ease-out) both; }
    .enter-drop   { animation: dk-drop var(--dur-3) var(--ease-out) both; }
    .enter-start  { animation: dk-in-start var(--dur-3) var(--ease-out) both; }
    .enter-end    { animation: dk-in-end var(--dur-3) var(--ease-out) both; }
    .enter-pop    { animation: dk-pop var(--dur-3) var(--ease-spring) both; }
    .enter-blur   { animation: dk-blur var(--dur-4) var(--ease-out) both; }

    @keyframes dk-fade { from { opacity: 0; } }
    @keyframes dk-rise { from { opacity: 0; translate: 0 var(--travel); } }
    @keyframes dk-drop { from { opacity: 0; translate: 0 calc(var(--travel) * -1); } }
    @keyframes dk-in-start { from { opacity: 0; translate: calc(var(--travel) * -1.6) 0; } }
    @keyframes dk-in-end { from { opacity: 0; translate: calc(var(--travel) * 1.6) 0; } }
    @keyframes dk-pop { from { opacity: 0; scale: .92; } }
    @keyframes dk-blur { from { opacity: 0; filter: blur(8px); scale: 1.01; } }

    /* --- Stagger ----------------------------------------------------------
       Put .stagger on the parent. Children animate in sequence. sibling-index()
       counts them, so the sequence has no ceiling and no JavaScript. The twelve
       nth-child rules it replaced are kept below for browsers without it --
       they stop at twelve, which is why deck.js used to patch past that.
       -------------------------------------------------------------------- */
    @supports (animation-delay: calc(sibling-index() * 1ms)) {
      .stagger > * { animation-delay: calc((sibling-index() - 1) * var(--stagger-step)); }
    }
    @supports not (animation-delay: calc(sibling-index() * 1ms)) {
      .stagger > * { animation-delay: calc(var(--n, 0) * var(--stagger-step)); }
      .stagger > :nth-child(1)  { --n: 0; }
      .stagger > :nth-child(2)  { --n: 1; }
      .stagger > :nth-child(3)  { --n: 2; }
      .stagger > :nth-child(4)  { --n: 3; }
      .stagger > :nth-child(5)  { --n: 4; }
      .stagger > :nth-child(6)  { --n: 5; }
      .stagger > :nth-child(7)  { --n: 6; }
      .stagger > :nth-child(8)  { --n: 7; }
      .stagger > :nth-child(9)  { --n: 8; }
      .stagger > :nth-child(10) { --n: 9; }
      .stagger > :nth-child(11) { --n: 10; }
      .stagger > :nth-child(12) { --n: 11; }
      /* Past twelve everything holds the last step rather than snapping back
         to zero. Without sibling-index() that is as far as CSS can count.
         .pile in 21-space3d.css has the same fallback and is MISSING this
         line, so its ninth card stacks on top of the first. */
      .stagger > :nth-child(n+13) { --n: 11; }
    }
    .stagger-fast { --stagger-step: 28ms; }
    .stagger-slow { --stagger-step: 80ms; }

    /* --- Scroll-driven reveals -------------------------------------------
       No IntersectionObserver, no JS. The animation is tied to the element's
       position in the scrollport. deck.js provides a fallback for browsers
       without animation-timeline by adding .is-revealed.
       -------------------------------------------------------------------- */
    @supports (animation-timeline: view()) {
      .reveal {
        animation: dk-rise linear both;
        animation-timeline: view();
        animation-range: entry 8% cover 26%;
      }
      .reveal-fade { animation-name: dk-fade; }
      .reveal-pop { animation-name: dk-pop; }
      .reveal-late { animation-range: entry 20% cover 42%; }

      /* A progress bar that fills as the page scrolls — good for long reports */
      .scroll-progress {
        position: fixed;
        inset-block-start: 0;
        inset-inline: 0;
        block-size: 3px;
        background: var(--brand-500);
        transform-origin: left center;
        z-index: var(--z-toast);
        animation: dk-scale-x linear both;
        animation-timeline: scroll(root block);
      }
      @keyframes dk-scale-x { from { scale: 0 1; } to { scale: 1 1; } }

      /* Sticky header that condenses once you scroll past the top */
      .shrink-on-scroll {
        animation: dk-shrink linear both;
        animation-timeline: scroll(root block);
        animation-range: 0 120px;
      }
      @keyframes dk-shrink {
        to { --control-h-lg: 42px; box-shadow: var(--shadow-2); }
      }
    }
    @supports not (animation-timeline: view()) {
      .reveal, .reveal-fade, .reveal-pop { opacity: 0; }
      .reveal.is-revealed { animation: dk-rise var(--dur-4) var(--ease-out) both; }
      .reveal-fade.is-revealed { animation: dk-fade var(--dur-4) var(--ease-out) both; }
      .reveal-pop.is-revealed { animation: dk-pop var(--dur-4) var(--ease-spring) both; }
    }

    /* --- Attention and feedback ------------------------------------------- */
    .shake { animation: dk-shake 420ms var(--ease-in-out); }
    @keyframes dk-shake {
      10%, 90% { translate: -2px 0; }
      20%, 80% { translate: 4px 0; }
      30%, 50%, 70% { translate: -6px 0; }
      40%, 60% { translate: 6px 0; }
    }
    /* A field that failed validation shakes once, then stops */
    .field.is-invalid:not(.was-shaken) :is(.input, .textarea, .select, .combo-control) {
      animation: dk-shake 420ms var(--ease-in-out);
    }

    .flash { animation: dk-flash 1.2s var(--ease-out); }
    @keyframes dk-flash {
      from { background-color: var(--brand-soft); }
      to { background-color: transparent; }
    }
    .flash-good { animation: dk-flash-good 1.4s var(--ease-out); }
    @keyframes dk-flash-good {
      from { background-color: var(--good-100); }
      to { background-color: transparent; }
    }

    .pulse { animation: dk-pulse 2s var(--ease-in-out) infinite; }
    @keyframes dk-pulse { 50% { opacity: .55; } }

    /* Ping ring for a live indicator — a dot with a halo going out */
    .ping { position: relative; }
    .ping::after {
      content: "";
      position: absolute;
      inset: 0;
      border-radius: inherit;
      background: currentColor;
      animation: dk-ping 1.8s var(--ease-out) infinite;
      pointer-events: none;
    }
    @keyframes dk-ping {
      from { opacity: .6; scale: 1; }
      to { opacity: 0; scale: 2.2; }
    }

    .nudge { animation: dk-nudge 1.6s var(--ease-in-out) infinite; }
    @keyframes dk-nudge { 50% { translate: 4px 0; } }

    /* --- Micro-interactions ----------------------------------------------- */
    .lift { transition: translate var(--dur-2) var(--ease-out), box-shadow var(--dur-2) var(--ease-out); }
    .lift:hover { translate: 0 -2px; box-shadow: var(--shadow-3); }
    .lift:active { translate: 0 0; box-shadow: var(--shadow-1); }

    .press { transition: scale var(--dur-1) var(--ease-out); }
    .press:active { scale: .97; }

    /* Underline that draws in from the start edge */
    .sweep {
      position: relative;
      text-decoration: none;
      background-image: linear-gradient(currentColor, currentColor);
      background-size: 0 1.5px;
      background-position: 0 100%;
      background-repeat: no-repeat;
      transition: background-size var(--dur-2) var(--ease-out);
      padding-block-end: 1px;
    }
    .sweep:hover, .sweep:focus-visible { background-size: 100% 1.5px; }

    /* An arrow that steps forward when its button is hovered */
    .btn:hover .icon-follow, .list-row:hover .icon-follow, .card-link:hover .icon-follow {
      translate: 3px 0;
    }
    .icon-follow { transition: translate var(--dur-2) var(--ease-spring); }

    /* Ripple. deck.js sets --rx/--ry from the pointer position. */
    .ripple { position: relative; overflow: hidden; }
    .ripple > .ripple-ink {
      position: absolute;
      inset-block-start: var(--ry, 50%);
      inset-inline-start: var(--rx, 50%);
      inline-size: 8px;
      block-size: 8px;
      margin: -4px 0 0 -4px;
      border-radius: var(--r-full);
      background: currentColor;
      opacity: .28;
      pointer-events: none;
      animation: dk-ripple 520ms var(--ease-out) forwards;
    }
    @keyframes dk-ripple {
      to { scale: 26; opacity: 0; }
    }

    /* --- Expand and collapse ----------------------------------------------
       Real height:auto transitions, no measuring in JS. Needs
       interpolate-size, which tokens opts into on :root.
       -------------------------------------------------------------------- */
    .expand {
      block-size: 0;
      overflow: hidden;
      transition: block-size var(--dur-3) var(--ease-out),
                  opacity var(--dur-2) var(--ease-out);
      opacity: 0;
    }
    .expand.is-open { block-size: auto; opacity: 1; }

    /* --- Discrete-property transitions ------------------------------------
       Anything that goes display:none can still animate out.
       -------------------------------------------------------------------- */
    .fade-swap {
      transition: opacity var(--dur-2) var(--ease-out),
                  display var(--dur-2) allow-discrete;
    }
    .fade-swap[hidden] { opacity: 0; }
    @starting-style { .fade-swap:not([hidden]) { opacity: 0; } }

    /* --- Marquee / ticker --------------------------------------------------
       For a status strip: recalls, open campaigns, parts on backorder.
       -------------------------------------------------------------------- */
    .marquee {
      display: flex;
      overflow: hidden;
      gap: var(--space-8);
      mask-image: linear-gradient(90deg, transparent, #000 5%, #000 95%, transparent);
      -webkit-mask-image: linear-gradient(90deg, transparent, #000 5%, #000 95%, transparent);
    }
    .marquee-track {
      display: flex;
      gap: var(--space-8);
      flex: 0 0 auto;
      padding-inline-end: var(--space-8);
      animation: dk-marquee var(--marquee-dur, 26s) linear infinite;
      white-space: nowrap;
    }
    .marquee:hover .marquee-track { animation-play-state: paused; }
    @keyframes dk-marquee { to { translate: -100% 0; } }

    /* --- Number and value changes ------------------------------------------ */
    .tick { transition: translate var(--dur-2) var(--ease-spring); }
    .tick.is-up { animation: dk-tick-up var(--dur-3) var(--ease-out); }
    .tick.is-down { animation: dk-tick-down var(--dur-3) var(--ease-out); }
    @keyframes dk-tick-up { from { translate: 0 6px; opacity: 0; color: var(--good-500); } }
    @keyframes dk-tick-down { from { translate: 0 -6px; opacity: 0; color: var(--bad-500); } }
  }

  /* --- View transitions ----------------------------------------------------
     This is the big one for Keel. Add this to your app CSS:

       @view-transition { navigation: auto; }

     and full page loads in a plain PHP multi-page app cross-fade like a single
     page app, with no router and no JavaScript. The rules below style what the
     browser generates. They are inert until you opt in.
     ---------------------------------------------------------------------- */

  ::view-transition-group(root) { animation-duration: var(--dur-3); }

  ::view-transition-old(root) {
    animation: dk-vt-out var(--dur-2) var(--ease-in) both;
  }
  ::view-transition-new(root) {
    animation: dk-vt-in var(--dur-3) var(--ease-out) both;
  }
  @keyframes dk-vt-out { to { opacity: 0; translate: 0 -6px; } }
  @keyframes dk-vt-in { from { opacity: 0; translate: 0 8px; } }

  /* Forward and back get direction. Set data-vt="back" on <html> before you
     navigate, or let deck.js do it on popstate. */
  html[data-vt="back"]::view-transition-old(root) { animation-name: dk-vt-out-back; }
  html[data-vt="back"]::view-transition-new(root) { animation-name: dk-vt-in-back; }
  @keyframes dk-vt-out-back { to { opacity: 0; translate: 6% 0; } }
  @keyframes dk-vt-in-back { from { opacity: 0; translate: -6% 0; } }

  /* Elements that persist across the navigation. Give the same
     view-transition-name to the matching element on both pages and the browser
     tweens between them — a row in a list morphing into a detail header. */
  .vt-hold { view-transition-name: var(--vt); }
  .vt-header { view-transition-name: dk-header; }
  .vt-main { view-transition-name: dk-main; }
  .vt-tabbar { view-transition-name: dk-tabbar; }

  /* The nav chrome should sit still while the content changes */
  ::view-transition-group(dk-header),
  ::view-transition-group(dk-tabbar) { animation-duration: var(--dur-1); }
  ::view-transition-old(dk-header), ::view-transition-new(dk-header),
  ::view-transition-old(dk-tabbar), ::view-transition-new(dk-tabbar) {
    animation: none;
    mix-blend-mode: normal;
  }

  @media (prefers-reduced-motion: reduce) {
    ::view-transition-group(*),
    ::view-transition-old(*),
    ::view-transition-new(*) {
      animation-duration: 1ms !important;
    }
  }
}

/* ==========================================================================
   Deck — 18 Container queries
   A component should respond to the space it was given, not to the size of the
   window. A card in a narrow sidebar and the same card in a wide main column
   are the same markup and should lay out differently without either one
   knowing where it was placed.
   ========================================================================== */

@layer deck.layout {

  /* --- Declaring a container ----------------------------------------------
     .cq is the plain one. The named versions let a nested component query a
     specific ancestor instead of the closest one.
     ---------------------------------------------------------------------- */
  .cq        { container-type: inline-size; }
  .cq-size   { container-type: size; }
  .cq-normal { container-type: normal; }

  .cq-panel  { container: panel / inline-size; }
  .cq-pane   { container: pane / inline-size; }
  .cq-row    { container: row / inline-size; }
  .cq-shell  { container: shell / inline-size; }

  /* Every layout primitive can become a container without extra markup */
  .card.cq, .panel.cq, .list.cq, .split > .cq, .grid > .cq { container-type: inline-size; }

  /* --- Container query units -----------------------------------------------
     cqi is 1% of the container's inline size. Type that scales with its column
     rather than with the viewport — a heading in a sidebar stays small even on
     a 32 inch monitor.
     ---------------------------------------------------------------------- */
  .cq :is(.text-cq) { font-size: clamp(var(--text-sm), 3.4cqi, var(--text-lg)); }
  .cq :is(.display-cq) {
    font-size: clamp(1.5rem, 8cqi, 3.25rem);
    line-height: 1.05;
    letter-spacing: -.03em;
  }
  .cq :is(.pad-cq) { padding: clamp(var(--space-3), 4cqi, var(--space-8)); }
  .cq :is(.gap-cq) { gap: clamp(var(--space-2), 2.5cqi, var(--space-6)); }

  /* --- Container-driven components ---------------------------------------- */

  /* A card that turns into a horizontal media object once it has the room */
  .card-flex { display: flex; flex-direction: column; }
  .card-flex .card-media { flex: 0 0 auto; }
  .card-flex .card-media img { inline-size: 100%; aspect-ratio: 16 / 9; object-fit: cover; }

  @container (min-width: 30rem) {
    .card-flex { flex-direction: row; }
    .card-flex .card-media { inline-size: 40%; max-inline-size: 15rem; }
    .card-flex .card-media img { block-size: 100%; aspect-ratio: auto; }
  }

  /* A stat block that stacks tight, then spreads, then goes inline */
  .stat-cq { display: flex; flex-direction: column; gap: var(--space-1); }
  @container (min-width: 18rem) {
    .stat-cq .stat-value { font-size: var(--text-2xl); }
  }
  @container (min-width: 28rem) {
    .stat-cq {
      flex-direction: row;
      align-items: baseline;
      gap: var(--space-3);
      flex-wrap: wrap;
    }
    .stat-cq .stat-label { order: -1; min-inline-size: 100%; }
  }

  /* List rows that reveal their metadata as space allows */
  .metarow { display: flex; align-items: center; gap: var(--space-3); }
  .metarow .row-meta { display: none; }
  @container (min-width: 26rem) {
    .metarow .row-meta { display: block; color: var(--text-muted); font-size: var(--text-sm); }
  }
  @container (min-width: 38rem) {
    .metarow .row-meta-wide { display: block; }
  }
  .metarow .row-meta-wide { display: none; }

  /* Form fields that pair up inside their own container, not the viewport.
     This is the one that keeps biting people: a two-up field row inside a
     narrow modal should stay stacked even on a desktop. */
  .field-row-cq { display: grid; gap: var(--space-4); grid-template-columns: 1fr; }
  @container (min-width: 32rem) {
    .field-row-cq { grid-template-columns: repeat(auto-fit, minmax(min(13rem, 100%), 1fr)); }
  }

  /* Button rows that go full width when cramped */
  .actions-cq { display: flex; flex-direction: column; gap: var(--space-2); }
  .actions-cq > .btn { inline-size: 100%; }
  @container (min-width: 24rem) {
    .actions-cq { flex-direction: row; }
    .actions-cq > .btn { inline-size: auto; }
  }

  /* A data grid that drops to cards based on its own width. This is strictly
     better than the media-query version, because a grid inside a sheet is
     narrow no matter how wide the screen is. */
  @container (max-width: 44rem) {
    .dg-cq thead, .dg-cq tfoot { display: none; }
    .dg-cq, .dg-cq tbody, .dg-cq tr, .dg-cq td { display: block; inline-size: 100%; }
    .dg-cq tr {
      background: var(--surface);
      border: 1px solid var(--line);
      border-radius: var(--r-md);
      margin-block-end: var(--space-3);
      overflow: hidden;
    }
    .dg-cq td {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: var(--space-4);
      block-size: auto;
      min-block-size: var(--tap);
      text-align: end;
      white-space: normal;
      position: static !important;
      box-shadow: none !important;
      clip-path: none !important;
    }
    .dg-cq td::before {
      content: attr(data-label);
      font-size: var(--text-xs);
      font-weight: 620;
      color: var(--text-muted);
      text-align: start;
    }
  }

  /* Named-container example: a sidebar nav that collapses to icons when the
     shell is narrow, regardless of what is nested inside it. */
  /* FINDINGS 64: display:none removes the label from the accessibility tree,
     not just from the screen. The icon inside the link is aria-hidden by
     convention, so a collapsed .sidebar-link has NO accessible name -- a
     screen reader announces "link" for every destination in the app's primary
     navigation.

     Note that .sidebar-group two lines down gets it right: font-size: 0 hides
     the heading visually and keeps it in the tree. The link rule should hide
     the span the same way, or with an .sr-only clip. Until it does, authors
     have to put aria-label on every .sidebar-link by hand. */
  @container shell (max-width: 15rem) {
    .sidebar-link span { display: none; }
    .sidebar-link { justify-content: center; }
    .sidebar-group { text-align: center; font-size: 0; }
  }

  /* --- Container breakpoint utilities --------------------------------------
     Same idea as the responsive utilities, but keyed to the container.
     ---------------------------------------------------------------------- */
  @container (min-width: 24rem) {
    .cq-sm\:row { flex-direction: row; }
    .cq-sm\:flex { display: flex; }
    .cq-sm\:hidden { display: none; }
    .cq-sm\:block { display: block; }
    .cq-sm\:w-auto { inline-size: auto; }
  }
  @container (min-width: 32rem) {
    .cq-md\:row { flex-direction: row; }
    .cq-md\:col { flex-direction: column; }
    .cq-md\:flex { display: flex; }
    .cq-md\:grid { display: grid; }
    .cq-md\:hidden { display: none; }
    .cq-md\:block { display: block; }
    .cq-md\:items-center { align-items: center; }
    .cq-md\:justify-between { justify-content: space-between; }
  }
  @container (min-width: 44rem) {
    .cq-lg\:row { flex-direction: row; }
    .cq-lg\:flex { display: flex; }
    .cq-lg\:hidden { display: none; }
    .cq-lg\:block { display: block; }
    .cq-lg\:grid-2 { display: grid; grid-template-columns: 1fr 1fr; }
  }

  /* --- Style queries -------------------------------------------------------
     Query a custom property rather than a size. Set --tone on a container and
     everything inside it adapts, with no extra classes on the children.
     Supported in current Chrome and Safari; the fallback is simply no change.
     ---------------------------------------------------------------------- */
  .cq-tone { container-type: inline-size; }

  @container style(--tone: critical) {
    .tone-surface { background: var(--bad-100); border-color: color-mix(in oklab, var(--bad-500) 35%, transparent); }
    .tone-text { color: var(--bad-700); }
    .tone-icon { color: var(--bad-500); }
  }
  @container style(--tone: caution) {
    .tone-surface { background: var(--warn-100); border-color: color-mix(in oklab, var(--warn-500) 35%, transparent); }
    .tone-text { color: var(--warn-700); }
    .tone-icon { color: var(--warn-500); }
  }
  @container style(--tone: clear) {
    .tone-surface { background: var(--good-100); border-color: color-mix(in oklab, var(--good-500) 35%, transparent); }
    .tone-text { color: var(--good-700); }
    .tone-icon { color: var(--good-500); }
  }
}

/* ==========================================================================
   Deck — 19 Logical properties and direction
   The framework is written in logical properties end to end: inline-size,
   block-size, inset-inline-start, padding-block, margin-inline, border-start-
   start-radius. That means Deck already lays out correctly in Arabic, Hebrew,
   Urdu, and Farsi with nothing but dir="rtl" on the html element.

   What logical properties cannot fix by themselves is content: icons that
   point somewhere, characters that are inherently one-directional, and the few
   places where a physical direction is genuinely meant. That is what this file
   handles.
   ========================================================================== */

@layer deck.utilities {

  /* --- Logical spacing utilities -------------------------------------------
     The short physical-looking names in the utility layer (.mt-4, .px-3) are
     already logical underneath — .mt-4 sets margin-block-start. These are the
     explicitly named versions for when the distinction matters in review.
     ---------------------------------------------------------------------- */
  .mis-auto { margin-inline-start: auto; }
  .mie-auto { margin-inline-end: auto; }

  /* Same seven steps as every other spacing family: 0 1 2 3 4 6 8. */
  .mis-0 { margin-inline-start: 0; }
  .mis-1 { margin-inline-start: var(--space-1); }
  .mis-2 { margin-inline-start: var(--space-2); }
  .mis-3 { margin-inline-start: var(--space-3); }
  .mis-4 { margin-inline-start: var(--space-4); }
  .mis-6 { margin-inline-start: var(--space-6); }
  .mis-8 { margin-inline-start: var(--space-8); }
  .mie-0 { margin-inline-end: 0; }
  .mie-1 { margin-inline-end: var(--space-1); }
  .mie-2 { margin-inline-end: var(--space-2); }
  .mie-3 { margin-inline-end: var(--space-3); }
  .mie-4 { margin-inline-end: var(--space-4); }
  .mie-6 { margin-inline-end: var(--space-6); }
  .mie-8 { margin-inline-end: var(--space-8); }


  .pis-0 { padding-inline-start: 0; }
  .pis-1 { padding-inline-start: var(--space-1); }
  .pis-2 { padding-inline-start: var(--space-2); }
  .pis-3 { padding-inline-start: var(--space-3); }
  .pis-4 { padding-inline-start: var(--space-4); }
  .pis-6 { padding-inline-start: var(--space-6); }
  .pis-8 { padding-inline-start: var(--space-8); }
  .pie-0 { padding-inline-end: 0; }
  .pie-1 { padding-inline-end: var(--space-1); }
  .pie-2 { padding-inline-end: var(--space-2); }
  .pie-3 { padding-inline-end: var(--space-3); }
  .pie-4 { padding-inline-end: var(--space-4); }
  .pie-6 { padding-inline-end: var(--space-6); }
  .pie-8 { padding-inline-end: var(--space-8); }

  .pbs-0 { padding-block-start: 0; }
  .pbs-1 { padding-block-start: var(--space-1); }
  .pbs-2 { padding-block-start: var(--space-2); }
  .pbs-3 { padding-block-start: var(--space-3); }
  .pbs-4 { padding-block-start: var(--space-4); }
  .pbs-6 { padding-block-start: var(--space-6); }
  .pbs-8 { padding-block-start: var(--space-8); }
  .pbe-0 { padding-block-end: 0; }
  .pbe-1 { padding-block-end: var(--space-1); }
  .pbe-2 { padding-block-end: var(--space-2); }
  .pbe-3 { padding-block-end: var(--space-3); }
  .pbe-4 { padding-block-end: var(--space-4); }
  .pbe-6 { padding-block-end: var(--space-6); }
  .pbe-8 { padding-block-end: var(--space-8); }

  .bis { border-inline-start: 1px solid var(--line); }
  .bie { border-inline-end: 1px solid var(--line); }

  .min-is-0 { min-inline-size: 0; }

  .inset-is-0 { inset-inline-start: 0; }
  .inset-ie-0 { inset-inline-end: 0; }
  .inset-bs-0 { inset-block-start: 0; }
  .inset-be-0 { inset-block-end: 0; }

  /* Radius on the leading and trailing edges, not left and right */
  .r-start { border-start-start-radius: var(--r-md); border-end-start-radius: var(--r-md); }
  .r-end { border-start-end-radius: var(--r-md); border-end-end-radius: var(--r-md); }

  /* --- Writing mode --------------------------------------------------------
     Vertical text for a table header that would otherwise need a 40 character
     column, or for a sidebar label.
     ---------------------------------------------------------------------- */
  .writing-vertical {
    writing-mode: vertical-rl;
    text-orientation: mixed;
  }
  .writing-vertical-lr { writing-mode: vertical-lr; }
  .writing-upright { text-orientation: upright; letter-spacing: .1em; }
  .writing-sideways { writing-mode: sideways-lr; }
  .writing-normal { writing-mode: horizontal-tb; }

  /* A rotated column header that still measures and aligns correctly */
  .th-vertical {
    writing-mode: vertical-rl;
    rotate: 180deg;
    white-space: nowrap;
    block-size: 8rem;
    padding-block: var(--space-3);
    text-align: start;
  }

  /* --- Explicit direction --------------------------------------------------
     Sometimes a value has one true direction regardless of the page. An order
     number, an account number, a URL, and a code block read left to right even
     on an Arabic page. Isolate them so they do not scramble the text around
     them.
     ---------------------------------------------------------------------- */
  .dir-ltr { direction: ltr; unicode-bidi: isolate; }
  .dir-rtl { direction: rtl; unicode-bidi: isolate; }
  .bidi-isolate { unicode-bidi: isolate; }
  .bidi-plaintext { unicode-bidi: plaintext; }

  /* Identifiers read left to right in every language: an order number, a commit
     hash, an API key, a part code. Keep them LTR wherever they appear, and keep
     them from scrambling the text around them. */
  .mono, code, kbd, samp, .nums { unicode-bidi: isolate; }
  .code-ltr { direction: ltr; unicode-bidi: isolate; text-align: start; }
}

@layer deck.rtl {

  /* --- Icons that point ----------------------------------------------------
     An arrow, a chevron, a reply glyph, and a back button all mean "the
     direction of reading". They must mirror. A checkmark, a clock, a wrench,
     and a logo must not. Mark the ones that point.
     ---------------------------------------------------------------------- */
  [dir="rtl"] .mirror-rtl { scale: -1 1; }

  /* The pointing icons in Deck's own sprite, mirrored automatically */
  [dir="rtl"] :is(
    .icon:has(use[href$="#chevron-left"]),
    .icon:has(use[href$="#chevron-right"]),
    .icon:has(use[href$="#arrow-left"]),
    .icon:has(use[href$="#arrow-right"]),
    .icon:has(use[href$="#arrow-up-right"]),
    .icon:has(use[href$="#external"]),
    .icon:has(use[href$="#log-out"]),
    .icon:has(use[href$="#send"]),
    .icon:has(use[href$="#trend-up"]),
    .icon:has(use[href$="#trend-down"])
  ) { scale: -1 1; }

  /* Things that must never mirror, even inside a flipped ancestor */
  [dir="rtl"] .no-flip { scale: 1 1; }

  /* --- Direction-aware motion ---------------------------------------------
     The entrance animations move toward the reader. In RTL the start edge is
     on the right, so the keyframes flip too.
     ---------------------------------------------------------------------- */
  @media (prefers-reduced-motion: no-preference) {
    [dir="rtl"] .enter-start { animation-name: dk-in-start-rtl; }
    [dir="rtl"] .enter-end { animation-name: dk-in-end-rtl; }
    @keyframes dk-in-start-rtl { from { opacity: 0; translate: calc(var(--travel) * 1.6) 0; } }
    @keyframes dk-in-end-rtl { from { opacity: 0; translate: calc(var(--travel) * -1.6) 0; } }

    [dir="rtl"] .icon-follow { transition: translate var(--dur-2) var(--ease-spring); }
    [dir="rtl"] .btn:hover .icon-follow,
    [dir="rtl"] .list-row:hover .icon-follow,
    [dir="rtl"] .card-link:hover .icon-follow { translate: -3px 0; }

    [dir="rtl"] .sweep { background-position: 100% 100%; }
    [dir="rtl"] .marquee-track { animation-name: dk-marquee-rtl; }
    @keyframes dk-marquee-rtl { to { translate: 100% 0; } }
  }

  /* --- Components that hard-code a direction ------------------------------- */

  /* The select arrow is drawn with background-position from the right edge */
  [dir="rtl"] .select {
    background-position: 19px calc(50% + 1px), 14px calc(50% + 1px);
    background-image: linear-gradient(135deg, transparent 50%, currentColor 50%),
                      linear-gradient(45deg, currentColor 50%, transparent 50%);
    padding-inline: var(--space-10) var(--space-3);
  }

  /* Search icon sits on the start edge, which is the right in RTL */
  [dir="rtl"] .search > .icon { inset-inline-start: var(--space-3); }

  /* Pinned grid columns and their shadows. Which edge is "start" is already
     logical in both branches of 12-datagrid; only the offsets are physical,
     so mirroring the four variables flips the scroll-state path and the
     class-based fallback at the same time. */
  [dir="rtl"] .dg-wrap {
    --dg-pin-shadow-start: -6px 0 8px -6px oklch(20% .03 var(--hue-neutral) / .28);
    --dg-pin-shadow-end: 6px 0 8px -6px oklch(20% .03 var(--hue-neutral) / .28);
    --dg-pin-clip-start: inset(0 0 0 -12px);
    --dg-pin-clip-end: inset(0 -12px 0 0);
  }

  /* Switch knob travels toward the end edge */
  [dir="rtl"] .switch input::after { translate: 0 0; }
  [dir="rtl"] .switch input:checked::after { translate: -18px 0; }

  /* Toast swipe and chart bars grow from the start edge, and transform-origin
     has no logical form.

     FINDINGS 59: .progress::-webkit-progress-value is also in this list and
     does nothing. A pseudo-element is not valid inside :is(), and :is() parses
     forgivingly -- the browser drops the invalid item and keeps the rest, so
     this rule serialises as [dir="rtl"] :is(.chart-bar-fill, .toast-timer).
     No warning, no failed rule, no visual difference. It is harmless here
     because the progress fill animates inline-size rather than a transform,
     and because the engine mirrors <progress> under dir="rtl" on its own.

     Due for deletion. If a vendor pseudo-element ever does need a logical
     flip, it has to be its own rule -- grouping it will fail silently. */
  [dir="rtl"] :is(.chart-bar-fill, .progress::-webkit-progress-value, .toast-timer) {
    transform-origin: right center;
  }
  [dir="rtl"] .scroll-progress { transform-origin: right center; }

  /* Breadcrumb separator */
  [dir="rtl"] .breadcrumb li + li::before { content: "\\\\"; }

  /* Timeline rail and dots */
  [dir="rtl"] .timeline-item:not(:last-child)::before { inset-inline-start: 10px; }

  /* Mixed-direction pages: a block that is LTR inside an RTL document */
  [dir="ltr"] [dir="rtl"], [dir="rtl"] [dir="ltr"] { unicode-bidi: isolate; }
}

/* ==========================================================================
   Deck — 20 Gradients
   Every gradient here is built from the brand hue, so the hue slider retunes
   them along with everything else. Interpolation is in oklab or oklch, which
   avoids the grey dead zone you get when sRGB blends two saturated colors.
   ========================================================================== */

@layer deck.effects {

  /* --- Surface washes ------------------------------------------------------
     Quiet. These sit behind content and must never fight it.
     ---------------------------------------------------------------------- */
  .g-surface {
    background-image: linear-gradient(in oklab 180deg,
      color-mix(in oklab, var(--brand-500) 7%, transparent),
      transparent 60%);
  }
  .g-sunken {
    background-image: linear-gradient(in oklab 180deg,
      transparent,
      color-mix(in oklab, var(--ink-900) 5%, transparent));
  }
  .g-brand {
    background-image: linear-gradient(in oklab 135deg,
      var(--brand-600), oklch(from var(--brand-600) l c calc(h + 34)));
    color: var(--text-on-brand);
  }
  .g-brand-soft {
    background-image: linear-gradient(in oklab 135deg,
      var(--brand-soft), color-mix(in oklab, var(--brand-soft) 40%, var(--surface)));
  }
  .g-dark {
    background-image: linear-gradient(in oklab 160deg, var(--ink-900), var(--ink-1000));
    color: var(--ink-100);
  }

  /* --- Aurora / mesh -------------------------------------------------------
     Three soft radial blooms. No image, no SVG filter, no blur — blur on a
     large area is expensive and this reads the same.
     ---------------------------------------------------------------------- */
  .g-mesh {
    background-color: var(--bg);
    background-image:
      radial-gradient(closest-side at 18% 22%,
        oklch(72% .14 var(--hue-brand) / .30), transparent),
      radial-gradient(closest-side at 82% 12%,
        oklch(76% .13 calc(var(--hue-brand) + 62) / .24), transparent),
      radial-gradient(closest-side at 62% 88%,
        oklch(70% .12 calc(var(--hue-brand) - 48) / .22), transparent);
    background-size: 70% 70%, 60% 60%, 78% 78%;
    background-repeat: no-repeat;
  }
  .g-mesh-warm {
    --hue-brand: 34;
  }
  .g-mesh-subtle {
    background-image:
      radial-gradient(closest-side at 20% 0%, oklch(74% .10 var(--hue-brand) / .18), transparent),
      radial-gradient(closest-side at 90% 30%, oklch(78% .09 calc(var(--hue-brand) + 70) / .14), transparent);
    background-size: 80% 80%, 70% 70%;
    background-repeat: no-repeat;
  }

  /* Slow drift for a hero. Off under reduced motion. */
  @media (prefers-reduced-motion: no-preference) {
    .g-mesh-drift {
      animation: g-drift 22s var(--ease-in-out) infinite alternate;
    }
    @keyframes g-drift {
      to { background-position: 8% 6%, -6% 10%, 4% -8%; }
    }
  }

  /* --- Text gradients ------------------------------------------------------
     background-clip on the text. Always keep a real color underneath so the
     text is still readable if the clip fails.
     ---------------------------------------------------------------------- */
  .g-text {
    color: var(--brand-700);
    background-image: linear-gradient(in oklab 92deg,
      var(--brand-600), oklch(from var(--brand-500) l c calc(h + 52)));
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
    /* keeps descenders from clipping in some engines */
    padding-block-end: .06em;
  }
  .g-text-shine {
    color: var(--text);
    background-image: linear-gradient(in oklab 92deg,
      var(--text) 0 40%, var(--brand-400) 50%, var(--text) 60% 100%);
    background-size: 260% 100%;
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
  }
  @media (prefers-reduced-motion: no-preference) {
    .g-text-shine { animation: g-shine 4.5s var(--ease-in-out) infinite; }
    @keyframes g-shine { to { background-position: -160% 0; } }
  }

  /* --- Gradient borders ----------------------------------------------------
     Two backgrounds with different clip boxes. Works with any border-radius,
     unlike border-image, and leaves the inside a real surface color.
     ---------------------------------------------------------------------- */
  .g-border {
    --g-width: 1.5px;
    border: var(--g-width) solid transparent;
    background:
      linear-gradient(var(--surface), var(--surface)) padding-box,
      linear-gradient(in oklab var(--g-angle, 135deg),
        var(--brand-500), oklch(from var(--brand-400) l c calc(h + 70))) border-box;
  }
  .g-border-soft {
    background:
      linear-gradient(var(--surface), var(--surface)) padding-box,
      linear-gradient(in oklab 160deg,
        color-mix(in oklab, var(--brand-400) 60%, transparent),
        transparent 70%) border-box;
  }
  /* The angle is a registered property, so it can animate */
  @media (prefers-reduced-motion: no-preference) {
    .g-border-spin { animation: g-spin 6s linear infinite; }
    @keyframes g-spin { to { --g-angle: 495deg; } }
  }

  /* A single glowing hairline along the top edge — good for a nav or a modal */
  .g-hairline {
    position: relative;
  }
  .g-hairline::before {
    content: "";
    position: absolute;
    inset-block-start: 0;
    inset-inline: 0;
    block-size: 1px;
    background: linear-gradient(90deg, transparent,
      color-mix(in oklab, var(--brand-400) 70%, transparent) 30% 70%, transparent);
  }

  /* --- Scrims ---------------------------------------------------------------
     A caption over a photo needs a legible floor. A flat 50% black overlay
     dulls the whole image; an eased scrim only darkens where the text sits.
     ---------------------------------------------------------------------- */
  .g-scrim {
    position: relative;
    isolation: isolate;
  }
  .g-scrim::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    background: var(--scrim);
    pointer-events: none;
  }
  .g-scrim-top::after {
    background: linear-gradient(to bottom,
      oklch(12% .02 var(--hue-neutral) / .72), transparent 62%);
  }
  .g-scrim > * { position: relative; }

  /* --- Fade masks ----------------------------------------------------------
     Content that runs off the edge should fade, not be cut with a hard line.
     ---------------------------------------------------------------------- */
  .g-fade-inline {
    mask-image: linear-gradient(90deg, transparent, #000 var(--fade, 4%),
                                #000 calc(100% - var(--fade, 4%)), transparent);
    -webkit-mask-image: linear-gradient(90deg, transparent, #000 var(--fade, 4%),
                                #000 calc(100% - var(--fade, 4%)), transparent);
  }
  .g-fade-end {
    mask-image: linear-gradient(90deg, #000 70%, transparent);
    -webkit-mask-image: linear-gradient(90deg, #000 70%, transparent);
  }
  .g-fade-block {
    mask-image: linear-gradient(180deg, #000 60%, transparent);
    -webkit-mask-image: linear-gradient(180deg, #000 60%, transparent);
  }
  /* Long text that is collapsed: fade the bottom instead of a hard cut */
  .g-fade-more {
    max-block-size: var(--collapsed, 9rem);
    overflow: hidden;
    mask-image: linear-gradient(180deg, #000 65%, transparent);
    -webkit-mask-image: linear-gradient(180deg, #000 65%, transparent);
  }
  .g-fade-more.is-open {
    max-block-size: none;
    mask-image: none;
    -webkit-mask-image: none;
  }
  .scroller { mask-image: linear-gradient(90deg, #000 92%, transparent); -webkit-mask-image: linear-gradient(90deg, #000 92%, transparent); }

  /* --- Sheen ---------------------------------------------------------------
     A highlight that sweeps across on hover. --sheen is registered, so it
     animates as a real percentage instead of jumping.
     ---------------------------------------------------------------------- */
  .g-sheen {
    position: relative;
    overflow: hidden;
    isolation: isolate;
  }
  .g-sheen::after {
    content: "";
    position: absolute;
    inset-block: 0;
    inset-inline-start: var(--sheen);
    inline-size: 40%;
    background: linear-gradient(105deg, transparent,
      oklch(100% 0 0 / .32) 45%, oklch(100% 0 0 / .32) 55%, transparent);
    pointer-events: none;
    transition: --sheen var(--dur-5) var(--ease-out);
  }
  @media (prefers-reduced-motion: no-preference) {
    .g-sheen:hover::after { --sheen: 110%; }
  }

  /* --- Status gradients ----------------------------------------------------- */
  .g-good { background-image: linear-gradient(in oklab 135deg, var(--good-600), oklch(from var(--good-600) l c calc(h - 26))); color: var(--text-on-good); }
  .g-warn { background-image: linear-gradient(in oklab 135deg, var(--warn-500), oklch(from var(--warn-500) l c calc(h + 22))); color: var(--text-on-warn); }
  .g-bad  { background-image: linear-gradient(in oklab 135deg, var(--bad-600), oklch(from var(--bad-600) l c calc(h - 18))); color: var(--text-on-bad); }

  /* --- Conic accents -------------------------------------------------------- */
  .g-conic {
    background-image: conic-gradient(from var(--g-angle, 0deg) in oklch longer hue,
      oklch(70% .16 0), oklch(70% .16 360));
  }
  .g-ring {
    --thickness: 3px;
    border-radius: var(--r-full);
    background: conic-gradient(from var(--g-angle, 0deg),
      transparent, var(--brand-500), transparent 62%);
    mask: radial-gradient(farthest-side, transparent calc(100% - var(--thickness)), #000 calc(100% - var(--thickness) + 1px));
    -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - var(--thickness)), #000 calc(100% - var(--thickness) + 1px));
  }
  @media (prefers-reduced-motion: no-preference) {
    .g-ring-spin { animation: g-spin-360 1.6s linear infinite; }
    @keyframes g-spin-360 { to { --g-angle: 360deg; } }
  }

  /* --- Structural patterns --------------------------------------------------
     Not gradients as decoration — gradients standing in for an image. These
     cost nothing to download and retint automatically.
     ---------------------------------------------------------------------- */
  .g-grid-lines {
    background-image:
      linear-gradient(to right, var(--line) 1px, transparent 1px),
      linear-gradient(to bottom, var(--line) 1px, transparent 1px);
    background-size: var(--cell, 24px) var(--cell, 24px);
  }
  .g-dots {
    background-image: radial-gradient(var(--line-strong) 1px, transparent 1px);
    background-size: var(--cell, 18px) var(--cell, 18px);
  }
  .g-stripes {
    background-image: repeating-linear-gradient(45deg,
      color-mix(in oklab, var(--ink-500) 8%, transparent) 0 6px,
      transparent 6px 12px);
  }
  /* Hatching for a "pending" or "unavailable" block in a schedule */
  .g-hatch {
    background-image: repeating-linear-gradient(-45deg,
      color-mix(in oklab, var(--warn-500) 22%, transparent) 0 5px,
      transparent 5px 10px);
  }

  /* --- Skeleton and shimmer, unified with the palette ---------------------- */
  .g-shimmer {
    background-image: linear-gradient(100deg,
      transparent 20%,
      color-mix(in oklab, var(--brand-200) 55%, transparent) 45%,
      transparent 70%);
    background-size: 250% 100%;
    background-repeat: no-repeat;
  }
  @media (prefers-reduced-motion: no-preference) {
    .g-shimmer { animation: dk-shimmer 1.6s var(--ease-in-out) infinite; }
  }

  /* --- Chart fills ---------------------------------------------------------
     Area charts read better with a gradient falling off to nothing.
     ---------------------------------------------------------------------- */
  .chart-area-g {
    fill: url(#deck-area-gradient);
    stroke: none;
  }
  .g-bar-fill {
    background-image: linear-gradient(in oklab 180deg,
      var(--series, var(--c1)),
      color-mix(in oklab, var(--series, var(--c1)) 70%, var(--ink-900)));
  }

  /* --- Fallback ------------------------------------------------------------
     Engines without oklch get the plain brand color rather than a broken
     gradient. The declarations above simply do not apply.
     ---------------------------------------------------------------------- */
  @supports not (color: oklch(50% .1 200)) {
    .g-brand, .g-good, .g-warn, .g-bad { background-image: none; background-color: var(--brand-600); }
    .g-text { -webkit-text-fill-color: currentColor; background-image: none; }
    .g-mesh { background-image: none; }
  }
}

/* ==========================================================================
   Deck — 21 Space (3D transforms)
   3D earns its place when depth carries meaning: a card that has two sides, a
   stack you are working down through, a control that physically depresses.
   It does not earn its place as decoration on a landing page.

   Every transform here uses translate / rotate / scale as individual
   properties rather than the transform shorthand, so two effects on the same
   element compose instead of overwriting each other.
   ========================================================================== */

@layer deck.effects {

  /* --- Scene ---------------------------------------------------------------
     A scene establishes the vanishing point. Children need preserve-3d to
     stay in the same space instead of being flattened.
     ---------------------------------------------------------------------- */
  .scene {
    perspective: var(--perspective, 1000px);
    perspective-origin: var(--vanish, 50% 50%);
  }
  .scene-near { --perspective: 560px; }
  .scene-far { --perspective: 2000px; }
  .space { transform-style: preserve-3d; }
  .flat { transform-style: flat; }
  .backface-hidden { backface-visibility: hidden; }

  /* --- Flip card -----------------------------------------------------------
     Two faces, one footprint. Front and back are stacked in the same grid
     cell so the card is exactly as tall as its taller side.
     ---------------------------------------------------------------------- */
  .flip {
    display: grid;
    transform-style: preserve-3d;
    transition: rotate var(--dur-4) var(--ease-spring);
  }
  .flip > .flip-front, .flip > .flip-back {
    grid-area: 1 / 1;
    backface-visibility: hidden;
    -webkit-backface-visibility: hidden;
  }
  .flip > .flip-back { rotate: y 180deg; }
  .flip.is-flipped { rotate: y 180deg; }
  .flip-x { transition: rotate var(--dur-4) var(--ease-spring); }
  .flip-x > .flip-back { rotate: x 180deg; }
  .flip-x.is-flipped { rotate: x 180deg; }

  /* Hover flip for pointer devices only — on touch it needs a real tap */
  @media (hover: hover) and (prefers-reduced-motion: no-preference) {
    .flip-hover:hover { rotate: y 180deg; }
  }

  /* --- Tilt ----------------------------------------------------------------
     deck.js writes --tilt-x and --tilt-y from the pointer position. Both are
     registered angles, so they interpolate smoothly back to rest.
     ---------------------------------------------------------------------- */
  .tilt {
    transform-style: preserve-3d;
    rotate: var(--tilt-axis, 1 1 0) var(--tilt-amount, 0deg);
    transition: rotate var(--dur-3) var(--ease-out), scale var(--dur-2) var(--ease-out);
    will-change: auto;
  }
  @media (hover: hover) and (prefers-reduced-motion: no-preference) {
    .tilt.is-tilting { transition: rotate 80ms linear; }
    .tilt:hover { scale: 1.015; }
    /* Content that should float above the card face while it tilts */
    .tilt .tilt-lift { transform: translateZ(var(--depth, 28px)); }
    .tilt .tilt-lift-sm { transform: translateZ(12px); }
  }

  /* --- Depth stack ---------------------------------------------------------
     A pile of records you are working through. The one on top is live, the
     ones behind are context. --i is the child's own position, 0 for the top
     card; deck.js parks a dismissed card at the end so the rest close up.
     ---------------------------------------------------------------------- */
  @supports (animation-delay: calc(sibling-index() * 1ms)) {
    .pile > * { --i: calc(sibling-index() - 1); }
  }
  @supports not (animation-delay: calc(sibling-index() * 1ms)) {
    .pile > :nth-child(1)   { --i: 0; }
    .pile > :nth-child(2)   { --i: 1; }
    .pile > :nth-child(3)   { --i: 2; }
    .pile > :nth-child(4)   { --i: 3; }
    .pile > :nth-child(5)   { --i: 4; }
    .pile > :nth-child(6)   { --i: 5; }
    .pile > :nth-child(7)   { --i: 6; }
    .pile > :nth-child(8)   { --i: 7; }
  }
  .pile {
    display: grid;
    transform-style: preserve-3d;
    perspective: 1200px;
  }
  .pile > * {
    grid-area: 1 / 1;
    translate: 0 calc(var(--i, 0) * 10px);
    scale: calc(1 - var(--i, 0) * .045);
    z-index: calc(10 - var(--i, 0));
    filter: brightness(calc(1 - var(--i, 0) * .04));
    transition: translate var(--dur-3) var(--ease-out),
                scale var(--dur-3) var(--ease-out),
                rotate var(--dur-3) var(--ease-out),
                opacity var(--dur-2) var(--ease-out);
  }
  .pile > :nth-child(n+4) { opacity: 0; pointer-events: none; }

  /* Fan the pile out to see what is in it */
  .pile.is-fanned > * {
    translate: calc(var(--i, 0) * 14px) calc(var(--i, 0) * 22px);
    rotate: calc(var(--i, 0) * 2.5deg);
    scale: 1;
    filter: none;
  }
  .pile.is-fanned > :nth-child(n+4):not(.is-dismissed) { opacity: 1; pointer-events: auto; }

  /* Dismiss the top card away from the pile */
  .pile > .is-dismissed {
    translate: 120% -20px;
    rotate: 14deg;
    opacity: 0;
  }

  /* --- Coverflow -----------------------------------------------------------
     A horizontal scroller where the off-center items rotate away. Uses scroll
     snap for the mechanics and 3D only for the read.
     ---------------------------------------------------------------------- */
  .coverflow {
    display: flex;
    gap: var(--space-4);
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    perspective: 1200px;
    padding-block: var(--space-6);
    padding-inline: max(var(--space-gutter), 20%);
    scrollbar-width: none;
    overscroll-behavior-inline: contain;
  }
  .coverflow::-webkit-scrollbar { display: none; }
  .coverflow > * {
    flex: 0 0 auto;
    scroll-snap-align: center;
    transform-style: preserve-3d;
    transition: rotate var(--dur-3) var(--ease-out), scale var(--dur-3) var(--ease-out);
  }
  /* deck.js marks the centered item; without JS everything simply sits flat */
  .coverflow > .is-behind-start { rotate: y 32deg; scale: .88; }
  .coverflow > .is-behind-end { rotate: y -32deg; scale: .88; }
  .coverflow > .is-front { rotate: y 0deg; scale: 1; }

  /* --- Cube ----------------------------------------------------------------
     Six faces for a rotating status block or an empty-state illustration.
     ---------------------------------------------------------------------- */
  .cube {
    --size: 120px;
    position: relative;
    inline-size: var(--size);
    block-size: var(--size);
    transform-style: preserve-3d;
    transition: rotate var(--dur-5) var(--ease-spring);
  }
  .cube > * {
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
    border: 1px solid var(--line);
    background: color-mix(in oklab, var(--surface) 88%, transparent);
    backface-visibility: visible;
  }
  .cube > .face-front  { transform: translateZ(calc(var(--size) / 2)); }
  .cube > .face-back   { transform: rotateY(180deg) translateZ(calc(var(--size) / 2)); }
  .cube > .face-end    { transform: rotateY(90deg) translateZ(calc(var(--size) / 2)); }
  .cube > .face-start  { transform: rotateY(-90deg) translateZ(calc(var(--size) / 2)); }
  .cube > .face-top    { transform: rotateX(90deg) translateZ(calc(var(--size) / 2)); }
  .cube > .face-bottom { transform: rotateX(-90deg) translateZ(calc(var(--size) / 2)); }

  .cube[data-face="front"]  { rotate: 0deg; }
  .cube[data-face="end"]    { rotate: y -90deg; }
  .cube[data-face="back"]   { rotate: y 180deg; }
  .cube[data-face="start"]  { rotate: y 90deg; }
  .cube[data-face="top"]    { rotate: x -90deg; }
  .cube[data-face="bottom"] { rotate: x 90deg; }

  @media (prefers-reduced-motion: no-preference) {
    .cube-spin { animation: dk-cube 14s linear infinite; }
    @keyframes dk-cube {
      from { transform: rotateX(-18deg) rotateY(0deg); }
      to   { transform: rotateX(-18deg) rotateY(360deg); }
    }
  }

  /* --- Depressible controls ------------------------------------------------
     A button with real thickness. The face moves down into the shadow instead
     of just changing color.
     ---------------------------------------------------------------------- */
  .btn-3d {
    --lift: 3px;
    position: relative;
    border: 0;
    box-shadow:
      0 var(--lift) 0 0 color-mix(in oklab, var(--brand-600) 62%, var(--ink-900)),
      0 calc(var(--lift) + 3px) 8px oklch(20% .03 var(--hue-neutral) / .22);
    translate: 0 0;
    transition: translate 70ms var(--ease-out), box-shadow 70ms var(--ease-out);
  }
  .btn-3d:active {
    translate: 0 var(--lift);
    box-shadow: 0 0 0 0 color-mix(in oklab, var(--brand-600) 62%, var(--ink-900));
  }

  /* --- Parallax layers -----------------------------------------------------
     True 3D parallax: layers sit at different Z depths inside a perspective
     container, so the browser does the math on the compositor. No scroll
     handler, no jank.
     ---------------------------------------------------------------------- */
  .parallax {
    block-size: 100%;
    overflow-y: auto;
    overflow-x: hidden;
    perspective: 8px;
    perspective-origin: 0 0;
  }
  .parallax-layer {
    position: absolute;
    inset: 0;
    transform-origin: 0 0;
  }
  .parallax-back  { transform: translateZ(-6px) scale(1.75); }
  .parallax-mid   { transform: translateZ(-3px) scale(1.375); }
  .parallax-front { transform: translateZ(0); }

  @media (prefers-reduced-motion: reduce) {
    .parallax { perspective: none; }
    .parallax-layer { transform: none; position: relative; }
  }

  /* --- Page turn -----------------------------------------------------------
     For stepping through a multi-page form or a wizard. Pairs with
     Deck.transition() so the DOM swap and the motion happen together.
     ---------------------------------------------------------------------- */
  @media (prefers-reduced-motion: no-preference) {
    .turn-out { animation: dk-turn-out var(--dur-3) var(--ease-in) both; transform-origin: left center; }
    .turn-in  { animation: dk-turn-in var(--dur-4) var(--ease-out) both; transform-origin: left center; }
    @keyframes dk-turn-out {
      to { transform: perspective(1400px) rotateY(-22deg); opacity: 0; }
    }
    @keyframes dk-turn-in {
      from { transform: perspective(1400px) rotateY(24deg); opacity: 0; }
    }
    [dir="rtl"] :is(.turn-out, .turn-in) { transform-origin: right center; }
  }

  /* --- Layered elevation ---------------------------------------------------
     Lift an element toward the viewer inside a scene, rather than faking it
     with a bigger shadow.
     ---------------------------------------------------------------------- */
  .z-lift-1 { transform: translateZ(8px); }
  .z-lift-2 { transform: translateZ(20px); }
  .z-lift-3 { transform: translateZ(40px); }
  .z-sink   { transform: translateZ(-16px); }

  /* --- Performance guard ---------------------------------------------------
     3D forces layers onto the GPU. Promote only while something is actually
     moving, and hand the memory back when it stops.
     ---------------------------------------------------------------------- */
  .tilt.is-tilting, .flip.is-flipped, .pile.is-fanned > *,
  .coverflow > *, .cube {
    will-change: transform, rotate, scale;
  }
  .tilt:not(.is-tilting), .flip:not(.is-flipped) { will-change: auto; }

  @media (prefers-reduced-motion: reduce) {
    .flip, .flip-x, .tilt, .cube, .pile > *, .coverflow > * {
      transition-duration: 1ms;
      rotate: none;
    }
    .flip.is-flipped, .flip-x.is-flipped { rotate: y 180deg; }
    .coverflow > * { rotate: none; scale: 1; }
  }
}

/* ==========================================================================
   Deck — 22 Navigation and structure
   ========================================================================== */

@layer deck.components {

  /* --- Carousel ------------------------------------------------------------
     Scroll snap does the work. Arrows and dots are progressive enhancement,
     so it still swipes correctly with JavaScript switched off.
     ---------------------------------------------------------------------- */
  .carousel { position: relative; }

  /* The scroll-snap container holding the slides. */
  .carousel-track {
    display: flex;
    gap: var(--gap, var(--space-4));
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    overscroll-behavior-inline: contain;
    scroll-behavior: smooth;
    scrollbar-width: none;
    border-radius: var(--r-md);
  }
  .carousel-track::-webkit-scrollbar { display: none; }
  /* One slide. flex-basis: 100% by default; .carousel-peek makes it 86% so
     the next one is partly visible, which is the only persistent cue that the
     track scrolls once the scrollbar is hidden. */
  .carousel-slide {
    flex: 0 0 100%;
    scroll-snap-align: center;
    scroll-snap-stop: always;
    min-inline-size: 0;
    /* The slide is the containing block for its own .carousel-caption. Without
       this the nearest positioned ancestor is .carousel, so every caption in
       the track lands in the same place and they stack on top of each other. */
    position: relative;
  }
  .carousel-peek .carousel-slide { flex-basis: 86%; }
  .carousel-multi .carousel-slide { flex-basis: calc((100% - var(--space-4) * 2) / 3); }
  @media (max-width: 48rem) {
    .carousel-multi .carousel-slide { flex-basis: 78%; }
  }
  .carousel-slide img { inline-size: 100%; block-size: 100%; object-fit: cover; }

  /* Previous and next. Hidden on a coarse pointer, where the reader swipes
     instead — so do not also hide the dots. */
  .carousel-arrow {
    position: absolute;
    inset-block-start: 50%;
    translate: 0 -50%;
    z-index: 2;
    inline-size: 40px;
    block-size: 40px;
    display: grid;
    place-items: center;
    border-radius: var(--r-full);
    background: color-mix(in oklab, var(--surface) 88%, transparent);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    border: 1px solid var(--line);
    box-shadow: var(--shadow-2);
    color: var(--text);
    cursor: pointer;
    transition: opacity var(--dur-2) var(--ease-out), scale var(--dur-1) var(--ease-out);
  }
  .carousel-arrow:hover { scale: 1.06; }
  .carousel-arrow[disabled] { opacity: 0; pointer-events: none; }
  .carousel-prev { inset-inline-start: var(--space-3); }
  .carousel-next { inset-inline-end: var(--space-3); }
  @media (pointer: coarse) { .carousel-arrow { display: none; } }

  .carousel-dots {
    display: flex;
    justify-content: center;
    gap: var(--space-2);
    padding-block-start: var(--space-3);
  }
  .carousel-dot {
    inline-size: 7px;
    block-size: 7px;
    border-radius: var(--r-full);
    background: var(--ink-300);
    cursor: pointer;
    padding: 0;
    transition: inline-size var(--dur-2) var(--ease-out), background-color var(--dur-2) var(--ease-out);
  }
  .carousel-dot[aria-current="true"] { inline-size: 22px; background: var(--brand-600); }
  /* the tap target is bigger than the dot */
  .carousel-dot::before { content: ""; position: absolute; inline-size: 32px; block-size: 32px; translate: -50% -50%; inset: 50% auto auto 50%; }
  .carousel-dots > * { position: relative; }

  /* --- Native carousel -----------------------------------------------------
     ::scroll-marker gives every slide its own dot and ::scroll-button gives the
     track its arrows, with the active state, the disabled state, the click
     handling and the arrow-key focus order all done by the browser.
     Where these exist, deck-extras.js skips building and wiring the DIY ones
     and .carousel-dots / .carousel-arrow simply step aside -- the markup stays
     exactly as it was. The buttons take the logical keywords rather than left
     and right, so an RTL carousel flips them without a rule in deck.rtl.
     ---------------------------------------------------------------------- */
  @supports selector(::scroll-marker) {
    .carousel-dots, .carousel-arrow { display: none; }

    .carousel-track { scroll-marker-group: after; }
    .carousel-track::scroll-marker-group {
      display: flex;
      justify-content: center;
      gap: var(--space-2);
      padding-block-start: var(--space-3);
    }
    .carousel-slide::scroll-marker {
      content: "";
      inline-size: 7px;
      block-size: 7px;
      border-radius: var(--r-full);
      background: var(--ink-300);
      cursor: pointer;
      transition: inline-size var(--dur-2) var(--ease-out),
                  background-color var(--dur-2) var(--ease-out);
    }
    .carousel-slide::scroll-marker:target-current {
      inline-size: 22px;
      background: var(--brand-600);
    }

    .carousel-track::scroll-button(*) {
      position: absolute;
      inset-block-start: 50%;
      translate: 0 -50%;
      z-index: 2;
      inline-size: 40px;
      block-size: 40px;
      display: grid;
      place-items: center;
      border-radius: var(--r-full);
      background: color-mix(in oklab, var(--surface) 88%, transparent);
      backdrop-filter: blur(8px);
      -webkit-backdrop-filter: blur(8px);
      border: 1px solid var(--line);
      box-shadow: var(--shadow-2);
      color: var(--text);
      font-size: var(--text-xl);
      line-height: 1;
      cursor: pointer;
      transition: opacity var(--dur-2) var(--ease-out), scale var(--dur-1) var(--ease-out);
    }
    .carousel-track::scroll-button(*):hover { scale: 1.06; }
    .carousel-track::scroll-button(*):disabled { opacity: 0; pointer-events: none; }
    .carousel-track::scroll-button(inline-start) {
      content: "\2039" / "Previous slide";
      inset-inline-start: var(--space-3);
    }
    .carousel-track::scroll-button(inline-end) {
      content: "\203A" / "Next slide";
      inset-inline-end: var(--space-3);
    }
    @media (pointer: coarse) {
      .carousel-track::scroll-button(*) { display: none; }
    }
  }

  /* Text over a slide. The generous block-start padding is there to give the
     scrim room to fade — without a background the near-white text sits
     directly on the image and disappears over anything light. */
  .carousel-caption {
    position: absolute;
    inset-inline: 0;
    inset-block-end: 0;
    padding: var(--space-8) var(--space-5) var(--space-5);
    color: oklch(98% 0 0);
    background: var(--scrim);
    pointer-events: none;
  }
  .carousel-caption :is(a, button) { pointer-events: auto; }

  /* --- Drawer --------------------------------------------------------------
     A side panel. The bottom sheet is in 08-mobile; this one comes in from an
     edge and is the right shape for filters, settings, and a mobile nav.
     ---------------------------------------------------------------------- */
  .drawer {
    --drawer-size: min(22rem, 88vw);
    position: fixed;
    inset-block: 0;
    inset-inline-start: 0;
    inset-inline-end: auto;
    margin: 0;
    max-block-size: none;
    block-size: 100dvh;
    inline-size: var(--drawer-size);
    max-inline-size: var(--drawer-size);
    padding: 0;
    border: 0;
    border-inline-end: 1px solid var(--line);
    background: var(--surface);
    color: var(--text);
    box-shadow: var(--shadow-4);
    display: flex;
    flex-direction: column;
    translate: -100% 0;
    transition: translate var(--dur-3) var(--ease-emphasized),
                display var(--dur-3) allow-discrete,
                overlay var(--dur-3) allow-discrete;
  }
  .drawer[open] { translate: 0; }
  @starting-style { .drawer[open] { translate: -100% 0; } }

  /* Pins the drawer to the ending edge instead of the starting one. The inset
     is logical and mirrors for free; the translate is not, which is why this
     rule declares its own. */
  .drawer-end {
    inset-inline: auto 0;
    border-inline: 1px solid var(--line) 0;
    border-inline-start: 1px solid var(--line);
    border-inline-end: 0;
    translate: 100% 0;
  }
  @starting-style { .drawer-end[open] { translate: 100% 0; } }

  .drawer::backdrop {
    background: oklch(18% .02 var(--hue-neutral) / .5);
    opacity: 0;
    transition: opacity var(--dur-3) var(--ease-out),
                display var(--dur-3) allow-discrete,
                overlay var(--dur-3) allow-discrete;
  }
  .drawer[open]::backdrop { opacity: 1; }
  @starting-style { .drawer[open]::backdrop { opacity: 0; } }

  /* The title row. Pads for a notch with max(--space-4, safe-area-inset-top). */
  .drawer-header {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-4) var(--space-5);
    padding-block-start: max(var(--space-4), env(safe-area-inset-top));
    border-block-end: 1px solid var(--line);
    flex: 0 0 auto;
  }
  /* The drawer's heading. Point aria-labelledby at it. */
  .drawer-title { font-size: var(--text-md); font-weight: 640; }
  /* The scrolling region. overscroll-behavior: contain stops a scroll that
     reaches the end from chaining to the page behind. */
  .drawer-body {
    flex: 1 1 auto;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: var(--space-4) var(--space-5);
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
  }
  /* The action row, pinned to the bottom. Pads for the home indicator with
     max(--space-4, safe-area-inset-bottom). */
  .drawer-footer {
    flex: 0 0 auto;
    display: flex;
    gap: var(--space-3);
    padding: var(--space-4) var(--space-5);
    padding-block-end: max(var(--space-4), env(safe-area-inset-bottom));
    border-block-start: 1px solid var(--line);
    background: var(--surface-2);
  }
  .drawer-footer > .btn { flex: 1 1 auto; }

  /* --- Mega menu -----------------------------------------------------------
     A wide panel under a nav item. Built on the popover attribute, so escape
     and outside-click are the browser's job.
     ---------------------------------------------------------------------- */
  .mega {
    position: absolute;
    inset: auto;
    margin: 0;
    inline-size: min(64rem, calc(100vw - var(--space-8)));
    padding: var(--space-5);
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
    box-shadow: var(--shadow-4);
    opacity: 0;
    translate: 0 -8px;
    transition: opacity var(--dur-2) var(--ease-out),
                translate var(--dur-2) var(--ease-out),
                overlay var(--dur-2) allow-discrete,
                display var(--dur-2) allow-discrete;
  }
  .mega:popover-open { opacity: 1; translate: 0; }
  @starting-style { .mega:popover-open { opacity: 0; translate: 0 -8px; } }
  .mega::backdrop { background: transparent; }

  .mega-grid {
    display: grid;
    gap: var(--space-5);
    grid-template-columns: repeat(auto-fit, minmax(min(14rem, 100%), 1fr));
  }
  .mega-col { display: flex; flex-direction: column; gap: var(--space-1); }
  .mega-heading {
    font-size: var(--text-xs);
    font-weight: 700;
    color: var(--text-faint);
    padding: var(--space-2) var(--space-2) var(--space-1);
  }
  .mega-item {
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
    padding: var(--space-2);
    border-radius: var(--r-sm);
    color: var(--text);
    text-decoration: none;
    transition: background-color var(--dur-1) var(--ease-out);
  }
  .mega-item:hover { background: var(--surface-hover); }
  .mega-item-title { font-weight: 560; font-size: var(--text-sm); }
  .mega-item-note { font-size: var(--text-xs); color: var(--text-muted); }
  .mega-feature {
    grid-column: span 1;
    padding: var(--space-4);
    border-radius: var(--r-sm);
    background: var(--brand-soft);
    color: var(--brand-soft-text);
  }
  .mega-footer {
    display: flex;
    gap: var(--space-4);
    flex-wrap: wrap;
    margin-block-start: var(--space-5);
    padding-block-start: var(--space-4);
    border-block-start: 1px solid var(--line);
    font-size: var(--text-sm);
  }

  /* --- Speed dial ----------------------------------------------------------
     A FAB that fans out into related actions.
     ---------------------------------------------------------------------- */
  .speed-dial {
    position: fixed;
    inset-block-end: calc(var(--space-5) + env(safe-area-inset-bottom));
    inset-inline-end: var(--space-5);
    z-index: var(--z-nav);
    display: flex;
    flex-direction: column-reverse;
    align-items: flex-end;
    gap: var(--space-3);
  }
  body:has(.tabbar) .speed-dial {
    inset-block-end: calc(var(--space-4) + var(--tap) + var(--space-3) + env(safe-area-inset-bottom));
  }
  @media (min-width: 48rem) {
    body:has(.tabbar) .speed-dial { inset-block-end: calc(var(--space-5) + env(safe-area-inset-bottom)); }
  }

  .speed-dial-actions {
    display: flex;
    flex-direction: column-reverse;
    align-items: flex-end;
    gap: var(--space-3);
  }
  @supports (animation-delay: calc(sibling-index() * 1ms)) {
    .speed-dial-action { --i: calc(sibling-index() - 1); }
  }
  @supports not (animation-delay: calc(sibling-index() * 1ms)) {
    .speed-dial-action:nth-child(1)   { --i: 0; }
    .speed-dial-action:nth-child(2)   { --i: 1; }
    .speed-dial-action:nth-child(3)   { --i: 2; }
    .speed-dial-action:nth-child(4)   { --i: 3; }
    .speed-dial-action:nth-child(5)   { --i: 4; }
    .speed-dial-action:nth-child(6)   { --i: 5; }
    .speed-dial-action:nth-child(7)   { --i: 6; }
    .speed-dial-action:nth-child(8)   { --i: 7; }
  }
  .speed-dial-action {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    opacity: 0;
    translate: 0 12px;
    scale: .85;
    pointer-events: none;
    transition: opacity var(--dur-2) var(--ease-out),
                translate var(--dur-2) var(--ease-spring),
                scale var(--dur-2) var(--ease-spring);
    transition-delay: calc(var(--i, 0) * 35ms);
  }
  .speed-dial.is-open .speed-dial-action {
    opacity: 1;
    translate: 0;
    scale: 1;
    pointer-events: auto;
  }
  .speed-dial-label {
    padding: var(--space-1) var(--space-3);
    border-radius: var(--r-sm);
    background: var(--ink-900);
    color: var(--ink-50);
    font-size: var(--text-xs);
    font-weight: 560;
    white-space: nowrap;
    box-shadow: var(--shadow-2);
  }
  .speed-dial-button {
    inline-size: 42px;
    block-size: 42px;
    border-radius: var(--r-full);
    display: grid;
    place-items: center;
    background: var(--surface);
    border: 1px solid var(--line);
    color: var(--text);
    box-shadow: var(--shadow-3);
    cursor: pointer;
  }
  .speed-dial-button:hover { background: var(--surface-hover); }
  .speed-dial > .fab { position: static; }
  .speed-dial > .fab .icon { transition: rotate var(--dur-3) var(--ease-spring); }
  .speed-dial.is-open > .fab .icon { rotate: 135deg; }

  /* --- Back to top ---------------------------------------------------------
     A link, not a button. #top is a real target at the head of the document,
     so the browser moves focus there along with the scroll. A button calling
     scrollTo() leaves a keyboard user parked at the bottom of the page while
     looking at the top of it, which is the whole bug this component usually
     ships with.

     Showing and hiding is a scroll-driven animation, so there is no scroll
     listener anywhere -- same mechanism as .scroll-progress in 16-motion.
     visibility is in the keyframe on purpose: it takes the link out of the tab
     order and out of the accessibility tree while it is off screen, which is
     what aria-hidden reaches for and which CSS can actually do by itself.
     deck.js only steps in where animation-timeline is missing, and it uses an
     IntersectionObserver on a sentinel, not a scroll handler.
     ---------------------------------------------------------------------- */
  .back-to-top {
    position: fixed;
    inset-block-end: calc(var(--space-5) + env(safe-area-inset-bottom));
    inset-inline-end: var(--space-5);
    /* One above the rest of the bottom chrome, so it stays clickable whatever
       order the markup happens to be in. Same token, same band. */
    z-index: calc(var(--z-nav) + 1);
    inline-size: var(--tap);
    block-size: var(--tap);
    border-radius: var(--r-full);
    display: grid;
    place-items: center;
    background: var(--surface);
    color: var(--text);
    border: 1px solid var(--line);
    box-shadow: var(--shadow-3);
    text-decoration: none;
    transition: opacity var(--dur-2) var(--ease-out),
                visibility var(--dur-2) var(--ease-out),
                background-color var(--dur-2) var(--ease-out),
                border-color var(--dur-2) var(--ease-out),
                scale var(--dur-2) var(--ease-spring);
  }
  .back-to-top:hover {
    background: var(--brand-600);
    color: var(--text-on-brand);
    border-color: transparent;
    scale: 1.06;
  }
  .back-to-top:active { scale: .94; }

  /* Stack above whatever else is parked in that corner. .fab and .speed-dial
     both sit at inset-block-end: var(--space-5) and both already lift over a
     tab bar, so back-to-top repeats their arithmetic one storey higher. 56px
     is the FAB's own size from 05-buttons; it has no token, so neither does
     this. Every branch keeps env(safe-area-inset-bottom) on the end. */
  body:has(:is(.fab, .speed-dial)) .back-to-top {
    inset-block-end: calc(var(--space-5) + 56px + var(--space-3) + env(safe-area-inset-bottom));
  }
  body:has(.tabbar) .back-to-top {
    inset-block-end: calc(var(--space-4) + var(--tap) + var(--space-3) + env(safe-area-inset-bottom));
  }
  body:has(.tabbar):has(:is(.fab, .speed-dial)) .back-to-top {
    inset-block-end: calc(var(--space-4) + var(--tap) + var(--space-3) + 56px + var(--space-3) + env(safe-area-inset-bottom));
  }
  @media (min-width: 48rem) {
    /* The tab bar is display:none at this width, so the offsets collapse back */
    body:has(.tabbar) .back-to-top {
      inset-block-end: calc(var(--space-5) + env(safe-area-inset-bottom));
    }
    body:has(.tabbar):has(:is(.fab, .speed-dial)) .back-to-top {
      inset-block-end: calc(var(--space-5) + 56px + var(--space-3) + env(safe-area-inset-bottom));
    }
  }

  /* A speed dial fans out across the band the button sits in -- its collapsed
     actions still take their layout space, so the corner is the dial's while it
     is open. display is the one property the reveal animation does not touch,
     so this wins the cascade without !important. */
  body:has(.speed-dial.is-open) .back-to-top { display: none; }

  @media (prefers-reduced-motion: no-preference) {
    @supports (animation-timeline: scroll()) {
      .back-to-top {
        animation: dk-btt-in linear both;
        animation-timeline: scroll(root block);
        animation-range: 400px 520px;
      }
    }
    /* No scroll timeline: deck.js marks the link with data-deck-btt and toggles
       .is-visible from an IntersectionObserver on a sentinel at the top of the
       page. Unmarked -- no JS -- the link simply stays visible and still works. */
    @supports not (animation-timeline: scroll()) {
      .back-to-top[data-deck-btt] { opacity: 0; visibility: hidden; }
      .back-to-top[data-deck-btt].is-visible { opacity: 1; visibility: visible; }
    }
  }

  /* Under prefers-reduced-motion: reduce neither branch above applies, so the
     link is simply always there -- no entrance, and 02-reset has already put
     html back to scroll-behavior: auto, so the jump is instant. */
  @keyframes dk-btt-in {
    from { opacity: 0; visibility: hidden; }
    to   { opacity: 1; visibility: visible; }
  }

  /* --- Stepper -------------------------------------------------------------
     A wizard. The timeline in 07 records what happened; this one shows where
     you are in a process you are actively moving through.
     ---------------------------------------------------------------------- */
  .stepper {
    display: flex;
    gap: var(--space-2);
    overflow-x: auto;
    scrollbar-width: none;
    counter-reset: step;
  }
  .stepper::-webkit-scrollbar { display: none; }
  .step {
    flex: 1 1 0;
    min-inline-size: 7rem;
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    position: relative;
    counter-increment: step;
  }
  .step-marker {
    display: flex;
    align-items: center;
    gap: var(--space-2);
  }
  .step-marker::before {
    content: counter(step);
    inline-size: 26px;
    block-size: 26px;
    flex: 0 0 auto;
    display: grid;
    place-items: center;
    border-radius: var(--r-full);
    border: 1.5px solid var(--line-strong);
    background: var(--surface);
    font-size: var(--text-xs);
    font-weight: 640;
    color: var(--text-muted);
    transition: background-color var(--dur-2) var(--ease-out),
                border-color var(--dur-2) var(--ease-out),
                color var(--dur-2) var(--ease-out);
  }
  .step-marker::after {
    content: "";
    flex: 1 1 auto;
    block-size: 2px;
    background: var(--line);
    border-radius: var(--r-full);
  }
  .step:last-child .step-marker::after { display: none; }
  .step-label { font-size: var(--text-sm); font-weight: 560; color: var(--text-muted); }
  .step-note { font-size: var(--text-xs); color: var(--text-faint); }

  .step.is-done .step-marker::before {
    content: "";
    background: var(--brand-600);
    border-color: var(--brand-600);
    background-image: linear-gradient(var(--text-on-brand), var(--text-on-brand));
    background-size: 10px 10px;
    background-position: center;
    background-repeat: no-repeat;
    -webkit-mask: none;
    clip-path: none;
  }
  .step.is-done .step-marker::after { background: var(--brand-500); }
  .step.is-done .step-label { color: var(--text); }
  .step.is-current .step-marker::before {
    border-color: var(--brand-600);
    color: var(--brand);
    box-shadow: 0 0 0 3px var(--brand-soft);
  }
  .step.is-current .step-label { color: var(--text); font-weight: 620; }

  /* Vertical on a phone, where five steps side by side never fits */
  .stepper-vertical { flex-direction: column; gap: var(--space-5); }
  .stepper-vertical .step { flex-direction: row; gap: var(--space-3); min-inline-size: 0; }
  .stepper-vertical .step-marker { flex-direction: column; align-self: stretch; }
  .stepper-vertical .step-marker::after { inline-size: 2px; block-size: auto; flex: 1 1 auto; }
  @media (max-width: 40rem) {
    /* Horizontal where there is room, vertical below a breakpoint. Prefer this
       over .stepper-vertical: four steps with notes do not fit across a phone,
       and a horizontal stepper that squashes is unreadable. */
    .stepper-auto { flex-direction: column; gap: var(--space-5); }
    .stepper-auto .step { flex-direction: row; gap: var(--space-3); min-inline-size: 0; }
    .stepper-auto .step-marker { flex-direction: column; align-self: stretch; }
    .stepper-auto .step-marker::after { inline-size: 2px; block-size: auto; flex: 1 1 auto; }
  }

  /* --- Sticky banner -------------------------------------------------------
     An announcement strip. Top or bottom, dismissible, out of the way.
     ---------------------------------------------------------------------- */
  .banner {
    position: sticky;
    inset-block-start: 0;
    z-index: calc(var(--z-nav) + 1);
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-gutter);
    padding-block-start: max(var(--space-3), env(safe-area-inset-top));
    background: var(--brand-soft);
    color: var(--brand-soft-text);
    border-block-end: 1px solid color-mix(in oklab, var(--brand-400) 30%, transparent);
    font-size: var(--text-sm);
  }
  .banner-bottom {
    inset-block: auto 0;
    border-block: 1px solid color-mix(in oklab, var(--brand-400) 30%, transparent) 0;
    border-block-start: 1px solid color-mix(in oklab, var(--brand-400) 30%, transparent);
    border-block-end: 0;
    padding-block-end: max(var(--space-3), env(safe-area-inset-bottom));
  }
  .banner-fixed { position: fixed; inset-inline: 0; }
  .banner-dark { background: var(--ink-900); color: var(--ink-100); border-color: transparent; }
  .banner-warn { background: var(--warn-100); color: var(--warn-700); border-color: transparent; }
  .banner-close { margin-inline-start: auto; opacity: .7; cursor: pointer; flex: 0 0 auto; }
  .banner-close:hover { opacity: 1; }
  .banner[hidden] { display: none; }

  /* --- Jumbotron -----------------------------------------------------------
     A hero block. Kept deliberately plain so the content carries it.
     ---------------------------------------------------------------------- */
  .jumbotron {
    display: flex;
    flex-direction: column;
    gap: var(--space-5);
    padding: var(--space-section) var(--space-gutter);
    border-radius: var(--r-lg);
    background: var(--surface);
    border: 1px solid var(--line);
    text-align: start;
  }
  .jumbotron-center { text-align: center; align-items: center; }
  .jumbotron-center .lede { margin-inline: auto; }
  .jumbotron-media {
    display: grid;
    gap: var(--space-8);
    align-items: center;
    grid-template-columns: 1fr;
  }
  @media (min-width: 56rem) {
    .jumbotron-media { grid-template-columns: 1.1fr .9fr; }
  }
  .jumbotron h1 { max-inline-size: 18ch; }
  .jumbotron-center h1 { margin-inline: auto; }
  .jumbotron-actions { display: flex; flex-wrap: wrap; gap: var(--space-3); }
  @media (max-width: 30rem) {
    .jumbotron-actions > .btn { inline-size: 100%; }
  }

  /* --- Footer --------------------------------------------------------------
     A real component rather than the demo's ad-hoc one.
     ---------------------------------------------------------------------- */
  .footer {
    border-block-start: 1px solid var(--line);
    padding-block: var(--space-10) var(--space-8);
    color: var(--text-muted);
    font-size: var(--text-sm);
  }
  .footer-grid {
    display: grid;
    gap: var(--space-8);
    grid-template-columns: repeat(auto-fit, minmax(min(11rem, 100%), 1fr));
    padding-block-end: var(--space-8);
  }
  .footer-brand { display: flex; flex-direction: column; gap: var(--space-3); max-inline-size: 26ch; }
  .footer-col { display: flex; flex-direction: column; gap: var(--space-2); }
  .footer-heading {
    font-size: var(--text-xs);
    font-weight: 700;
    color: var(--text);
    margin-block-end: var(--space-1);
  }
  /* FINDINGS 65: this is the same token .footer sets on itself, and the
     underline is removed -- so a link inside a SENTENCE in the footer has no
     colour difference, no weight difference and no underline. Nothing marks it
     until hover, which does not help a keyboard or touch user.

     It is fine in a .footer-col, which is a stack of links and nothing else.
     It is a WCAG 1.4.1 failure in prose. Scope the underline removal to the
     link containers rather than to every descendant. */
  .footer a { color: var(--text-muted); text-decoration: none; }
  .footer a:hover { color: var(--brand); text-decoration: underline; }
  .footer-bottom {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-3);
    padding-block-start: var(--space-5);
    border-block-start: 1px solid var(--line);
    font-size: var(--text-xs);
  }
  .footer-social { display: flex; gap: var(--space-1); margin-inline-start: auto; }
  .footer-social a {
    inline-size: 34px;
    block-size: 34px;
    display: grid;
    place-items: center;
    border-radius: var(--r-sm);
  }
  .footer-social a:hover { background: var(--surface-hover); }

  /* --- Indicators ----------------------------------------------------------
     Status dots, counts on an icon, and unread markers.
     ---------------------------------------------------------------------- */
  .indicator {
    display: inline-block;
    inline-size: 8px;
    block-size: 8px;
    border-radius: var(--r-full);
    background: var(--ink-400);
    flex: 0 0 auto;
  }
  .indicator-good { background: var(--good-500); }
  .indicator-warn { background: var(--warn-500); }
  .indicator-bad { background: var(--bad-500); }
  .indicator-brand { background: var(--brand-500); }
  .indicator-lg { inline-size: 11px; block-size: 11px; }
  .indicator-ring { box-shadow: 0 0 0 2px var(--surface); }

  /* A count or dot anchored to a corner of something */
  .with-indicator { position: relative; display: inline-flex; }
  .with-indicator > .indicator-badge {
    position: absolute;
    inset-block-start: -4px;
    inset-inline-end: -6px;
    min-inline-size: 18px;
    block-size: 18px;
    padding-inline: 5px;
    border-radius: var(--r-full);
    background: var(--bad-600);
    color: var(--text-on-bad);
    font-size: var(--text-2xs);
    font-weight: 700;
    line-height: 18px;
    text-align: center;
    box-shadow: 0 0 0 2px var(--surface);
    font-variant-numeric: tabular-nums;
  }
  .with-indicator > .indicator-badge-dot {
    min-inline-size: 9px;
    inline-size: 9px;
    block-size: 9px;
    padding: 0;
    inset-block-start: -1px;
    inset-inline-end: -1px;
  }
  .with-indicator > .indicator-badge-brand { background: var(--brand-600); color: var(--text-on-brand); }

  /* Live status line: dot plus label, the pattern used in status pages */
  .status-line { display: inline-flex; align-items: center; gap: var(--space-2); font-size: var(--text-sm); }
  .status-line .indicator { position: relative; }
}

/* ==========================================================================
   Deck — 23 Inputs, extended
   ========================================================================== */

@layer deck.components {

  /* --- Floating label ------------------------------------------------------
     No JavaScript. :placeholder-shown tells us whether the field is empty, so
     the label lifts on focus or on content and never gets out of sync.
     The input needs placeholder=" " for the selector to work.
     ---------------------------------------------------------------------- */
  /* Two things about the markup that this file cannot enforce and that fail
     silently when they are wrong:

       1. The label must come AFTER the control. Every lift selector below is
          `~ label`, and the general sibling combinator only looks forward.
          Label first and it simply never moves.
       2. The control needs placeholder=" " (one space). Without a placeholder
          attribute :placeholder-shown never matches, so the label sits lifted
          over an empty field.

     FINDINGS 61: .float.is-invalid only colours the label. .field.is-invalid
     in 06-forms.css also reddens the control's border; there is no equivalent
     rule here, so the same class name does two different amounts of work
     depending on which wrapper it is on. :user-invalid is unaffected -- it
     matches the control directly through the 06-forms.css rule. */
  .float {
    position: relative;
    display: flex;
    flex-direction: column;
  }
  .float > :is(.input, .textarea, .select) {
    padding-block: var(--space-5) var(--space-2);
    min-block-size: var(--control-h-lg);
  }
  .float > .textarea { padding-block-start: var(--space-6); }
  .float > label {
    position: absolute;
    inset-block-start: 0;
    inset-inline-start: var(--space-3);
    padding-inline: 2px;
    block-size: var(--control-h-lg);
    display: flex;
    align-items: center;
    color: var(--text-faint);
    font-size: var(--text-base);
    pointer-events: none;
    transform-origin: inline-start center;
    transition: translate var(--dur-2) var(--ease-out),
                scale var(--dur-2) var(--ease-out),
                color var(--dur-2) var(--ease-out);
  }
  .float:focus-within > label,
  .float > .input:not(:placeholder-shown) ~ label,
  .float > .textarea:not(:placeholder-shown) ~ label,
  .float > .select ~ label {
    translate: 0 calc(var(--control-h-lg) * -.28);
    scale: .78;
    color: var(--text-muted);
  }
  .float:focus-within > label { color: var(--brand); }
  .float.is-invalid > label,
  .float > :user-invalid ~ label { color: var(--bad-700); }

  /* Outlined variant, where the label sits in a notch in the border */
  .float-outline > label {
    background: var(--surface);
    block-size: auto;
    inset-block-start: 50%;
    translate: 0 -50%;
    padding-inline: var(--space-1);
  }
  .float-outline:focus-within > label,
  .float-outline > .input:not(:placeholder-shown) ~ label,
  .float-outline > .textarea:not(:placeholder-shown) ~ label,
  .float-outline > .select ~ label {
    translate: 0 calc(var(--control-h-lg) * -.5 - 50%);
    scale: .8;
  }
  .float-outline > :is(.input, .textarea, .select) { padding-block: var(--space-3); }

  /* --- Number input --------------------------------------------------------
     Native spinners are tiny and inconsistent across browsers. These are real
     buttons that clear the touch target.
     ---------------------------------------------------------------------- */
  .number {
    display: flex;
    align-items: stretch;
    inline-size: 100%;
    max-inline-size: var(--number-width, 11rem);
    border: 1px solid var(--line-strong);
    border-radius: var(--r-sm);
    background: var(--surface);
    box-shadow: var(--shadow-1);
    overflow: hidden;
  }
  .number:focus-within { border-color: var(--focus); box-shadow: var(--ring); }
  .number > input {
    flex: 1 1 auto;
    min-inline-size: 0;
    border: 0;
    outline: none;
    background: none;
    text-align: center;
    font-variant-numeric: tabular-nums;
    font-size: var(--text-base);
    color: inherit;
    padding-inline: var(--space-1);
  }
  @media (pointer: coarse) { .number > input { font-size: 16px; } }
  .number > input::-webkit-outer-spin-button,
  .number > input::-webkit-inner-spin-button { appearance: none; margin: 0; }
  .number > input[type="number"] { appearance: textfield; -moz-appearance: textfield; }
  .number > button {
    inline-size: var(--control-h);
    min-block-size: var(--control-h);
    display: grid;
    place-items: center;
    background: var(--surface-2);
    color: var(--text-muted);
    cursor: pointer;
    flex: 0 0 auto;
    user-select: none;
    transition: background-color var(--dur-1) var(--ease-out), color var(--dur-1) var(--ease-out);
  }
  .number > button:hover { background: var(--surface-hover); color: var(--text); }
  .number > button:active { background: var(--brand-soft); color: var(--brand-soft-text); }
  .number > button[disabled] { opacity: .4; pointer-events: none; }
  .number > button:first-child { border-inline-end: 1px solid var(--line); }
  .number > button:last-child { border-inline-start: 1px solid var(--line); }
  .number-unit {
    display: grid;
    place-items: center;
    padding-inline: var(--space-3);
    background: var(--surface-2);
    border-inline-start: 1px solid var(--line);
    font-size: var(--text-sm);
    color: var(--text-muted);
    white-space: nowrap;
  }
  .number-sm { --number-width: 8.5rem; }
  .number-sm > button { inline-size: var(--control-h-sm); min-block-size: var(--control-h-sm); }

  /* --- Phone input ---------------------------------------------------------
     Country select welded to the number. The number stays LTR even in an RTL
     document, because a phone number always reads left to right.
     ---------------------------------------------------------------------- */
  .phone {
    display: flex;
    align-items: stretch;
    inline-size: 100%;
    border: 1px solid var(--line-strong);
    border-radius: var(--r-sm);
    background: var(--surface);
    box-shadow: var(--shadow-1);
    overflow: hidden;
  }
  .phone:focus-within { border-color: var(--focus); box-shadow: var(--ring); }
  .phone > .phone-country {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding-inline: var(--space-3);
    background: var(--surface-2);
    border-inline-end: 1px solid var(--line);
    cursor: pointer;
    font-size: var(--text-sm);
    color: var(--text-muted);
    white-space: nowrap;
    flex: 0 0 auto;
  }
  .phone > .phone-country:hover { background: var(--surface-hover); }
  .phone > .phone-country select {
    appearance: none;
    border: 0;
    background: none;
    outline: none;
    font: inherit;
    color: inherit;
    cursor: pointer;
    max-inline-size: 4.5rem;
  }
  .phone-flag { font-size: 1.1em; line-height: 1; }
  .phone-code { font-variant-numeric: tabular-nums; color: var(--text); font-weight: 560; }
  .phone > input {
    flex: 1 1 auto;
    min-inline-size: 0;
    border: 0;
    outline: none;
    background: none;
    padding: var(--space-2) var(--space-3);
    font-size: var(--text-base);
    font-variant-numeric: tabular-nums;
    letter-spacing: .02em;
    color: inherit;
    direction: ltr;
    unicode-bidi: isolate;
    text-align: start;
  }
  @media (pointer: coarse) { .phone > input { font-size: 16px; } }
  .phone.is-valid { border-color: color-mix(in oklab, var(--good-500) 55%, transparent); }
  .phone-status { display: grid; place-items: center; padding-inline: var(--space-3); flex: 0 0 auto; }

  /* --- Rating --------------------------------------------------------------
     Radio inputs underneath, so it posts a real value and works with the
     keyboard. Reversed flex order makes the CSS-only hover fill work.
     ---------------------------------------------------------------------- */
  /* The inputs are written HIGHEST value first -- 5, 4, 3, 2, 1 -- and
     row-reverse draws them 1 to 5. That is not a quirk, it is the whole
     mechanism: to light a star and everything below it, the selector has to
     reach forward from the chosen one, and CSS has no previous-sibling
     combinator. Reversing the row turns "everything below" into "everything
     after".

     Get the order wrong and it fails recognisably: the stars fill from the
     wrong end and the posted value is the mirror of the one clicked.

     :focus-visible + label uses the ADJACENT sibling, so each label must come
     immediately after its own input. */
  .rating {
    display: inline-flex;
    flex-direction: row-reverse;
    justify-content: flex-end;
    gap: 2px;
  }
  .rating > input { position: absolute; opacity: 0; inline-size: 1px; block-size: 1px; }
  .rating > label {
    inline-size: var(--star, 26px);
    block-size: var(--star, 26px);
    display: grid;
    place-items: center;
    color: var(--ink-300);
    cursor: pointer;
    transition: color var(--dur-1) var(--ease-out), scale var(--dur-1) var(--ease-spring);
  }
  .rating > label .icon { inline-size: 100%; block-size: 100%; fill: currentColor; stroke: none; }
  .rating > label:hover { scale: 1.14; }
  /* the checked star and everything visually before it (after it in the DOM) */
  .rating > input:checked ~ label,
  .rating > label:hover,
  .rating > label:hover ~ label {
    color: var(--accent-500);
  }
  .rating > input:focus-visible + label { outline: 2px solid var(--focus); outline-offset: 2px; border-radius: var(--r-xs); }
  .rating-sm { --star: 18px; }
  .rating-lg { --star: 36px; }

  /* Read-only display: a fill percentage rather than whole stars */
  .rating-static {
    display: inline-flex;
    gap: 2px;
    color: var(--ink-300);
    position: relative;
  }
  .rating-static > .rating-fill {
    position: absolute;
    inset-block: 0;
    inset-inline-start: 0;
    inline-size: calc(var(--value) * 1%);
    overflow: hidden;
    display: flex;
    gap: 2px;
    color: var(--accent-500);
    white-space: nowrap;
  }
  .rating-static .icon { fill: currentColor; stroke: none; inline-size: var(--star, 18px); block-size: var(--star, 18px); flex: 0 0 auto; }

  /* --- Range selector (two handles) ---------------------------------------
     Two stacked native range inputs. Real inputs mean real keyboard support
     and a real form post; the track between them is drawn from --lo and --hi.
     ---------------------------------------------------------------------- */
  .range-pair {
    position: relative;
    display: grid;
    block-size: var(--tap);
    align-items: center;
  }
  .range-pair::before {
    content: "";
    grid-area: 1 / 1;
    block-size: 6px;
    border-radius: var(--r-full);
    background: var(--ink-200);
  }
  .range-pair::after {
    content: "";
    grid-area: 1 / 1;
    block-size: 6px;
    border-radius: var(--r-full);
    background: var(--brand-500);
    margin-inline-start: calc(var(--lo, 0) * 1%);
    inline-size: calc((var(--hi, 100) - var(--lo, 0)) * 1%);
  }
  .range-pair > input[type="range"] {
    grid-area: 1 / 1;
    inline-size: 100%;
    background: none;
    appearance: none;
    block-size: var(--tap);
    pointer-events: none;
    margin: 0;
  }
  .range-pair > input[type="range"]::-webkit-slider-runnable-track { background: transparent; block-size: 6px; }
  .range-pair > input[type="range"]::-moz-range-track { background: transparent; block-size: 6px; }
  .range-pair > input[type="range"]::-webkit-slider-thumb {
    appearance: none;
    pointer-events: auto;
    margin-block-start: -8px;
    inline-size: 22px;
    block-size: 22px;
    border-radius: var(--r-full);
    background: var(--brand-600);
    border: 3px solid var(--ink-0);
    box-shadow: var(--shadow-2);
    cursor: grab;
  }
  .range-pair > input[type="range"]::-moz-range-thumb {
    pointer-events: auto;
    inline-size: 22px;
    block-size: 22px;
    border-radius: var(--r-full);
    background: var(--brand-600);
    border: 3px solid var(--ink-0);
    box-shadow: var(--shadow-2);
    cursor: grab;
  }
  .range-pair > input[type="range"]:focus-visible::-webkit-slider-thumb { outline: 2px solid var(--focus); outline-offset: 2px; }
  /* The current value of a slider. Put it on an <output>, which is an
     implicit live region, so the value is announced as the slider moves. */
  .range-readout {
    display: flex;
    justify-content: space-between;
    font-size: var(--text-sm);
    font-variant-numeric: tabular-nums;
    color: var(--text-muted);
  }

  /* Ticks under a single or paired range */
  .range-ticks {
    display: flex;
    justify-content: space-between;
    font-size: var(--text-2xs);
    color: var(--text-faint);
    font-variant-numeric: tabular-nums;
    padding-inline: 11px;
  }

  /* --- Copy to clipboard ----------------------------------------------------
     The behaviour hangs off [data-deck-copy], not off .copy-btn. The class is
     the look and nothing else, so a button with the class and no attribute
     looks exactly right and does nothing at all -- the single most common way
     to get this component wrong.

     Whether a successful copy raises a toast depends on what is inside the
     button: a .copy-done element swaps in place and no toast fires; without
     one, deck-extras.js toasts instead. .copy-inline never toasts.
     ---------------------------------------------------------------------- */
  .copy {
    display: flex;
    align-items: stretch;
    inline-size: 100%;
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    background: var(--surface-2);
    overflow: hidden;
  }
  .copy > code, .copy > .copy-value {
    flex: 1 1 auto;
    min-inline-size: 0;
    padding: var(--space-2) var(--space-3);
    background: none;
    border: 0;
    border-radius: 0;
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    color: var(--text);
    overflow-x: auto;
    white-space: nowrap;
    scrollbar-width: none;
    direction: ltr;
    unicode-bidi: isolate;
    display: flex;
    align-items: center;
  }
  .copy > :is(code, .copy-value)::-webkit-scrollbar { display: none; }
  .copy-btn {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding-inline: var(--space-3);
    min-inline-size: var(--tap);
    background: var(--surface);
    border-inline-start: 1px solid var(--line);
    color: var(--text-muted);
    font-size: var(--text-sm);
    font-weight: 560;
    cursor: pointer;
    transition: background-color var(--dur-1) var(--ease-out), color var(--dur-1) var(--ease-out);
  }
  .copy-btn:hover { background: var(--surface-hover); color: var(--text); }
  .copy-btn.is-copied { color: var(--good-700); background: var(--good-100); }
  .copy-btn .copy-done { display: none; }
  .copy-btn.is-copied .copy-done { display: inline-flex; }
  .copy-btn.is-copied .copy-idle { display: none; }

  /* Inline icon-only variant, for an order number or an API key in a table */
  .copy-inline {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    color: var(--text-faint);
    cursor: pointer;
    border-radius: var(--r-xs);
    padding: 2px;
  }
  .copy-inline:hover { color: var(--brand); background: var(--surface-hover); }
  .copy-inline.is-copied { color: var(--good-700); }

  /* --- WYSIWYG editor -------------------------------------------------------
     A toolbar over a contenteditable region. Deck styles the chrome; deck.js
     wires the commands. The content area inherits .prose, so what you type
     looks the way it will look when it is published.
     ---------------------------------------------------------------------- */
  .editor {
    display: flex;
    flex-direction: column;
    border: 1px solid var(--line-strong);
    border-radius: var(--r-sm);
    background: var(--surface);
    box-shadow: var(--shadow-1);
    overflow: clip;
  }
  .editor:focus-within { border-color: var(--focus); box-shadow: var(--ring); }

  /* The tool row. Pressed state is [aria-pressed="true"], which deck-extras.js
     rewrites from queryCommandState after every command and selection change,
     so the toolbar reflects the caret rather than the last button pressed. */
  .editor-toolbar {
    display: flex;
    align-items: center;
    gap: 2px;
    flex-wrap: wrap;
    padding: var(--space-1);
    background: var(--surface-2);
    border-block-end: 1px solid var(--line);
    position: sticky;
    inset-block-start: 0;
    z-index: 1;
  }
  .editor-tool {
    inline-size: 32px;
    block-size: 32px;
    display: grid;
    place-items: center;
    border-radius: var(--r-xs);
    color: var(--text-muted);
    cursor: pointer;
    font-size: var(--text-sm);
    font-weight: 620;
    transition: background-color var(--dur-1) var(--ease-out), color var(--dur-1) var(--ease-out);
  }
  .editor-tool:hover { background: var(--surface-hover); color: var(--text); }
  .editor-tool[aria-pressed="true"] { background: var(--brand-soft); color: var(--brand-soft-text); }
  .editor-tool-wide { inline-size: auto; padding-inline: var(--space-2); }
  .editor-sep { inline-size: 1px; block-size: 20px; background: var(--line); margin-inline: var(--space-1); }
  .editor-select {
    block-size: 32px;
    padding-inline: var(--space-2);
    border: 0;
    border-radius: var(--r-xs);
    background: none;
    font-size: var(--text-sm);
    color: var(--text-muted);
    cursor: pointer;
  }
  .editor-select:hover { background: var(--surface-hover); }

  /* The editable region. Its placeholder comes from data-placeholder through
     :empty::before, so it needs no element and no script to hide it. */
  .editor-content {
    min-block-size: var(--editor-height, 12rem);
    max-block-size: var(--editor-max, 30rem);
    overflow-y: auto;
    padding: var(--space-4);
    outline: none;
    line-height: var(--leading-normal);
  }
  .editor-content:empty::before {
    content: attr(data-placeholder);
    color: var(--text-faint);
    pointer-events: none;
  }
  .editor-content > * + * { margin-block-start: var(--space-3); }
  .editor-content :is(h1, h2, h3) { margin-block-start: var(--space-5); }
  .editor-content :is(ul, ol) { padding-inline-start: var(--space-6); }
  .editor-content ul { list-style: disc; }
  .editor-content ol { list-style: decimal; }
  .editor-content blockquote {
    border-inline-start: 3px solid var(--brand-300);
    padding-inline-start: var(--space-4);
    color: var(--text-muted);
  }
  .editor-content a { color: var(--brand); }
  .editor-content img { max-inline-size: 100%; border-radius: var(--r-sm); }

  .editor-footer {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-2) var(--space-3);
    border-block-start: 1px solid var(--line);
    background: var(--surface-2);
    font-size: var(--text-xs);
    color: var(--text-faint);
  }
  /* The character count. .is-over turns it red past data-limit — a soft
     limit, since silently refusing keystrokes reads as a broken keyboard. The
     server has to enforce it, and role="status" makes it audible. */
  .editor-count { margin-inline-start: auto; font-variant-numeric: tabular-nums; }
  .editor-count.is-over { color: var(--bad-700); font-weight: 620; }
}

/* ==========================================================================
   Deck — 24 Media and messaging
   ========================================================================== */

@layer deck.components {

  /* --- Video ---------------------------------------------------------------
     A responsive frame for a native video or an embed, plus styling for the
     native controls where the browser allows it.
     ---------------------------------------------------------------------- */
  .video {
    position: relative;
    inline-size: 100%;
    aspect-ratio: var(--ratio, 16 / 9);
    background: var(--ink-950);
    border-radius: var(--r-md);
    overflow: clip;
    isolation: isolate;
  }
  .video > :is(video, iframe, embed, object) {
    position: absolute;
    inset: 0;
    inline-size: 100%;
    block-size: 100%;
    border: 0;
    object-fit: contain;
  }
  .video-cover > video { object-fit: cover; }
  .video-square { --ratio: 1; }
  .video-portrait { --ratio: 9 / 16; }
  .video-wide { --ratio: 21 / 9; }

  .video video::-webkit-media-controls-panel { background-image: none; }
  .video video { accent-color: var(--brand-500); }

  /* Poster overlay with a play button, for a click-to-load embed */
  .video-poster {
    position: absolute;
    inset: 0;
    z-index: 1;
    display: grid;
    place-items: center;
    cursor: pointer;
    border: 0;
    padding: 0;
    background: none;
  }
  .video-poster img { inline-size: 100%; block-size: 100%; object-fit: cover; }
  .video-poster::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(to top, oklch(12% .02 var(--hue-neutral) / .55), transparent 60%);
  }
  .video-play {
    position: relative;
    z-index: 1;
    inline-size: 64px;
    block-size: 64px;
    border-radius: var(--r-full);
    display: grid;
    place-items: center;
    background: color-mix(in oklab, var(--surface) 90%, transparent);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    color: var(--text);
    box-shadow: var(--shadow-3);
    transition: scale var(--dur-2) var(--ease-spring), background-color var(--dur-1) var(--ease-out);
  }
  .video-poster:hover .video-play { scale: 1.08; background: var(--surface); }
  .video-meta {
    position: absolute;
    inset-block-end: var(--space-3);
    inset-inline-start: var(--space-3);
    z-index: 2;
    display: flex;
    gap: var(--space-2);
    color: oklch(98% 0 0);
    font-size: var(--text-xs);
    text-shadow: 0 1px 3px oklch(0% 0 0 / .6);
  }
  .video-duration {
    padding: 2px 6px;
    border-radius: var(--r-xs);
    background: oklch(12% 0 0 / .72);
    font-variant-numeric: tabular-nums;
  }

  /* --- Masonry gallery -----------------------------------------------------
     Two implementations. Columns is universal and good for photos where the
     reading order down each column is fine. Grid masonry is the real thing
     and preserves row order; it is used automatically where supported.
     ---------------------------------------------------------------------- */
  .masonry {
    columns: var(--cols, 1);
    column-gap: var(--gap, var(--space-3));
  }
  @media (min-width: 30rem) { .masonry { --cols: 2; } }
  @media (min-width: 48rem) { .masonry { --cols: 3; } }
  @media (min-width: 72rem) { .masonry { --cols: 4; } }
  .masonry-2 { --cols: 2; }
  .masonry-3 { --cols: 3; }

  .masonry > * {
    break-inside: avoid;
    margin-block-end: var(--gap, var(--space-3));
    border-radius: var(--r-md);
    overflow: clip;
    display: block;
  }
  .masonry img { inline-size: 100%; block-size: auto; display: block; }

  @supports (grid-template-rows: masonry) {
    .masonry {
      columns: auto;
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(min(14rem, 100%), 1fr));
      grid-template-rows: masonry;
      gap: var(--gap, var(--space-3));
    }
    .masonry > * { margin-block-end: 0; }
  }

  /* A simple gallery grid, when equal tiles are wanted instead */
  .gallery {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(9rem, 100%), 1fr));
    gap: var(--space-2);
  }
  .gallery > * {
    aspect-ratio: 1;
    border-radius: var(--r-sm);
    overflow: clip;
    position: relative;
  }
  .gallery img { inline-size: 100%; block-size: 100%; object-fit: cover; transition: scale var(--dur-3) var(--ease-out); }
  .gallery a:hover img { scale: 1.05; }
  /* Let one tile take a bigger cell */
  .gallery .span-2 { grid-column: span 2; grid-row: span 2; aspect-ratio: 1; }
  .gallery .span-wide { grid-column: span 2; aspect-ratio: 2; }

  /* --- Lazy loading --------------------------------------------------------
     A placeholder that holds the space so nothing shifts, and fades the image
     in once it decodes. Set the aspect ratio on the frame and the layout is
     stable before a byte arrives.
     ---------------------------------------------------------------------- */
  .lazy {
    position: relative;
    display: block;
    background: var(--bg-sunken);
    overflow: clip;
    aspect-ratio: var(--ratio, 3 / 2);
  }
  .lazy > img {
    inline-size: 100%;
    block-size: 100%;
    object-fit: cover;
    opacity: 0;
    transition: opacity var(--dur-3) var(--ease-out);
  }
  .lazy > img.is-loaded, .lazy > img[data-loaded] { opacity: 1; }
  /* Two things about this shimmer are recorded in FINDINGS.md:

     67: it runs the same dk-shimmer keyframes as .skeleton, for the same
     purpose, but .lazy::after is NOT in 02-reset.css's reduced-motion
     exemption list -- so it freezes where .skeleton slows to 2.4s. The
     durations differ too (1.6s here, 1.4s there). One of the two is wrong.

     66: deck-extras.js adds .is-error to the wrapper when an image fails to
     load, and nothing in src/ styles that class. So the shimmer carries on
     forever and a broken image is indistinguishable from a loading one. */
  .lazy::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(100deg, transparent 20%,
      color-mix(in oklab, var(--ink-300) 40%, transparent) 45%, transparent 70%);
    background-size: 250% 100%;
    background-repeat: no-repeat;
    animation: dk-shimmer 1.6s var(--ease-in-out) infinite;
    pointer-events: none;
  }
  .lazy:has(img.is-loaded)::after, .lazy:has(img[data-loaded])::after { display: none; }

  /* A blurred low-res preview underneath, if you have one */
  .lazy-blur { background-size: cover; background-position: center; }
  .lazy-blur::before {
    content: "";
    position: absolute;
    inset: -8px;
    background: inherit;
    filter: blur(14px);
    transition: opacity var(--dur-3) var(--ease-out);
  }
  .lazy-blur:has(img.is-loaded)::before { opacity: 0; }

  /* Generic content placeholder while a region streams in */
  .lazy-block {
    display: grid;
    place-items: center;
    min-block-size: 8rem;
    border: 1px dashed var(--line);
    border-radius: var(--r-md);
    color: var(--text-faint);
    gap: var(--space-2);
  }

  /* --- Chat bubbles --------------------------------------------------------
     Conversation view. Consecutive messages from one side group together and
     only the last one in a run gets the tail.
     ---------------------------------------------------------------------- */
  .chat {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    padding: var(--space-4);
    overflow-y: auto;
    overscroll-behavior: contain;
  }
  /* A centred date pill between days. Styling, not structure — a screen
     reader hears ordinary text. */
  .chat-day {
    align-self: center;
    margin-block: var(--space-4) var(--space-2);
    padding: var(--space-1) var(--space-3);
    border-radius: var(--r-full);
    background: var(--surface-2);
    color: var(--text-faint);
    font-size: var(--text-xs);
    font-weight: 560;
  }

  /* One message row. .msg-out flips it to the other edge and reverses the row
     so the avatar swaps sides. The incoming/outgoing distinction is position
     and colour only, so name the sender in text for anyone not seeing it. */
  .msg {
    display: flex;
    gap: var(--space-2);
    align-items: flex-end;
    max-inline-size: min(80%, 34rem);
    align-self: flex-start;
  }
  .msg-out { align-self: flex-end; flex-direction: row-reverse; }

  /* The message body. Its corner radii are logical, which is what lets the
     grouping flatten the correct side in either writing direction. */
  .bubble {
    position: relative;
    padding: var(--space-2) var(--space-3);
    border-radius: var(--r-md);
    background: var(--surface-2);
    border: 1px solid var(--line);
    font-size: var(--text-sm);
    line-height: 1.45;
    overflow-wrap: anywhere;
    border-end-start-radius: var(--r-xs);
  }
  .msg-out .bubble {
    background: var(--brand-600);
    color: var(--text-on-brand);
    border-color: transparent;
    border-end-start-radius: var(--r-md);
    border-end-end-radius: var(--r-xs);
  }
  /* Grouping: only the last bubble of a run keeps its tail */
  .msg:not(:last-of-type) + .msg .bubble { border-start-start-radius: var(--r-xs); }
  .msg-out:not(:last-of-type) + .msg-out .bubble { border-start-end-radius: var(--r-xs); }

  .bubble-meta {
    display: flex;
    align-items: center;
    gap: var(--space-1);
    margin-block-start: 2px;
    font-size: var(--text-2xs);
    opacity: .7;
    justify-content: flex-end;
    font-variant-numeric: tabular-nums;
  }
  .msg:not(.msg-out) .bubble-meta { justify-content: flex-start; }
  .bubble-name { font-weight: 620; font-size: var(--text-xs); margin-block-end: 2px; color: var(--brand); }
  .msg-out .bubble-name { color: inherit; }

  .bubble-attachment {
    margin: calc(var(--space-2) * -1) calc(var(--space-3) * -1) var(--space-2);
    border-start-start-radius: var(--r-md);
    border-start-end-radius: var(--r-md);
    overflow: clip;
  }
  .bubble-attachment img { inline-size: 100%; display: block; }

  .msg .avatar { --size: 28px; align-self: flex-end; margin-block-end: 2px; }
  .msg:not(:last-of-type) + .msg .avatar { visibility: hidden; }

  .bubble-system {
    align-self: center;
    background: none;
    border: 0;
    color: var(--text-faint);
    font-size: var(--text-xs);
    text-align: center;
    max-inline-size: 42ch;
  }

  /* Typing indicator */
  .bubble-typing { display: inline-flex; gap: 4px; padding-block: var(--space-3); }
  .bubble-typing > span {
    inline-size: 6px;
    block-size: 6px;
    border-radius: var(--r-full);
    background: var(--text-faint);
  }
  @media (prefers-reduced-motion: no-preference) {
    .bubble-typing > span { animation: dk-typing 1.2s var(--ease-in-out) infinite; }
    .bubble-typing > span:nth-child(2) { animation-delay: .16s; }
    .bubble-typing > span:nth-child(3) { animation-delay: .32s; }
    @keyframes dk-typing {
      0%, 60%, 100% { translate: 0 0; opacity: .45; }
      30% { translate: 0 -4px; opacity: 1; }
    }
  }

  /* Composer pinned to the bottom */
  .chat-composer {
    display: flex;
    align-items: flex-end;
    gap: var(--space-2);
    padding: var(--space-3) var(--space-4);
    padding-block-end: max(var(--space-3), env(safe-area-inset-bottom));
    border-block-start: 1px solid var(--line);
    background: var(--surface);
  }
  .chat-composer .textarea {
    min-block-size: var(--control-h);
    max-block-size: 8rem;
    field-sizing: content;
    border-radius: var(--r-lg);
    resize: none;
  }

  /* --- QR code -------------------------------------------------------------
     deck.js renders the modules into this frame. Kept square, with a quiet
     zone, so it stays scannable at any size.
     ---------------------------------------------------------------------- */
  .qr {
    display: inline-grid;
    place-items: center;
    padding: var(--qr-quiet, 12px);
    background: var(--qr-bg, #fff);
    border-radius: var(--r-sm);
    border: 1px solid var(--line);
    inline-size: fit-content;
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }
  .qr svg { display: block; inline-size: var(--qr-size, 160px); block-size: var(--qr-size, 160px); }
  .qr svg rect { fill: var(--qr-fg, #000); shape-rendering: crispEdges; }
  .qr-lg { --qr-size: 240px; }
  .qr-sm { --qr-size: 104px; }
  /* A logo punched out of the middle. Only safe at error correction Q or H. */
  .qr-logo {
    position: relative;
  }
  .qr-logo > .qr-logo-mark {
    position: absolute;
    inset: 50%;
    translate: -50% -50%;
    inline-size: 22%;
    block-size: 22%;
    display: grid;
    place-items: center;
    background: var(--qr-bg, #fff);
    border-radius: var(--r-xs);
    padding: 4px;
  }
  .qr-caption {
    margin-block-start: var(--space-2);
    font-size: var(--text-xs);
    color: var(--text-muted);
    text-align: center;
    max-inline-size: var(--qr-size, 160px);
    overflow-wrap: anywhere;
  }
}

@layer deck.print {
  @media print {
    /* A QR code is often the whole point of printing the page */
    .qr { border: 0; break-inside: avoid; }
    .video, .chat-composer, .carousel-arrow, .carousel-dots,
    .speed-dial, .drawer, .mega, .banner { display: none !important; }
    .masonry { columns: 2; }
    .chat { overflow: visible; max-block-size: none; }
    .msg { break-inside: avoid; }
    .bubble { border: 1px solid #bbb !important; background: #fff !important; color: #000 !important; }
    .msg-out .bubble { background: #f2f2f2 !important; }
    .lazy::after { display: none !important; }
    .editor-toolbar, .editor-footer { display: none !important; }
    .editor-content { max-block-size: none !important; overflow: visible !important; }
  }
}

/* ==========================================================================
   Deck — 25 Anchor positioning
   Every floating thing in Deck was positioned by hand with
   getBoundingClientRect, which does not flip when it hits the bottom of the
   window, does not shift back inside when it hits an edge, and does not
   follow its anchor inside a scrolling container. Those are exactly the
   problems Floating UI solves, and they are also exactly what CSS anchor
   positioning solves natively — with no JavaScript, on the compositor, and
   without a resize or scroll listener.

   So Deck uses anchor positioning where the browser has it, and hands off to
   Floating UI where it does not, if you have loaded it. Neither is required.
   ========================================================================== */

@layer deck.components {

  @supports (anchor-name: --a) {

    /* --- Naming an anchor --------------------------------------------------
       Put .anchor on the trigger and --anchor on both trigger and panel.
       Each pair needs a unique name, so generate it server-side in Keel:
         <button class="anchor" style="--anchor:--menu-<?= $id ?>">
       ---------------------------------------------------------------------- */
    .anchor { anchor-name: var(--anchor); }

    /* --- Menus and mega menus ---------------------------------------------- */
    .menu, .mega, .datepicker {
      position-anchor: var(--anchor);
      position-area: block-end span-inline-end;
      position-try-fallbacks:
        block-end span-inline-start,
        block-start span-inline-end,
        block-start span-inline-start,
        flip-block, flip-inline;
      position-try-order: most-block-size;
      margin-block: var(--space-2);
      /* stay inside the viewport no matter which fallback wins */
      max-block-size: calc(100dvh - var(--space-8));
      max-inline-size: calc(100vw - var(--space-6));
      inset: auto;
    }
    .menu { min-inline-size: max(12rem, anchor-size(inline)); }

    .mega {
      position-area: block-end;
      justify-self: anchor-center;
    }

    /* A dropdown that should match the width of the field it belongs to */
    .menu-match { inline-size: anchor-size(inline); min-inline-size: 0; }

    /* --- Combobox list ------------------------------------------------------
       The listbox is a sibling today, positioned with absolute. With anchor
       positioning it can go in the top layer and escape overflow:hidden
       ancestors, which is the bug that bites every combobox inside a modal.
       ---------------------------------------------------------------------- */
    .combo-control { anchor-name: var(--anchor, --combo); }
    .combo-list {
      position-anchor: var(--anchor, --combo);
      position-area: block-end;
      position-try-fallbacks: flip-block;
      inline-size: anchor-size(inline);
      inset: auto;
      margin-block: 4px;
    }
    .combo-list.is-above { inset: auto; }

    /* --- Tooltips -----------------------------------------------------------
       The ::after tooltip could never flip. This one can, and it can carry a
       real arrow that stays pointed at the anchor.
       ---------------------------------------------------------------------- */
    .tip {
      position: fixed;
      position-anchor: var(--anchor);
      position-area: block-start;
      position-try-fallbacks: flip-block, flip-inline;
      justify-self: anchor-center;
      margin: var(--space-2);
      inline-size: max-content;
      max-inline-size: 16rem;
      padding: var(--space-2) var(--space-3);
      border: 0;
      border-radius: var(--r-xs);
      background: var(--ink-900);
      color: var(--ink-50);
      font-size: var(--text-xs);
      line-height: 1.4;
      box-shadow: var(--shadow-3);
      opacity: 0;
      transition: opacity var(--dur-2) var(--ease-out),
                  overlay var(--dur-2) allow-discrete,
                  display var(--dur-2) allow-discrete;
    }
    .tip:popover-open { opacity: 1; }
    @starting-style { .tip:popover-open { opacity: 0; } }
    .tip::backdrop { background: transparent; }

    /* The arrow on a .tip. background: inherit takes the tip's colour, and
       justify-self: anchor-center keeps it pointed at the trigger whichever way
       the tip has flipped. */
    .tip-arrow {
      position: absolute;
      inline-size: 8px;
      block-size: 8px;
      rotate: 45deg;
      background: inherit;
      position-anchor: var(--anchor);
      justify-self: anchor-center;
    }

    /* --- Popover card -------------------------------------------------------
       A richer tooltip: a heading, body copy, and actions. Used for the
       "what does this labor code mean" pattern.
       ---------------------------------------------------------------------- */
    .pop {
      position: fixed;
      position-anchor: var(--anchor);
      position-area: block-end;
      position-try-fallbacks: flip-block, flip-inline, block-start span-inline-start;
      justify-self: anchor-center;
      margin: var(--space-2);
      inline-size: min(20rem, calc(100vw - var(--space-6)));
      padding: var(--space-4);
      background: var(--surface);
      color: var(--text);
      border: 1px solid var(--line);
      border-radius: var(--r-md);
      box-shadow: var(--shadow-4);
      opacity: 0;
      translate: 0 -4px;
      transition: opacity var(--dur-2) var(--ease-out),
                  translate var(--dur-2) var(--ease-out),
                  overlay var(--dur-2) allow-discrete,
                  display var(--dur-2) allow-discrete;
    }
    .pop:popover-open { opacity: 1; translate: 0; }
    @starting-style { .pop:popover-open { opacity: 0; translate: 0 -4px; } }
    .pop::backdrop { background: transparent; }
    /* The heading inside a popover card. */
    .pop-title { font-weight: 620; margin-block-end: var(--space-1); }
    /* The explanatory text inside a popover card. */
    .pop-body { font-size: var(--text-sm); color: var(--text-muted); }
    /* The action row at the bottom of a popover card. A button here can close
       the panel with popovertargetaction="hide" and no script. */
    .pop-actions { display: flex; gap: var(--space-2); margin-block-start: var(--space-3); }

    /* --- Hide the anchored element when its anchor scrolls out of view ------ */
    .menu, .mega, .tip, .pop, .datepicker, .combo-list {
      position-visibility: anchors-visible;
    }
  }

  /* --- Floating UI handoff --------------------------------------------------
     Where anchor positioning is missing, deck-adapters.js uses Floating UI if
     it is on the page and falls back to Deck's own placement if it is not.
     These rules just get out of the way so the adapter can set inset.
     ---------------------------------------------------------------------- */
  @supports not (anchor-name: --a) {
    .tip, .pop {
      position: fixed;
      inset: auto;
      margin: 0;
      inline-size: max-content;
      max-inline-size: 16rem;
      border: 0;
      border-radius: var(--r-xs);
      background: var(--ink-900);
      color: var(--ink-50);
      font-size: var(--text-xs);
      padding: var(--space-2) var(--space-3);
      box-shadow: var(--shadow-3);
      opacity: 0;
      transition: opacity var(--dur-2) var(--ease-out),
                  overlay var(--dur-2) allow-discrete,
                  display var(--dur-2) allow-discrete;
    }
    .pop {
      inline-size: min(20rem, calc(100vw - var(--space-6)));
      max-inline-size: none;
      background: var(--surface);
      color: var(--text);
      border: 1px solid var(--line);
      border-radius: var(--r-md);
      box-shadow: var(--shadow-4);
      padding: var(--space-4);
    }
    .tip:popover-open, .pop:popover-open { opacity: 1; }
    @starting-style { .tip:popover-open, .pop:popover-open { opacity: 0; } }
    .tip::backdrop, .pop::backdrop { background: transparent; }
    .tip-arrow { position: absolute; inline-size: 8px; block-size: 8px; rotate: 45deg; background: inherit; }
  }
}

/* ==========================================================================
   Deck — 26 Performance and drag
   The usual answer to a ten thousand row grid is a virtualization library:
   measure the viewport, render a window of rows, absolutely position a
   spacer, and reconcile on every scroll frame. It works, it costs about five
   kilobytes, and it breaks find-in-page, printing, accessibility tree order,
   and text selection across the boundary.

   content-visibility does the same job in the engine. Off-screen subtrees are
   skipped during layout, style, paint, and hit testing, but they stay in the
   DOM, so Ctrl+F still finds them, the print stylesheet still prints them,
   and a screen reader still reads them. It needs one line of CSS and no
   JavaScript at all.
   ========================================================================== */

@layer deck.components {

  /* --- Virtualized rows ----------------------------------------------------
     contain-intrinsic-size is the placeholder height for a skipped row. Get it
     roughly right and the scrollbar is stable; the `auto` keyword makes the
     browser remember the real height once a row has been rendered once.
     ---------------------------------------------------------------------- */
  .dg-virtual tbody tr {
    content-visibility: auto;
    contain-intrinsic-size: auto var(--dg-row-h, 44px);
  }
  .dg-virtual.dg-compact tbody tr { contain-intrinsic-size: auto 34px; }
  .dg-virtual.dg-comfy tbody tr { contain-intrinsic-size: auto 56px; }

  /* Skips layout, style and paint for offscreen rows with
     content-visibility, using contain-intrinsic-size as the placeholder
     height. One class, no windowing library. If your rows are not 56px,
     override the intrinsic size or the scrollbar will jump. */
  .list-virtual > .list-row {
    content-visibility: auto;
    contain-intrinsic-size: auto 56px;
  }

  /* Any long stack of cards: an activity feed, a chat log, search results */
  .virtual > * {
    content-visibility: auto;
    contain-intrinsic-size: auto var(--item-size, 120px);
  }
  .virtual-sm > * { --item-size: 64px; }
  .virtual-lg > * { --item-size: 220px; }

  /* Whole sections below the fold. Cheap and safe on any page. */
  .defer {
    content-visibility: auto;
    contain-intrinsic-size: auto 600px;
  }

  /* Printing must render everything, or half the report comes out blank */
  @media print {
    .dg-virtual tbody tr, .list-virtual > .list-row, .virtual > *, .defer {
      content-visibility: visible !important;
      contain-intrinsic-size: none !important;
    }
  }

  /* --- Containment ---------------------------------------------------------
     Tell the engine that a component's layout cannot affect anything outside
     it. On a dashboard with twenty cards this is the difference between one
     layout pass and twenty.
     ---------------------------------------------------------------------- */
  .contain { contain: layout style; }
  .contain-paint { contain: layout style paint; }
  .card, .panel, .list, .dg-wrap, .chart { contain: layout style; }

  /* --- Drag and drop -------------------------------------------------------
     Deck styles the states; deck-adapters.js drives them with SortableJS when
     it is present, or with the native HTML drag and drop API when it is not.
     The class names are SortableJS's defaults, so no configuration is needed.
     ---------------------------------------------------------------------- */
  .sortable { position: relative; }
  .sortable [draggable="true"], .drag-handle { cursor: grab; touch-action: none; }
  .sortable [draggable="true"]:active, .drag-handle:active { cursor: grabbing; }

  .drag-handle {
    display: grid;
    place-items: center;
    inline-size: 28px;
    min-block-size: var(--tap);
    color: var(--text-faint);
    flex: 0 0 auto;
    opacity: 0;
    transition: opacity var(--dur-1) var(--ease-out), color var(--dur-1) var(--ease-out);
  }
  :is(.list-row, .dg tbody tr, .kanban-card):is(:hover, :focus-within) .drag-handle { opacity: 1; }
  .drag-handle:hover { color: var(--text); }
  @media (pointer: coarse) { .drag-handle { opacity: 1; } }

  /* The item being dragged, following the pointer */
  .sortable-drag {
    opacity: 1 !important;
    box-shadow: var(--shadow-4);
    rotate: 1.5deg;
    scale: 1.02;
    cursor: grabbing;
  }
  /* The gap left where the item will land */
  .sortable-ghost {
    opacity: .4;
    background: var(--brand-soft) !important;
    outline: 2px dashed var(--brand-400);
    outline-offset: -2px;
  }
  .sortable-ghost > * { visibility: hidden; }
  .sortable-chosen { background: var(--surface-hover); }
  .sortable-fallback { opacity: .9; }

  /* A list that is accepting a drop */
  .drop-target {
    outline: 2px dashed var(--brand-400);
    outline-offset: 3px;
    border-radius: var(--r-md);
    background: color-mix(in oklab, var(--brand-soft) 55%, transparent);
  }
  .drop-reject { outline-color: var(--bad-500); background: color-mix(in oklab, var(--bad-100) 55%, transparent); }

  /* --- Kanban --------------------------------------------------------------
     The obvious thing to build once you have drag and drop. An issue queue by
     status is the same shape as a board.
     ---------------------------------------------------------------------- */
  .kanban {
    display: flex;
    gap: var(--space-4);
    overflow-x: auto;
    overscroll-behavior-inline: contain;
    padding-block-end: var(--space-3);
    align-items: flex-start;
    scroll-snap-type: x proximity;
  }
  /* One column: a fixed width with a viewport cap so a phone still shows a
     sliver of the next, capped in height by --kanban-height. */
  .kanban-col {
    flex: 0 0 min(19rem, 82vw);
    scroll-snap-align: start;
    display: flex;
    flex-direction: column;
    max-block-size: var(--kanban-height, 70dvh);
    background: var(--bg-sunken);
    border: 1px solid var(--line);
    border-radius: var(--r-md);
  }
  /* The column title and count, outside the scrolling area so they stay
     visible while cards scroll. */
  .kanban-head {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-3) var(--space-4);
    border-block-end: 1px solid var(--line);
    font-size: var(--text-sm);
    font-weight: 620;
    position: sticky;
    inset-block-start: 0;
    background: inherit;
    border-start-start-radius: var(--r-md);
    border-start-end-radius: var(--r-md);
    z-index: 1;
  }
  .kanban-count {
    margin-inline-start: auto;
    font-size: var(--text-xs);
    font-weight: 600;
    color: var(--text-muted);
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--r-full);
    padding: 1px 7px;
    font-variant-numeric: tabular-nums;
  }
  /* The scrolling card region, with a 5rem floor so an empty column is still
     something you can drop onto. */
  .kanban-body {
    flex: 1 1 auto;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: var(--space-3);
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    min-block-size: 5rem;
  }
  /* One card. .kanban-card-title is styling, not a heading — if the card
     opens something, make the title a link. */
  .kanban-card {
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--r-sm);
    padding: var(--space-3);
    box-shadow: var(--shadow-1);
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    content-visibility: auto;
    contain-intrinsic-size: auto 110px;
  }
  .kanban-card:hover { box-shadow: var(--shadow-2); }
  .kanban-card-title { font-size: var(--text-sm); font-weight: 580; }
  .kanban-card-meta {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    font-size: var(--text-xs);
    color: var(--text-muted);
  }
  /* Shown in an empty column and hidden by :has(.kanban-card) as soon as one
     arrives, so nothing has to remember to toggle it. */
  .kanban-empty {
    border: 1px dashed var(--line-strong);
    border-radius: var(--r-sm);
    padding: var(--space-5);
    text-align: center;
    color: var(--text-faint);
    font-size: var(--text-sm);
  }
  .kanban-col:has(.kanban-card) .kanban-empty { display: none; }

  /* --- Reorderable list ----------------------------------------------------- */
  .reorder .list-row { user-select: none; }
  .reorder .list-row.is-lifted {
    box-shadow: var(--shadow-3);
    background: var(--surface);
    border-radius: var(--r-sm);
    z-index: 2;
    position: relative;
  }

  @media (prefers-reduced-motion: reduce) {
    .sortable-drag { rotate: none; scale: 1; }
  }
}

/* ==========================================================================
   Deck — 15 Print
   Real work still gets printed. Invoices, packing slips, purchase orders, and
   reports all end up on paper or in a PDF, so this is a real stylesheet and
   not an afterthought.
   ========================================================================== */

@layer deck.print {

  @page {
    size: letter;
    margin: 16mm 14mm;
  }
  @page :first { margin-block-start: 12mm; }

  @media print {

    /* --- Force the light palette. Dark mode is a screen idea. -------------- */
    :root {
      color-scheme: light only;
      --bg: #fff;
      --bg-sunken: #fff;
      --surface: #fff;
      --surface-2: #fff;
      --surface-hover: #fff;
      --text: #000;
      --text-muted: #333;
      --text-faint: #555;
      --line: #bbb;
      --line-strong: #888;
      --shadow-1: none; --shadow-2: none; --shadow-3: none; --shadow-4: none;
      --shadow-inset: none;
    }

    html, body {
      background: #fff !important;
      color: #000 !important;
      font-size: 10.5pt;
      line-height: 1.4;
    }

    body { padding-block-end: 0 !important; }

    * {
      box-shadow: none !important;
      text-shadow: none !important;
      backdrop-filter: none !important;
      -webkit-backdrop-filter: none !important;
      animation: none !important;
      transition: none !important;
    }

    /* Keep intentional fills — badges, status chips, chart series */
    .badge, .chip, .chart-col, .chart-col-stack > span, .chart-bar-fill,
    .chart-meter > span, .donut, .icon-tile, .avatar, .chart-heat > * {
      -webkit-print-color-adjust: exact;
      print-color-adjust: exact;
    }

    /* --- Chrome that should never print ------------------------------------ */
    .tabbar, .fab, .speed-dial, .back-to-top, .toast-region, .skip-link, .theme-dock,
    .navbar-links, .sidebar, .datepicker, .combo-list, .menu,
    .dg-toolbar, .dg-resize, .sheet-grip, .no-print,
    .pagination, .segmented, .tabs, .scroller::-webkit-scrollbar {
      display: none !important;
    }

    /* Dialogs print only if they are actually open, and then as a block */
    dialog:not([open]) { display: none !important; }
    dialog[open] {
      position: static !important;
      display: block !important;
      max-block-size: none !important;
      inline-size: auto !important;
      border: 0 !important;
    }
    dialog::backdrop { display: none !important; }

    /* --- Layout ------------------------------------------------------------ */
    .container, .container-sm, .container-md, .container-lg, .container-xl {
      max-inline-size: none !important;
      padding-inline: 0 !important;
    }
    .section { padding-block: 0 0 8mm !important; }
    .split { display: block !important; }
    .grid { display: block !important; }
    .grid > * + * { margin-block-start: 4mm; }
    .sticky-top, .sticky, .form-actions-sticky { position: static !important; }

    /* --- Surfaces ---------------------------------------------------------- */
    .card, .panel, .list, .table-wrap, .dg-wrap, .alert, .fieldset {
      border: 1px solid #bbb !important;
      border-radius: 0 !important;
      background: #fff !important;
      break-inside: avoid;
      max-block-size: none !important;
      overflow: visible !important;
    }
    .card-footer, .list-header, .dg thead th, .table thead th {
      background: #f2f2f2 !important;
      -webkit-print-color-adjust: exact;
      print-color-adjust: exact;
    }

    /* --- Type -------------------------------------------------------------- */
    h1, .h1 { font-size: 20pt; }
    h2, .h2 { font-size: 15pt; }
    h3, .h3 { font-size: 12.5pt; }
    h4, .h4, h5, .h5, h6, .h6 { font-size: 11pt; }
    .display { font-size: 22pt; }
    .lede { font-size: 11pt; color: #333 !important; }

    h1, h2, h3, h4, h5, h6 { break-after: avoid; page-break-after: avoid; }
    p, blockquote, li, figure, .stat, .timeline-item { break-inside: avoid; }
    .prose { max-inline-size: none; }

    /* Show where a link actually goes, but not for anchors or JS handles */
    a { color: #000 !important; text-decoration: underline; }
    a[href^="http"]::after {
      content: " (" attr(href) ")";
      font-size: 8.5pt;
      color: #555;
      word-break: break-all;
    }
    a[href^="#"]::after, a[href^="javascript:"]::after, .btn::after, .no-print-url::after { content: ""; }

    abbr[title]::after { content: " (" attr(title) ")"; font-size: 8.5pt; }

    /* --- Tables ------------------------------------------------------------ */
    .table, .dg { inline-size: 100% !important; font-size: 9.5pt; }
    .table thead, .dg thead { display: table-header-group; }
    .dg tfoot, .table tfoot { display: table-footer-group; }
    .table tr, .dg tr { break-inside: avoid; page-break-inside: avoid; }
    .dg :is(th, td) {
      position: static !important;
      block-size: auto !important;
      padding: 4pt 6pt !important;
      white-space: normal !important;
      clip-path: none !important;
      border-block-end: 1px solid #ddd !important;
    }
    .dg-wrap { max-block-size: none !important; overflow: visible !important; }

    /* The mobile card fallbacks are a screen behaviour, not a paper one */
    .table-stack thead, .dg-cards thead {
      position: static !important;
      inline-size: auto !important;
      block-size: auto !important;
      clip-path: none !important;
      display: table-header-group !important;
      overflow: visible !important;
    }
    .table-stack, .dg-cards { display: table !important; }
    .table-stack tbody, .dg-cards tbody { display: table-row-group !important; }
    .table-stack tr, .dg-cards tr { display: table-row !important; margin: 0 !important; border-radius: 0 !important; }
    .table-stack td, .dg-cards td { display: table-cell !important; text-align: start !important; }
    .table-stack td::before, .dg-cards td::before { content: none !important; }

    /* --- Forms ------------------------------------------------------------- */
    .input, .textarea, .select, .combo-control {
      border: 1px solid #999 !important;
      background: #fff !important;
      min-block-size: auto !important;
    }
    .textarea { max-block-size: none !important; block-size: auto !important; }
    .select { background-image: none !important; padding-inline-end: var(--space-3) !important; }
    .btn, .form-actions { display: none !important; }
    .btn.print-keep { display: inline-flex !important; border: 1px solid #999 !important; }

    /* Checkboxes print as boxes with a mark, not as filled squares */
    .check input[type="checkbox"], .check input[type="radio"] {
      -webkit-print-color-adjust: exact;
      print-color-adjust: exact;
      border: 1px solid #000 !important;
      background: #fff !important;
    }
    .check input:checked { background: #000 !important; border-color: #000 !important; }
    .switch input { border: 1px solid #000 !important; }

    /* --- Charts ------------------------------------------------------------ */
    .chart, .chart-columns, .chart-svg { break-inside: avoid; }
    .chart-col[data-value]::before { display: none !important; }
    .chart-columns { background-image: none !important; border-block-end: 1px solid #999; }

    /* --- Page break helpers ------------------------------------------------ */
    .page-break { break-before: page; page-break-before: always; }
    .page-break-after { break-after: page; page-break-after: always; }
    .keep-together { break-inside: avoid; page-break-inside: avoid; }
    .print-only { display: revert !important; }

    /* Repeat a header on every page of a long report */
    .print-header { position: fixed; inset-block-start: 0; inset-inline: 0; }
    .print-footer { position: fixed; inset-block-end: 0; inset-inline: 0; font-size: 8.5pt; color: #555; }
  }

  /* Hidden on screen, shown on paper */
  .print-only { display: none; }
}
