
  :root {
    --azure: #21a8e1; --azure-text: #197ea9; --azure-deep: #305d93;
    --turmeric: #f9ec0f; --saffron: #e99415; --saffron-text: #a1660f;
    --chilli: #da411d; --chilli-text: #d13e1c; --paprika: #d52822;
    --rose: #cc2c71; --plum: #981965;
    --paper: #ffffff; --surface: #f4f9fc; --surface-sink: #e8f1f7; --hairline: #d9e6ef;
    /* --ink-faint is #5a6c7c, not the #6b8194 this shell shipped with: that
       value measures 4.04:1 on --paper and fails WCAG AA for normal text,
       and it is the colour of the small labels — timestamps, hints, meta
       lines. Kept byte-identical to apps/web/src/styles/tokens.css, which
       carries the measurements; this :root is a hand copy because a Worker
       cannot import that stylesheet. */
    --ink: #132a3d; --ink-muted: #4a6070; --ink-faint: #5a6c7c;
    --font-display: 'Fraunces Variable', 'Fraunces', Georgia, serif;
    --font-body: 'Figtree Variable', 'Figtree', system-ui, -apple-system, sans-serif;
    --radius-s: 10px; --radius-m: 16px; --radius-l: 26px; --radius-pill: 999px;
    --shadow-card: 0 2px 4px rgb(19 42 61 / 0.05), 0 12px 28px -10px rgb(19 42 61 / 0.16);
    --shadow-lift: 0 6px 12px rgb(19 42 61 / 0.07), 0 22px 48px -14px rgb(19 42 61 / 0.24);
    --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
    --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
    /* The seam: byte-identical to apps/web/src/styles/tokens.css, where the
       stops are documented and must not be retyped. The public site paints it
       where a visitor commits (the booking panel, the talk band); in the admin
       the surface where a person commits is the save bar, and it is painted
       on no other admin surface. */
    --seam: linear-gradient(90deg, var(--saffron) 0%, var(--chilli) 34%, var(--rose) 67%, var(--plum) 100%);
    /* SpiceOS shell — light mode: white frost over an azure sky, the same
       ground the public site's Spice Rack sits on. */
    --dock-base: 54px;
    --os-glass: rgb(255 255 255 / 0.52);
    --os-hairline: rgb(19 42 61 / 0.12);
  }
  * { box-sizing: border-box; }
  html { -webkit-text-size-adjust: 100%; }
  body {
    margin: 0; min-height: 100dvh; background: var(--surface); color: var(--ink);
    font-family: var(--font-body); font-size: 1rem; line-height: 1.6;
    -webkit-font-smoothing: antialiased;
  }
  h1, h2, h3 {
    font-family: var(--font-display);
    font-variation-settings: 'SOFT' 30, 'WONK' 1, 'opsz' 100;
    font-weight: 600; line-height: 1.08; letter-spacing: -0.015em; margin: 0;
  }
  a { color: var(--azure-text); text-decoration-color: color-mix(in oklab, var(--azure) 55%, transparent); }
  :focus-visible { outline: 3px solid var(--azure-text); outline-offset: 2px; border-radius: var(--radius-s); }

  /* width:100% matters: in a column-flex parent, the auto margins would
     otherwise suppress stretching and shrink the column to fit-content. */
  .wrap { width: 100%; max-width: clamp(62rem, 92vw, 110rem); margin-inline: auto; padding-inline: clamp(1.1rem, 2.5vw, 2.5rem); }

  /* ==========================================================================
     The SpiceOS desktop shell

     Layers, back to front: the Spice Drift wallpaper (a designed gradient
     scene in the logo's six colours — no image request, nothing to load),
     a scrim for contrast, the menu bar (top), the Dock (bottom) — with the
     window floating in between. body itself never scrolls; only
     .window-body does, exactly like a windowed app rather than a page.
     ========================================================================== */
  html, body { height: 100%; overflow: hidden; }
  body { display: flex; flex-direction: column; }

  /* Cross-document View Transitions: when a REAL navigation does happen
     (the no-JS/narrow fallback, a frame moving between pages, or any full
     load), the browser morphs old page into new instead of white-flashing.
     The menubar and dock carry stable view-transition-names, so the shell
     visibly PERSISTS across the swap — the "always signed in" feel even
     without the window manager. Chromium-only today; elsewhere it is
     simply a normal navigation. */
  @media (prefers-reduced-motion: no-preference) {
    @view-transition { navigation: auto; }

    /* The OS glide: every navigation — full desktop or inside a window —
       plays the same controlled exit/entry. The menubar and dock carry their
       own view-transition-names, so they hold still while content moves. */
    @media (prefers-reduced-motion: no-preference) {
      ::view-transition-old(root) { animation: spice-vt-out 120ms var(--ease-out) both; }
      ::view-transition-new(root) { animation: spice-vt-in 220ms 40ms var(--ease-out) both; }
    }
    @keyframes spice-vt-out { to { opacity: 0; transform: translateY(-4px); } }
    @keyframes spice-vt-in { from { opacity: 0; transform: translateY(7px); } }
  }
  .menubar { view-transition-name: spice-menubar; }
  .dock-zone { view-transition-name: spice-dock; }
  /* A second zone must not share the bottom dock's transition name, or the
     browser treats the pair as invalid and skips the transition for both. */
  .dock-zone--side { view-transition-name: spice-side-dock; }

  /* --- Spice Drift wallpaper ---------------------------------------------
     The faux-mesh technique from the Big Sur wallpaper recreations: soft
     radial blobs, one per logo colour, over a deep plum-navy ground. The
     slow drift animates background-position only — compositor-cheap — and
     is stilled for reduced motion. */
  .wallpaper {
    position: fixed; inset: 0; z-index: 0;
    background-color: #cfe8f7;
    background-image:
      radial-gradient(at 80% 10%, rgb(249 236 15 / 0.30) 0px, transparent 42%),
      radial-gradient(at 66% 44%, rgb(204 44 113 / 0.16) 0px, transparent 48%),
      radial-gradient(at 14% 22%, rgb(33 168 225 / 0.42) 0px, transparent 52%),
      radial-gradient(at 26% 78%, rgb(152 25 101 / 0.14) 0px, transparent 48%),
      radial-gradient(at 90% 86%, rgb(233 148 21 / 0.24) 0px, transparent 52%),
      linear-gradient(168deg, #eef8ff 0%, #cfe8f7 55%, #a9cfe8 100%);
    background-size: 100% 100%;
  }
  @media (prefers-reduced-motion: no-preference) {
    .wallpaper {
      background-size: 135% 135%;
      animation: wallpaper-drift 90s ease-in-out infinite alternate;
    }
  }
  @keyframes wallpaper-drift {
    from { background-position: 0% 0%; }
    to { background-position: 100% 100%; }
  }
  /* The full lockup at the centre of the desk — the same artwork the login
     screen wears, as a quiet watermark when no window covers it. */
  .wallpaper::after {
    content: ''; position: absolute; inset: 0;
    /* A FILE, NOT A DATA URI. Base64 inflates by a third — this one mark was
       104,296 characters inside the stylesheet, which meant every admin page
       carried 78KB of decoded PNG whether it showed the desk or not, and no
       browser could cache it separately from the CSS. Served from
       /brand/logo.png it is fetched once, cached for a day, and only by the
       pages that actually paint a wallpaper. */
    background: url('/brand/logo.png') center / clamp(26rem, 44vw, 42rem) no-repeat;
    opacity: 0.18;
  }
  html.evening .wallpaper::after { opacity: 0.05; filter: invert(1) brightness(1.4); }
  .wallpaper-scrim {
    position: fixed; inset: 0; z-index: 1;
    background:
      radial-gradient(120% 100% at 50% 40%, transparent 60%, rgb(19 42 61 / 0.16) 100%),
      linear-gradient(180deg, rgb(255 255 255 / 0.16) 0%, transparent 30%, rgb(19 42 61 / 0.08) 100%);
  }

  /* --- Wallpaper depth --------------------------------------------------
     desktop.js adds html.plx only when a fine pointer is present and
     motion is allowed, then lerps --plx-x/--plx-y (px) on .wallpaper.
     Two layers from one property pair: the gradient field takes 0.55 of
     the drift and the watermark another 0.55 on top of its parent's
     (pseudo-element transforms compound) — about 5px and 10px at a screen
     edge. Oversized so the drift never reveals an edge; transform3d only,
     so the whole effect lives on the compositor. */
  html.plx .wallpaper {
    inset: -12px;
    transform: translate3d(calc(var(--plx-x, 0px) * 0.55), calc(var(--plx-y, 0px) * 0.55), 0);
    will-change: transform;
  }
  html.plx .wallpaper::after {
    transform: translate3d(calc(var(--plx-x, 0px) * 0.55), calc(var(--plx-y, 0px) * 0.55), 0);
    will-change: transform;
  }



  /* --- The module experience (mx): the house design language for app pages.
     One hero band with a soft brand aurora, stat chips, filter pills, and a
     responsive card grid that composes cleanly from 300px wide upward
     (min(100%,…) keeps cards from ever forcing a horizontal scroll). ------- */
  .mx-hero {
    position: relative; border-radius: 1.2rem; padding: 1.35rem 1.5rem 1.2rem; margin-bottom: 1.1rem;
    background:
      radial-gradient(60% 120% at 12% 0%, rgb(33 168 225 / 0.14) 0%, transparent 60%),
      radial-gradient(50% 120% at 88% 10%, rgb(204 44 113 / 0.10) 0%, transparent 55%),
      radial-gradient(40% 100% at 55% 100%, rgb(249 236 15 / 0.10) 0%, transparent 60%),
      linear-gradient(180deg, rgb(255 255 255 / 0.85), rgb(244 250 254 / 0.65));
    border: 1px solid rgb(255 255 255 / 0.8);
    box-shadow: 0 10px 34px -18px rgb(19 42 61 / 0.35), inset 0 1px 0 rgb(255 255 255 / 0.9);
  }
  .mx-hero h1 { margin: 0 0 0.15rem; font-size: clamp(1.5rem, 4vw, 2rem); }
  .mx-hero .sub { margin: 0; }
  .mx-hero-row { display: flex; align-items: flex-end; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
  .mx-chips { display: flex; gap: 0.5rem; flex-wrap: wrap; }
  .mx-chip { background: rgb(255 255 255 / 0.75); border: 1px solid rgb(255 255 255 / 0.85); border-radius: 0.85rem;
             padding: 0.4rem 0.8rem; min-width: 5.4rem;
             box-shadow: 0 5px 16px -9px rgb(19 42 61 / 0.35), inset 0 1px 0 #fff; }
  .mx-chip b { display: block; font-size: 1.3rem; line-height: 1.1; font-variant-numeric: tabular-nums; }
  .mx-chip span { font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.07em; opacity: 0.65; white-space: nowrap; }
  .mx-chip.azure b { color: var(--azure-text, #1d84b8); }
  .mx-chip.saffron b { color: #b36f0e; }
  .mx-chip.rose b { color: #b02561; }
  .mx-pills { display: flex; gap: 0.4rem; flex-wrap: wrap; margin: 0.95rem 0 1rem; }
  .mx-pill { font-size: 0.8rem; font-weight: 650; padding: 0.32rem 0.8rem; border-radius: 999px;
             border: 1px solid rgb(19 42 61 / 0.14); color: var(--ink); text-decoration: none;
             background: transparent; box-shadow: inset 0 0 0 999px rgb(255 255 255 / 0.6); }
  .mx-pill:hover { box-shadow: inset 0 0 0 999px rgb(255 255 255 / 0.9); }
  .mx-pill.on { border-color: transparent; color: #fff; box-shadow: inset 0 0 0 999px #1d84b8; }
  .mx-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 20rem), 1fr)); gap: 0.9rem; }
  .mx-card {
    position: relative; border-radius: 1rem; padding: 1rem 1.1rem 0.95rem 1.25rem; min-width: 0;
    background: linear-gradient(180deg, rgb(255 255 255 / 0.9), rgb(248 252 255 / 0.75));
    border: 1px solid rgb(255 255 255 / 0.85);
    box-shadow: 0 10px 28px -16px rgb(19 42 61 / 0.35), inset 0 1px 0 rgb(255 255 255 / 0.95);
  }
  @media (prefers-reduced-motion: no-preference) {
    .mx-card { transition: transform 150ms var(--ease-out), box-shadow 150ms var(--ease-out); }
    .mx-card:hover { transform: translateY(-1px); box-shadow: 0 16px 34px -16px rgb(19 42 61 / 0.4), inset 0 1px 0 #fff; }
  }
  .mx-card::before { content: ''; position: absolute; left: 0; top: 0.85rem; bottom: 0.85rem; width: 4px;
                     border-radius: 4px; background: var(--mx-edge, linear-gradient(180deg, #21a8e1, #7cc7ec)); }
  .mx-card[data-tone='saffron'] { --mx-edge: linear-gradient(180deg, #f9ec0f, #e99415); }
  .mx-card[data-tone='rose'] { --mx-edge: linear-gradient(180deg, #e37fb2, #cc2c71); }
  .mx-card[data-tone='green'] { --mx-edge: linear-gradient(180deg, #6fd49a, #17b26a); }
  .mx-card[data-tone='plum'] { --mx-edge: linear-gradient(180deg, #cc2c71, #981965); }
  .mx-card[data-tone='graphite'] { --mx-edge: linear-gradient(180deg, #7b8aa0, #4a5568); }
  .mx-top { display: flex; align-items: center; gap: 0.65rem; min-width: 0; }
  .mx-disc { flex: none; width: 2.3rem; height: 2.3rem; border-radius: 999px; display: grid; place-items: center;
             color: #fff; font-weight: 750; font-size: 0.95rem; letter-spacing: 0.02em;
             background: var(--mx-edge, linear-gradient(135deg, #21a8e1, #1d84b8));
             box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.45), 0 4px 10px -4px rgb(19 42 61 / 0.5); }
  /* A real photograph where we have one — same disc, no letters. object-fit so
     a portrait is cropped to the circle rather than squashed into it. */
  .mx-disc--photo { object-fit: cover; padding: 0; background: none; }
  .mx-who { min-width: 0; flex: 1; }
  .mx-who b { display: block; font-size: 0.98rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .mx-who .hint { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .mx-when { flex: none; font-size: 0.72rem; color: var(--muted, #55707f); opacity: 0.85; white-space: nowrap; align-self: flex-start; }
  .mx-kind { display: inline-block; font-size: 0.68rem; font-weight: 750; letter-spacing: 0.06em; text-transform: uppercase;
             padding: 0.12rem 0.5rem; border-radius: 999px; background: rgb(19 42 61 / 0.07); }
  .mx-msg { margin: 0.6rem 0 0.2rem; font-size: 0.9rem; line-height: 1.5; overflow: hidden;
            display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; }
  .mx-ctx { display: flex; gap: 0.4rem; flex-wrap: wrap; margin-top: 0.5rem; }
  .mx-ctx .mx-kind { background: rgb(33 168 225 / 0.12); color: #135a85; text-transform: none; letter-spacing: 0; font-size: 0.72rem; }
  .mx-actions { display: flex; gap: 0.45rem; flex-wrap: wrap; margin-top: 0.8rem; }
  .mx-actions form { display: contents; }
  .mx-btn { font-size: 0.78rem; font-weight: 650; padding: 0.36rem 0.8rem; border-radius: 0.6rem; cursor: pointer;
            border: 1px solid rgb(19 42 61 / 0.15); color: var(--ink); text-decoration: none; display: inline-block;
            background: transparent; box-shadow: inset 0 0 0 999px #f2f8fc; }
  .mx-btn:hover { box-shadow: inset 0 0 0 999px #e5f2fa; }
  .mx-btn.primary { border-color: transparent; color: #fff; box-shadow: inset 0 0 0 999px #1d84b8; }
  .mx-btn.primary:hover { box-shadow: inset 0 0 0 999px #1a76a5; }
  .mx-btn.ghost { box-shadow: none; opacity: 0.72; }
  .mx-empty { text-align: center; padding: 3.2rem 1rem 3.4rem; border-radius: 1.1rem;
              background: linear-gradient(180deg, rgb(255 255 255 / 0.7), rgb(244 250 254 / 0.5));
              border: 1px dashed rgb(19 42 61 / 0.18); }
  .mx-empty img { width: 3.4rem; height: 3.4rem; opacity: 0.5; margin-bottom: 0.6rem; }
  .mx-empty p { margin: 0.2rem auto; max-width: 26rem; }
  @media (max-width: 420px) {
    .mx-hero { padding: 1.05rem 1rem 1rem; }
    .mx-actions .mx-btn { flex: 1 1 auto; text-align: center; }
    .mx-when { display: none; }
  }

  /* ==========================================================================
     THE POSTBAG — the comms-inbox (cx-*).

     A three-column mail client inside the admin window: a mailbox rail, a
     dense enquiry list, and a detail pane that opens beside the list. Same
     fixed-height / internal-scroll shape Messages (im-*) uses, so the body
     itself never scrolls; each column does. Tones reuse the Spice Rack tokens
     so an enquiry keeps the colour it had in the old grid.
     ========================================================================== */
  /* A CONTAINER, not the viewport. Comms runs inside a window in the desktop
     shell, and that window is about 945px wide on a 1280px screen — so a
     viewport media query at 1180px was permanently true, Comms permanently
     believed it was on a small screen, and opening a conversation hid the
     conversation LIST. The panel "disappearing" was this: the fold measuring
     the browser instead of the app. Same lesson as the site's nav, which
     switches on 74rem of container for the same reason. */
  .cx { display: grid; grid-template-columns: minmax(13.5rem, 15rem) minmax(0, 1fr) minmax(0, 1.4fr);
        gap: 0.85rem; height: calc(100dvh - 11.5rem); min-height: 30rem; }
  .cx-rail, .cx-list, .cx-pane { min-height: 0; overflow-y: auto; border-radius: 18px;
        background: rgb(255 255 255 / 0.6); border: 1px solid rgb(255 255 255 / 0.85);
        box-shadow: 0 10px 30px -18px rgb(19 42 61 / 0.35); backdrop-filter: blur(10px); }

  /* --- the mailbox rail --------------------------------------------------- */
  .cx-rail { display: flex; flex-direction: column; padding: 0.6rem; gap: 0.15rem; }
  .cx-brand { display: flex; align-items: center; gap: 0.55rem; padding: 0.45rem 0.5rem 0.65rem; }
  .cx-brand-mark { width: 34px; height: 34px; border-radius: 9px; box-shadow: 0 4px 12px -5px rgb(19 42 61 / 0.5); }
  .cx-brand-name { font-family: var(--font-display); font-weight: 600; font-size: 1.05rem; letter-spacing: -0.01em; }
  .cx-nav { display: flex; flex-direction: column; gap: 0.12rem; }
  .cx-navsect { font-size: 0.68rem; font-weight: 800; letter-spacing: 0.11em; text-transform: uppercase;
                color: var(--ink-faint); padding: 0.7rem 0.6rem 0.25rem; display: flex; align-items: baseline; gap: 0.4rem; }
  .cx-navsect-note { font-weight: 600; letter-spacing: 0.01em; text-transform: none; opacity: 0.75; font-size: 0.68rem; }
  .cx-box { display: flex; align-items: center; gap: 0.5rem; padding: 0.42rem 0.6rem; border-radius: 0.65rem;
            color: var(--ink); text-decoration: none; font-size: 0.85rem; font-weight: 600; }
  .cx-box:hover { background: rgb(33 168 225 / 0.1); }
  .cx-box.on { background: linear-gradient(135deg, #1d84b8, #21a8e1); color: #fff;
               box-shadow: 0 4px 14px -5px rgb(33 168 225 / 0.6); }
  .cx-box-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .cx-count { font-size: 0.7rem; font-variant-numeric: tabular-nums; color: var(--ink-faint);
              background: rgb(19 42 61 / 0.08); border-radius: 999px; padding: 0.05rem 0.45rem; }
  .cx-box.on .cx-count { background: rgb(255 255 255 / 0.25); color: #fff; }
  .cx-badge { font-size: 0.7rem; font-variant-numeric: tabular-nums; color: var(--ink-faint);
              background: rgb(19 42 61 / 0.08); border-radius: 999px; padding: 0.05rem 0.45rem; min-width: 1.1rem; text-align: center; }
  .cx-badge.unread { color: #fff; font-weight: 800;
              background: linear-gradient(135deg, #d9536f, #e8747f); box-shadow: 0 3px 8px -3px rgb(217 83 111 / 0.6); }
  .cx-box.on .cx-badge { background: rgb(255 255 255 / 0.25); color: #fff; }
  .cx-dot { width: 9px; height: 9px; border-radius: 50%; flex: none; background: #7b8aa0; }

  /* --- shared tones (disc / dot / chip) ----------------------------------- */
  [data-tone='azure']   { --cx-tone: #21a8e1; --cx-tone-2: #1d84b8; --cx-tone-ink: #135a85; }
  [data-tone='rose']    { --cx-tone: #e37fb2; --cx-tone-2: #cc2c71; --cx-tone-ink: #b02561; }
  [data-tone='saffron'] { --cx-tone: #f9ec0f; --cx-tone-2: #e99415; --cx-tone-ink: #a1660f; }
  [data-tone='green']   { --cx-tone: #6fd49a; --cx-tone-2: #17b26a; --cx-tone-ink: #0f7a49; }
  [data-tone='plum']    { --cx-tone: #cc2c71; --cx-tone-2: #981965; --cx-tone-ink: #7a1450; }
  [data-tone='graphite']{ --cx-tone: #7b8aa0; --cx-tone-2: #4a5568; --cx-tone-ink: #3a4756; }
  .cx-dot[data-tone] { background: var(--cx-tone-2); }
  /* "All enquiries" wears every kind at once. */
  .cx-dot[data-tone='all'] { background: conic-gradient(from 210deg, #21a8e1, #cc2c71, #e99415, #17b26a, #981965, #21a8e1); }

  /* --- the dense list ----------------------------------------------------- */
  .cx-list { display: flex; flex-direction: column; }
  .cx-listhead { position: sticky; top: 0; z-index: 1; padding: 0.8rem 0.95rem 0.6rem;
                 background: linear-gradient(180deg, rgb(255 255 255 / 0.92), rgb(255 255 255 / 0.72));
                 backdrop-filter: blur(8px); border-bottom: 1px solid rgb(20 60 90 / 0.08);
                 display: flex; align-items: baseline; justify-content: space-between; gap: 0.6rem; }
  .cx-listhead h1 { font-size: 1.15rem; }
  .cx-listhead .toast { margin: 0; }
  .cx-rows { padding: 0.35rem; display: flex; flex-direction: column; gap: 0.1rem; }
  .cx-row { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 0.6rem; align-items: center;
            padding: 0.5rem 0.6rem; border-radius: 12px; text-decoration: none; color: inherit;
            border: 1px solid transparent; }
  .cx-row:hover { background: rgb(29 132 184 / 0.07); }
  .cx-row.active { background: linear-gradient(120deg, rgb(29 132 184 / 0.14), rgb(33 168 225 / 0.08));
                   border-color: rgb(29 132 184 / 0.25); }
  .cx-row.unread .cx-rtop b { font-weight: 800; }
  .cx-row.unread .cx-disc::after { content: ''; position: absolute; top: -1px; right: -1px; width: 9px; height: 9px;
                   border-radius: 50%; background: #d9536f; border: 2px solid #fff; }
  .cx-disc { position: relative; flex: none; width: 2.3rem; height: 2.3rem; border-radius: 999px; display: grid;
             place-items: center; color: #fff; font-weight: 750; font-size: 0.9rem; letter-spacing: 0.02em;
             background: linear-gradient(135deg, var(--cx-tone, #21a8e1), var(--cx-tone-2, #1d84b8));
             box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.45), 0 4px 10px -5px rgb(19 42 61 / 0.5); }
  .cx-disc.lg { width: 2.8rem; height: 2.8rem; font-size: 1.05rem; }
  .cx-rmain { min-width: 0; }
  .cx-rtop { display: flex; align-items: center; gap: 0.4rem; min-width: 0; }
  .cx-rtop b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.88rem; }
  .cx-rprev { display: block; font-size: 0.75rem; color: var(--ink-faint); overflow: hidden;
              text-overflow: ellipsis; white-space: nowrap; }
  .cx-rside { display: flex; flex-direction: column; align-items: flex-end; gap: 0.3rem; }
  .cx-chip { display: inline-block; font-size: 0.68rem; font-weight: 750; letter-spacing: 0.02em;
             padding: 0.1rem 0.5rem; border-radius: 999px; white-space: nowrap;
             background: color-mix(in oklab, var(--cx-tone-2, #7b8aa0) 16%, transparent); color: var(--cx-tone-ink, #3a4756); }
  .cx-chip.ghost { background: rgb(19 42 61 / 0.07); color: var(--ink-muted); }
  .cx-age { display: inline-flex; align-items: center; gap: 0.3rem; font-size: 0.72rem; font-variant-numeric: tabular-nums;
            color: var(--ink-faint); white-space: nowrap; }
  .cx-heat { width: 7px; height: 7px; border-radius: 50%; flex: none; background: #9bb0bf; }
  .cx-age[data-heat='green'] .cx-heat { background: #17b26a; }
  .cx-age[data-heat='amber'] .cx-heat { background: #e99415; }
  .cx-age[data-heat='red'] .cx-heat { background: #da411d; box-shadow: 0 0 0 3px rgb(218 65 29 / 0.16); }
  .cx-statustag { font-size: 0.68rem; font-weight: 800; text-transform: uppercase; letter-spacing: 0.05em;
                  border-radius: 999px; padding: 0.04rem 0.42rem; background: rgb(20 60 90 / 0.08); color: #6b8194; }
  .cx-statustag[data-status='actioned'] { background: rgb(23 178 106 / 0.14); color: #0f7a49; }
  .cx-statustag[data-status='archived'] { background: rgb(19 42 61 / 0.08); color: #6b8194; }

  /* --- the detail pane ---------------------------------------------------- */
  .cx-pane { display: flex; flex-direction: column; }
  .cx-pane[aria-busy='true'] { opacity: 0.55; }
  .cx-phead { display: flex; align-items: center; gap: 0.7rem; padding: 0.9rem 1rem; color: #fff;
              background: linear-gradient(130deg, #0e2a3d, #123a54 45%, #1d84b8); position: relative; }
  .cx-phead .cx-disc { box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.35), 0 4px 12px -5px rgb(0 0 0 / 0.5); }
  .cx-pwho { min-width: 0; }
  .cx-pwho > b { display: block; font-size: 1.02rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .cx-pmeta { display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem; margin-top: 0.2rem; }
  .cx-pmeta .cx-chip { background: rgb(255 255 255 / 0.18); color: #fff; }
  .cx-pmeta .cx-chip.ghost { background: rgb(255 255 255 / 0.12); }
  .cx-pwhen { font-size: 0.7rem; opacity: 0.85; }
  .cx-phead .cx-statustag { background: rgb(255 255 255 / 0.22); color: #fff; }
  .cx-pbody { flex: 1; overflow-y: auto; padding: 0.9rem 1rem 1rem; display: flex; flex-direction: column; gap: 0.75rem; }
  .cx-contact { display: flex; flex-wrap: wrap; gap: 0.4rem; }
  .cx-contact-btn { font-size: 0.8rem; font-weight: 650; text-decoration: none; color: var(--azure-text);
                    background: rgb(33 168 225 / 0.1); border: 1px solid rgb(33 168 225 / 0.2);
                    border-radius: 999px; padding: 0.3rem 0.7rem; }
  .cx-contact-btn:hover { background: rgb(33 168 225 / 0.18); }
  .cx-trip { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.35rem 0.6rem; padding: 0.6rem 0.75rem;
             border-radius: 0.8rem; background: rgb(33 168 225 / 0.07); border: 1px solid rgb(33 168 225 / 0.14); }
  .cx-trip-k { font-size: 0.68rem; font-weight: 800; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-faint); }
  .cx-trip b { font-size: 0.95rem; }
  .cx-trip-meta { font-size: 0.78rem; color: var(--ink-muted); }
  .cx-trip-meta::before { content: '· '; color: var(--ink-faint); }
  /* the message as a conversation */
  .cx-conv { display: flex; flex-direction: column; gap: 0.5rem; }
  .cx-bubble { max-width: 82%; padding: 0.5rem 0.7rem; border-radius: 14px; }
  .cx-bubble[data-side='visitor'] { align-self: flex-start; background: #eef5fa; border: 1px solid rgb(29 132 184 / 0.14); border-bottom-left-radius: 5px; }
  .cx-bubble[data-side='rosie'] { align-self: flex-end; background: #fbf3e2; border: 1px solid rgb(233 148 21 / 0.28); border-bottom-right-radius: 5px; }
  .cx-bubble[data-side='team'] { align-self: flex-end; background: linear-gradient(135deg, #1d84b8, #21a8e1); color: #fff; border-bottom-right-radius: 5px; }
  .cx-bubble-head { display: flex; gap: 0.5rem; align-items: baseline; font-size: 0.68rem; margin-bottom: 0.15rem; }
  .cx-bubble-head b { font-size: 0.72rem; }
  .cx-bubble-head span { opacity: 0.7; font-variant-numeric: tabular-nums; }
  .cx-bubble[data-side='team'] .cx-bubble-head { color: rgb(255 255 255 / 0.85); }
  .cx-bubble-text { font-size: 0.86rem; line-height: 1.45; white-space: pre-wrap; word-break: break-word; }
  .cx-note { font-size: 0.9rem; line-height: 1.55; white-space: pre-wrap; word-break: break-word;
             padding: 0.7rem 0.85rem; border-radius: 0.8rem; background: rgb(255 255 255 / 0.7);
             border: 1px solid rgb(20 60 90 / 0.1); }
  .cx-note--empty { color: var(--ink-faint); font-style: italic; }
  /* a still-present "[… removed]" redaction, made impossible to skim past */
  .cx-redact { font-weight: 700; color: #7a2712; background: #f6c9bd; border-radius: 5px; padding: 0 0.28rem; white-space: nowrap; }
  .cx-field { display: flex; flex-direction: column; gap: 0.1rem; }
  .cx-field > label { font-size: 0.68rem; font-weight: 800; text-transform: uppercase; letter-spacing: 0.07em; color: var(--ink-faint); }
  .cx-field > p { margin: 0; font-size: 0.9rem; word-break: break-word; }
  .cx-companions { margin: 0.15rem 0 0; padding-left: 1.1rem; font-size: 0.85rem; }
  .cx-actions { display: flex; flex-wrap: wrap; gap: 0.45rem; padding: 0.7rem 1rem; border-top: 1px solid rgb(20 60 90 / 0.08);
                background: rgb(255 255 255 / 0.72); }
  .cx-actions form { display: contents; }
  .cx-btn { font-size: 0.8rem; font-weight: 650; padding: 0.4rem 0.85rem; border-radius: 0.6rem; cursor: pointer;
            border: 1px solid rgb(19 42 61 / 0.15); color: var(--ink); text-decoration: none; display: inline-block;
            background: transparent; box-shadow: inset 0 0 0 999px #f2f8fc; }
  .cx-btn:hover { box-shadow: inset 0 0 0 999px #e5f2fa; }
  .cx-btn.primary { border-color: transparent; color: #fff; box-shadow: inset 0 0 0 999px #1d84b8; }
  .cx-btn.primary:hover { box-shadow: inset 0 0 0 999px #1a76a5; }
  .cx-btn.ghost { box-shadow: none; opacity: 0.75; }

  /* --- empty states ------------------------------------------------------- */
  .cx-empty, .cx-pane-empty { flex: 1; display: grid; place-items: center; text-align: center; color: var(--ink-muted); padding: 2.4rem 1.4rem; }
  .cx-empty img, .cx-pane-empty img { width: 3.2rem; height: 3.2rem; opacity: 0.55; margin: 0 auto 0.7rem; display: block; }
  .cx-empty p, .cx-pane-empty p { margin: 0.2rem auto; max-width: 22rem; }

  /* --- responsive: three columns fold to a stage ------------------------- */
  /* 52rem (832px), not 1180px. Rail 15rem + list and pane sharing the rest
     still reads at a 945px window, which is the ordinary size of the Comms
     window on a laptop — folding there cost the list for no benefit. Below
     this the three genuinely do not fit and the stage is right. */
  @container comms (max-width: 52rem) {
    .cx { grid-template-columns: minmax(12rem, 14rem) minmax(0, 1fr); }
    .cx-pane { display: none; }
    .cx.cx--open .cx-list { display: none; }
    .cx.cx--open .cx-pane { display: flex; }
  }
  @container comms (max-width: 42rem) {
    .cx { display: block; height: auto; }
    .cx-rail, .cx-list, .cx-pane { height: auto; max-height: none; overflow: visible; }
    .cx-rail { margin-bottom: 0.8rem; }
  }
  @media (prefers-reduced-motion: reduce) { .cx-row, .cx-box, .cx-btn { transition: none !important; } }

  /* --- Module sub-navigation: the app-within-an-app sidebar ---------------- */
  .mx-shell { display: grid; grid-template-columns: 12.5rem minmax(0, 1fr); gap: 1.4rem; align-items: start; }
  @media (max-width: 860px) { .mx-shell { grid-template-columns: 1fr; } .mx-side { position: static; flex-direction: row; overflow-x: auto; } }
  .mx-side { position: sticky; top: 0.5rem; display: flex; flex-direction: column; gap: 0.25rem;
             background: linear-gradient(180deg, rgb(255 255 255 / 0.72), rgb(240 248 253 / 0.5));
             border: 1px solid rgb(255 255 255 / 0.7); border-radius: 1.1rem; padding: 0.55rem;
             box-shadow: 0 8px 30px -12px rgb(19 42 61 / 0.25), inset 0 1px 0 rgb(255 255 255 / 0.8); }
  .mx-sitem { display: flex; align-items: center; gap: 0.55rem; padding: 0.5rem 0.7rem; border-radius: 0.7rem;
              color: var(--ink); text-decoration: none; font-size: 0.86rem; font-weight: 600; white-space: nowrap; }
  .mx-sitem:hover { background: rgb(33 168 225 / 0.1); }
  .mx-sitem.on { background: linear-gradient(135deg, #1d84b8, #21a8e1); color: #fff;
                 box-shadow: 0 4px 14px -4px rgb(33 168 225 / 0.6); }
  .mx-sitem .count { margin-left: auto; font-size: 0.68rem; background: rgb(19 42 61 / 0.1); border-radius: 999px; padding: 0.05rem 0.45rem; }
  .mx-sitem.on .count { background: rgb(255 255 255 / 0.25); }

  /* --- Theme variants: alternative skies over the same OS ------------------ */
  html[data-spice-theme='sunset'] .wallpaper, html.evening[data-spice-theme='sunset'] .wallpaper {
    background-color: #f7dcc0;
    background-image:
      radial-gradient(at 78% 12%, rgb(233 148 21 / 0.5) 0px, transparent 46%),
      radial-gradient(at 18% 30%, rgb(249 236 15 / 0.34) 0px, transparent 48%),
      radial-gradient(at 70% 70%, rgb(204 44 113 / 0.28) 0px, transparent 52%),
      radial-gradient(at 12% 86%, rgb(216 65 33 / 0.22) 0px, transparent 48%),
      linear-gradient(168deg, #fff4e4 0%, #f7dcc0 55%, #eab98f 100%);
  }
  html[data-spice-theme='orchid'] .wallpaper, html.evening[data-spice-theme='orchid'] .wallpaper {
    background-color: #ecd9f2;
    background-image:
      radial-gradient(at 80% 14%, rgb(204 44 113 / 0.35) 0px, transparent 46%),
      radial-gradient(at 16% 26%, rgb(152 25 101 / 0.26) 0px, transparent 50%),
      radial-gradient(at 68% 74%, rgb(33 168 225 / 0.2) 0px, transparent 52%),
      radial-gradient(at 24% 84%, rgb(249 236 15 / 0.16) 0px, transparent 46%),
      linear-gradient(168deg, #f8ecfb 0%, #ecd9f2 55%, #d4b3e0 100%);
  }
  html[data-spice-theme='ocean'] .wallpaper, html.evening[data-spice-theme='ocean'] .wallpaper {
    background-color: #bfe0f2;
    background-image:
      radial-gradient(at 76% 16%, rgb(24 90 157 / 0.4) 0px, transparent 50%),
      radial-gradient(at 20% 30%, rgb(33 168 225 / 0.45) 0px, transparent 50%),
      radial-gradient(at 66% 76%, rgb(23 178 106 / 0.2) 0px, transparent 52%),
      radial-gradient(at 18% 84%, rgb(152 25 101 / 0.12) 0px, transparent 46%),
      linear-gradient(168deg, #e2f3fc 0%, #bfe0f2 55%, #86b8d9 100%);
  }
  html[data-spice-theme='noir'] .wallpaper, html.evening[data-spice-theme='noir'] .wallpaper {
    background-color: #23283a;
    background-image:
      radial-gradient(at 78% 12%, rgb(33 168 225 / 0.2) 0px, transparent 46%),
      radial-gradient(at 20% 32%, rgb(204 44 113 / 0.16) 0px, transparent 50%),
      radial-gradient(at 70% 74%, rgb(233 148 21 / 0.12) 0px, transparent 52%),
      linear-gradient(168deg, #2d3348 0%, #23283a 55%, #171b29 100%);
  }
  html[data-spice-theme='noir'] .wallpaper::after, html.evening[data-spice-theme='noir'] .wallpaper::after { opacity: 0.07; filter: invert(1) brightness(1.4); }
  html[data-spice-theme='sunset'] .wallpaper::after, html.evening[data-spice-theme='sunset'] .wallpaper::after, html[data-spice-theme='orchid'] .wallpaper::after, html.evening[data-spice-theme='orchid'] .wallpaper::after { opacity: 0.14; }

  /* A chosen theme overrides the automatic evening look entirely. */
  html.evening[data-spice-theme] .wallpaper-scrim {
    background:
      radial-gradient(120% 100% at 50% 40%, transparent 60%, rgb(19 42 61 / 0.16) 100%),
      linear-gradient(180deg, rgb(255 255 255 / 0.16) 0%, transparent 30%, rgb(19 42 61 / 0.08) 100%);
  }

  /* --- Theme picker (menubar, CSS-only dropdown) --------------------------- */
  .menubar-themes { position: relative; }
  .menubar-themes summary { list-style: none; cursor: pointer; display: grid; place-items: center; width: 1.4rem; height: 1.4rem; border-radius: 0.45rem; }
  .menubar-themes summary::-webkit-details-marker { display: none; }
  .menubar-themes summary:hover { background: rgb(19 42 61 / 0.08); }
  .theme-dot { width: 0.78rem; height: 0.78rem; border-radius: 999px; display: inline-block; flex: none;
               box-shadow: inset 0 0 0 1px rgb(19 42 61 / 0.2), inset 0 2px 3px rgb(255 255 255 / 0.7); }
  .theme-dot[data-dot='drift'] { background: linear-gradient(135deg, #8fd0f2, #21a8e1); }
  .theme-dot[data-dot='sunset'] { background: linear-gradient(135deg, #f9ec0f, #e99415); }
  .theme-dot[data-dot='orchid'] { background: linear-gradient(135deg, #e37fb2, #981965); }
  .theme-dot[data-dot='ocean'] { background: linear-gradient(135deg, #21a8e1, #185a9d); }
  .theme-dot[data-dot='noir'] { background: linear-gradient(135deg, #4a5168, #171b29); }
  .menubar-themes-menu {
    position: absolute; right: 0; top: 1.9rem; z-index: 40; min-width: 10.5rem;
    display: flex; flex-direction: column; gap: 0.1rem; padding: 0.4rem;
    background: rgb(255 255 255 / 0.82); border: 1px solid rgb(255 255 255 / 0.8); border-radius: 0.9rem;
    backdrop-filter: blur(24px) saturate(1.8); -webkit-backdrop-filter: blur(24px) saturate(1.8);
    box-shadow: 0 18px 50px -18px rgb(19 42 61 / 0.5), inset 0 1px 0 rgb(255 255 255 / 0.9);
  }
  .theme-row { display: flex; align-items: center; gap: 0.55rem; padding: 0.38rem 0.6rem; border-radius: 0.55rem;
               font-size: 0.82rem; font-weight: 600; color: var(--ink); text-decoration: none; }
  .theme-row:hover { background: rgb(33 168 225 / 0.12); }
  .theme-row.on { background: rgb(33 168 225 / 0.16); }

  /* --- Menu bar ------------------------------------------------------------
     Liquid glass. The blur lives on an oversized child masked back to the
     bar (the edge-bleed trick): backdrop-filter only samples pixels directly
     behind an element, so without this the wallpaper just below the bar
     never scatters up into the glass the way real frosted glass behaves. */
  .menubar {
    position: relative; z-index: 3; flex: none;
    display: flex; align-items: center; justify-content: space-between;
    height: 2rem; padding-inline: 1rem;
    background: var(--os-glass);
    border-bottom: 1px solid var(--os-hairline);
    box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.6);
    color: var(--ink); font-size: 0.82rem;
  }
  /* INSTALLED WITH THE MENU BAR AS THE TITLE BAR. When SpiceOS is installed
     and the browser grants window-controls-overlay, the OS's own window
     buttons sit over the top edge and env(titlebar-area-*) says where the
     rest of the title bar is. The menu bar moves into that strip and becomes
     the drag handle (app-region: drag), with every control opted back out so
     it still clicks. In a tab none of this applies. */
  @media (display-mode: window-controls-overlay) {
    .menubar {
      position: fixed; top: env(titlebar-area-y, 0); left: env(titlebar-area-x, 0);
      width: env(titlebar-area-width, 100%); height: env(titlebar-area-height, 2rem);
      -webkit-app-region: drag; app-region: drag;
    }
    .menubar a, .menubar button, .menubar input, .menubar [role="button"] { -webkit-app-region: no-drag; app-region: no-drag; }
    body.framed .desktop, .desktop { padding-top: env(titlebar-area-height, 2rem); }
  }
  .menubar-backdrop {
    position: absolute; left: 0; right: 0; top: 0; height: 200%;
    backdrop-filter: blur(28px) saturate(2) brightness(1.06);
    -webkit-backdrop-filter: blur(28px) saturate(2) brightness(1.06);
    mask-image: linear-gradient(to bottom, #000 0% 50%, transparent 50% 100%);
    -webkit-mask-image: linear-gradient(to bottom, #000 0% 50%, transparent 50% 100%);
    pointer-events: none;
  }
  .menubar-left, .menubar-right { display: flex; align-items: center; gap: 0.6rem; min-width: 0; position: relative; }
  .menubar-mark { display: block; width: 18px; height: 18px; filter: drop-shadow(0 1px 1px rgb(19 42 61 / 0.2)); }
  .menubar-app { font-weight: 700; }
  .hub-toasts { position: fixed; right: 1rem; bottom: 1rem; z-index: 100050; display: flex;
                flex-direction: column; gap: 0.5rem; max-width: min(24rem, calc(100vw - 2rem)); }
  .hub-toast { display: grid; gap: 0.15rem; padding: 0.7rem 0.9rem; border-radius: 13px;
               background: rgb(255 255 255 / 0.92); backdrop-filter: blur(10px);
               border: 1px solid rgb(255 255 255 / 0.9); box-shadow: 0 14px 34px -16px rgb(19 42 61 / 0.55);
               font-size: 0.84rem; color: #16222c; animation: hub-in 0.24s cubic-bezier(0.22, 1, 0.36, 1) both; }
  .hub-toast.out { animation: hub-out 0.5s ease forwards; }
  .hub-toast strong { font-size: 0.88rem; }
  .hub-toast span { color: #55707f; font-size: 0.79rem; }
  .hub-toast a { justify-self: start; margin-top: 0.2rem; font-weight: 700; font-size: 0.78rem;
                 color: #1d84b8; text-decoration: none; }
  .hub-toast::before { content: ''; position: absolute; inset: 0 auto 0 0; width: 3px;
                       border-radius: 13px 0 0 13px; background: #e8a33d; }
  .hub-toast { position: relative; overflow: hidden; }
  .hub-chat::before { background: #21a8e1; }
  .hub-enquiry::before { background: #17b26a; }
  .hub-publish::before { background: #1d84b8; }
  /* A handover is the one toast addressed to you personally, and the only
     one that is a request rather than news. It gets its own colour and it
     stays put — see the dwell rule in the hub. */
  .hub-transfer::before { background: #981965; }
  .hub-transfer { border-color: rgb(152 25 101 / 0.35); }
  /* A visitor asking for a person. The one toast that is a phone ringing
     rather than a message: red where the handover is plum, pulsing while
     the loop sounds, and it stays until somebody picks up, the ring is
     missed, or it is dismissed here. .stale is the toast after its 45 s —
     still there, still a link to the room, no longer pretending to ring. */
  .hub-chat-ring::before { background: #e11d48; }
  .hub-chat-ring { border-color: rgb(225 29 72 / 0.4);
                   animation: hub-in 0.24s cubic-bezier(0.22, 1, 0.36, 1) both, hub-pulse 1.2s ease-in-out infinite alternate; }
  .hub-chat-ring.stale { animation: none; }
  .hub-staff-in::before { background: #17b26a; }
  .hub-staff-out::before, .hub-staff-quiet::before { background: #8fa6b5; }
  .hub-team::before { background: #21a8e1; }
  .hub-team-mention::before { background: #cc2c71; }
  /* Who is signed in, as initials in the menu bar. A green dot because the
     list only ever holds people who are here now. */
  .menubar-presence { display: inline-flex; gap: 0.2rem; align-items: center; margin-right: 0.35rem; }
  /* Not signed in: the same chip, dimmed, no dot. Present so the bar reads as
     the whole team and absence is visible, rather than a bar that shrinks to
     whoever happens to be here. */
  .menubar-presence .presence-chip.off { opacity: 0.38; filter: grayscale(1); }
  .menubar-presence .presence-chip.off::after { display: none; }
  .presence-chip { position: relative; width: 22px; height: 22px; border-radius: 50%; display: inline-grid; place-items: center;
                   font-size: 0.68rem; font-weight: 800; letter-spacing: 0.02em; color: #0e2a3d;
                   background: rgb(255 255 255 / 0.55); border: 1px solid rgb(14 42 61 / 0.14); }
  .presence-chip.me { opacity: 0.7; }
  .presence-chip::after { content: ''; position: absolute; right: -1px; bottom: -1px; width: 7px; height: 7px; border-radius: 50%;
                          background: #17b26a; border: 1.5px solid #fff; }
  @keyframes hub-pulse { to { box-shadow: 0 0 0 6px rgb(225 29 72 / 0.15); } }
  @media (prefers-reduced-motion: reduce) { .hub-chat-ring { animation: none; } }
  .hub-shut { position: absolute; top: 0.3rem; right: 0.4rem; border: 0; background: none; cursor: pointer;
              font-size: 1rem; line-height: 1; color: #8aa0ad; padding: 0.1rem 0.2rem; }
  .hub-shut:hover { color: #16222c; }
  @keyframes hub-in { from { opacity: 0; transform: translateY(10px) scale(0.98); } }
  @keyframes hub-out { to { opacity: 0; transform: translateY(-6px); } }
  @media (prefers-reduced-motion: reduce) { .hub-toast, .hub-toast.out { animation: none; } }
  .menubar-sep { opacity: 0.4; }
  .menubar-title { opacity: 0.85; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .menubar-user { opacity: 0.75; }
  .menubar-clock { font-variant-numeric: tabular-nums; opacity: 0.9; min-width: 3.2em; text-align: right; white-space: nowrap; }

  /* --- The window ------------------------------------------------------------
     Floats over the wallpaper with visible margin on every side — that
     margin is what actually reads as "a window on a desktop" rather than
     "a page with a rounded top". Shadow stack from the macos-web clone's
     active window, scaled to this size. */
  .desktop {
    position: relative; z-index: 2; flex: 1; min-height: 0;
    display: flex; justify-content: center;
    padding: 1.1rem 1.1rem 7.2rem; /* bottom clears the floating dock */
  }
  .window {
    display: flex; flex-direction: column;
    width: 100%; max-width: min(100rem, 100%);
    background: transparent;
    isolation: isolate;
    border: 1px solid rgb(255 255 255 / 0.65);
    border-radius: var(--radius-l);
    /* The brand's chamfer, cut natively: corner-shape bevels the top-right
       corner through the border-radius engine itself, so the border, the
       overflow clip AND the shadow all follow the cut — none of the
       clip-path/drop-shadow contortions the same silhouette used to need.
       Browsers without corner-shape simply keep the round corner. */
    border-top-right-radius: 2.2rem;
    corner-shape: round bevel round round;
    box-shadow:
      0 10px 14px rgb(6 10 18 / 0.2),
      0 55px 90px -18px rgb(6 10 18 / 0.42),
      -28px 44px 110px -38px rgb(33 168 225 / 0.5),
      28px 50px 120px -38px rgb(204 44 113 / 0.35),
      0 2px 0 rgb(255 255 255 / 0.7) inset,
      0 0 0 1px rgb(255 255 255 / 0.28) inset;
    overflow: hidden;
    transform-origin: 50% 62%;
  }
  .window::before {
    content: ''; position: absolute; inset: 0; z-index: -1;
    background: color-mix(in oklab, var(--paper) 82%, transparent);
    backdrop-filter: blur(32px) saturate(1.7);
    -webkit-backdrop-filter: blur(32px) saturate(1.7);
  }
  @media (prefers-reduced-motion: no-preference) {
    .window { animation: window-open 200ms var(--ease-out) backwards; }
  }
  @keyframes window-open {
    from { opacity: 0; transform: scale(0.985) translateY(5px); }
    to { opacity: 1; transform: none; }
  }
  /* Back and forward sit beside the traffic lights.
     THEY WERE TOO QUIET. At 0.28 opacity a disabled chevron on a pale title
     bar is not "quiet", it is invisible — and since a freshly opened window
     has been nowhere, BOTH are disabled at exactly the moment somebody is
     forming their idea of what a window can do. Reported as "we've no
     navigation in the windows", which was never true: the history, the
     per-window depth tracking and the title that follows the page were all
     working. Nobody could see the control.
     So: an enclosing hairline so the pair reads as one control whatever
     state it is in, and a disabled step that still shows. Learnable when
     idle, obvious when live. */
  /* An action living in the filter bar, pushed to the far end so the pills
     stay a set and the button does not read as one of them. */
  .mx-pills-act { margin-left: auto; }
  /* A page-level action above a list, right-aligned so the list starts where
     the eye already is. */
  .st-act { display: flex; justify-content: flex-end; margin: 0 0 0.8rem; }

  .win-nav {
    display: inline-flex; gap: 0.1rem; margin-left: 0.55rem; padding: 0.05rem;
    border: 1px solid rgb(19 42 61 / 0.14); border-radius: 0.45rem;
  }
  .win-nav-btn {
    width: 1.35rem; height: 1.35rem; padding: 0; border: 0; border-radius: 0.35rem;
    background: transparent; color: var(--ink-muted, #55707f);
    font: inherit; font-size: 1rem; line-height: 1; cursor: pointer;
    display: inline-flex; align-items: center; justify-content: center;
    transition: background 120ms ease, color 120ms ease, opacity 120ms ease;
  }
  .win-nav-btn:hover:not(:disabled) { background: rgb(19 42 61 / 0.08); color: var(--ink, #132a3d); }
  .win-nav-btn:active:not(:disabled) { transform: scale(0.92); }
  .win-nav-btn:focus-visible { outline: 2px solid var(--azure-text, #166f95); outline-offset: 1px; }
  .win-nav-btn:disabled { opacity: 0.42; cursor: default; }
  /* The bar centres its title, so the keep-on-top button is pinned to the
     right the way the traffic lights are pinned to the left. */
  .win-pop { position: absolute; right: 0.9rem; top: 50%; transform: translateY(-50%); }
  /* Dictation (desktop.js): drawn under the box it fills, and only where the
     browser has speech recognition at all. */
  .dictate-row { display: flex; align-items: center; gap: 0.5rem; margin: 0.35rem 0 0; }
  .dictate {
    display: inline-flex; align-items: center; gap: 0.35rem;
    padding: 0.25rem 0.6rem; border-radius: 999px;
    border: 1px solid rgb(19 42 61 / 0.16); background: transparent;
    font: inherit; font-size: 0.82rem; color: var(--ink-muted, #55707f); cursor: pointer;
  }
  .dictate:hover { color: var(--ink, #132a3d); background: rgb(19 42 61 / 0.05); }
  .dictate:focus-visible { outline: 2px solid var(--azure-text, #166f95); outline-offset: 1px; }
  .dictate.on { color: var(--azure-text, #166f95); border-color: var(--azure-text, #166f95); box-shadow: 0 0 0 3px rgb(22 111 149 / 0.16); }
  .dictate-say { font-size: 0.8rem; color: var(--ink-muted, #55707f); }
  .win-popped {
    position: absolute; inset: 2.75rem 0 0; z-index: 3;
    display: grid; align-content: center; justify-items: center; gap: 0.7rem;
    padding: 1.2rem; text-align: center; background: rgb(238 246 251 / 0.94);
  }
  .win-popped p { margin: 0; color: var(--ink-muted, #55707f); }
  @media (prefers-reduced-motion: reduce) { .win-nav-btn { transition: none; } }

  .window-bar {
    flex: none; position: relative;
    display: flex; align-items: center; justify-content: center;
    height: 2.75rem; padding-inline: 0.9rem;
    background: linear-gradient(180deg, rgb(255 255 255 / 0.66) 0%, rgb(238 246 251 / 0.42) 100%);
    backdrop-filter: blur(26px) saturate(1.8);
    -webkit-backdrop-filter: blur(26px) saturate(1.8);
    box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.85);
    /* Dragging must feel wired to the cursor: no browser gesture handling,
       no scroll hijack — the pointermove stream goes straight to the
       compositor transform with zero smoothing in between. */
    touch-action: none;
  }
  /* The spice ribbon — the six logo colours as the titlebar's base line,
     the same device the public site's sign-in card wears. It carries the
     FOCUS state: lit on the active window, ash on the rest. It also
     ignites (sweeps in) whenever a window opens. */
  .window-bar::after {
    content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 2px;
    background: linear-gradient(90deg,
      #21a8e1 0%, #cc2c71 26%, #d52822 44%, #da411d 58%, #e99415 76%, #f9ec0f 100%);
    background-size: 200% 100%;
    transition: opacity 0.25s, filter 0.25s;
  }
  @media (prefers-reduced-motion: no-preference) {
    .window-bar::after { animation: ribbon-ignite 0.9s var(--ease-out) both; }
  }
  @keyframes ribbon-ignite {
    from { background-position: 100% 0; opacity: 0.25; }
    to { background-position: 0% 0; opacity: 1; }
  }

  /* Spice buds: the traffic-light idiom in the rack's own colours —
     close is paprika, minimise saffron, zoom azure — shaped as tiny
     squircles so they read as kin to the dock's app tiles. Glyphs still
     appear on group hover, exactly the inherited behaviour. */
  .traffic { position: absolute; left: 0.9rem; top: 50%; transform: translateY(-50%); display: flex; gap: 0.5rem; }
  .tl {
    width: 0.8rem; height: 0.8rem; border-radius: 40%;
    display: grid; place-items: center; padding: 0;
  }
  .tl svg { opacity: 0; transition: opacity 0.1s linear; }
  .traffic:hover .tl svg, .traffic:focus-within .tl svg { opacity: 1; }
  .tl-red { background: #d52822; box-shadow: 0 0 0 0.5px #a91b16; }
  .tl-amber { background: #e99415; box-shadow: 0 0 0 0.5px #b96f07; }
  .tl-green { background: #21a8e1; box-shadow: 0 0 0 0.5px #157fb0; }
  a.tl-red:focus-visible { outline: 2px solid var(--azure-text); outline-offset: 2px; border-radius: 40%; }

  .window-title {
    font-family: var(--font-display);
    font-variation-settings: 'SOFT' 30, 'WONK' 0, 'opsz' 40;
    font-weight: 600; font-size: 0.98rem; color: var(--ink);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 60%;
  }
  .menubar-signout { margin: 0; }
  .menubar-account { font-size: 0.82rem; color: inherit; text-decoration: none; opacity: 0.85; margin-right: 0.6rem; }
  .menubar-account:hover { opacity: 1; text-decoration: underline; text-underline-offset: 0.2em; }
  .menubar-signout button.secondary {
    min-height: 1.45rem; padding: 0 0.6rem; font-size: 0.72rem; border-radius: var(--radius-pill);
    background: rgb(255 255 255 / 0.55); color: var(--ink); border-color: rgb(19 42 61 / 0.18);
  }
  .menubar-signout button.secondary:hover { background: #fff; border-color: var(--azure); }
  .window-body { flex: 1; overflow-y: auto; padding-block: 1.75rem 4rem; }

  /* ==========================================================================
     The window manager — SpiceOS as an actual multi-window OS.

     desktop.js upgrades the desktop to "wm" mode on capable machines (fine
     pointer, wide viewport, no reduced-motion opt-out): the served page's
     window becomes draggable, and dock launches open further windows as
     same-origin iframes whose pages render in FRAMED mode. Every geometry
     property that moves at speed moves on transform — translate3d on the
     compositor, never left/top layout — which is what keeps motion at the
     display's own frame rate. The cinematic grammar: windows fly out of and
     back into their dock icon, and unfocused windows fall away slightly
     (dimmer, softer shadow) like a depth of field.
     ========================================================================== */
  .desktop.wm { display: block; padding: 0; overflow: hidden; }
  .desktop.wm .window {
    position: absolute; left: 0; top: 0; margin: 0;
    width: 58rem; height: 100%;
    max-width: none;
    will-change: transform;
  }
  .window-frame { flex: 1; width: 100%; border: 0; display: block; background: #fafcfd; }
  .window.dragging, .window.resizing { transition: none !important; animation: none !important; }
  .window.dragging { cursor: grabbing; }
  /* While any drag/resize is live, iframes must not swallow pointermoves. */
  body.wm-interacting iframe { pointer-events: none; }
  body.wm-interacting { user-select: none; -webkit-user-select: none; }

  /* Depth of field: the focused window is the shot; the rest fall back —
     and their spice ribbon goes to ash, so colour itself says "focused". */
  .desktop.wm .window.inactive {
    filter: saturate(0.86) brightness(0.965);
    box-shadow: 0 6px 10px rgb(6 10 18 / 0.16), 0 28px 50px -18px rgb(6 10 18 / 0.32);
  }
  .desktop.wm .window.inactive .window-bar { background: linear-gradient(180deg, #f7fafc 0%, #e9eef2 100%); }
  .desktop.wm .window.inactive .window-bar::after { opacity: 0.15; filter: saturate(0.2); }
  .desktop.wm .window.inactive .tl { background: #cfd6dc; box-shadow: 0 0 0 0.5px #b6bfc7; }
  .desktop.wm .window.inactive .window-title { color: var(--ink-faint); }

  /* NO GLASS ON WINDOWS YOU ARE NOT LOOKING AT.
     backdrop-filter is the most expensive property in this stylesheet — the
     compositor keeps a live GPU buffer per blurred surface, sized to the
     element, and re-blurs it whenever anything behind it moves. Chrome and
     dock are two such surfaces and they are fixed. Windows are two EACH
     (.window::before and .window-bar), so the cost grows with how many the
     team happens to have open, which is exactly the wrong thing to scale.

     The bar's blur was already pure waste: the rule four lines above paints
     an opaque gradient over it on inactive windows, so the browser computed
     a 26px blur and then covered it up.

     The pane's blur is visible, and dropping it is the treatment this
     stylesheet already chose once — body.mc-open turns off both for exactly
     this reason. macOS does the same thing to background windows. The
     inactive tint above (saturate .86, brightness .965) already says "not
     this one", so the glass was not carrying the meaning anyway.

     Focused window keeps everything. Live blur layers are now a constant
     rather than 2n. */
  .desktop.wm .window.inactive::before {
    background: color-mix(in oklab, var(--paper) 92%, transparent);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
  .desktop.wm .window.inactive .window-bar {
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }

  /* The resize grip: three lines in the rack's colours — always faintly
     present so the affordance is discoverable, full-strength on approach.
     The hit area is bigger than the drawing. */
  .desktop.wm .window-resize { width: 1.6rem; height: 1.6rem; }
  .desktop.wm .window-resize::before {
    content: ''; position: absolute; right: 4px; bottom: 4px; width: 12px; height: 12px;
    background: linear-gradient(135deg,
      transparent 0 38%, #21a8e1 38% 47%, transparent 47% 58%,
      #e99415 58% 67%, transparent 67% 78%, #cc2c71 78% 87%, transparent 87%);
    opacity: 0.45; transition: opacity 0.15s;
  }
  .desktop.wm .window:hover .window-resize::before,
  .desktop.wm .window-resize:hover::before { opacity: 1; }

  /* Minimised windows live as pills at the dock's end — the macOS shelf.
     desktop.js creates one per minimised window; clicking restores it. */
  .dock-min {
    /* Above the dock's absolutely-positioned glass layers, or the blur
       strip swallows the pill's clicks. */
    position: relative; z-index: 2;
    flex: none; display: flex; align-items: center; gap: 0.4rem;
    max-width: 11rem; height: 2.2rem; padding: 0 0.75rem; margin-bottom: 0.55rem;
    align-self: flex-end;
    border: 0; min-height: 0; clip-path: none; border-radius: var(--radius-pill);
    background: rgb(255 255 255 / 0.55); color: var(--ink);
    backdrop-filter: blur(18px) saturate(1.8);
    -webkit-backdrop-filter: blur(18px) saturate(1.8);
    box-shadow: 0 4px 12px -4px rgb(19 42 61 / 0.35), inset 0 1px 0 #fff;
    font-size: 0.78rem; font-weight: 650; cursor: pointer;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    transition: transform 0.15s var(--ease-out), background 0.15s;
  }
  .dock-min:hover { transform: translateY(-2px); background: #fff; box-shadow: 0 6px 16px -4px rgb(19 42 61 / 0.4); }
  .dock-min .dock-min-icon { flex: none; width: 1.25rem; height: 1.25rem; }
  .dock-min .dock-min-icon svg { width: 100%; height: 100%; display: block; }
  html.evening .dock-min { background: rgb(38 24 50 / 0.8); color: #fff; box-shadow: 0 4px 12px -4px rgb(0 0 0 / 0.5), inset 0 1px 0 rgb(255 255 255 / 0.2); }
  html.evening .dock-min:hover { background: rgb(52 34 68 / 0.95); }

  .window-resize {
    display: none;
    position: absolute; right: 0; bottom: 0; width: 1.2rem; height: 1.2rem;
    cursor: nwse-resize;
    touch-action: none;
  }
  .desktop.wm .window-resize { display: block; }
  .window.minimized { display: none; }

  /* ==========================================================================
     Mission Control — the exposé.

     desktop.js tilts every open window back into a perspective deck: each
     card's inline transform becomes translate3d + perspective() + rotateX +
     scale, so the whole act runs on the compositor exactly like a drag.
     The stagger is declarative — transition-delay counts up per card via
     --mc-i (stamped by JS, capped at 10). While the deck is out, each
     window's backdrop-filter is swapped for near-solid paint:
     backdrop-filter on a 3D-transformed element is undefined territory
     across engines (the .dock-blur doctrine), and near-opaque cards read
     better over the dimmed scrim anyway.
     ========================================================================== */
  #menubar-mission { display: none; }
  body.mc-able #menubar-mission { display: grid; }

  /* Above the wallpaper, below every card (windows carry inline z >= 11).
     Clicking the empty scrim restores the desktop — the macOS gesture. */
  .mc-scrim {
    position: absolute; inset: 0; z-index: 1;
    background: radial-gradient(120% 85% at 50% 10%, rgb(19 42 61 / 0.16) 0%, rgb(6 10 18 / 0.46) 100%);
    opacity: 0;
  }
  .mc-scrim.on { opacity: 1; }

  .desktop.wm .window.mc-deck { cursor: pointer; }
  /* Minimised windows join the deck's bottom row while it is out. */
  .desktop.wm .window.minimized.mc-deck { display: flex; }

  /* Cards read as equals: the depth-of-field dimming pauses in the deck. */
  body.mc-open .desktop.wm .window.inactive { filter: none; }
  body.mc-open .desktop.wm .window::before {
    background: color-mix(in oklab, var(--paper) 96%, transparent);
    backdrop-filter: none; -webkit-backdrop-filter: none;
  }
  body.mc-open .window-bar { backdrop-filter: none; -webkit-backdrop-filter: none; }
  body.mc-open iframe { pointer-events: none; } /* veils intercept anyway; belt */
  body.mc-open { user-select: none; -webkit-user-select: none; }

  @media (prefers-reduced-motion: no-preference) {
    .mc-scrim { transition: opacity 0.34s ease; }
    .desktop.wm .window.mc-deck {
      transition: transform 0.46s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.34s linear;
      transition-delay: calc(var(--mc-i, 0) * 26ms), calc(var(--mc-i, 0) * 26ms);
    }
    /* The picked card: the flight() grammar — no delay, expo-out with a
       whisper of overshoot as it lands home. Declared after .mc-deck so it
       wins their shared properties at equal specificity. */
    .desktop.wm .window.mc-picked {
      transition: transform 0.4s cubic-bezier(0.3, 1.34, 0.32, 1), opacity 0.3s linear;
      transition-delay: 0s, 0s;
    }
  }

  /* The veil: a real button over each card. It intercepts every pointer
     event before the iframe can, carries the title pill, and gives the
     deck Tab/Enter/Space for free. The pill's font-size and the ring's
     thickness are counter-scaled inline by desktop.js, so both render at
     constant size whatever the card's scale. */
  .mc-veil {
    position: absolute; inset: 0; z-index: 40;
    display: flex; align-items: flex-end; justify-content: center;
    padding: 0 0 4.5%; margin: 0;
    border: 0; min-height: 0; clip-path: none;
    background: transparent; border-radius: inherit;
    cursor: pointer; font: inherit; color: inherit;
  }
  .mc-veil:hover, .mc-veil:focus {
    outline: none;
    box-shadow: inset 0 0 0 var(--mc-ring, 3px) var(--azure);
  }
  .mc-veil-title {
    display: block; max-width: 88%;
    padding: 0.42em 1em; border-radius: 999em;
    background: rgb(19 42 61 / 0.66); color: #fff;
    font-weight: 650; letter-spacing: 0.01em;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    box-shadow: 0 0.35em 1.1em rgb(6 10 18 / 0.35);
    pointer-events: none;
  }
  .mc-veil--min .mc-veil-title { background: rgb(19 42 61 / 0.5); }

  /* The window's flight in and out of the dock is driven by desktop.js with
     WAAPI (it needs live dock-icon coordinates); these are only the resting
     states. Fast motion reads as intentional when the landing is soft —
     the JS uses expo-out ~320ms with a whisper of overshoot. */

  /* --- FRAMED mode: page content alone, hosted inside a shell window.
     html stays overflow:hidden from the shell styles, so the body is the
     scroll container. --- */
  body.framed {
    display: block; height: 100%; overflow: auto;
    background: #fafcfd;
  }
  /* THE CONTAINER FOR EVERY IN-WINDOW FOLD.
     Declared on the page wrapper rather than on the thing being folded,
     because a container query resolves against an ANCESTOR: a rule inside an
     at-container block can never restyle the element that declares the
     container, so putting it on .cx would have matched nothing and looked
     like the query simply did not work.

     NO BACKTICKS IN THIS FILE'S COMMENTS. All of this CSS lives inside a
     template literal, so one closes it and every test in the worker fails to
     parse — which is precisely how this comment broke the build. */
  .framed-body { container-type: inline-size; container-name: comms; padding: 1.4rem 0 3rem; }

  /* In wm mode the entrance flight is WAAPI-driven from the dock icon
     (desktop.js) — the CSS window-open keyframes would fight the inline
     translate3d and teleport the window to the origin mid-animation. */
  .desktop.wm .window { animation: none; }

  /* --- The Dock: a 3D liquid-glass shelf -------------------------------------
     Three layers plus the icons:
       .dock-blur   — flat strip carrying the backdrop blur (and, in
                      Chromium under .svg-glass, real refraction). Kept
                      unrotated: backdrop-filter on a perspective-transformed
                      element is undefined territory across engines.
       .dock-shelf  — the 3D floor: rotateX in perspective, so it renders as
                      a receding trapezoid. Gradient glass material; its
                      slanted sides come free from the projection.
       .dock-reflect— each icon mirrored below itself, masked to fade, the
                      one effect that most says "the icons are STANDING on
                      something".
     Magnification is width-driven (see /desktop.js): items are flex
     children, so growing one pushes its neighbours apart with zero overlap
     math — the same architecture as the best clones. */
  .dock-zone {
    position: fixed; z-index: 4; left: 50%; bottom: 0.6rem; transform: translateX(-50%);
    max-width: calc(100vw - 0.75rem);
  }
  .dock {
    position: relative;
    display: flex; align-items: flex-end; gap: 0.5rem;
    padding: 0.4rem 1.1rem 1.7rem;
  }
  .dock-blur {
    position: absolute; left: 0; right: 0; bottom: 0; height: 3.4rem;
    border-radius: 1.1rem;
    background: linear-gradient(180deg, rgb(255 255 255 / 0.44) 0%, rgb(255 255 255 / 0.24) 100%);
    backdrop-filter: blur(32px) saturate(2.1) brightness(1.04);
    -webkit-backdrop-filter: blur(32px) saturate(2.1) brightness(1.04);
    box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.75), inset 0 0 0 1px rgb(255 255 255 / 0.3);
    mask-image: linear-gradient(to bottom, transparent 0, #000 0.6rem);
    -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 0.6rem);
  }
  .svg-glass .dock-blur {
    backdrop-filter: url(#spice-refract) blur(32px) saturate(2.1) brightness(1.04);
  }
  .dock-shelf {
    position: absolute; left: 0.35rem; right: 0.35rem; bottom: 0.15rem; height: 4.4rem;
    transform: perspective(30rem) rotateX(52deg);
    transform-origin: 50% 100%;
    border-radius: 0.9rem;
    background:
      linear-gradient(180deg,
        rgb(255 255 255 / 0.8) 0%,
        rgb(255 255 255 / 0.45) 30%,
        rgb(178 214 235 / 0.5) 62%,
        rgb(255 255 255 / 0.6) 100%);
    border: 1px solid rgb(255 255 255 / 0.8);
    border-top-color: #fff;
    box-shadow:
      0 16px 34px -10px rgb(19 42 61 / 0.35),
      inset 0 1px 0 #fff,
      inset 0 -10px 18px -12px rgb(255 255 255 / 0.9);
  }
  /* The polished front edge — the bright line where the shelf meets air. */
  .dock-shelf::after {
    content: ''; position: absolute; left: 4%; right: 4%; bottom: -1px; height: 2px;
    border-radius: 2px;
    background: linear-gradient(90deg, transparent, rgb(255 255 255 / 0.95) 15% 85%, transparent);
  }

  .dock-item {
    position: relative; flex: none; display: block; text-decoration: none;
    width: var(--dock-base);
    /* Magnification is a transform, so the item's box never changes and the
       compositor can move it without the main thread. It used to be
       will-change: width, which is a contradiction — width cannot be
       composited, so the hint bought nothing while the layout it promised to
       optimise ran anyway. Origin at the bottom centre so an icon grows up
       out of the shelf rather than through it. */
    will-change: transform;
    transform-origin: bottom center;
  }
  .dock-icon { display: block; position: relative; z-index: 2; }
  .dock-icon svg { display: block; width: 100%; height: auto; }
  /* Contact shadow: the icon grounded on the shelf. */
  .dock-item::before {
    content: ''; position: absolute; z-index: 1; left: 12%; right: 12%; bottom: -0.3rem; height: 0.55rem;
    background: radial-gradient(50% 50% at 50% 50%, rgb(6 4 12 / 0.4), transparent 70%);
  }
  .dock-reflect {
    position: absolute; z-index: 1; top: 100%; left: 0; width: 100%;
    transform: scaleY(-1) translateY(-2px);
    opacity: 0.4; filter: blur(0.5px);
    mask-image: linear-gradient(to top, rgb(0 0 0 / 0.55), transparent 58%);
    -webkit-mask-image: linear-gradient(to top, rgb(0 0 0 / 0.55), transparent 58%);
    pointer-events: none;
  }
  .dock-reflect svg { display: block; width: 100%; height: auto; }

  /* No-JS / coarse-pointer nicety: a whisper of lift on hover. The real
     magnification (width spring) replaces this wherever desktop.js runs. */
  .dock-item:hover .dock-icon { transform: translateY(-2px); }
  @media (prefers-reduced-motion: no-preference) {
    .dock-icon { transition: transform 0.18s var(--ease-out); }
  }

  /* App launch: the macOS double bounce, mirrored by the reflection.
     Navigation is NOT delayed — whatever plays before unload is a bonus. */
  .dock-item.launching .dock-icon {
    animation: dock-launch 0.3s cubic-bezier(0.36, 0, 0.64, 1) 4 alternate;
  }
  .dock-item.launching .dock-reflect {
    animation: dock-launch-r 0.3s cubic-bezier(0.36, 0, 0.64, 1) 4 alternate;
  }
  @keyframes dock-launch { to { transform: translateY(-34%); } }
  @keyframes dock-launch-r { to { transform: scaleY(-1) translateY(calc(-2px + 34%)); } }

  .dock-running {
    position: absolute; left: 50%; bottom: -0.62rem; transform: translateX(-50%);
    width: 0.28rem; height: 0.28rem; border-radius: 50%;
    background: rgb(19 42 61 / 0.72);
    box-shadow: 0 0 5px 1px rgb(33 168 225 / 0.6);
  }

  /* The red count in the icon's corner — a queue waiting for a person,
     exactly what the OS badge idiom means. */
  .dock-badge {
    position: absolute; z-index: 3; top: -0.28rem; right: -0.28rem;
    min-width: 1.1rem; height: 1.1rem; padding: 0 0.28rem;
    border-radius: var(--radius-pill);
    background: #ec4d3d; color: #fff;
    border: 1.5px solid #fff;
    font-size: 0.68rem; font-weight: 700; line-height: 1rem; text-align: center;
    box-shadow: 0 2px 6px rgb(19 42 61 / 0.3);
  }

  .dock-sep {
    flex: none; width: 1px; height: calc(var(--dock-base) * 0.72);
    margin: 0 0.15rem;
    background: linear-gradient(180deg, transparent, rgb(19 42 61 / 0.25) 30% 70%, transparent);
  }

  /* --- The side dock: the superuser menu, down the left edge ------------
     The same glass as the bottom dock, stood on its side. No mouse
     magnification — that effect is tuned for a horizontal row and the
     hover lift reads better on a column. Hidden below the width at which the
     window manager itself switches off, where a fixed column would sit on top
     of the page. */
  .dock-zone--side {
    left: 0.6rem; bottom: auto; top: 50%; transform: translateY(-50%);
    max-width: none; max-height: calc(100vh - 6rem);
  }
  .side-dock {
    position: relative;
    display: flex; flex-direction: column; align-items: center; gap: 0.45rem;
    padding: 0.7rem 0.45rem;
  }
  .side-dock-glass {
    position: absolute; inset: 0;
    border-radius: 1.1rem;
    background: linear-gradient(90deg, rgb(255 255 255 / 0.5) 0%, rgb(255 255 255 / 0.26) 100%);
    backdrop-filter: blur(32px) saturate(2.1) brightness(1.04);
    -webkit-backdrop-filter: blur(32px) saturate(2.1) brightness(1.04);
    border: 1px solid rgb(255 255 255 / 0.8);
    border-left-color: #fff;
    box-shadow: 16px 0 34px -14px rgb(19 42 61 / 0.3), inset 1px 0 0 #fff, inset 0 0 0 1px rgb(255 255 255 / 0.3);
  }
  .side-dock .dock-item {
    width: calc(var(--dock-base) * 0.82);
    transform-origin: center left;
    transition: transform 0.18s var(--ease-out);
  }
  .side-dock .dock-item:hover, .side-dock .dock-item:focus-visible { transform: translateX(3px) scale(1.08); }
  .side-dock .dock-item:hover .dock-icon { transform: none; }
  /* The tooltip sits to the RIGHT of a side icon, not above it. */
  .side-dock .dock-item::after {
    bottom: auto; top: 50%; left: calc(100% + 0.7rem); transform: translateY(-50%);
  }
  .side-dock .dock-running { left: -0.35rem; bottom: auto; top: 50%; transform: translateY(-50%); }
  @media (max-width: 899px) { .dock-zone--side { display: none; } }

  /* Tooltip: the app name above the icon — glass pill, like the real Dock's. */
  .dock-item::after {
    content: attr(data-tip);
    position: absolute; bottom: calc(100% + 0.7rem); left: 50%; transform: translateX(-50%);
    padding: 0.3rem 0.7rem; border-radius: var(--radius-s);
    background: rgb(26 16 34 / 0.82); color: #fff; font-size: 0.76rem; font-weight: 600;
    border: 1px solid rgb(255 255 255 / 0.16);
    box-shadow: 0 6px 18px -6px rgb(6 10 18 / 0.6), inset 0 1px 0 rgb(255 255 255 / 0.14);
    white-space: nowrap; pointer-events: none;
    opacity: 0; transition: opacity 0.12s var(--ease-out);
  }
  .dock-item:hover::after, .dock-item:focus-visible::after { opacity: 1; }
  .dock-item:focus-visible { outline: 2px solid var(--azure-text); outline-offset: 3px; border-radius: 0.85rem; }

  /* --- First sign-in of the session: the desktop assembles itself.
     desktop.js adds .booting exactly once per browser session; every later
     page load skips straight to the finished desktop. */
  @media (prefers-reduced-motion: no-preference) {
    /* Delays start as the boot splash lifts (~1.85s): the OS assembles out
       of the splash rather than after it. */
    .booting .dock-item { animation: dock-rise 0.5s var(--ease-out) backwards; animation-delay: calc(1.7s + var(--i) * 40ms); }
    .booting .menubar { animation: bar-drop 0.4s 1.6s var(--ease-out) backwards; }
    .booting .window { animation: window-boot 0.5s 1.8s var(--ease-out) backwards; }
    .booting .boot { display: grid; }
  }
  /* ==========================================================================
     The boot splash — the SpiceOS power-on moment, played on every sign-in.

     The showpiece is a Houdini @property: --ring-spin is a REGISTERED angle,
     so the conic-gradient it feeds animates as a real interpolated value —
     a spinning ring of the logo's own spectrum, cut into a band by a radial
     mask, with a blurred twin behind it for glow and a slow conic aurora
     washing the whole screen. All compositor/paint-cheap, zero JavaScript
     animation, and absent entirely under prefers-reduced-motion.
     ========================================================================== */
  .boot { display: none; }
  .boot {
    position: fixed; inset: 0; z-index: 100;
    place-items: center; place-content: center; gap: 0.9rem;
    overflow: hidden;
    background: linear-gradient(180deg, #eef8ff 0%, #cfe8f7 60%, #b3d7ec 100%);
    animation: boot-lift 0.6s 1.8s var(--ease-out) forwards;
  }
  .boot-aurora {
    position: absolute; inset: -40%;
    background: conic-gradient(from 0deg at 50% 50%,
      rgb(33 168 225 / 0.18), rgb(204 44 113 / 0.12), rgb(233 148 21 / 0.15),
      rgb(249 236 15 / 0.10), rgb(152 25 101 / 0.10), rgb(33 168 225 / 0.18));
    filter: blur(70px);
    animation: boot-aurora 7s linear infinite;
  }
  @keyframes boot-aurora { to { transform: rotate(1turn); } }
  .boot-core { position: relative; width: 10rem; height: 10rem; display: grid; place-items: center; }
  @property --ring-spin {
    syntax: '<angle>';
    inherits: false;
    initial-value: 0deg;
  }

  /* The spice-mote canvas desktop.js inserts between the aurora and the
     core: over the wash, under the ring and wordmark. */
  .boot-canvas { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
  .boot-ring {
    position: absolute; inset: 0; border-radius: 50%;
    background: conic-gradient(from var(--ring-spin),
      #21a8e1, #cc2c71, #d52822, #da411d, #e99415, #f9ec0f, #21a8e1);
    -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 9px), #000 calc(100% - 8px));
    mask: radial-gradient(farthest-side, transparent calc(100% - 9px), #000 calc(100% - 8px));
    animation: ring-spin 1.15s linear infinite, boot-pop 0.7s var(--ease-spring) backwards;
  }
  .boot-ring--soft { filter: blur(16px); opacity: 0.6; }
  @keyframes ring-spin { to { --ring-spin: 360deg; } }
  @keyframes boot-pop { from { transform: scale(0.55); opacity: 0; } }
  .boot-mark {
    width: 5.4rem; height: 5.4rem;
    filter: drop-shadow(0 10px 26px rgb(19 42 61 / 0.3));
    animation: boot-pop 0.7s 0.1s var(--ease-spring) backwards;
  }
  .boot-word {
    position: relative; font-family: var(--font-display); font-size: 2.2rem; font-weight: 600;
    letter-spacing: -0.01em; color: #132a3d;
    clip-path: inset(0 100% 0 0);
    animation: boot-word 0.7s 0.5s var(--ease-out) forwards;
  }
  .boot-word b { color: #197ea9; }
  @keyframes boot-word { to { clip-path: inset(0 -5% 0 0); } }
  .boot-tag {
    font-size: 0.76rem; font-weight: 600; letter-spacing: 0.16em; text-transform: uppercase;
    color: #4a6070; opacity: 0;
    animation: boot-fade 0.6s 0.95s forwards;
  }
  @keyframes boot-fade { to { opacity: 1; } }
  @keyframes boot-lift {
    to { opacity: 0; transform: scale(1.05); visibility: hidden; }
  }


  /* The mini-splash: every desktop load gets the power-on gesture in
     miniature — same core, compressed timeline, no aurora overture. */
  .booting-mini .boot { display: grid; animation: boot-lift 0.45s 1.0s var(--ease-out) forwards; }
  .booting-mini .boot .boot-core { animation: mini-pop 0.5s var(--ease-out) backwards; }
  @keyframes mini-pop {
    from { transform: scale(0.86); opacity: 0; }
    to { transform: scale(1); opacity: 1; }
  }

  /* Window resolving veil: a NATIVE moment, not a page loader. A macOS window
     opens with its chrome instantly and its content resolves a beat later; this
     is that beat. The window bar and traffic lights are already drawn — the veil
     only softens the blank body for the split second before the app's first
     paint lands, as a light frosted glass with a slim brand sweep at the top
     edge (motion, no spinner, no "Loading…" label), then fades. It is a FIRST-
     open shimmer only: re-selecting an open app just refocuses its window, and
     the veil is re-shown solely when a window's frame is deliberately navigated
     (a deep link). */
  .win-veil { position: absolute; inset: 2.2rem 0 0; z-index: 5; overflow: hidden;
              background: linear-gradient(180deg, rgb(250 253 255 / 0.66), rgb(236 247 255 / 0.5));
              -webkit-backdrop-filter: blur(7px) saturate(1.06); backdrop-filter: blur(7px) saturate(1.06);
              transition: opacity 0.26s var(--ease-out); }
  .win-veil.off { opacity: 0; pointer-events: none; }
  /* The indeterminate sweep: a slim strip of the logo's own spectrum gliding
     across the top edge, gone the instant the frame loads. */
  .win-veil-bar { position: absolute; top: 0; left: 0; height: 2px; width: 38%; border-radius: 2px;
                  background: linear-gradient(90deg, transparent, #21a8e1 30%, #cc2c71 70%, transparent);
                  animation: veil-sweep 1.05s ease-in-out infinite; }
  @keyframes veil-sweep { from { transform: translateX(-100%); } to { transform: translateX(360%); } }
  @media (prefers-reduced-motion: reduce) { .win-veil-bar { animation: none; width: 100%; opacity: 0.5; } }

  /* ==========================================================================
     Menu-bar notification centre + the ⌘K command palette.
     ========================================================================== */
  .menubar-bell {
    position: relative; display: grid; place-items: center;
    width: 1.7rem; height: 1.45rem;
    margin: 0; padding: 0; border: 0; min-height: 0;
    background: transparent; clip-path: none; border-radius: 6px;
    color: var(--ink); cursor: pointer;
    transition: background 0.15s;
  }
  .menubar-bell:hover { background: rgb(19 42 61 / 0.1); transform: none; }
  .bell-count {
    position: absolute; top: -0.28rem; right: -0.34rem;
    min-width: 0.95rem; height: 0.95rem; padding: 0 0.2rem;
    border-radius: var(--radius-pill);
    background: #ec4d3d; color: #fff;
    font-size: 0.68rem; font-weight: 700; line-height: 0.95rem; text-align: center;
  }
  /* The context menu. Same glass as the notification panel and the palette
     — it is the third member of that family, not a fourth idiom. z-index
     sits above Mission Control's scrim so a right-click during the exposé
     still reads. */
  .ctx {
    position: fixed; z-index: 100060; min-width: 12rem; max-width: 17rem; padding: 0.3rem;
    border-radius: 12px; background: rgb(255 255 255 / 0.94); backdrop-filter: blur(14px);
    -webkit-backdrop-filter: blur(14px); border: 1px solid rgb(255 255 255 / 0.9);
    box-shadow: 0 18px 44px -18px rgb(19 42 61 / 0.6); font-size: 0.84rem;
    animation: ctx-in 0.13s var(--ease-out) both;
  }
  @keyframes ctx-in { from { opacity: 0; transform: scale(0.97) translateY(-3px); } }
  @media (prefers-reduced-motion: reduce) { .ctx { animation: none; } }
  .ctx-item {
    display: flex; justify-content: space-between; gap: 1rem; align-items: baseline; width: 100%;
    padding: 0.32rem 0.55rem; border: 0; border-radius: 8px; background: none; cursor: pointer;
    font: inherit; color: #16222c; text-align: left;
  }
  .ctx-item:hover, .ctx-item:focus-visible { background: #1d84b8; color: #fff; outline: none; }
  .ctx-item:hover .ctx-hint, .ctx-item:focus-visible .ctx-hint { color: rgb(255 255 255 / 0.75); }
  .ctx-item.danger { color: #b4433a; }
  .ctx-item.danger:hover, .ctx-item.danger:focus-visible { background: #b4433a; color: #fff; }
  .ctx-hint { font-size: 0.72rem; color: #7d94a3; white-space: nowrap; }
  .ctx-sep { height: 1px; margin: 0.25rem 0.4rem; background: rgb(20 60 90 / 0.12); }

  .notif-panel {
    position: fixed; top: 2.35rem; right: 0.6rem; z-index: 50;
    width: min(94vw, 22rem); max-height: 70vh; overflow-y: auto;
    background: rgb(255 255 255 / 0.85);
    backdrop-filter: blur(24px) saturate(1.8);
    -webkit-backdrop-filter: blur(24px) saturate(1.8);
    border: 1px solid rgb(255 255 255 / 0.9);
    border-radius: var(--radius-m);
    box-shadow: 0 24px 56px rgb(19 42 61 / 0.3), inset 0 1px 0 #fff;
    padding: 0.6rem;
    animation: palette-in 0.16s var(--ease-out);
  }
  .notif-panel[hidden] { display: none; }

  /* --- volume ------------------------------------------------------------- */
  .snd-panel { position: absolute; top: 2.1rem; right: 3.4rem; z-index: 60; width: 13.5rem;
        padding: 0.7rem 0.8rem 0.6rem; border-radius: 14px;
        background: rgb(255 255 255 / 0.92); border: 1px solid rgb(255 255 255 / 0.9);
        box-shadow: 0 18px 40px -20px rgb(19 42 61 / 0.5); backdrop-filter: blur(14px); }
  .snd-panel[hidden] { display: none; }
  .snd-row { display: flex; align-items: baseline; justify-content: space-between; gap: 0.6rem; }
  .snd-lab { font-size: 0.78rem; font-weight: 600; }
  .snd-val { font-size: 0.78rem; color: var(--ink-muted); font-variant-numeric: tabular-nums; }
  .snd-range { width: 100%; margin: 0.45rem 0 0.2rem; accent-color: #1d84b8; }
  .snd-hint { margin: 0; font-size: 0.7rem; color: var(--ink-muted); }
  .snd-chat { display: flex; align-items: center; gap: 0.45rem; margin: 0.6rem 0 0; font-size: 0.74rem; cursor: pointer; }
  .snd-chat input { margin: 0; accent-color: #1d84b8; }
  .snd-foot { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; margin-top: 0.55rem; }
  .snd-state { font-size: 0.7rem; line-height: 1.25; color: var(--ink-muted); }
  .snd-panel[data-sound="blocked"] .snd-state { color: #9a6a12; font-weight: 600; }
  .snd-test { flex: none; margin: 0; padding: 0.25rem 0.6rem; min-height: 0; font-size: 0.7rem; font-weight: 600;
        border-radius: 999px; border: 1px solid rgb(19 42 61 / 0.18); background: #fff; color: var(--ink);
        cursor: pointer; clip-path: none; transition: background 0.15s; }
  .snd-test:hover { background: rgb(19 42 61 / 0.06); transform: none; }
  .snd-help { margin: 0.5rem 0 0; font-size: 0.68rem; line-height: 1.35; color: var(--ink-muted); }
  .snd-help[hidden] { display: none; }
  .hub-ring-allow { margin: 0.35rem 0 0; padding: 0.3rem 0.7rem; min-height: 0; font-size: 0.74rem; font-weight: 700;
        border-radius: 999px; border: 0; background: #e0a52c; color: #132a3d; cursor: pointer; clip-path: none;
        align-self: flex-start; transition: background 0.15s; }
  .hub-ring-allow:hover { background: #f0b73b; transform: none; }
  /* The speaker tells the truth: an amber dot while the browser is holding
     sound back (it clears on the first click or key), a faded wave at 0. */
  .menubar-bell[data-sound="blocked"]::after {
    content: ""; position: absolute; top: 0.1rem; right: 0.08rem; width: 0.42rem; height: 0.42rem;
    border-radius: 50%; background: #e0a52c; box-shadow: 0 0 0 2px rgb(255 255 255 / 0.85);
    animation: snd-wait 1.6s ease-in-out infinite;
  }
  .menubar-bell[data-sound="silent"] .snd-wave, .menubar-bell[data-sound="silent-ring"] .snd-wave { opacity: 0.25; }
  @keyframes snd-wait { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
  @media (prefers-reduced-motion: reduce) { .menubar-bell[data-sound="blocked"]::after { animation: none; } }
  .notif-label {
    font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em;
    color: var(--ink-faint); padding: 0.5rem 0.55rem 0.2rem;
  }
  .notif-item {
    display: block; padding: 0.45rem 0.55rem; border-radius: var(--radius-s);
    color: var(--ink); text-decoration: none; font-size: 0.88rem;
  }
  .notif-item:hover { background: var(--surface-sink); }
  .notif-item .hint { display: block; margin-top: 0.05rem; }
  /* Who else has this document open. Quiet by default — it is information,
     not an alarm — and it only appears when somebody actually is. */
  .doc-here { font-size: 0.75rem; color: #a1660f; background: rgb(233 148 21 / 0.12);
              border: 1px solid rgb(233 148 21 / 0.3); border-radius: 999px;
              padding: 0.12rem 0.6rem; white-space: nowrap; overflow: hidden;
              text-overflow: ellipsis; max-width: 26rem; }
  .notif-empty { padding: 0.8rem 0.6rem; color: var(--ink-faint); font-size: 0.88rem; margin: 0; }
  /* The ring's opt-in, at the foot of the notification centre. A label so
     the whole row is the click target, not the 13px box. */
  .notif-opt { display: flex; align-items: center; gap: 0.45rem; margin-top: 0.4rem; padding: 0.55rem 0.55rem 0.2rem;
               border-top: 1px solid rgb(20 60 90 / 0.1); font-size: 0.8rem; color: var(--ink-muted, #4a6070); cursor: pointer; }
  .notif-opt input { margin: 0; }

  .palette {
    position: fixed; inset: 0; z-index: 60;
    background: rgb(19 42 61 / 0.2);
    display: grid; justify-items: center; align-items: start;
    padding: 13vh 1rem 0;
  }
  .palette[hidden] { display: none; }
  .palette-panel {
    width: min(100%, 34rem);
    background: rgb(255 255 255 / 0.85);
    backdrop-filter: blur(26px) saturate(1.8);
    -webkit-backdrop-filter: blur(26px) saturate(1.8);
    border: 1px solid rgb(255 255 255 / 0.9);
    border-radius: var(--radius-m);
    box-shadow: 0 30px 70px rgb(19 42 61 / 0.35), inset 0 1px 0 #fff;
    overflow: hidden;
    animation: palette-in 0.18s var(--ease-out);
  }
  @keyframes palette-in { from { opacity: 0; transform: scale(0.97) translateY(-8px); } }
  #palette-input {
    width: 100%; border: 0; border-bottom: 1px solid var(--hairline); border-radius: 0;
    min-height: 3.2rem; padding: 0 1.1rem;
    font-size: 1.02rem; background: transparent; color: var(--ink);
  }
  #palette-input:focus-visible { outline: none; }
  .palette-results { max-height: 46vh; overflow-y: auto; padding: 0.4rem; }
  .palette-group {
    font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em;
    color: var(--ink-faint); padding: 0.55rem 0.7rem 0.15rem;
  }
  .palette-item {
    display: flex; justify-content: space-between; align-items: baseline; gap: 0.8rem;
    padding: 0.48rem 0.7rem; border-radius: var(--radius-s);
    color: var(--ink); text-decoration: none; cursor: pointer; font-size: 0.94rem;
  }
  .palette-sub { color: var(--ink-faint); font-size: 0.8rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .palette-item.sel, .palette-item:hover { background: var(--azure); color: #fff; }
  .palette-item.sel .palette-sub, .palette-item:hover .palette-sub { color: rgb(255 255 255 / 0.85); }
  .palette-empty { padding: 0.9rem 1.1rem; color: var(--ink-faint); font-size: 0.9rem; margin: 0; }

  /* --- Pulse table --------------------------------------------------------- */
  .pulse-card { overflow-x: auto; }
  .pulse-row {
    display: grid; grid-template-columns: minmax(14rem, 1fr) repeat(5, 5.4rem);
    gap: 0.6rem; align-items: center; min-width: 42rem;
    padding: 0.55rem 0; border-bottom: 1px solid var(--hairline);
  }
  .pulse-row:last-child { border-bottom: 0; }
  .pulse-head { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--ink-faint); }
  .pulse-trip { min-width: 0; }
  .pulse-trip a { font-weight: 650; color: inherit; text-decoration: none; }
  .pulse-trip a:hover { color: var(--azure-text); }
  .pulse-trip .hint { display: block; margin: 0; }
  .pulse-cell { text-align: center; font-variant-numeric: tabular-nums; }
  .pulse-cell b { display: block; font-size: 1rem; }
  .pulse-cell i { font-style: normal; font-size: 0.68rem; color: var(--ink-faint); }
  .pulse-cell em { font-style: normal; font-weight: 700; }
  .pulse-cell em.up { color: #1c7c3c; }
  .pulse-cell em.down { color: var(--chilli-text); }
  .pulse-singles-available { color: #1c7c3c; }
  .pulse-singles-waitlist { color: var(--saffron-text); }
  .pulse-singles-soldout { color: var(--chilli-text); }
  .pulse-singles-none { color: var(--ink-faint); font-weight: 400; }

  @keyframes dock-rise {
    from { opacity: 0; transform: translateY(2.6rem); }
    to { opacity: 1; transform: none; }
  }
  @keyframes bar-drop {
    from { transform: translateY(-100%); }
    to { transform: none; }
  }
  @keyframes window-boot {
    from { opacity: 0; transform: scale(0.94) translateY(1.4rem); }
    to { opacity: 1; transform: none; }
  }

  @media (prefers-reduced-motion: reduce) {
    .dock-icon, .dock-item::after { transition: none; }
  }

  /* --- Narrow: the window loses most of its floating margin, and the dock
       scrolls (scrollbar hidden — it's a dock, not a table) since twelve
       tiles will not fit a phone. Magnification is pointer-driven so it is
       simply absent on touch. */
  @media (max-width: 46rem) {
    :root { --dock-base: 44px; }
    .desktop { padding: 0.75rem 0.5rem 6.25rem; }
    .window { border-radius: var(--radius-m); }
    .menubar-title { display: none; }
    /* An email address does not fit a phone's menu bar; the clock is the
       glanceable part, so it stays. */
    .menubar-user { display: none; }
    .dock-zone { overflow-x: auto; overflow-y: hidden; scrollbar-width: none; padding-top: 2.4rem; }
    .dock-zone::-webkit-scrollbar { display: none; }
  }

  /* ==========================================================================
     The SpiceOS login screen — wallpaper, the real logo mark as the account
     avatar, and a frosted glass card. (The macOS login composition, with
     email+password because that is what this auth actually needs.)
     ========================================================================== */
  /* THE LOCK SCREEN MUST SCROLL. The desktop shell above pins html and body
     to the viewport with overflow: hidden — right for a window manager, and
     it used to be harmless here because the password card is short. The
     authenticator enrolment card (QR, key, code box, button) is not, and
     centred inside a fixed box the bottom of it was simply unreachable on a
     laptop screen (9 Sep 2026, Stuart's 1280×720). So on this screen the
     document is allowed its natural height and scrolls, and "safe center"
     keeps the short cards centred without ever pushing a tall one off the
     top. The compact rules below give a 720px-high screen room to spare. */
  html:has(body.login-screen) { height: auto; min-height: 100%; overflow: auto; }
  body.login-screen { display: grid; place-items: safe center; min-height: 100dvh; height: auto; overflow: visible; }
  .login {
    position: relative; z-index: 2; text-align: center;
    width: min(100% - 2.5rem, 23rem); padding: clamp(1rem, 3vh, 2rem) 0 3rem;
  }
  /* The enrolment and recovery-code cards need the room: QR beside the words. */
  .login--wide { width: min(100% - 2.5rem, 42rem); }
  .login-enrol { display: grid; gap: 0.9rem 1.4rem; align-items: start; }
  .login-enrol .qr { display: grid; place-items: center; }
  .login-enrol .qr svg { max-width: 100%; height: auto; }
  @media (min-width: 40rem) {
    .login-enrol { grid-template-columns: 220px minmax(0, 1fr); }
    .login-enrol .qr { grid-row: 1 / span 3; }
    .login-enrol .qr svg { width: 220px; }
  }
  @media (max-height: 820px) {
    .login-screen .lock-time { font-size: 3rem; }
    .login-screen .lock-date { margin-bottom: 0.9rem; }
    .login-screen .login-logo { width: 165px; height: auto; margin-bottom: 0.6rem; }
    .login-screen .login-sub { margin-bottom: 0.9rem; }
  }
  /* The lock screen's clock — the one thing every real lock screen leads
     with. Ticks via desktop.js; harmless empty elements without JS. */
  .lock-time {
    font-family: var(--font-display);
    font-variation-settings: 'SOFT' 30, 'WONK' 0, 'opsz' 144;
    font-size: 4.6rem; font-weight: 350; line-height: 1; letter-spacing: -0.02em;
    color: var(--ink);
    text-shadow: 0 2px 18px rgb(255 255 255 / 0.8);
  }
  .lock-date {
    font-size: 0.95rem; font-weight: 600; color: var(--ink-muted);
    margin: 0.3rem 0 2rem;
  }
  .login-logo {
    width: 215px; height: 63px; margin-bottom: 1.1rem;
    filter: drop-shadow(0 8px 20px rgb(19 42 61 / 0.25));
  }
  .login-sub {
    color: var(--ink-muted); font-size: 0.85rem; font-weight: 600;
    letter-spacing: 0.12em; text-transform: uppercase; margin: 0 0 1.5rem;
  }
  .login-card {
    text-align: left;
    background: rgb(255 255 255 / 0.62);
    backdrop-filter: blur(24px) saturate(1.8);
    -webkit-backdrop-filter: blur(24px) saturate(1.8);
    border: 1px solid rgb(255 255 255 / 0.85);
    border-radius: var(--radius-m);
    box-shadow:
      0 24px 48px rgb(19 42 61 / 0.22),
      inset 0 1px 0 #fff;
    padding: 1.4rem 1.4rem 1.5rem;
  }
  @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    .login-card { background: rgb(255 255 255 / 0.94); }
  }
  .login-card input {
    /* The translucent fill lives in an inset shadow, not background-color:
       Chromium (at least on Windows GPUs) draws visible diagonal tessellation
       seams — a corner-to-corner X — when an <input> combines a translucent
       background with border-radius. Divs are fine; inputs are not. The
       999px-spread inset shadow paints the identical fill through a path
       that doesn't artifact. */
    background: transparent;
    box-shadow: inset 0 0 0 999px rgb(255 255 255 / 0.65);
    border-color: rgb(19 42 61 / 0.18);
  }
  .login-card .err {
    background: rgb(218 65 29 / 0.1);
    border-left: 3px solid var(--chilli);
    color: #7a2712;
    padding: 0.5rem 0.75rem; border-radius: 0 var(--radius-s) var(--radius-s) 0;
    margin: 0 0 0.9rem; font-size: 0.85rem;
  }
  /* Wrong password: the whole card shakes, exactly like the real login. */
  @media (prefers-reduced-motion: no-preference) {
    .login-card:has(.err:not([hidden])) { animation: login-shake 0.4s var(--ease-out); }
  }
  @keyframes login-shake {
    0%, 100% { transform: translateX(0); }
    20% { transform: translateX(-9px); }
    40% { transform: translateX(8px); }
    60% { transform: translateX(-5px); }
    80% { transform: translateX(3px); }
  }
  .login-card button[type='submit'] { width: 100%; margin-top: 0.4rem; }
  .login .nojs { font-size: 0.78rem; color: var(--ink-faint); margin-top: 1rem; }

  /* "Remember this device for 7 days". The whole row is the label, so the
     sentence is the hit target as well as the box — on a phone the box alone
     is a 13px target, which is half what a finger needs. */
  .login-card .remember {
    display: flex; align-items: flex-start; gap: 0.55rem;
    margin: 0.75rem 0 0.2rem; font-size: 0.86rem; line-height: 1.35; cursor: pointer;
  }
  .login-card .remember input {
    width: auto; margin: 0.1rem 0 0; flex: none; accent-color: var(--azure, #197ea9);
    min-height: 1rem;
  }
  .login-card .remember small {
    display: block; margin-top: 0.1rem; font-size: 0.76rem; opacity: 0.75;
  }

  h1 { font-size: clamp(1.5rem, 1.3rem + 1vw, 2rem); margin-bottom: 0.35rem; }
  .sub { color: var(--ink-muted); margin: 0 0 1.5rem; }

  .card {
    background: var(--paper); border: 1px solid var(--hairline);
    border-radius: var(--radius-l); box-shadow: var(--shadow-card);
    padding: 1.25rem; margin-bottom: 1rem;
  }

  /* A TABLE IN A WINDOW HAS TO BE ABLE TO GO SIDEWAYS.
     This is a windowed desktop: every screen can be dragged narrow, and 35 of
     the admin's tables sat in a card with nothing to scroll them. A commerce
     table with eight columns in a half-width window had no way to show its
     last three — the columns squashed until the numbers wrapped mid-figure,
     which on a money screen is worse than a scrollbar.

     :has() rather than a wrapper element around each one: the markup is
     spread across seventeen template literals in commerce alone, and a rule
     that applies to the container of any table is right for the tables nobody
     has written yet as well as the ones that exist. Only cards that actually
     contain a table are affected, so nothing else gains a scroll context. */
  .card:has(table), .mx-card:has(table) { overflow-x: auto; }
  .row { display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; }

  /* A FIELD KICKER, WHICH IS NOT EVERY LABEL. This styled every <label> in
     the admin as a small uppercase kicker, which is right above a text box
     and wrong for a checkbox — a .checkbox-row IS a label, so every category
     and destination name rendered as a 0.72rem uppercase kicker in
     --ink-muted instead of as readable text. Excluded by :not() rather than
     re-stated inside .checkbox-row, because an override would still inherit
     the letter-spacing and the colour and would have to undo four
     declarations to say one thing. */
  label:not(.checkbox-row) {
    display: block; font-size: 0.72rem; font-weight: 650; text-transform: uppercase;
    letter-spacing: 0.07em; color: var(--ink-muted); margin-bottom: 0.35rem;
  }
  /* A TEXT BOX, WHICH IS NOT EVERY INPUT, and this one was the reported bug:
     "i cannot see text next to checkboxes". A width of 100% and a min-height
     of 2.9rem applied to input[type=checkbox] as readily as to a text field,
     and .checkbox-row's "flex: 0 0 auto" resolves its basis to auto — which
     falls back to the width property. Measured: a 598px wide, 46px tall
     checkbox in a 598px row, with the label text starting at x=598, outside
     the card entirely.

     NO BACKTICKS IN THIS COMMENT, and that is not a style note: this whole
     stylesheet is a template literal, so a backtick here closes it. Writing
     one cost a broken typecheck about ninety seconds after this fix landed,
     which is the trap the admin shell note in memory already records.

     The exclusion list is by TYPE rather than by naming the text-like types,
     so a date or a colour picker added later cannot arrive unstyled — only
     the controls that are genuinely not boxes are left alone. */
  input:not([type='checkbox'], [type='radio'], [type='range'], [type='color'],
            [type='file'], [type='image'], [type='submit'], [type='reset'],
            [type='button'], [type='hidden']),
  textarea, select {
    width: 100%; min-height: 2.9rem; padding: 0.5rem 0.75rem;
    border: 1px solid var(--hairline); border-radius: var(--radius-s);
    font: inherit; font-size: 16px; color: var(--ink); background: var(--paper);
  }
  /* THE BOX ITSELF. accent-color rather than a hand-built control: it keeps
     the platform's own focus ring, its indeterminate state and its forced-
     colours behaviour, all of which a re-implementation loses. 1.15rem is
     the smallest that still reads as tickable at arm's length. */
  input[type='checkbox'], input[type='radio'] {
    width: 1.15rem; height: 1.15rem; min-height: 0; padding: 0;
    accent-color: var(--azure-text); cursor: pointer;
  }
  textarea { min-height: 6rem; padding-block: 0.6rem; resize: vertical; }
  input:focus-visible, textarea:focus-visible, select:focus-visible { outline: 2px solid var(--azure-text); outline-offset: 1px; }
  .field { margin-bottom: 0.9rem; }
  /* .checkbox-row had NO rule anywhere in the repository. It is used by
     hosts.ts and trips.ts, so every checkbox list in the admin rendered as a
     bare inline <label>: the box sat on the text baseline instead of beside
     it, the hint span wrapped into the label it described, and nothing lined
     up down the column. That is the misalignment, and this is the whole fix.
     Baseline rather than centre, so a wrapped two-line label still hangs off
     its box; the box itself gets a fixed flex basis so the text of every row
     starts on the same vertical line however long the labels are. */
  .checkbox-row { display: flex; align-items: baseline; gap: 0.55rem;
    padding: 0.3rem 0; line-height: 1.45; cursor: pointer; }
  .checkbox-row input[type='checkbox'] { flex: 0 0 auto; margin: 0;
    position: relative; top: 0.15rem; }
  .checkbox-row .hint { display: block; margin: 0.05rem 0 0; }
  /* A whole-row hit target. The label already wraps the box, so the row was
     clickable; it did not LOOK clickable, and a 20-item list where nothing
     responds to the pointer reads as static text. */
  .checkbox-row { border-radius: var(--radius-s); padding-inline: 0.4rem;
    margin-inline: -0.4rem; transition: background 0.12s ease; }
  .checkbox-row:hover { background: color-mix(in oklab, var(--azure) 7%, transparent); }
  .checkbox-row:focus-within { background: color-mix(in oklab, var(--azure) 10%, transparent); }
  /* TICKED IS LEGIBLE AT A GLANCE. :has() on the row, so the state shows on
     the words and not only in a 18px box — the question an editor asks of a
     twenty-item list is "which ones are on", and scanning twenty small
     squares is the slow way to answer it. */
  .checkbox-row:has(input:checked) { font-weight: 600; color: var(--ink); }

  /* --------------------------------------------------------------------
     THE LISTS THEMSELVES.

     Ten categories and twenty destinations rendered as two single columns,
     which is several screens of scrolling for what is a two-second task. A
     grid that fits its content puts the whole set in view at once, which is
     the only way a reader can answer "which are on" without scrolling.

     auto-fill with a min, so the column count follows the width rather than
     being written down and drifting: four across on a desktop, two on a
     tablet, one on a phone, no breakpoints to keep in step.
     -------------------------------------------------------------------- */
  .checkbox-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 15rem), 1fr));
    gap: 0.1rem 1rem;
    margin-top: 0.35rem;
  }

  /* --------------------------------------------------------------------
     STATE, AS A BADGE RATHER THAN AS PROSE.

     "(assigned by the agent — unconfirmed)", "saved for review only",
     "due to expire 14 days from now" were all sentences in --ink-muted,
     indistinguishable from the explanatory text around them. Each is a state
     that carries consequence: an unconfirmed category decides which pages a
     holiday appears on, and an expiring places-count is a scarcity claim the
     DMCC Act 2024 treats as false once it is stale.

     Three tones and no more. A fourth would stop any of them meaning
     anything.
     -------------------------------------------------------------------- */
  .tag {
    display: inline-flex; align-items: center; gap: 0.3rem;
    font-size: 0.68rem; font-weight: 700; letter-spacing: 0.04em;
    text-transform: uppercase; white-space: nowrap;
    padding: 0.14rem 0.45rem; border-radius: 999px;
    vertical-align: 0.1em;
  }
  /* Waiting for a person: the agent guessed, or a draft is unpublished. */
  .tag--check { background: color-mix(in oklab, var(--saffron) 20%, transparent); color: var(--saffron-text); }
  /* Live and correct. */
  .tag--live { background: color-mix(in oklab, var(--azure) 16%, transparent); color: var(--azure-text); }
  /* Running out, or already untrue. */
  .tag--stale { background: color-mix(in oklab, var(--rose) 16%, transparent); color: var(--plum); }

  /* --------------------------------------------------------------------
     THE HELP TEXT, WHICH THERE IS A LOT OF, AND RIGHTLY.

     Every field on the holiday screen carries a paragraph explaining what it
     does and what happens if it is wrong. That is a real virtue — it is the
     reason somebody can use the screen without being trained — and at twelve
     sections it also makes the form unscannable, because the prose and the
     controls have the same visual weight.

     So: capped to a reading measure, and set off by a rule rather than
     floating. The words stay; they stop competing.
     -------------------------------------------------------------------- */
  .card > .hint, .card > p.hint { max-width: 62ch; }
  details.why {
    margin: 0.2rem 0 0.7rem; font-size: 0.82rem; color: var(--ink-muted);
    border-left: 2px solid var(--hairline); padding-left: 0.7rem;
  }
  details.why > summary {
    cursor: pointer; font-weight: 650; color: var(--azure-text);
    font-size: 0.75rem; letter-spacing: 0.02em; list-style: none;
  }
  details.why > summary::-webkit-details-marker { display: none; }
  /* The words live in the summary element, not in ::before, so a screen
     reader has an accessible name for the disclosure; only the arrow is
     generated. */
  details.why > summary::after { content: '  ▾'; }
  details.why[open] > summary::after { content: '  ▴'; }
  details.why > summary:focus-visible { outline: 2px solid var(--azure-text); outline-offset: 2px; border-radius: 4px; }
  details.why > :not(summary) { max-width: 62ch; margin-top: 0.4rem; }
  details.why p { margin: 0 0 0.5rem; }

  /* --------------------------------------------------------------------
     ONE HEADING RULE FOR THE EDIT SCREENS, AND THE STATE ON THE HEADING.

     A heading at 16.8px beside 13px hints and 11.5px kickers does not read
     as a heading, and a state written as a sentence in the help colour is
     not a state. The three tag tones above were built for the three
     conditions an editor scans for — needs a person, live, untrue — so the
     tag sits on the heading, where the eye lands first. Flex so a tag wraps
     beside the words rather than under them; scroll-margin so a section
     reached from the strip lands clear of the sticky bar (57px measured at
     947 wide, 4.5rem clears it).

     Scoped to .edit-form rather than every .card: forty-four other cards in
     the admin carry an h2, and none of them was measured for this.
     -------------------------------------------------------------------- */
  .edit-form .card > h2 {
    font-size: 1.25rem; margin: 0 0 0.6rem;
    display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap;
  }
  /* The strip's links target the CARD (its id), so the margin that keeps a
     section clear of the sticky bar goes on the card, not its heading — on
     the heading it measured as a title landing 21px under the bar. Measured
     12 Sep 2026: the strip is one row (43px) at 947 wide with two tags and
     two rows (76px) at the 680px window minimum, so 6rem clears both. */
  .edit-form .card[id] { scroll-margin-top: 6rem; }
  .edit-form .card > h2 .tag { font-family: var(--font-body); letter-spacing: 0.04em; }
  /* The line under a heading that says when the card reaches a visitor,
     derived from goes-live.ts rather than written into the prose. */
  .edit-form .card > .reaches { margin: -0.3rem 0 0.7rem; }
  /* A kicker between two groups of cards: the holiday's own cards above,
     the one site-wide control below. */
  .section-divider {
    margin: 1.4rem 0 0.4rem; font-size: 0.72rem; font-weight: 650;
    text-transform: uppercase; letter-spacing: 0.07em; color: var(--ink-muted);
  }

  /* --------------------------------------------------------------------
     THE SECTION STRIP: THE STICKY BAR THAT DOUBLES AS THE TO-DO LIST.

     Measured at 947 by 720 in the framed shell, both sticky bars work, so
     two identical primary Saves were on screen at rest. The top bar now
     carries one pill per card, each wearing that card's tag, so "what
     needs me" is answered before any scrolling and each pill is the door to
     it. Hash links pass through the soft-navigation handler untouched, and
     body.framed is the scroller, so native fragment scrolling does the
     work.
     -------------------------------------------------------------------- */
  /* Two classes on purpose: .actionbar is declared later in this sheet with
     the same specificity and a 1rem gap and space-between, and a single
     class here lost to it — measured as 16px gaps and the last pill flung to
     the far edge. */
  .actionbar.section-strip { justify-content: flex-start; gap: 0.3rem; padding: 0.35rem 0.55rem; }
  .section-strip .mx-pill { font-size: 0.74rem; padding: 0.2rem 0.5rem; display: inline-flex; align-items: center; }
  .section-strip .tag { margin-left: 0.25rem; }

  /* A FOLD FOR A CONTROL MOST HOLIDAYS DO NOT NEED. Inputs inside a closed
     details still submit, so an unopened grid posts the current cast
     unchanged. */
  details.more > summary {
    cursor: pointer; font-weight: 650; color: var(--azure-text); font-size: 0.82rem;
    list-style: none; letter-spacing: 0.02em;
  }
  details.more > summary::-webkit-details-marker { display: none; }
  details.more > summary::before { content: '+ '; }
  details.more[open] > summary::before { content: '− '; }
  details.more > summary:focus-visible { outline: 2px solid var(--azure-text); outline-offset: 2px; border-radius: 4px; }

  /* Room to write. The three prose boxes the office actually writes in were
     the smallest boxes on the destination screen: three lines of a
     multi-paragraph introduction. field-sizing is progressive; a browser
     without it keeps the 12rem floor. */
  textarea[data-prose] { min-height: 12rem; field-sizing: content; max-height: 40rem; }

  /* --------------------------------------------------------------------
     THE SHARED EDIT HERO. The destination screen answered "what is this
     and how is it doing" in 180px with a photograph; the holiday screen
     described the screen. One hero for both, with the gradient standing in
     when a row has no image — honest degradation, never a blank band.
     Moved here from an inline style block in destinations.ts (12 Sep 2026).
     -------------------------------------------------------------------- */
  .edit-hero { position: relative; border-radius: 18px; overflow: hidden; isolation: isolate; color: #fff;
               padding: 1.6rem 1.4rem; margin-bottom: 1rem; background: linear-gradient(130deg, #0e2a3d, #1d84b8); }
  .edit-hero > img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; z-index: -2; }
  .edit-hero .veil { position: absolute; inset: 0; z-index: -1;
                     background: linear-gradient(100deg, rgb(14 42 61 / 0.86) 20%, rgb(14 42 61 / 0.45) 65%, rgb(14 42 61 / 0.2)); }
  .edit-hero h1 { margin: 0 0 0.15rem; text-shadow: 0 2px 10px rgb(14 42 61 / 0.5); }
  .edit-hero .sub { color: rgb(255 255 255 / 0.9); margin: 0 0 0.7rem; }
  .edit-chips { display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: center; }
  .edit-chips span:not(.tag), .edit-chips a { font-size: 0.74rem; font-weight: 700; background: rgb(255 255 255 / 0.16); color: #fff;
               border-radius: 999px; padding: 0.25rem 0.75rem; text-decoration: none; border: 1px solid rgb(255 255 255 / 0.25); }
  .edit-chips a:hover { background: rgb(255 255 255 / 0.3); }
  /* A tag on the photograph keeps its tone on a paper ground, so saffron
     and azure text stay readable over the veil. */
  .edit-chips .tag { background: var(--paper); }
  .edit-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 26rem), 1fr)); gap: 1rem; align-items: start; }

  /* THE DANGER ZONE, IN TOKENS. It was painted in a pink that exists in no
     token; --rose is the admin's own colour for "running out or already
     untrue" (.tag--stale), which is the right register for a delete. Only
     rendered when the button can actually be pressed — a destructive button
     that can never be pressed is a door to nothing and the loudest colour on
     the screen. */
  .danger-zone { border: 1px solid color-mix(in oklab, var(--rose) 35%, transparent); border-radius: 14px; padding: 0.9rem 1rem;
                 margin-top: 2rem; background: color-mix(in oklab, var(--rose) 5%, var(--paper));
                 display: flex; gap: 0.8rem; align-items: center; flex-wrap: wrap; }
  .danger-zone b { color: var(--plum); }
  .danger-zone button { border: 0; border-radius: 999px; padding: 0.4rem 1rem; font: inherit; font-weight: 700; cursor: pointer;
                        background: var(--rose); color: #fff; }
  .danger-zone button[disabled] { opacity: 0.45; cursor: not-allowed; }

  /* --------------------------------------------------------------------
     THE SAVE, WHERE THE EDITOR IS RATHER THAN WHERE THE FORM STARTED.

     The holiday screen is twelve cards long and its only Save was in the bar
     at the top, so finishing an edit at the bottom meant scrolling back past
     everything to find it. A sticky footer keeps it one press away from
     wherever the reader is, and says WHICH holiday it is saving, because on a
     screen this long the title has been off-screen for some time.
     -------------------------------------------------------------------- */
  .savebar {
    position: sticky; bottom: 0; z-index: 5;
    display: flex; align-items: center; justify-content: space-between;
    flex-wrap: wrap; gap: 0.6rem;
    /* No negative bottom margin: a sticky element reserves its space in flow,
       and pulling that space upwards is the one way to make it cover the card
       above at rest.

       STICKY HERE DEPENDS ON THE ADMIN SHELL, and html/body carry
       overflow: hidden, so the document itself never scrolls. Both shells
       still give this what sticky needs:

         framed   body.framed is overflow:auto and IS the scroller;
                  .framed-body adds 3rem of bottom padding under it.
         window   .window-body is overflow-y:auto with padding-block
                  1.75rem 4rem, so the clearance is already there.

       .framed-body's container-type: inline-size brings layout containment,
       which makes it a containing block for absolute descendants but does not
       clip — sticky resolves against the scrolling ancestor either way.

       Checked by reading those rules rather than in a synthetic page: a
       harness with no scroll container reports whatever its own root does,
       which here was nothing, and a 207px "overlap" that measured neither
       shell. */
    margin: 1rem 0 0; padding: 0.7rem 0.9rem;
    border-radius: var(--radius-m);
    border: 1px solid var(--hairline);
    background: color-mix(in oklab, var(--paper) 88%, transparent);
    -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px);
    box-shadow: 0 -8px 24px -18px rgb(19 42 61 / 0.5);
    /* overflow: hidden clips the seam to the rounded corner. It is on the
       sticky element itself, not an ancestor, so it does not break sticky —
       only an overflow on the scrolling ancestor would. */
    overflow: hidden;
  }
  /* The seam, where a person commits — see --seam in :root. */
  .savebar::before { content: ''; position: absolute; inset-inline: 0; top: 0; height: 3px; background: var(--seam); }
  .savebar .hint { margin: 0; }
  /* Clearance under the bar, so the last field of a long form is never behind
     it mid-scroll either. Its own height plus a little; a form that ends in a
     control the reader cannot see is the whole reason the bar exists. */
  form:has(> .savebar) { padding-bottom: 0.5rem; }
  @media (prefers-reduced-motion: reduce) { .checkbox-row { transition: none; } }

  /* Chamfer, matching the header/footer CTA on the public site — the one
     shape reserved for a primary action, now in the site's own
     saffron→chilli gradient rather than an admin-only rose. */
  button {
    min-height: 2.9rem; padding: 0 1.4rem; border: 0;
    border-radius: 0;
    clip-path: polygon(0.7rem 0, 100% 0, 100% calc(100% - 0.7rem), calc(100% - 0.7rem) 100%, 0 100%, 0 0.7rem);
    background: linear-gradient(135deg, var(--saffron) 0%, var(--chilli) 100%);
    color: #fff; font: inherit; font-weight: 700; cursor: pointer;
    transition: box-shadow 0.2s, transform 0.2s var(--ease-spring), filter 0.2s;
  }
  button:hover { filter: saturate(1.1); transform: translateY(-1px); box-shadow: 0 10px 24px -10px rgb(218 65 29 / 0.6); }
  button.secondary {
    background: var(--paper); color: var(--ink); border: 1px solid var(--hairline);
    clip-path: none; border-radius: var(--radius-s);
  }
  button.secondary:hover { background: var(--surface); border-color: var(--azure); transform: none; }

  a.item {
    display: flex; justify-content: space-between; align-items: center; gap: 1rem;
    padding: 0.9rem 0.5rem; border-bottom: 1px solid var(--hairline);
    text-decoration: none; color: inherit; border-radius: var(--radius-s);
    transition: background 0.15s;
  }
  a.item::after {
    content: '›'; color: var(--ink-faint); font-size: 1.15rem; line-height: 1;
    margin-left: 0.25rem; transition: transform 0.15s, color 0.15s;
  }
  /* Rows that stack their content vertically opt out of the chevron. */
  a.item[style*='column']::after { display: none; }
  a.item:hover { background: var(--surface); }
  a.item:hover::after { transform: translateX(3px); color: var(--azure-text); }
  a.item strong { font-weight: 650; }

  /* Status chips — one vocabulary for "is this visible to customers". */
  .chip {
    display: inline-flex; align-items: center; gap: 0.35rem;
    padding: 0.16rem 0.6rem; border-radius: var(--radius-pill);
    font-size: 0.74rem; font-weight: 650; white-space: nowrap;
  }
  .chip::before { content: ''; width: 0.4rem; height: 0.4rem; border-radius: 50%; background: currentColor; }
  .chip--live { background: #e7f6ec; color: #1c5a30; }
  /* A fact the reader needs to act on, not an error. Saffron is this admin's
     tone for that everywhere else, so it is mixed from the token rather than
     picked by eye. */
  .chip--warn {
    background: color-mix(in srgb, var(--saffron) 16%, var(--paper));
    color: var(--saffron-text);
  }
  .chip--draft { background: #fdf0d5; color: #8a5a0b; }

  /* Page toolbar: the page's identity on the left, its one primary act on
     the right — so the main action never hides below the fold. */
  .page-toolbar {
    display: flex; justify-content: space-between; align-items: flex-end; gap: 1rem;
    margin-bottom: 1.4rem; flex-wrap: wrap;
  }
  .page-toolbar h1 { margin-bottom: 0.2rem; }
  .page-toolbar .sub { margin-bottom: 0; }
  .page-toolbar a { text-decoration: none; }

  /* --- The Blog library ----------------------------------------------------- */
  .art-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(19.5rem, 1fr)); gap: 0.8rem; }
  .art-card {
    display: flex; align-items: center; gap: 0.85rem;
    background: var(--paper); border: 1px solid var(--hairline); border-radius: var(--radius-m);
    padding: 0.7rem 0.85rem; text-decoration: none; color: inherit;
    box-shadow: var(--shadow-card);
    transition: transform 0.16s var(--ease-out), box-shadow 0.16s;
  }
  .art-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-lift); }
  .art-thumb {
    flex: none; width: 4.6rem; height: 3.4rem; border-radius: var(--radius-s);
    overflow: hidden; background: var(--surface-sink);
    display: grid; place-items: center; color: var(--ink-faint); font-size: 1.3rem;
  }
  .art-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
  .art-main { min-width: 0; flex: 1; }
  .art-main strong { display: block; font-weight: 650; line-height: 1.3; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
  .art-main .hint { margin-top: 0.15rem; }

  /* --- The compose surface: writing belongs in a quiet room ----------------- */
  .actionbar {
    position: sticky; top: 0; z-index: 5;
    display: flex; justify-content: space-between; align-items: center; gap: 1rem; flex-wrap: wrap;
    padding: 0.55rem 0.8rem; margin-bottom: 1.3rem;
    background: rgb(255 255 255 / 0.86);
    backdrop-filter: blur(14px) saturate(1.5);
    -webkit-backdrop-filter: blur(14px) saturate(1.5);
    border: 1px solid var(--hairline); border-radius: var(--radius-m);
    box-shadow: 0 8px 20px -10px rgb(19 42 61 / 0.2);
  }
  .actionbar .row { gap: 0.5rem; }
  .actionbar select { min-height: 2.2rem; width: auto; padding: 0 1.8rem 0 0.6rem; font-size: 0.88rem; }
  .actionbar button { min-height: 2.35rem; padding: 0 1.1rem; }
  .actionbar button.secondary { min-height: 2.2rem; }
  .actionbar .hint { margin: 0; }

  .compose-title {
    width: 100%; border: 0; background: transparent; min-height: 0;
    font-family: var(--font-display);
    font-variation-settings: 'SOFT' 30, 'WONK' 1, 'opsz' 100;
    font-size: clamp(1.7rem, 1.4rem + 1.4vw, 2.3rem); font-weight: 600; line-height: 1.15;
    color: var(--ink); letter-spacing: -0.015em;
    padding: 0.1rem 0; margin-bottom: 0.3rem;
  }
  .compose-title::placeholder { color: var(--ink-faint); opacity: 0.7; }
  .compose-title:focus-visible { outline: none; box-shadow: inset 0 -2px 0 var(--azure); }
  .compose-excerpt {
    width: 100%; border: 0; background: transparent; resize: vertical;
    font-size: 1.05rem; line-height: 1.5; color: var(--ink-muted);
    min-height: 3rem; padding: 0; margin-bottom: 1.1rem;
  }
  .compose-excerpt::placeholder { color: var(--ink-faint); opacity: 0.7; }
  .compose-excerpt:focus-visible { outline: none; box-shadow: inset 0 -2px 0 var(--azure); }

  .editor-tools { display: flex; align-items: center; gap: 0.35rem; flex-wrap: wrap; margin-bottom: 0.7rem; }
  .editor-tools .tool {
    min-height: 1.9rem; padding: 0 0.7rem; font-size: 0.8rem; font-weight: 600;
    background: var(--surface); color: var(--ink); border: 1px solid var(--hairline);
    clip-path: none; border-radius: var(--radius-pill);
  }
  .editor-tools .tool:hover { background: var(--surface-sink); border-color: var(--azure); transform: none; box-shadow: none; filter: none; }
  .editor-toggle { display: inline-flex; align-items: center; gap: 0.4rem; margin-left: auto; text-transform: none; letter-spacing: 0; font-size: 0.82rem; font-weight: 600; color: var(--ink-muted); }
  .editor-toggle input { width: auto; min-height: 0; }

  .editor-split { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; align-items: stretch; }
  .editor-split.no-preview { grid-template-columns: 1fr; }
  .editor-split.no-preview .editor-preview { display: none; }
  #compose-body {
    font-family: ui-monospace, 'SF Mono', Consolas, monospace; font-size: 13.5px; line-height: 1.6;
    min-height: 27rem; background: var(--surface); resize: vertical;
  }
  /* The preview typesets like the article page itself: what you see is what
     the reader gets, minus the site chrome. */
  .editor-preview {
    border: 1px solid var(--hairline); border-radius: var(--radius-s);
    background: var(--paper); padding: 1.1rem 1.25rem;
    overflow-y: auto; max-height: 36rem; min-height: 27rem;
  }
  /* Metrics mirrored from the article page's own .prose rules
     (apps/web/src/pages/blog/[slug].astro): step-2 h2 with generous
     headroom, step-1 h3, 1.5rem paragraph rhythm. The on-site preview
     route is the pixel authority; this keeps the writing pane honest. */
  .editor-preview { font-size: 1.08rem; }
  .editor-preview h2, .editor-preview h3 {
    font-family: var(--font-display); font-weight: 600; line-height: 1.15;
  }
  .editor-preview h2 { font-size: 1.8rem; margin: 2.8rem 0 1rem; }
  .editor-preview h3 { font-size: 1.4rem; margin: 2rem 0 0.5rem; }
  .editor-preview h2:first-child, .editor-preview h3:first-child { margin-top: 0.2rem; }
  .editor-preview p { margin: 0 0 1.5rem; line-height: 1.6; }
  .editor-preview ul, .editor-preview ol { margin: 0 0 0.85em; padding-left: 1.3rem; }
  .editor-preview li { margin-bottom: 0.3em; }
  .editor-preview img { max-width: 100%; border-radius: var(--radius-s); display: block; margin: 0.8em 0; }
  .editor-preview a { color: var(--azure-text); }
  @media (max-width: 46rem) {
    .editor-split { grid-template-columns: 1fr; }
  }

  /* --- Rich mode: the editor surface IS the article page. ------------------- */
  .editor-mode { display: inline-flex; gap: 0; border: 1px solid var(--hairline); border-radius: var(--radius-pill); overflow: hidden; margin-right: 0.4rem; }
  .editor-mode .tool { border: 0; border-radius: 0; }
  .tool--on { background: var(--azure) !important; color: #fff !important; border-color: var(--azure) !important; }
  .rich-hidden { display: none; }
  .editor-split.rich-mode { grid-template-columns: 1fr; }
  .editor-split.rich-mode .editor-preview { display: none; }
  .rich-tools { margin: 0 0 0.6rem; }
  .rich-surface {
    border: 1px solid var(--hairline); border-radius: var(--radius-s);
    background: var(--paper); padding: 1.1rem 1.35rem; min-height: 27rem;
  }
  .rich-surface { font-size: 1.08rem; max-width: 48.5rem; margin-inline: auto; }
  .rich-surface .ProseMirror { outline: none; min-height: 25rem; }
  .rich-surface h2, .rich-surface h3 {
    font-family: var(--font-display); font-weight: 600; line-height: 1.15;
  }
  .rich-surface h2 { font-size: 1.8rem; margin: 2.8rem 0 1rem; }
  .rich-surface h3 { font-size: 1.4rem; margin: 2rem 0 0.5rem; }
  .rich-surface h2:first-child, .rich-surface h3:first-child { margin-top: 0.2rem; }
  .rich-surface p { margin: 0 0 1.5rem; line-height: 1.6; }
  .rich-surface ul, .rich-surface ol { margin: 0 0 0.85em; padding-left: 1.3rem; }
  .rich-surface blockquote { border-left: 3px solid var(--azure); margin: 0 0 0.85em; padding: 0.2em 0 0.2em 1em; color: var(--ink-muted); }
  .rich-surface img { max-width: 100%; border-radius: var(--radius-s); display: block; margin: 0.8em 0; }
  .rich-surface a { color: var(--azure-text); }

  .thumbs { display: grid; grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr)); gap: 0.6rem; }
  .thumbs label { margin: 0; cursor: pointer; position: relative; display: block; text-transform: none; }
  .thumbs img {
    width: 100%; aspect-ratio: 4/3; object-fit: cover; border-radius: var(--radius-m);
    border: 3px solid transparent; display: block; transition: border-color 0.18s;
  }
  .thumbs input { position: absolute; inset: 0; opacity: 0; cursor: pointer; }
  /* Azure, not rose: rose is this admin's colour for "running out or already
     untrue" (.tag--stale) and the danger zone's, and the chosen photograph is
     neither. The focus ring is the same colour so keyboard choice is visible. */
  .thumbs input:checked + img { border-color: var(--azure-text); }
  .thumbs input:focus-visible + img { outline: 2px solid var(--azure-text); outline-offset: 2px; }

  .flash {
    padding: 0.8rem 1.1rem; border-radius: var(--radius-m);
    background: #e7f6ec; border: 1px solid #b7e3c6; color: #1c5a30;
    margin-bottom: 1.25rem; font-weight: 600;
  }

  /* A FLASH THAT IS NOT GOOD NEWS.
     There was only the green one, so any screen needing to say "careful" or
     "this failed" reached for an inline style. Trade billing had four of them,
     in a warm amber and a warm red that appear nowhere else in the admin —
     invented at the call site because the shared sheet had no answer.
     Mixed from the palette rather than picked by eye, so they sit in the same
     family as every other tone and follow the tokens if those ever move. */
  .flash--warn {
    background: color-mix(in srgb, var(--saffron) 14%, var(--paper));
    border-color: color-mix(in srgb, var(--saffron) 42%, var(--paper));
    color: var(--saffron-text);
  }
  .flash--bad {
    background: color-mix(in srgb, var(--chilli) 10%, var(--paper));
    border-color: color-mix(in srgb, var(--chilli) 38%, var(--paper));
    color: var(--chilli-text);
  }

  /* The save toast — dark glass pill, floats in, holds three seconds, slips
     away. Inside a framed window it sits above the frame's own bottom edge. */
  .toast {
    position: fixed; left: 50%; bottom: 5.8rem; transform: translateX(-50%);
    z-index: 70; margin: 0; padding: 0.6rem 1.25rem;
    border-radius: var(--radius-pill);
    background: rgb(19 42 61 / 0.9); color: #fff;
    font-weight: 650; font-size: 0.92rem; white-space: nowrap;
    box-shadow: 0 12px 30px -8px rgb(19 42 61 / 0.5);
  }
  .toast::before { content: '✓'; color: #7be3a1; margin-right: 0.5rem; font-weight: 700; }
  body.framed .toast { bottom: 1.4rem; }
  /* A whole sentence, which the pill above cannot hold: it wraps, it centres,
     and it does not claim success with a tick it has not earned. */
  .toast--say {
    white-space: normal; max-width: min(34rem, calc(100vw - 3rem));
    text-align: center; line-height: 1.4; font-weight: 550;
  }
  .toast--say::before { content: none; }

  /* --- Micro-interactions: everything that can be pressed answers back ----- */
  @media (prefers-reduced-motion: no-preference) {
    .window-body button:not(:disabled), .framed-body button:not(:disabled),
    .window-body a.btn-sm, .framed-body a.btn-sm, .menubar button {
      transition: transform 90ms var(--ease-out), box-shadow 150ms var(--ease-out), background 150ms var(--ease-out);
    }
    .window-body button:not(:disabled):active, .framed-body button:not(:disabled):active,
    .window-body a.btn-sm:active, .framed-body a.btn-sm:active { transform: scale(0.98); }
    .window-body .card, .framed-body .card { transition: box-shadow 180ms var(--ease-out), transform 180ms var(--ease-out); }
    .window-body .card:hover, .framed-body .card:hover { box-shadow: 0 14px 34px -16px rgb(19 42 61 / 0.35); }

    /* Modals arrive, they don't appear: the palette pops from centre, the
       notification sheet unrolls from the bell, theme menu breathes open. */
    .palette:not([hidden]) { animation: spice-fade 130ms var(--ease-out) both; }
    .palette:not([hidden]) .palette-panel { animation: spice-pop 190ms var(--ease-spring) both; }
    .notif-panel:not([hidden]) { animation: spice-sheet 200ms var(--ease-out) both; }
    .menubar-themes[open] .menubar-themes-menu { animation: spice-sheet 160ms var(--ease-out) both; }
    .stx-slot, .tr-chip, .stx-chip { transition: transform 150ms var(--ease-out), box-shadow 150ms var(--ease-out); }
  }
  @keyframes spice-fade { from { opacity: 0; } }
  @keyframes spice-pop { from { opacity: 0; transform: scale(0.955) translateY(8px); } }
  @keyframes spice-sheet { from { opacity: 0; transform: translateY(-9px); } }
  @media (prefers-reduced-motion: no-preference) {
    .toast { animation: toast-in 0.35s var(--ease-spring) backwards, toast-out 0.3s 3s var(--ease-out) forwards; }
    /* Long enough to read a sentence twice. Three seconds is right for "Saved",
       which you already knew, and wrong for a reason you did not. */
    .toast--say { animation: toast-in 0.35s var(--ease-spring) backwards, toast-out 0.3s 9s var(--ease-out) forwards; }
  }
  @keyframes toast-in { from { opacity: 0; transform: translate(-50%, 0.9rem); } }
  @keyframes toast-out { to { opacity: 0; transform: translate(-50%, 0.5rem); visibility: hidden; } }

  /* --- Settings, in the System-Settings idiom: a label column that says
     what and why, a control column that does it. ---------------------------- */
  .settings-sheet { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 34rem), 1fr)); column-gap: 2.6rem; }
  .settings-row {
    display: grid; grid-template-columns: minmax(11rem, 17rem) 1fr;
    gap: 0.5rem 1.4rem; align-items: start;
    padding: 0.85rem 0.25rem; border-bottom: 1px solid var(--hairline);
  }
  .settings-row:last-child { border-bottom: 0; }
  .settings-row label { margin: 0.55rem 0 0; }
  .settings-row .hint { display: block; margin-top: 0.2rem; text-transform: none; letter-spacing: 0; }
  @media (max-width: 46rem) { .settings-row { grid-template-columns: 1fr; gap: 0.25rem; } }

  /* --- Empty states: an invitation, never a shrug. -------------------------- */
  .empty { text-align: center; padding: 2.6rem 1.5rem; }
  .empty-icon { font-size: 1.9rem; opacity: 0.6; margin-bottom: 0.6rem; }
  .empty strong {
    display: block; font-family: var(--font-display); font-weight: 600;
    font-size: 1.18rem; color: var(--ink); margin-bottom: 0.35rem;
  }
  .empty p { margin: 0 auto; max-width: 36rem; font-size: 0.92rem; color: var(--ink-muted); }
  .empty .row { justify-content: center; margin-top: 1rem; }

  /* --- The media picker: the photo library as a summonable sheet. ----------- */
  .picker {
    position: fixed; inset: 0; z-index: 65;
    background: rgb(19 42 61 / 0.24);
    display: grid; justify-items: center; align-items: start;
    padding: 8vh 1rem 0;
  }
  .picker[hidden] { display: none; }
  .picker-panel {
    width: min(100%, 46rem); max-height: 78vh; display: flex; flex-direction: column;
    background: rgb(255 255 255 / 0.9);
    backdrop-filter: blur(26px) saturate(1.8);
    -webkit-backdrop-filter: blur(26px) saturate(1.8);
    border: 1px solid rgb(255 255 255 / 0.95); border-radius: var(--radius-m);
    box-shadow: 0 30px 70px rgb(19 42 61 / 0.4), inset 0 1px 0 #fff;
    overflow: hidden; animation: palette-in 0.18s var(--ease-out);
  }
  .picker-head { display: flex; gap: 0.6rem; align-items: center; padding: 0.7rem 0.8rem; border-bottom: 1px solid var(--hairline); }
  .picker-head input { min-height: 2.4rem; }
  .picker-head .secondary { min-height: 2.4rem; }
  .picker-grid {
    display: grid; grid-template-columns: repeat(auto-fill, minmax(7.5rem, 1fr));
    gap: 0.5rem; padding: 0.8rem; overflow-y: auto;
  }
  .picker-grid button {
    all: unset; cursor: pointer; border-radius: var(--radius-s); overflow: hidden;
    aspect-ratio: 4 / 3; background: var(--surface-sink);
    outline-offset: 2px;
  }
  .picker-grid button:focus-visible { outline: 2px solid var(--azure-text); }
  .picker-grid img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 0.15s var(--ease-out); }
  .picker-grid button:hover img { transform: scale(1.05); }
  .picker-foot { padding: 0.5rem 0.8rem 0.8rem; text-align: center; }
  .pick-btn { flex: none; }

  /* --- The link picker: same sheet, a searchable list instead of a grid. --- */
  .picker-list { display: flex; flex-direction: column; padding: 0.4rem; overflow-y: auto; gap: 0.1rem; }
  .picker-row {
    all: unset; cursor: pointer; display: flex; justify-content: space-between; align-items: baseline;
    gap: 0.8rem; padding: 0.55rem 0.7rem; border-radius: var(--radius-s);
  }
  .picker-row:hover, .picker-row:focus-visible { background: var(--surface); outline: none; }
  .picker-row strong { font-size: 0.92rem; font-weight: 600; color: var(--ink); }
  .picker-row .hint { flex: none; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.06em; }
  .picker-foot--ext { display: flex; gap: 0.5rem; border-top: 1px solid var(--hairline); }
  .picker-foot--ext input { flex: 1; min-height: 2.3rem; }

  /* --- Evening on the estate: after seven the chrome dims to dusk while
     every window keeps its daylight paper — atmosphere outside the glass,
     legibility inside it. ---------------------------------------------------- */
  html.evening .wallpaper {
    background-color: #251038;
    background-image:
      radial-gradient(at 78% 88%, rgb(233 148 21 / 0.4) 0px, transparent 45%),
      radial-gradient(at 62% 30%, rgb(204 44 113 / 0.3) 0px, transparent 52%),
      radial-gradient(at 14% 14%, rgb(33 168 225 / 0.28) 0px, transparent 50%),
      radial-gradient(at 30% 78%, rgb(152 25 101 / 0.42) 0px, transparent 50%),
      linear-gradient(172deg, #33124e 0%, #241040 55%, #140a26 100%);
  }
  html.evening .wallpaper-scrim {
    background:
      radial-gradient(120% 100% at 50% 40%, transparent 55%, rgb(6 4 12 / 0.45) 100%),
      linear-gradient(180deg, rgb(6 4 12 / 0.2) 0%, rgb(6 4 12 / 0.35) 100%);
  }
  html.evening .menubar {
    background: rgb(26 16 34 / 0.5);
    border-bottom-color: rgb(255 255 255 / 0.1);
    box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.12);
    color: rgb(255 255 255 / 0.94);
  }
  html.evening .menubar-bell { color: #fff; }
  html.evening .menubar-bell:hover { background: rgb(255 255 255 / 0.14); }
  html.evening .menubar-signout button.secondary {
    background: rgb(255 255 255 / 0.16); color: #fff; border-color: rgb(255 255 255 / 0.28);
  }
  html.evening .dock-blur { background: rgb(26 16 34 / 0.32); }
  html.evening .dock-shelf {
    background:
      linear-gradient(180deg,
        rgb(255 255 255 / 0.28) 0%,
        rgb(255 255 255 / 0.1) 30%,
        rgb(90 60 110 / 0.35) 62%,
        rgb(255 255 255 / 0.14) 100%);
    border-color: rgb(255 255 255 / 0.25);
    border-top-color: rgb(255 255 255 / 0.45);
  }
  html.evening .dock-sep { background: linear-gradient(180deg, transparent, rgb(255 255 255 / 0.3) 30% 70%, transparent); }
  html.evening .dock-running { background: rgb(255 255 255 / 0.92); box-shadow: 0 0 6px 1px rgb(255 255 255 / 0.5); }
  html.evening .lock-time { color: #fff; text-shadow: 0 2px 18px rgb(6 4 12 / 0.6); }
  html.evening .lock-date { color: rgb(255 255 255 / 0.75); }
  html.evening .login-sub { color: rgb(255 255 255 / 0.7); }
  /* Amber, not red: a trip with no categories is not an error state — an
     editor may be mid-reorganisation — but the consequence (invisible on
     category pages and search filters) needs to be impossible to miss. */
  .warn {
    padding: 0.8rem 1.1rem; border-radius: var(--radius-m);
    background: #fdecc8; border: 1px solid #f3cf8a; color: #7a4d06;
    margin-bottom: 1.25rem; font-weight: 600;
  }
  .badge.warn { background: #fdecc8; color: #7a4d06; }
  /* Red, distinct from the amber .warn above: a blocker is not "worth a
     look when you get a moment" like a warn card is — it is the reason a
     page cannot go live, and needs to read as more urgent at a glance. */
  .blocker {
    padding: 0.8rem 1.1rem; border-radius: var(--radius-m);
    background: #fbe4df; border: 1px solid #f0b6a8; color: #7a2712;
    margin-bottom: 1.25rem; font-weight: 600;
  }
  .badge.blocker { background: #f6c9bd; color: #7a2712; }
  .hint { font-size: 0.82rem; color: var(--ink-faint); margin-top: 0.3rem; }
  .preview { width: 100%; max-width: 22rem; border-radius: var(--radius-m); display: block; margin-bottom: 0.6rem; }

  code {
    display: inline-block; padding: 0.2rem 0.5rem; border-radius: var(--radius-s);
    background: var(--surface); border: 1px solid var(--hairline);
    font-family: ui-monospace, 'SF Mono', Consolas, monospace; font-size: 0.9em;
  }

  .badge {
    display: inline-block; padding: 0.15rem 0.6rem; border-radius: var(--radius-pill);
    font-size: 0.76rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em;
  }

  /* --- The review queue (reviews.ts) -----------------------------------------

     A READING TABLE, NOT A DATA TABLE. The reflex for an admin list is
     id / name / status / date with the content last or missing — but nobody
     can decide whether a review should go on the site by looking at its
     metadata. So the traveller's own words are column one, set at reading
     size in the display face, and every other fact is quiet to their right.

     No card chrome, no zebra striping, no borders round each row: hairlines
     between, and the row tints on hover. The state is a 7px dot rather than a
     badge, because nineteen badges shouting LIVE is nineteen things competing
     with the words you are trying to read. */
  /* Visually hidden, still announced. The state column's header has no
     visible label — the dot speaks for itself to anyone who can see it — but
     a table with a nameless column is a table a screen reader reads as
     "blank". */
  .sr-only {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip-path: inset(50%); white-space: nowrap;
  }
  /* overflow-x, not hidden: the metadata columns are nowrap by design, and a
     narrow admin window must scroll them rather than clip "Arrived" off the
     right-hand edge where nobody knows it was there. */
  .rv-wrap { margin-top: 1rem; border: 1px solid var(--hairline); border-radius: var(--radius-m); overflow-x: auto; background: var(--paper); }
  /* Two sources, one module (226): the switch above the funnel, and the
     tag chips a Google review carries. A suggested tag is dashed — drawn as
     a guess until somebody leaves it or strikes it out. */
  .rv-sources { display: flex; gap: 0.5rem; flex-wrap: wrap; margin: 0 0 0.9rem; }
  .rv-sources a {
    padding: 0.35rem 0.8rem; border-radius: 999px; border: 1px solid var(--hairline);
    color: var(--ink); text-decoration: none; font-size: 0.86rem; background: var(--paper);
  }
  .rv-sources a.on { background: var(--ink); color: var(--paper); border-color: transparent; }
  .rv-sources b { font-weight: 600; margin-left: 0.35rem; }
  .rv-tags-cell { max-width: 18rem; }
  .rv-chip {
    display: inline-block; font-size: 0.76rem; line-height: 1.5; padding: 0.05rem 0.55rem;
    border-radius: 999px; background: var(--surface); border: 1px solid var(--hairline);
    margin: 0 0.25rem 0.25rem 0; white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis;
  }
  .rv-chip--auto { border-style: dashed; background: transparent; }
  .rv-tag-list { list-style: none; margin: 0 0 0.6rem; padding: 0; display: grid; gap: 0.35rem; }
  .rv-tag-list li { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
  .rv-tag-x { margin: 0; }
  .rv-tag-x button { padding: 0.1rem 0.55rem; font-size: 0.76rem; }
  .rv-tag-add { gap: 0.5rem; align-items: center; }
  .rv-tag-add select { max-width: 26rem; }
  .rv-table { width: 100%; border-collapse: collapse; font-size: 0.88rem; table-layout: auto; }
  .rv-table thead th {
    padding: 0.5rem 0.7rem; text-align: left; font-size: 0.68rem; font-weight: 700;
    letter-spacing: 0.07em; text-transform: uppercase; color: var(--ink-muted);
    border-bottom: 1px solid var(--hairline); background: var(--surface); white-space: nowrap;
  }
  .rv-table td { padding: 0.6rem 0.7rem; border-bottom: 1px solid var(--hairline); vertical-align: baseline; }
  .rv-table tbody tr:last-child td { border-bottom: 0; }
  .rv-row { cursor: pointer; transition: background 120ms var(--ease-out); }
  .rv-row:hover { background: color-mix(in oklab, var(--azure) 6%, var(--paper)); }
  .rv-row:focus-visible { outline: 2px solid var(--azure-text); outline-offset: -2px; background: color-mix(in oklab, var(--azure) 6%, var(--paper)); }

  /* The words take whatever the fixed columns leave. "width: 100%" on this
     one cell plus "width: 1%; white-space: nowrap" on the rest is the old
     trick for "one column flexes, the others size to their content" — and it
     is what stops a long quote pushing the date off the edge. */
  /* "max-width: 0" with "width: 100%" is the trick that makes a table cell
     shrink instead of growing to fit its text. Without it the cell sizes to
     the longest quote, the ellipsis never engages, and the columns on the
     right are pushed off the edge — which is exactly what the first cut did. */
  .rv-quote { width: 100%; max-width: 0; }
  .rv-who, .rv-trip, .rv-stars, .rv-media, .rv-when { width: 1%; }
  .rv-quote q {
    display: block; quotes: none; font-family: var(--font-display, inherit);
    font-size: 0.95rem; line-height: 1.35; color: var(--ink);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .rv-who { white-space: nowrap; font-weight: 600; }
  .rv-trip, .rv-when, .rv-media { color: var(--ink-muted); white-space: nowrap; }
  .rv-when, .rv-media { font-variant-numeric: tabular-nums; }
  .rv-soft { color: var(--ink-faint); font-size: 0.8em; }
  /* Said in words, not a dash. Five of eleven published reviews are in this
     state and reach no trip page; punctuation would have hidden it. */
  .rv-untagged { color: var(--chilli-text); }
  .rv-stars { color: var(--saffron-text); white-space: nowrap; letter-spacing: -0.04em; }
  .rv-state { width: 1.6rem; }
  .rv-empty { margin: 0; padding: 2.2rem 1rem; text-align: center; color: var(--ink-muted); }
  .rv-showing { margin: 0.8rem 0 0; font-size: 0.84rem; color: var(--ink-muted); }
  /* The search sits between the funnel and the queue — the order somebody
     works in: see the shape, narrow it, act. */
  .rv-find { display: flex; gap: 0.4rem; align-items: center; margin: 0.6rem 0 0.4rem; flex-wrap: wrap; }
  .rv-find input[type="search"] {
    flex: 1 1 20rem; max-width: 30rem; padding: 0.42rem 0.7rem; font-size: 0.9rem;
    border: 1px solid rgb(19 42 61 / 0.16); border-radius: 999px; background: #fff;
  }
  .rv-find input[type="search"]:focus-visible { outline: 2px solid var(--azure, #2f6f95); outline-offset: 1px; }
  .rv-clear { font-size: 0.84rem; }
  .rv-showing b { font-variant-numeric: tabular-nums; color: var(--ink); }

  .dot { display: inline-block; width: 0.45rem; height: 0.45rem; border-radius: 50%; }
  .dot--live { background: var(--azure-text); }
  /* Hollow, so "waiting" reads as unfinished rather than as a second colour
     of finished. */
  .dot--wait { background: transparent; box-shadow: inset 0 0 0 1.5px var(--saffron-text); }

  .pipe li a { text-decoration: none; color: inherit; display: block; }
  .pipe li a:hover b { color: var(--azure-text); }
  .pipe-on { background: color-mix(in oklab, var(--azure) 8%, var(--paper)); }
  .pipe em a { color: inherit; }

  /* --- The sheet ------------------------------------------------------------
     Native <dialog>, so the top layer, the focus trap, the backdrop and
     Escape all come free. The glass is the same recipe as .picker — this is
     the same gesture, summoned from a row instead of a toolbar. */
  /* The visitor's own trail on an enquiry (migration 180). A quiet list —
     it is context for the colleague replying, not the subject of the pane. */
  .cx-journey { list-style: none; margin: .35rem 0 0; padding: 0; display: grid; gap: .12rem;
                font-size: 0.85rem; color: var(--ink-muted); max-height: 11rem; overflow-y: auto; }
  .cx-journey li { display: flex; gap: .5rem; }
  .cx-jt { color: var(--ink-faint); font-variant-numeric: tabular-nums; flex: none; }

  /* ------------------------------------------------------------------
     WIZARD. A step is a URL (see stepRail), so this is all presentation:
     where you are, and what the one decision on this screen is.
     ------------------------------------------------------------------ */
  /* The rare, consequential action tucked behind a disclosure: a new password
     for a colleague who has forgotten theirs. Quiet until opened. */
  .pw-reset { margin: 0 0 0.5rem; }
  .pw-reset > summary { cursor: pointer; font-size: 0.82rem; color: var(--azure-text, #166f95); list-style: none; }
  .pw-reset > summary::-webkit-details-marker { display: none; }
  .pw-reset > summary:hover { text-decoration: underline; }
  .pw-reset form { display: grid; gap: 0.35rem; margin-top: 0.5rem; padding: 0.75rem;
                   border: 1px solid rgb(19 42 61 / 0.12); border-radius: 12px; background: rgb(19 42 61 / 0.03); }
  .pw-reset .hint { font-size: 0.78rem; }
  .pw-reset button { justify-self: start; }

  .wiz-rail { display: flex; flex-wrap: wrap; gap: 0.35rem 1.1rem; list-style: none; margin: 0 0 1.4rem; padding: 0; }
  .wiz-step { display: flex; align-items: center; gap: 0.5rem; font-size: 0.88rem; color: var(--ink-faint); }
  .wiz-step a { display: flex; align-items: center; gap: 0.5rem; color: inherit; text-decoration: none; }
  .wiz-step a:hover .wiz-l { text-decoration: underline; }
  .wiz-n {
    flex: none; width: 1.55rem; height: 1.55rem; border-radius: 50%; display: grid; place-items: center;
    font-size: 0.78rem; font-weight: 700; background: rgb(19 42 61 / 0.08); color: var(--ink-muted);
    transition: background 0.18s ease, color 0.18s ease;
  }
  .wiz-step[data-state='now'] { color: var(--ink); font-weight: 650; }
  .wiz-step[data-state='now'] .wiz-n { background: var(--azure-text, #166f95); color: #fff; box-shadow: 0 0 0 4px rgb(22 111 149 / 0.14); }
  .wiz-step[data-state='done'] { color: var(--ink-muted); }
  .wiz-step[data-state='done'] .wiz-n { background: #2eb45c; color: #fff; }

  .wiz-choices { display: grid; gap: 0.8rem; grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr)); margin: 0 0 1.2rem; }
  .wiz-choice {
    display: grid; gap: 0.25rem; padding: 1.15rem 1.2rem; border-radius: 16px; text-decoration: none; color: inherit;
    background: var(--surface, #fff); border: 1px solid rgb(19 42 61 / 0.10);
    box-shadow: 0 2px 8px rgb(15 47 69 / 0.06);
    transition: transform 0.16s ease, box-shadow 0.16s ease, border-color 0.16s ease;
  }
  .wiz-choice:hover { transform: translateY(-2px); border-color: rgb(22 111 149 / 0.4); box-shadow: 0 10px 24px rgb(15 47 69 / 0.13); }
  .wiz-choice:focus-visible { outline: 2px solid var(--azure-text, #166f95); outline-offset: 3px; }
  .wiz-glyph { font-size: 1.5rem; line-height: 1; }

  /* A FORM IS READ DOWN A COLUMN, NOT ACROSS A WINDOW. The one-guest ask had
     three fields on a flex row with no ceiling, so on a maximised window a
     name box ran the full width of the screen — a 60-character input stretched
     to 1,200px, which reads as an empty page rather than a short question.
     34rem is the measure a form wants: wide enough for an email address, near
     enough to the eye that the label and its input are one object. */
  /* THE REVIEW SHEET. It had grown by accretion: a metadata line that
     repeated four facts you could edit lower down, version history as a stack
     of bordered blocks each carrying a full-height quote, and no way to reach
     the page the review actually appears on. The quote is the thing being
     judged, so it leads and everything else gets out of its way. */
  .rv-head { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 0.2rem; }
  .rv-flag {
    font-size: 0.72rem; font-weight: 650; letter-spacing: 0.02em;
    color: #a83426; background: rgb(168 52 38 / 0.09);
    border: 1px solid rgb(168 52 38 / 0.22); border-radius: 999px; padding: 0.1rem 0.5rem;
  }
  /* WHAT THE TRAVELLER TOLD US, under her own byline (190). Quieter than
     .rv-flag, which is red because it means something is wrong; these mean
     nothing is wrong, they are just facts she volunteered. The first-solo one
     alone gets the azure pill, because that one becomes a badge on the public
     card and the moderator is approving the claim as much as the words. */
  /* Commission payable, on the Agents desk. Figures line up on the decimal —
     tabular-nums, because a column of money you have to read twice is a column
     nobody reads. 30 days is not a contractual term anywhere; it is the point
     at which an unpaid invoice stops being administration and starts being a
     phone call. */
  .pay-t { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
  .pay-t th, .pay-t td { text-align: left; padding: 0.4rem 0.5rem; border-bottom: 1px solid var(--hairline); }
  .pay-t thead th, .pay-t tfoot th {
    font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--ink-muted);
  }
  .pay-t tfoot th { border-bottom: 0; border-top: 2px solid var(--hairline); color: var(--ink); }
  .pay-t .r { text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }
  .pay-t .late { color: var(--paprika, #a83426); font-weight: 650; }

  .rv-said { display: flex; gap: 0.45rem; flex-wrap: wrap; margin: 0 0 0.6rem; }
  .rv-said span {
    font-size: 0.74rem; color: var(--ink-faint, #55707f);
    background: rgb(19 42 61 / 0.05); border: 1px solid rgb(19 42 61 / 0.1);
    border-radius: 999px; padding: 0.1rem 0.5rem;
  }
  .rv-said .rv-said-badge {
    font-weight: 650; color: var(--azure-text, #166f95);
    background: rgb(22 111 149 / 0.08); border-color: rgb(22 111 149 / 0.22);
  }

  /* Links out, sized as furniture rather than as actions — you reach for them
     after deciding, not instead of. */
  .rv-links { display: flex; gap: 0.9rem; flex-wrap: wrap; margin: 0.55rem 0 0.2rem; }
  .rv-links a { font-size: 0.8rem; color: var(--azure-text, #166f95); text-decoration: none; }
  .rv-links a:hover { text-decoration: underline; }

  /* Two short fields pair; the grid decides, so neither carries a flex weight
     that drifts when a third is added. */
  .rv-grid { display: grid; gap: 0 1rem; grid-template-columns: repeat(auto-fit, minmax(min(100%, 12rem), 1fr)); }
  .rv-opt { text-transform: none; font-weight: 500; color: var(--ink-faint); }

  /* A version is one line of metadata and at most two of quote. Five
     revisions should be scannable in the space one used to take. */
  .rv-ver { padding: 0.45rem 0 0.45rem 0.7rem; border-left: 2px solid rgb(19 42 61 / 0.14); margin-bottom: 0.35rem; }
  .rv-ver-meta { margin: 0; font-size: 0.76rem; color: var(--ink-muted); }
  .rv-ver-quote {
    margin: 0.15rem 0 0; font-style: italic; font-size: 0.86rem; color: var(--ink-muted);
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
  }

  .wiz-form { max-width: 34rem; }
  /* Two short fields pair; anything that needs room takes the full column. The
     grid does the deciding, so no field carries a flex weight of its own. */
  .wiz-pair { display: grid; gap: 0.15rem 1rem; grid-template-columns: repeat(auto-fit, minmax(min(100%, 13rem), 1fr)); }
  .wiz-form .field { margin: 0 0 0.9rem; }
  .wiz-form .field:last-of-type { margin-bottom: 0.4rem; }

  /* History, not a step. Collapsed by default and counted in its own summary,
     so the record of who was asked is one click away instead of sitting under
     the form competing with it. */
  .wiz-past { margin-top: 1.6rem; }
  .wiz-past > summary {
    cursor: pointer; font-size: 0.92rem; font-weight: 600; color: var(--ink-muted);
    padding: 0.5rem 0; list-style: none;
  }
  .wiz-past > summary::-webkit-details-marker { display: none; }
  .wiz-past > summary::before { content: '▸'; display: inline-block; width: 1rem; transition: transform 140ms ease; }
  .wiz-past[open] > summary::before { transform: rotate(90deg); }
  .wiz-past > summary:hover { color: var(--ink); }
  .wiz-title { font-weight: 700; letter-spacing: -0.01em; }
  .wiz-detail { color: var(--ink-muted); font-size: 0.88rem; }
  @media (prefers-reduced-motion: reduce) { .wiz-choice { transition: none; } .wiz-choice:hover { transform: none; } }

  .sheet {
    width: min(100%, 52rem); max-height: 86vh; padding: 0; border: 0;
    background: transparent; overflow: visible;
  }
  .sheet::backdrop { background: rgb(19 42 61 / 0.34); backdrop-filter: blur(3px); }
  .sheet[open] { animation: palette-in 0.18s var(--ease-out); }
  .sheet-body {
    max-height: 86vh; overflow-y: auto; padding: 1.4rem;
    background: rgb(255 255 255 / 0.94);
    backdrop-filter: blur(26px) saturate(1.8);
    -webkit-backdrop-filter: blur(26px) saturate(1.8);
    border: 1px solid rgb(255 255 255 / 0.95); border-radius: var(--radius-m);
    box-shadow: 0 30px 70px rgb(19 42 61 / 0.4), inset 0 1px 0 #fff;
  }
  /* --- the shared sheet's own furniture --------------------------------
     Compact on purpose: a create form is read once and filled once, so the
     fields sit in a two-column grid that collapses to one, and the footer
     puts Cancel first and the commit last — the order a dialog is read in,
     and the opposite of the order a toolbar is. */
  .sheet--narrow { width: min(100%, 34rem); }
  .sheet-title { margin: 0 0 0.15rem; font-size: 1.05rem; letter-spacing: -0.01em; }
  .sheet-intro { margin: 0 0 0.9rem; font-size: 0.85rem; color: var(--ink-muted); line-height: 1.5; max-width: 54ch; }
  .sheet-form { display: block; }
  .sheet-fields { display: grid; grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); gap: 0.7rem 0.9rem; }
  .sheet-fields label { display: flex; flex-direction: column; gap: 0.2rem;
    font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.07em;
    color: var(--ink-faint); font-weight: 600; }
  .sheet-fields input, .sheet-fields select, .sheet-fields textarea {
    font: inherit; font-size: 0.92rem; text-transform: none; letter-spacing: 0;
    color: var(--ink); padding: 0.42rem 0.55rem; border: 1px solid var(--hairline);
    border-radius: 4px; background: var(--paper); width: 100%;
  }
  .sheet-fields textarea { min-height: 5rem; resize: vertical; }
  /* A field that wants the whole row — a message, an address. */
  .sheet-fields label.wide { grid-column: 1 / -1; }
  .sheet-foot { display: flex; justify-content: flex-end; align-items: center; gap: 0.6rem;
    margin-top: 1.1rem; padding-top: 0.9rem; border-top: 1px solid var(--hairline); }
  .sheet-cancel { background: transparent; color: var(--ink-muted); border: 1px solid var(--hairline); }
  .sheet-cancel:hover { background: rgb(19 42 61 / 0.05); color: var(--ink); }

  .sheet-x { position: absolute; top: 0.55rem; right: 0.6rem; z-index: 1; margin: 0; }
  .sheet-x button {
    all: unset; cursor: pointer; width: 1.9rem; height: 1.9rem; border-radius: 50%;
    display: grid; place-items: center; font-size: 0.8rem; color: var(--ink-muted);
    background: rgb(19 42 61 / 0.06);
  }
  .sheet-x button:hover { background: rgb(19 42 61 / 0.12); color: var(--ink); }
  .sheet-x button:focus-visible { outline: 2px solid var(--azure-text); outline-offset: 2px; }

  @media (prefers-reduced-motion: reduce) {
    .sheet[open] { animation: none; }
    .rv-row { transition: none; }
  }

  /* A phone gets the sheet full height and the table's quieter columns off. */
  /* The quote is the point, so the quiet columns go before it does. Stars and
     the date leave first at 64rem — both are glanceable in the sheet — and
     everything but the words and the state goes at 40rem. */
  @media (max-width: 64rem) {
    .rv-stars, .rv-when { display: none; }
  }
  @media (max-width: 46rem) {
    .rv-media { display: none; }
    .rv-quote { width: auto; }
    .sheet { width: 100%; max-height: 94vh; }
    .sheet-body { max-height: 94vh; padding: 1rem; }
  }

  /* --- Guest media, one decision each (reviews.ts) ---------------------------
     Was a bare row of thumbnails captioned "approved with the review", which
     is what migration 148 exists to undo. Each file now carries its own state
     and its own buttons, and an unpublished one is visibly not on the site. */
  .media-grid {
    display: grid; grid-template-columns: repeat(auto-fill, minmax(9.5rem, 1fr));
    gap: 0.55rem; margin-top: 0.6rem;
  }
  .media-item {
    margin: 0; border: 1px solid var(--hairline); border-radius: var(--radius-m, 0.6rem);
    overflow: hidden; background: var(--paper);
  }
  .media-item figcaption { padding: 0.5rem 0.6rem; display: grid; gap: 0.4rem; }
  .media-state {
    font-size: 0.7rem; font-weight: 700; letter-spacing: 0.05em;
    text-transform: uppercase; color: var(--ink-muted);
  }
  /* Describing a picture is part of publishing it, so the field sits with the
     decision rather than behind another click. */
  .media-alt { display: flex; gap: 0.25rem; margin-top: 0.3rem; }
  .media-alt input[type="text"] {
    flex: 1 1 auto; min-width: 0; font-size: 0.76rem; padding: 0.2rem 0.4rem;
    border: 1px solid rgb(19 42 61 / 0.14); border-radius: 4px;
  }
  .media-alt button { font-size: 0.74rem; padding: 0.2rem 0.45rem; }
  /* Green would say "good"; this says "published", which is the fact. */
  .media-item.is-live { border-color: var(--azure-text); }
  .media-item.is-live .media-state { color: var(--azure-text); }
  .media-item figcaption form { display: flex; gap: 0.35rem; }
  /* Quiet, and explicitly not the house primary. These are per-file calls a
     moderator makes several times in a row; the saffron-to-chilli gradient on
     a thumbnail turns a sheet with six photographs into six shouting buttons
     competing with the words above them. The state label carries the meaning,
     the button just does the thing. */
  .media-item figcaption button {
    flex: 1; padding: 0.3rem 0.4rem; font-size: 0.76rem; font-weight: 600;
    min-height: 1.9rem; white-space: nowrap; background: var(--surface);
    color: var(--ink); border: 1px solid var(--hairline); border-radius: var(--radius-s);
    box-shadow: none; background-image: none;
  }
  .media-item figcaption button:hover { background: var(--paper); border-color: var(--azure-text); color: var(--azure-text); }
  .media-item figcaption button:focus-visible { outline: 2px solid var(--azure-text); outline-offset: 1px; }

  /* --- The review funnel (reviews.ts) ----------------------------------------
     Four stages left to right, with exactly one highlighted: the one the
     operator can act on right now. The page it replaced opened on two counts
     and a sentence, which said what the page contained and never what to do. */
  .pipe {
    display: grid; grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
    gap: 0.5rem; margin: 1rem 0 0; padding: 0; list-style: none;
  }
  .pipe li {
    padding: 0.7rem 0.8rem; border-radius: var(--radius-m, 0.6rem);
    background: var(--paper); border: 1px solid var(--hairline);
  }
  .pipe b { display: block; font-size: 1.5rem; line-height: 1; font-variant-numeric: tabular-nums; }
  .pipe span { display: block; font-size: 0.78rem; color: var(--ink-muted); margin-top: 0.15rem; }
  .pipe em { display: block; margin-top: 0.4rem; font-size: 0.76rem; font-style: normal; font-weight: 650; }
  .pipe-now { border-color: var(--azure-text); box-shadow: inset 0 0 0 1px var(--azure-text); }
  .pipe-now em { color: var(--azure-text); }
  .pipe-warn em { color: var(--saffron-text); }
  .pipe-act { display: flex; flex-wrap: wrap; gap: 0.6rem 1rem; align-items: center; margin: 0.9rem 0 0; }
  .btn-lead {
    display: inline-flex; align-items: center; min-height: 2.75rem; padding: 0.55rem 1.2rem;
    border-radius: var(--radius-pill); background: var(--azure-text); color: #fff;
    font-weight: 700; text-decoration: none;
  }
  .btn-lead:hover { filter: brightness(1.08); }

  /* --- Where a review shows (reviews.ts) -------------------------------------
     Six surfaces decide where a review lands and one of the rules is
     counterintuitive: the homepage carries only reviews attached to NOTHING,
     so tagging one to a holiday takes it off the front page. That was
     invisible, and five of eleven approved reviews ended up attached to
     nothing at all. This panel is the fix; see review-routing.ts. */
  .placement {
    margin: 0.7rem 0 0; padding: 0.6rem 0.75rem;
    border-radius: var(--radius-s, 0.5rem); background: var(--surface-sink, rgb(19 42 61 / 0.04));
  }
  .placement-h {
    margin: 0 0 0.35rem; font-size: 0.72rem; font-weight: 700;
    letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-muted);
  }
  .placement ul { margin: 0; padding: 0; list-style: none; display: grid; gap: 0.2rem; }
  .placement li { display: flex; gap: 0.5rem; align-items: baseline; font-size: 0.85rem; }
  .placement-surface { font-weight: 650; min-width: 9.5rem; }
  .placement-where { color: var(--ink-muted); }
  /* Struck through rather than coloured as an error: leaving the homepage is
     the correct consequence of a correct decision, not a fault. */
  .placement-out .placement-surface,
  .placement-out .placement-where { text-decoration: line-through; opacity: 0.6; }
  .placement--none { margin: 0.7rem 0 0; font-size: 0.85rem; font-style: italic; color: var(--ink-muted); }

  /* --- The permission matrix ------------------------------------------------ */
  .matrix-card { overflow-x: auto; padding: 0.6rem 0.9rem; }
  .matrix { width: 100%; border-collapse: collapse; font-size: 0.88rem; min-width: 34rem; }
  .matrix th, .matrix td { padding: 0.45rem 0.5rem; text-align: center; border-bottom: 1px solid var(--hairline); }
  .matrix thead th { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.07em; color: var(--ink-muted); }
  .matrix tbody tr:last-child th, .matrix tbody tr:last-child td { border-bottom: 0; }
  .matrix-app { text-align: left !important; font-weight: 650; color: var(--ink); position: sticky; left: 0; background: var(--paper); }
  .matrix-superuser { background: var(--surface); }
  .matrix-level {
    min-height: 2rem; width: auto; padding: 0.15rem 1.6rem 0.15rem 0.5rem;
    font-size: 0.84rem; font-weight: 650; border-radius: var(--radius-pill);
  }
  .matrix-level--none { color: var(--ink-faint); }
  .matrix-level--read { color: var(--azure-text); border-color: color-mix(in oklab, var(--azure) 45%, var(--hairline)); }
  .matrix-level--write { color: #1c7c3c; border-color: #9ed8b1; background: #f0faf3; }
  /* A control that cannot do anything should not look like one that can. These
     rows belong to apps that check for the superuser themselves, so whatever is
     chosen here is saved and then ignored — the select stays usable (the row
     can still be cleared) but it reads as struck through rather than live. */
  .matrix-level--inert {
    opacity: 0.55;
    text-decoration: line-through;
    background: repeating-linear-gradient(
      -45deg,
      transparent, transparent 5px,
      color-mix(in srgb, var(--ink) 4%, transparent) 5px,
      color-mix(in srgb, var(--ink) 4%, transparent) 10px
    );
  }

  /* --- Audit log viewer ---------------------------------------------------- */
  .audit-row {
    display: grid; grid-template-columns: 9.5rem 9rem 1fr; gap: 0.75rem;
    align-items: baseline; padding: 0.55rem 0.25rem;
    border-bottom: 1px solid var(--hairline);
  }
  .audit-row:last-child { border-bottom: 0; }
  .audit-when { font-variant-numeric: tabular-nums; white-space: nowrap; margin: 0; }
  .audit-kind { justify-self: start; background: var(--surface-sink); color: var(--ink-muted); }
  .audit-kind--post { background: #fdecc8; color: #7a4d06; }
  .audit-kind--login { background: #e7f6ec; color: #1c5a30; }
  .audit-kind--error { background: #fbe4df; color: #7a2712; }
  .audit-kind--window, .audit-kind--dock { background: #e8f1f7; color: var(--azure-text); }
  .audit-body { min-width: 0; overflow-wrap: anywhere; }
  .audit-body strong { font-weight: 650; margin-right: 0.4rem; }
  .audit-body .hint { display: block; margin-top: 0.15rem; }
  .audit-filters select { min-width: 0; }
  @media (max-width: 46rem) {
    .audit-row { grid-template-columns: 1fr; gap: 0.15rem; }
  }
