/* ===========================================================================
   THE PLATFORM'S APP SHELL — one stylesheet, served by the hub, linked by every
   app. Ruled 22 Aug 2026 by Deepesh:

       "Keep the same ProLane logo and branding as the app hub on all apps.
        Apps should not decide on the header branding, the hub does."

   Before this, each app rendered its own header markup with its own CSS. That is
   four places to change a wordmark, four button styles that drift, and — the
   reason it is a rule rather than a preference — an app deciding how the
   PLATFORM identifies itself to a customer. Brand is already externalised for
   the trademark process (ADR 0003/AD-81) and realm-scoped (A18): a realm swaps
   its brand pack and every surface must follow. That is only true if the header
   comes from one place.

   Consumed at the app's OWN origin (`/brand/app-shell.css`), which each app
   already proxies from the platform — so no CORS, no cross-origin stylesheet,
   and no cache that outlives a brand publish.

   CONTAINS NO PRODUCT NAME AND NO ASSET PATH. Marks come from `/api/brand` at
   runtime; this file is layout and treatment only. AD-84 holds: nothing here
   needs to change when the platform is renamed.
   =========================================================================== */

/* THE TOKENS ARE THE HUB'S, VALUE FOR VALUE — copied from `web/app.css`, which
   implements the TTM App Hub UX design handoff. They are NOT re-derived here:
   an approximate second palette is a second design system wearing the first
   one's name, and that is the defect this file exists to remove.
   `--hub-*` aliases follow each token so the hub's own stylesheet is governed
   by this file rather than by a duplicate of the same numbers. */

/* THE `hidden` ATTRIBUTE MUST WIN. The UA stylesheet gives `[hidden]` a
   specificity of (0,1,0), so ANY author rule with two classes silently defeats
   it and the element stays on screen with no error and no failing test.

   This is not hypothetical. On 24 Aug 2026 the sign-in page shipped
   `<div class="err" id="pwerr" hidden>` against `.field .err { display: flex }`
   in the hub's app.css — (0,2,0) beats (0,1,0) — so EVERY visitor was told
   "Email or password is incorrect" before typing anything. A live sign-in
   screen accusing the customer of a mistake they have not made yet.

   `!important` is the right tool here and one of the few places it is: `hidden`
   is a statement about whether the element exists to the user, and no layout
   rule should be able to overrule it. Placed in the shell so it holds for every
   app, not just the one where it was found. */
[hidden] { display: none !important; }

:root {
  /* =================================================================== BRAND
     Identity colours. `--tsnz-cone` is the MARK, and only the mark. */
  --tsnz-ink:            #242423;
  --tsnz-navy:           #0E2A47;
  --tsnz-navy-hover:     #1B3D61;
  --tsnz-navy-pressed:   #081D33;
  --tsnz-cone:           #FF7400;   /* LOGO MARK ONLY — never UI, never text */

  /* ================================================================== ACTION
     THE ACTION FILL, separate from `--tsnz-navy` ON PURPOSE.
     `--tsnz-navy` is a TEXT colour in dark mode (it lightens), so a
     primary button using it as a FILL rendered near-white text on a
     near-white fill — one token doing two opposite jobs. A fill and
     the ink on it always travel as a pair.

     NAVY. Strata's sapphire #3E5FDB was adopted on 24 Aug 2026 and
     REVERSED the same day by Deepesh, who preferred ProLane's navy for
     controls. The navy fill and the navy wordmark being the same colour
     is the point, not an accident: this is a compliance tool, and a
     control that looks like the brand reads as part of the document
     rather than as decoration.

     What did NOT revert with it is the FOCUS ring — see §FOCUS below.
     The old focus #35648F measures 2.34:1 against this fill, so
     "revert everything" would have shipped a focus indicator that fails
     WCAG on the primary button. The ring stays in the brighter family.

     Full four-state ramp, and the disabled pair is a TOKEN rather than
     an opacity — a translucent control over a coloured row picks up the
     row, which is how "disabled" starts looking enabled. */
  --tsnz-action:            #0E2A47;
  --tsnz-action-hover:      #1B3D61;
  --tsnz-action-pressed:    #081D33;
  --tsnz-action-disabled:   #DEE5E8;
  --tsnz-on-action:         #FFFFFF;
  --tsnz-on-action-disabled:#71848E;
  --tsnz-selected-bg:       #E9EEF4;
  --tsnz-selected-fg:       #0E2A47;
  --tsnz-selected-border:   #0E2A47;
  --tsnz-link:              #1B3D61;
  --tsnz-link-hover:        #081D33;

  /* ================================================================== GROUND */
  --tsnz-canvas:         #F5F7F9;
  --tsnz-surface:        #FFFFFF;
  --tsnz-surface-sunken: #EEF1F5;
  --tsnz-line:           #DFE5EC;
  --tsnz-line-strong:    #A8B6C6;   /* slate-300 */
  --tsnz-muted:          #6B7280;   /* slate-600 */
  --tsnz-ink-2:          #6B7280;
  --tsnz-locked-surface: #F5F7F9;
  --tsnz-locked-fg:      #6B7280;
  --tsnz-requested-surface: #E9EEF4;
  --tsnz-requested-fg:   #0E2A47;

  /* ================================================================== STATUS
     FIVE families, each a background / border / foreground TRIPLE, because a
     status colour is never used alone — it is a tint with text on it, and a
     foreground picked separately from its background is how a badge ends up
     at 2.1:1. Every foreground here clears 4.5:1 on its own background in
     both themes.

     There is no orange family. The cone is the mark; warning is amber, and
     an amber tint means "caution, act soon", never "look here". Decoration
     that needs attention is a job for hierarchy, not for a colour.

     `--tsnz-danger` (no suffix) is the ACTION fill, kept under its historic
     name so every existing `.btn.danger` keeps working. It moved
     #B42318 -> #C52E3B with the Strata merge. */
  --tsnz-success-bg:     #ECFDF5;
  --tsnz-success-border: #05835F;
  --tsnz-success-fg:     #066448;
  --tsnz-warning-bg:     #FFFAEB;
  --tsnz-warning-border: #A96600;
  --tsnz-warning-fg:     #7A4900;
  --tsnz-danger-bg:      #FFF1F2;
  --tsnz-danger-border:  #C52E3B;
  --tsnz-danger-fg:      #9F2330;
  --tsnz-info-bg:        #EAFBFF;
  --tsnz-info-border:    #087E99;
  --tsnz-info-fg:        #07627A;
  --tsnz-neutral-bg:     #EEF2F4;
  --tsnz-neutral-border: #71848E;
  --tsnz-neutral-fg:     #3A4C55;
  --tsnz-danger:         #C52E3B;   /* action fill */
  --tsnz-danger-hover:   #9F2330;
  --tsnz-danger-pressed: #53151D;
  --tsnz-on-danger:      #FFFFFF;

  /* ==================================================================== TYPE
     Nine steps, each a size PAIRED WITH ITS LINE HEIGHT. A size chosen
     without its leading is the other half of the same drift.
     Default body is 14/20. 13/18 is `body-small` and is legitimate for dense
     rows and metadata; 11px is the floor and is reserved for non-critical
     metadata only. Nothing in a UI is smaller than that.
     (The one exemption is the 6.8px endorsement line inside the wordmark
     lockup — that is logotype artwork, not UI text. See §Brand below.) */
  --tsnz-fs-caption:       11px; --tsnz-lh-caption:       16px;
  --tsnz-fs-label:         12px; --tsnz-lh-label:         16px;
  --tsnz-fs-body-small:    13px; --tsnz-lh-body-small:    18px;
  --tsnz-fs-body:          14px; --tsnz-lh-body:          20px;
  --tsnz-fs-body-large:    16px; --tsnz-lh-body-large:    24px;
  --tsnz-fs-heading-sm:    18px; --tsnz-lh-heading-sm:    24px;
  --tsnz-fs-heading-md:    22px; --tsnz-lh-heading-md:    28px;
  --tsnz-fs-heading-lg:    28px; --tsnz-lh-heading-lg:    36px;
  --tsnz-fs-display:       32px; --tsnz-lh-display:       40px;
  --tsnz-fw-regular: 400; --tsnz-fw-medium: 500;
  --tsnz-fw-semibold: 600; --tsnz-fw-bold: 700;
  --tsnz-ls-tight:  -0.3px;
  --tsnz-ls-normal:  0px;
  --tsnz-ls-label:   0.1px;
  --tsnz-ls-caps:    0.6px;
  --tsnz-font-brand:     'Archivo', 'Inter', sans-serif;
  --tsnz-font-ui:        'Inter', 'Segoe UI', sans-serif;
  --tsnz-font-mono:      'JetBrains Mono', ui-monospace, monospace;

  /* ================================================================== SPACE
     A 4px grid. Every margin, padding and gap is one of these — a one-off
     `padding: 13px` is the smallest possible unit of design drift and the
     hardest to find later. */
  --tsnz-space-0:  0px;   --tsnz-space-1:  4px;   --tsnz-space-2:  8px;
  --tsnz-space-3:  12px;  --tsnz-space-4:  16px;  --tsnz-space-5:  20px;
  --tsnz-space-6:  24px;  --tsnz-space-8:  32px;  --tsnz-space-10: 40px;
  --tsnz-space-12: 48px;  --tsnz-space-16: 64px;  --tsnz-space-20: 80px;
  --tsnz-space-24: 96px;

  /* ================================================================= SHAPE */
  --tsnz-radius-xs:      2px;
  --tsnz-radius-sm:      4px;
  --tsnz-radius:         8px;       /* radius-control — the default */
  --tsnz-radius-modal:   12px;
  --tsnz-radius-xl:      16px;
  --tsnz-radius-round:   999px;

  /* =============================================================== CONTROLS
     Five heights, one per density intent. A control is one of these or it is
     a bug; `--tsnz-control-h` is what components read, and DENSITY (below)
     is the only thing that reassigns it. 44px is the accessible touch
     minimum — anything below it must not be the only way to do a thing. */
  --tsnz-control-h-compact:      32px;
  --tsnz-control-h-standard:     40px;
  --tsnz-control-h-comfortable:  44px;
  --tsnz-control-h-field:        48px;
  --tsnz-control-h-field-primary:52px;
  --tsnz-control-h: var(--tsnz-control-h-standard);
  --tsnz-header-h:       56px;      /* topbar-height */

  /* =================================================================== ICON
     One weight, one family, sized from this scale. 20px is the standard
     inline size; 16px is the compact one. Icons never carry meaning alone. */
  --tsnz-icon-xs: 14px; --tsnz-icon-sm: 16px; --tsnz-icon-md: 20px;
  --tsnz-icon-lg: 24px; --tsnz-icon-xl: 32px;

  /* ================================================================== FOCUS
     3px ring, 2px offset. The halo is painted in `--tsnz-surface` so the ring
     stays visible on a coloured fill as well as on a plain ground — the case
     a single-colour outline fails.

     THE FOCUS RING IS DELIBERATELY BRIGHTER THAN THE BRAND, and it did NOT
     revert when the action went back to navy on 24 Aug 2026. Measured:

         old focus #35648F  vs navy action #0E2A47  =  2.34:1   FAILS
         this      #5475F5  vs navy action #0E2A47  =  3.64:1   passes

     A focus indicator drawn in the brand's own hue disappears into the
     brand's own button. Focus is the one colour on the platform whose job
     is to NOT belong to the palette around it — it marks where the keyboard
     is, on any surface, including a filled control. Reverting it for visual
     tidiness would fail WCAG 2.4.13 on the primary button specifically. */
  --tsnz-focus:          #5475F5;
  --tsnz-focus-width:    3px;
  --tsnz-focus-offset:   2px;
  --tsnz-focus-ring:     0 0 0 2px var(--tsnz-surface), 0 0 0 5px var(--tsnz-focus);

  /* ================================================================= MOTION */
  --tsnz-motion-fast:      120ms;
  --tsnz-motion-standard:  180ms;
  --tsnz-motion-deliberate:240ms;
  --tsnz-motion-complex:   320ms;
  --tsnz-motion-ease:    cubic-bezier(0.2, 0, 0, 1);
  --tsnz-motion-enter:   cubic-bezier(0, 0, 0, 1);
  --tsnz-motion-exit:    cubic-bezier(0.3, 0, 1, 1);
  --tsnz-motion:         120ms cubic-bezier(0.2, 0, 0, 1);

  /* ============================================================ DEPTH/LAYER */
  --tsnz-z-base:    0;
  --tsnz-z-sticky:  100;
  --tsnz-z-popover: 400;
  --tsnz-z-modal:   800;
  --tsnz-z-toast:   900;
  --tsnz-opacity-disabled: 0.48;
  --tsnz-opacity-scrim:    0.64;
  --tsnz-opacity-subtle:   0.12;

  /* ================================================================ MEASURE
     HOW WIDE CONTENT IS ALLOWED TO GET. Three, because the constraint differs
     by what is IN the column, not by which app or route is showing it.

     `prose` 640 — one reading column, forms, the wizard. Minus padding that is
       ~608px, and at 16px Inter (~7.9px average advance) about 77 characters:
       the top of the comfortable 45-75 band, under the 80ch ceiling where
       return-sweep errors start.

     `content` 880 — the DEFAULT for a railed page: lists, cards, detail. The
       binding constraint here is SCANNING, not reading. A work-list row sends
       the eye from a road name at the left edge to a status chip at the right;
       past roughly 20 degrees of visual angle the return sweep misses and the
       reader loses their row. 1132px of row on a wide display is well past it.
       880 also composes: at 1440, rail 224 + 880 + 48 padding leaves a real
       margin rather than a full-bleed page.

     `wide` 1180 — genuinely tabular data ONLY, which is what licenses it under
       the no-two-dimensional-scrolling rule. Earned by measurement: the seven
       columns of a records grid need ~1070px before Location and Works start
       wrapping; above ~1200 the columns only gain whitespace. */
  --tsnz-measure-prose:    640px;
  --tsnz-measure-content:  880px;
  --tsnz-measure-wide:    1180px;
  --tsnz-rail-w:           224px;

  /* ============================================================= BREAKPOINTS
     Declared so an app reads the same numbers this file does. CSS cannot use
     a custom property in a media query, so these are documentation with a
     machine-readable home rather than a mechanism. */
  --tsnz-bp-compact: 720px;
  --tsnz-bp-medium: 1120px;
  --tsnz-bp-wide:   1440px;

  /* Aliases — one definition, both vocabularies. */
  --hub-ink: var(--tsnz-ink);
  --hub-navy: var(--tsnz-navy);
  --hub-navy-hover: var(--tsnz-navy-hover);
  --hub-navy-pressed: var(--tsnz-navy-pressed);
  --hub-slate-600: var(--tsnz-muted);
  --hub-slate-300: var(--tsnz-line-strong);
  --hub-cone: var(--tsnz-cone);
  --hub-canvas: var(--tsnz-canvas);
  --hub-surface: var(--tsnz-surface);
  --hub-surface-sunken: var(--tsnz-surface-sunken);
  --hub-border: var(--tsnz-line);
  --hub-border-strong: var(--tsnz-line-strong);
  --hub-danger: var(--tsnz-danger);
  --hub-radius-control: var(--tsnz-radius);
  --hub-radius-modal: var(--tsnz-radius-modal);
  --hub-topbar-height: var(--tsnz-header-h);
  --hub-focus-ring: var(--tsnz-focus-ring);
  --hub-font-brand: var(--tsnz-font-brand);
  --hub-font-ui: var(--tsnz-font-ui);
  --hub-font-mono: var(--tsnz-font-mono);

  /* Which wordmark is shown. Flipped by the dark blocks below; the assets
     themselves are emitted in pairs by the renderer. */
  --tsnz-wm-light-display: block;
  --tsnz-wm-dark-display:  none;
}

/* ------------------------------------------------------------- density ---
   DENSITY IS A DIAL, NOT A REDESIGN. It reassigns exactly one token —
   `--tsnz-control-h` — and nothing else. Type size, spacing and radius do
   NOT change with density: shrinking the text is how a "compact" mode
   becomes an accessibility failure, and every density mode here still
   renders 14px body.

   `comfortable` exists for gloved, outdoor, one-handed use: 44px is the
   accessible touch minimum, so a field surface should set it rather than
   inherit the desk default. Set on any ancestor:  <body data-density="comfortable">
   An app that sets nothing gets `standard`, which is what every surface
   rendered before this block existed. */
[data-density="compact"]     { --tsnz-control-h: var(--tsnz-control-h-compact); }
[data-density="standard"]    { --tsnz-control-h: var(--tsnz-control-h-standard); }
[data-density="comfortable"] { --tsnz-control-h: var(--tsnz-control-h-comfortable); }

/* ---------------------------------------------------------------- dark ---
   THREE STATES, not two. An explicit choice stamps `data-theme` on the root;
   the DEFAULT setting stamps nothing, and only `prefers-color-scheme`
   separates light from dark there. A rule written for one state leaves the
   other with a light wordmark on a dark bar — which is exactly the defect this
   block fixes.

   Every token is DEFINED in the light `:root` above and only REDEFINED here.
   A colour whose only definition lives inside a dark block is invisible in
   light mode, which is the standard way this pattern breaks. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --tsnz-ink:            #F2F4F7;
    --tsnz-ink-2:          #C3C9D4;
    --tsnz-muted:          #A8B6C6;   /* the endorsement, and it MUST lighten */
    --tsnz-line:           #2A3340;
    --tsnz-line-strong:    #3A4553;
    --tsnz-surface:        #15171B;
    --tsnz-surface-sunken: #0F1115;
    --tsnz-navy:           #E8EDF4;   /* text-on-dark, not the navy fill */
    --tsnz-navy-hover:     #FFFFFF;
    --tsnz-navy-pressed:   #D5DEEA;
    --tsnz-canvas:         #0F1115;
    --tsnz-action:            #2C4E77;
    --tsnz-action-hover:      #386191;
    --tsnz-action-pressed:    #24405F;
    --tsnz-action-disabled:   #27373F;
    --tsnz-on-action:         #FFFFFF;
    --tsnz-on-action-disabled:#71848E;
    --tsnz-selected-bg:       #1B2C40;
    --tsnz-selected-fg:       #D5DEEA;
    --tsnz-selected-border:   #386191;
    --tsnz-link:              #9DBEDF;
    --tsnz-link-hover:        #C6DCEF;
    --tsnz-focus:             #8FB6DE;
    --tsnz-success-bg:     #073D2E; --tsnz-success-border: #6EE7B7; --tsnz-success-fg: #6EE7B7;
    --tsnz-warning-bg:     #432A05; --tsnz-warning-border: #F7C568; --tsnz-warning-fg: #F7C568;
    --tsnz-danger-bg:      #53151D; --tsnz-danger-border:  #F79AA0; --tsnz-danger-fg:  #F79AA0;
    --tsnz-info-bg:        #073747; --tsnz-info-border:    #71D8EC; --tsnz-info-fg:    #71D8EC;
    --tsnz-neutral-bg:     #18252B; --tsnz-neutral-border: #9CADB5; --tsnz-neutral-fg: #C5D0D5;
    --tsnz-danger:         #F79AA0;
    --tsnz-danger-hover:   #FFE0E2;
    --tsnz-danger-pressed: #E24A56;
    --tsnz-on-danger:      #53151D;
    --tsnz-wm-light-display: none;
    --tsnz-wm-dark-display:  block;
    --hub-ink: var(--tsnz-ink);
    --hub-navy: var(--tsnz-navy);
    --hub-slate-600: var(--tsnz-muted);
    --hub-canvas: var(--tsnz-canvas);
    --hub-surface: var(--tsnz-surface);
    --hub-surface-sunken: var(--tsnz-surface-sunken);
    --hub-border: var(--tsnz-line);
    --hub-border-strong: var(--tsnz-line-strong);
  }
}

/* The toggle wins in BOTH directions — dark when chosen on a light OS, and
   light when chosen on a dark OS (that second case is what the media query
   above cannot express on its own). */
:root[data-theme="dark"] {
  --tsnz-ink:            #F2F4F7;
  --tsnz-ink-2:          #C3C9D4;
  --tsnz-muted:          #A8B6C6;
  --tsnz-line:           #2A3340;
  --tsnz-line-strong:    #3A4553;
  --tsnz-surface:        #15171B;
  --tsnz-surface-sunken: #0F1115;
  --tsnz-navy:           #E8EDF4;
  --tsnz-navy-hover:     #FFFFFF;
  --tsnz-navy-pressed:   #D5DEEA;
  --tsnz-canvas:         #0F1115;
  --tsnz-action:            #2C4E77;
  --tsnz-action-hover:      #386191;
  --tsnz-action-pressed:    #24405F;
  --tsnz-action-disabled:   #27373F;
  --tsnz-on-action:         #FFFFFF;
  --tsnz-on-action-disabled:#71848E;
  --tsnz-selected-bg:       #1B2C40;
  --tsnz-selected-fg:       #D5DEEA;
  --tsnz-selected-border:   #386191;
  --tsnz-link:              #9DBEDF;
  --tsnz-link-hover:        #C6DCEF;
  --tsnz-focus:             #8FB6DE;
  --tsnz-success-bg:     #073D2E; --tsnz-success-border: #6EE7B7; --tsnz-success-fg: #6EE7B7;
  --tsnz-warning-bg:     #432A05; --tsnz-warning-border: #F7C568; --tsnz-warning-fg: #F7C568;
  --tsnz-danger-bg:      #53151D; --tsnz-danger-border:  #F79AA0; --tsnz-danger-fg:  #F79AA0;
  --tsnz-info-bg:        #073747; --tsnz-info-border:    #71D8EC; --tsnz-info-fg:    #71D8EC;
  --tsnz-neutral-bg:     #18252B; --tsnz-neutral-border: #9CADB5; --tsnz-neutral-fg: #C5D0D5;
  --tsnz-danger:         #F79AA0;
  --tsnz-danger-hover:   #FFE0E2;
  --tsnz-danger-pressed: #E24A56;
  --tsnz-on-danger:      #53151D;
  --tsnz-wm-light-display: none;
  --tsnz-wm-dark-display:  block;
  --hub-ink: var(--tsnz-ink);
  --hub-navy: var(--tsnz-navy);
  --hub-slate-600: var(--tsnz-muted);
  --hub-canvas: var(--tsnz-canvas);
  --hub-surface: var(--tsnz-surface);
  --hub-surface-sunken: var(--tsnz-surface-sunken);
  --hub-border: var(--tsnz-line);
  --hub-border-strong: var(--tsnz-line-strong);
}

/* --------------------------------------------------------------- buttons ---
   THE UNDERLINE RULE. Every "button" that is really an anchor — and the lobby's
   Open buttons, the console's Sign out, and the app entry links all are —
   inherits `text-decoration: underline` from the user agent. It read as a link
   pretending to be a button. Killed here for every app at once, including the
   states, because `:hover` re-introduces it in several UA stylesheets.

   `:where()` keeps specificity at zero so an app can still override a single
   button deliberately, but gets the platform treatment by default. */
:where(a.btn, a.button, button, .btn, .button, [role="button"]),
:where(a.btn, a.button, button, .btn, .button, [role="button"]):hover,
:where(a.btn, a.button, button, .btn, .button, [role="button"]):focus,
:where(a.btn, a.button, button, .btn, .button, [role="button"]):active,
:where(a.btn, a.button, button, .btn, .button, [role="button"]):visited {
  text-decoration: none;
}

:where(.btn, .button, a.btn, a.button) {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--tsnz-space-2);
  height: var(--tsnz-control-h);
  padding: 0 var(--tsnz-space-4);
  border-radius: var(--tsnz-radius);
  border: 1px solid transparent;
  background: var(--tsnz-surface);
  color: var(--tsnz-navy);
  font: inherit;
  font-weight: 500;
  line-height: 1;
  cursor: pointer;
  white-space: nowrap;
  transition: background var(--tsnz-motion), border-color var(--tsnz-motion);
}

:where(.btn.primary, .button.primary) {
  background: var(--tsnz-action); color: var(--tsnz-on-action);
}
:where(.btn.primary, .button.primary):hover { background: var(--tsnz-action-hover); }
:where(.btn.primary, .button.primary):active { background: var(--tsnz-action-pressed); }

:where(.btn.secondary, .button.secondary) {
  background: var(--tsnz-surface);
  border-color: var(--tsnz-line-strong);
  color: var(--tsnz-navy);
}
:where(.btn.secondary, .button.secondary):hover { background: var(--tsnz-surface-sunken); }

:where(.btn.ghost, .button.ghost) { background: none; color: var(--tsnz-navy); }
:where(.btn.ghost, .button.ghost):hover { background: var(--tsnz-surface-sunken); }

:where(.btn.danger, .button.danger) {
  background: var(--tsnz-danger); color: var(--tsnz-on-danger);
}
:where(.btn.danger, .button.danger):hover { background: var(--tsnz-danger-hover); }
:where(.btn.danger, .button.danger):active { background: var(--tsnz-danger-pressed); }

/* DISABLED IS A COLOUR PAIR, NOT AN OPACITY. A translucent control over a
   coloured row picks the row up and starts reading as enabled again. */
:where(.btn, .button):disabled,
:where(.btn, .button)[aria-disabled="true"] {
  background: var(--tsnz-action-disabled);
  color: var(--tsnz-on-action-disabled);
  border-color: transparent;
  cursor: default;
}

/* Keyboard focus stays visible everywhere — WCAG 2.2 AA (2.4.11 Focus Not
   Obscured, 2.4.13 Focus Appearance), and the one thing a
   `text-decoration: none` sweep is most likely to quietly remove.

   3px at 2px offset, in the focus colour. It used to be 2px of `--tsnz-accent`,
   which was the CONE — an orange ring, on a platform whose own rule says the
   cone is the mark and never UI. That token is gone.

   TWO LAYERS, ON PURPOSE, and this is the part that is easy to get wrong.
   The `box-shadow` paints the 2px offset gap in `--tsnz-surface` first, so the
   ring is always separated by a light band from whatever sits behind it —
   a plain ground (4.01:1), a filled control, or a coloured table row alike.
   The ring's own colour cannot be chosen against all three at once, so the
   halo is what makes the guarantee rather than the hue.

   This is not hypothetical. Under the short-lived sapphire palette the ring
   measured 1.36:1 against its own button and the halo was the only thing
   holding SC 2.4.13; under the restored navy it is 3.64:1 and the halo is
   what keeps it true over a navy row or a dark card. Either way, a
   single-colour outline is the standard way focus fails on exactly the most
   important button on the page.

   The `outline` is kept rather than replaced by the shadow because forced-
   colors / Windows High Contrast drops box-shadow and honours outline. */
:where(a, button, .btn, .button, [role="button"], [tabindex]):focus-visible {
  outline: var(--tsnz-focus-width) solid var(--tsnz-focus);
  outline-offset: var(--tsnz-focus-offset);
  box-shadow: 0 0 0 var(--tsnz-focus-offset) var(--tsnz-surface);
}

/* ============================================================ ACCOUNT MENU

   THE ONE WAY OUT, and it is the same shape on every surface. Opened by
   `[data-tsnz-account]`, built by app-shell.js, appended to `<body>` so no
   app's `overflow` or stacking context can clip it.

   SIGN OUT IS THE LAST ROW OF THIS MENU AND IS NEVER A HEADER BUTTON. It ends
   a session someone may be mid-task in, and a bare button beside an avatar is
   permanently one mis-tap away. */
.tsnz-menu {
  position: fixed; z-index: var(--tsnz-z-popover);
  min-width: 240px; max-width: min(320px, calc(100vw - 16px));
  background: var(--tsnz-surface);
  border: 1px solid var(--tsnz-line);
  border-radius: var(--tsnz-radius);
  box-shadow: 0 8px 24px rgb(0 0 0 / 0.16);
  padding: var(--tsnz-space-2);
}
.tsnz-menu-who { padding: var(--tsnz-space-2) var(--tsnz-space-3) var(--tsnz-space-1); }
.tsnz-menu-name {
  font: var(--tsnz-fw-semibold) var(--tsnz-fs-body)/var(--tsnz-lh-body) var(--tsnz-font-ui);
  color: var(--tsnz-ink);
}
.tsnz-menu-email {
  font: var(--tsnz-fw-regular) var(--tsnz-fs-body-small)/var(--tsnz-lh-body-small) var(--tsnz-font-mono);
  color: var(--tsnz-muted); word-break: break-all;
}
.tsnz-menu-role {
  margin-top: 2px; color: var(--tsnz-muted);
  font: var(--tsnz-fw-regular) var(--tsnz-fs-caption)/var(--tsnz-lh-caption) var(--tsnz-font-ui);
}
.tsnz-menu hr {
  border: 0; border-top: 1px solid var(--tsnz-line);
  margin: var(--tsnz-space-2) 0;
}
.tsnz-menu-row {
  display: flex; align-items: center; gap: var(--tsnz-space-2);
  min-height: var(--tsnz-control-h-comfortable);
  padding: 0 var(--tsnz-space-3);
  border-radius: var(--tsnz-radius-sm);
  color: var(--tsnz-ink); text-decoration: none;
  font: var(--tsnz-fw-medium) var(--tsnz-fs-body)/var(--tsnz-lh-body) var(--tsnz-font-ui);
}
.tsnz-menu-row:hover { background: var(--tsnz-surface-sunken); }
.tsnz-account[aria-expanded="true"] { border-color: var(--tsnz-line-strong); }

/* ==================================================================== MEASURE

   Applied with `data-measure` on the content container. `content` is the
   default and needs no attribute. Prose stays prose even inside a wider
   container — a paragraph in an 1180px table page is still a paragraph. */
[data-measure] { width: 100%; margin-inline: auto; }
[data-measure="prose"]   { max-width: var(--tsnz-measure-prose); }
[data-measure="content"] { max-width: var(--tsnz-measure-content); }
[data-measure="wide"]    { max-width: var(--tsnz-measure-wide); }
:where([data-measure]) :where(p, .hint-text, .tsnz-page-meta) {
  max-width: var(--tsnz-measure-prose);
}

/* A BLOCK CONTROL IS A FIELD CONTROL, and this is the rule that keeps it one.
   `.btn-block` at 100% width and `.btn-lg` at 52px are correct on a 390px
   phone held at arm's length on a work site. Dropped into a desk page they
   become a 1100px navy slab that outweighs everything on the screen — which
   is exactly what shipped on 24 Aug 2026 when a railed shell stopped applying
   a measure to its content.

   Fixing the one instance would have left the trap. Above the compact
   breakpoint, inside a content or wide measure, a block control degrades to an
   ordinary button. A future `btn-block` dropped into a desk page cannot
   produce the same defect. */
@media (min-width: 720px) {
  :where([data-measure="content"], [data-measure="wide"])
    :where(.btn.block, .btn-block, .btn.full) {
    width: auto;
    display: inline-flex;
  }
  :where([data-measure="content"], [data-measure="wide"]) :where(.btn-lg) {
    height: var(--tsnz-control-h);
    min-height: var(--tsnz-control-h);
    font-size: var(--tsnz-fs-body);
  }
}

/* ================================================================ PAGE HEAD

   Title, optional orienting meta, right-aligned actions. Four apps were each
   rendering a bare `<h1>` with its own inline size and weight — 22/750 inline
   in one, 600 22px in another, 18px in a third, 28px in a fourth.

   THE COUNT BELONGS HERE, not under the list. It is orienting information, so
   it goes where reading starts; below a long list it is never seen. */
.tsnz-page-head {
  display: flex; align-items: flex-start; flex-wrap: wrap;
  gap: var(--tsnz-space-3) var(--tsnz-space-4);
  margin-bottom: var(--tsnz-space-5);
}
.tsnz-page-titles { flex: 1 1 auto; min-width: 0; }
.tsnz-page-head h1 {
  margin: 0;
  font: var(--tsnz-fw-semibold) var(--tsnz-fs-heading-md)/var(--tsnz-lh-heading-md) var(--tsnz-font-ui);
  letter-spacing: var(--tsnz-ls-tight);
  color: var(--tsnz-navy);
}
.tsnz-page-meta {
  margin: var(--tsnz-space-1) 0 0;
  color: var(--tsnz-muted);
  font: var(--tsnz-fw-regular) var(--tsnz-fs-body-small)/var(--tsnz-lh-body-small) var(--tsnz-font-ui);
}
/* The primary action is the LAST child: reading runs left to right, so the
   strongest control ends the row nearest the edge the eye returns to. */
.tsnz-page-actions {
  display: flex; align-items: center; gap: var(--tsnz-space-2);
  margin-left: auto; flex: 0 0 auto;
}
@media (max-width: 719px) {
  .tsnz-page-actions { margin-left: 0; width: 100%; }
}

/* =================================================================== RAIL

   A LIST OF DESTINATIONS. Declared a platform component on 24 Aug 2026 and
   implemented twice in two repos on the same day — rule 0 for a component
   rather than for a value. Defined here once; `.rail*` are kept as aliases so
   existing markup does not churn.

   NO PRIMARY ACTION LIVES IN IT. A filled button is the only filled surface in
   a 224px column, so it outweighs the page's actual primary action sitting
   ~900px to its right, and it reads as a fifth destination someone styled
   differently. Where an app genuinely needs a persistent create affordance
   here it is `.btn.secondary` inside `.tsnz-rail-action`, which carries a
   real divider — a gap alone says "a destination with more air". */
.tsnz-rail, .rail {
  display: flex; flex-direction: column; align-items: stretch;
  gap: var(--tsnz-space-1);
  padding: var(--tsnz-space-4) var(--tsnz-space-3);
  background: var(--tsnz-surface);
}
.tsnz-rail-item, .rail-item {
  display: flex; align-items: center; gap: var(--tsnz-space-2);
  min-height: var(--tsnz-control-h-comfortable);
  padding: 0 var(--tsnz-space-3);
  border: 0; border-radius: var(--tsnz-radius);
  background: none; color: var(--tsnz-ink-2);
  font: var(--tsnz-fw-medium) var(--tsnz-fs-body)/var(--tsnz-lh-body) var(--tsnz-font-ui);
  text-align: left; cursor: pointer; text-decoration: none;
  transition: background var(--tsnz-motion);
}
.tsnz-rail-item:hover, .rail-item:hover {
  background: var(--tsnz-surface-sunken); color: var(--tsnz-ink);
}
/* Selected carries FILL + INK + WEIGHT + an inset leading bar. Weight survives
   forced-colors where the fill may not; `aria-current` is what reaches a
   screen reader. Never colour alone. A full 1px border — which is what shipped
   — makes one row of a vertical list read as a button among non-buttons. */
.tsnz-rail-item[aria-current="page"], .rail-item[aria-current="page"] {
  background: var(--tsnz-selected-bg);
  color: var(--tsnz-selected-fg);
  font-weight: var(--tsnz-fw-semibold);
  border-color: transparent;
  box-shadow: inset 3px 0 0 var(--tsnz-selected-border);
}
.tsnz-rail-label, .rail-label { flex: 1 1 auto; }
.tsnz-rail-action {
  margin: 0 0 var(--tsnz-space-3);
  padding-bottom: var(--tsnz-space-3);
  border-bottom: 1px solid var(--tsnz-line);
}
.tsnz-rail-action .btn { width: 100%; justify-content: flex-start; }
.tsnz-rail-count, .rail-count {
  margin-left: auto; flex: none;
  min-width: 20px; height: 20px; padding: 0 6px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--tsnz-radius-round);
  background: var(--tsnz-neutral-bg); color: var(--tsnz-neutral-fg);
  font: var(--tsnz-fw-semibold) var(--tsnz-fs-caption)/1 var(--tsnz-font-ui);
  font-variant-numeric: tabular-nums;
}
/* An EXCEPTION count, not a quantity — work that is waiting on someone. */
.tsnz-rail-count.attention, .rail-count.attention {
  background: var(--tsnz-warning-bg); color: var(--tsnz-warning-fg);
}

/* ----------------------------------------------------------------- status ---
   ONE badge, five families. The whole point of the triple is that a caller
   picks a FAMILY, never a colour — `<span class="tsnz-status warning">`, and
   the foreground that clears 4.5:1 on that background comes with it.

   The dot is decorative. Colour is never the only carrier of meaning
   (WCAG 1.4.1), so the label is required — a bare dot is not a status. */
.tsnz-status {
  display: inline-flex; align-items: center; gap: var(--tsnz-space-2);
  height: 28px; padding: 0 var(--tsnz-space-3);
  border-radius: var(--tsnz-radius-sm);
  font: var(--tsnz-fw-medium) var(--tsnz-fs-label)/var(--tsnz-lh-label) var(--tsnz-font-ui);
  letter-spacing: var(--tsnz-ls-label);
  white-space: nowrap;
}
.tsnz-status::before {
  content: ""; width: 6px; height: 6px; border-radius: var(--tsnz-radius-round);
  background: currentColor; flex: none;
}
.tsnz-status.success { background: var(--tsnz-success-bg); color: var(--tsnz-success-fg); }
.tsnz-status.warning { background: var(--tsnz-warning-bg); color: var(--tsnz-warning-fg); }
.tsnz-status.danger  { background: var(--tsnz-danger-bg);  color: var(--tsnz-danger-fg); }
.tsnz-status.info    { background: var(--tsnz-info-bg);    color: var(--tsnz-info-fg); }
.tsnz-status.neutral,
.tsnz-status         { background: var(--tsnz-neutral-bg); color: var(--tsnz-neutral-fg); }

/* ------------------------------------------------------------------ links ---
   Underlined by default. A link identified by colour alone fails WCAG 1.4.1
   for anyone who cannot separate it from the body text around it. */
:where(a):not(.btn, .button, .tsnz-brand, [role="button"]) {
  color: var(--tsnz-link);
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
}
:where(a):not(.btn, .button, .tsnz-brand, [role="button"]):hover {
  color: var(--tsnz-link-hover);
}

/* ---------------------------------------------------------------- header ---
   ONE header for the platform and every app. The app contributes its TITLE and
   its right-hand actions; it contributes no marks, no colours and no layout.

   Markup contract:
     <header class="tsnz-header">
       <a class="tsnz-brand" href="{platform}"><img alt=""></a>   <- from /api/brand
       <span class="tsnz-app-title">{app name}</span>              <- app's only say
       <div class="tsnz-header-actions"> … </div>
     </header>                                                                */
.tsnz-header,
.topbar {
  display: flex;
  align-items: center;
  gap: 16px;
  height: var(--tsnz-header-h);
  padding: 0 20px;
  background: var(--tsnz-surface);
  border-bottom: 1px solid var(--tsnz-line);
  position: sticky;
  top: 0;
  z-index: 20;
}

/* THE BRAND SLOT — the platform's identity, rendered identically everywhere.
   These are the ruled sizes (A4, A5), and the odd numbers are load-bearing:
   wordmark 18 -> 21.6 -> 25.92 -> 31.104px (+20% three times); endorsement
   -20% once (8.5 -> 6.8px) and held there.

   `.brand-*` are the hub's original class names, kept as aliases so the hub's
   own header is governed by THIS file rather than a second copy of the same
   numbers. One definition, four surfaces. */
.tsnz-brand, .brand-slot {
  display: flex;
  align-items: center;
  text-decoration: none;
}

/* Centred on each other, not flush left: the tracked endorsement sets the wider
   box (~103px against the wordmark's ~95px), so left-aligning them reads as a
   mistake. */
.tsnz-brand-text, .brand-text {
  display: flex;
  flex-direction: column;
  align-items: center;
  line-height: 1.05;
}

/* The text rule is the no-image fallback for the same slot, so it scales with
   the image and an app with no marks yet still looks like the platform. */
.tsnz-brand-wordmark, .brand-wordmark {
  font: 700 29.376px var(--tsnz-font-brand);
  color: var(--tsnz-navy);
}
.tsnz-brand-wordmark img, .brand-wordmark img {
  height: 31.104px;
  width: auto;
  /* NO `display` HERE. It used to say `display: block`, and this selector is
     (0,1,1) while the swap rule below is (0,1,0) — so the sizing rule WON and
     forced BOTH wordmarks visible at once. `display` is the swap's business
     and belongs only where the swap is decided; the pair rules give every
     image in this slot a display value, so nothing is left unset. */
  vertical-align: middle;
}

/* The light/dark wordmark pair. Both are in the DOM; the tokens decide which is
   painted, so a theme change is instant and needs no script, no listener and no
   re-render. `.brand-wordmark img` above sets the size for both.

   DELIBERATELY NOT SCOPED TO A CONTAINER CLASS. These rules used to require a
   `.tsnz-brand-wordmark` ancestor, which meant the swap silently did nothing on
   any surface whose wordmark sat in a differently-named wrapper — and two hub
   pages had exactly that, so they painted the LIGHT wordmark on a dark ground:
   an all-but-invisible logo, which is the one element a customer notices. The
   swap is a property of the IMAGE, not of where it happens to live, so the
   class on the image is the whole selector and any surface that emits the pair
   is themed correctly with no further styling. */
.tsnz-wm-light { display: var(--tsnz-wm-light-display); }
.tsnz-wm-dark  { display: var(--tsnz-wm-dark-display); }

/* ------------------------------------------------------- the theme toggle ---
   Every surface gets one, and they all behave identically: three states in a
   cycle — system, light, dark — because a two-state toggle silently overrides
   the OS preference the first time it is touched and never gives it back.
   The chosen value is written to the root as `data-theme` and remembered; the
   absence of the attribute IS the system state. */
.tsnz-theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  padding: 0;
  border: 1px solid var(--tsnz-line);
  border-radius: var(--tsnz-radius);
  background: var(--tsnz-surface);
  color: var(--tsnz-ink-2);
  cursor: pointer;
  line-height: 1;
}
.tsnz-theme-toggle:hover { background: var(--tsnz-surface-sunken); }
.tsnz-theme-toggle svg { width: 18px; height: 18px; display: block; }

/* One icon per state; the toggle shows exactly the one that applies. */
.tsnz-theme-toggle .ic-system,
.tsnz-theme-toggle .ic-light,
.tsnz-theme-toggle .ic-dark { display: none; }
:root:not([data-theme]) .tsnz-theme-toggle .ic-system { display: block; }
:root[data-theme="light"] .tsnz-theme-toggle .ic-light { display: block; }
:root[data-theme="dark"]  .tsnz-theme-toggle .ic-dark  { display: block; }

/* ------------------------------------------------------------- page ground ---
   The body must paint an explicit token. A transparent body borrows whatever
   the host paints behind it, which in dark mode is how you get dark text on a
   dark ground — legible in neither theme. */
body {
  background: var(--tsnz-surface-sunken);
  color: var(--tsnz-ink);
  /* THE PLATFORM DEFAULT BODY TYPE, 14/20. Ruled by Deepesh 24 Aug 2026 with
     the Strata merge; it was 13/19. An app may step UP the scale (the TMD
     wizard runs at `body-large` 16/24 because it is read at arm's length in
     the field) but it may not step below `body-small` 13/18 for body copy. */
  font: var(--tsnz-fw-regular) var(--tsnz-fs-body)/var(--tsnz-lh-body) var(--tsnz-font-ui);
}

/* ------------------------------------------------------- reduced motion ---
   Honour the OS setting. Motion here is functional — it says where a thing
   came from — so it is REDUCED to a near-instant cut rather than removed
   outright: `animation: none` on a spinner leaves a frozen graphic that
   reads as a hang. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* letter-spacing appends a trailing space after the final Z, which sits inside
   the box being centred and would shove the glyphs ~1px left. A negative margin
   of exactly one track pulls the box back onto the visible text. */
.tsnz-brand-endorse, .brand-endorse {
  font: 600 6.8px var(--tsnz-font-brand);
  letter-spacing: 0.24em;
  color: var(--tsnz-muted);
  text-transform: uppercase;
  margin-top: 2px;
  margin-right: -0.24em;
  text-align: center;
  white-space: nowrap;
}

/* THE APP NAME IS A BADGE, not a heading. Ruled 22 Aug 2026 from TMP Studio's
   treatment, which reads correctly for the reason it was chosen: the platform
   wordmark is the identity and the app name is a LABEL on it. Set as a heading
   it competes with the wordmark; set as a badge it sits beside it and stays
   subordinate, which is the true relationship.

   Values are TMP Studio's, measured rather than approximated: 600/12px on
   `--tsnz-surface-sunken`, `--tsnz-muted` ink, 6px radius, 0 8px padding,
   0.01em tracking. The divider is drawn HERE, not in the app, so the
   relationship between the mark and the name stays the platform's decision. */
.tsnz-app-title {
  display: inline-flex;
  align-items: center;
  height: 24px;
  padding: 0 8px;
  border-radius: 6px;
  background: var(--tsnz-surface-sunken);
  color: var(--tsnz-muted);
  font: 600 12px/1.45 var(--tsnz-font-ui);
  letter-spacing: 0.01em;
  white-space: nowrap;
}

/* The rule between the mark and the badge — a SIBLING of the badge, never part
   of it. `app-shell.js` inserts one before every `.tsnz-app-title` it finds, so
   an app still writes only the badge and gets the separator for free.

   IT WAS A `.tsnz-app-title::before` UNTIL 24 AUG 2026, AND THAT WAS WRONG.
   The badge is `display: inline-flex`, so the pseudo-element became a flex item
   INSIDE it: a hairline sitting on the grey fill, 4px from the pill's own
   rounded edge, with 9px of the pill's width given over to a separator that is
   not part of the badge at all. The rule's whole job is to separate the mark
   from the name, and it was drawn inside the thing it was separating.

   The comment that shipped with it said the rule sat "on the badge's container
   rather than the badge itself so the gap is the header's `gap`" — which is the
   correct design, stated accurately, and then not what the selector did. A
   comment describing the intended behaviour is not a test of the actual one.

   Two consequences, both deliberate:
     - NO MARGINS HERE. The space either side is the header's own `gap`, so the
       separator breathes with whatever rhythm that header uses. Margins here
       would be that decision taken twice.
     - `--tsnz-line-strong`, not `--tsnz-line`. At 1px the lighter token all but
       disappears. A separator is decorative, so the strong line is right for
       it; the fill/ink pair rule in §2 does not apply — nothing sits on top of
       a 1px rule. */
.tsnz-app-rule {
  flex: none;
  align-self: center;
  width: 1px;
  height: 20px;
  background: var(--tsnz-line-strong);
}

.tsnz-header-actions {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-left: auto;
}
.tsnz-header-actions .who { color: var(--tsnz-muted); }

/* ------------------------------------------------------- the account chip ---
   THE STANDARD ACROSS EVERY SURFACE. Ruled 22 Aug 2026, adopted from TMP
   Studio's treatment and measured from the running app rather than
   approximated: 32px pill, hairline border, 24px round avatar, initials at
   700/10px.

   The asymmetric padding (`0 10px 0 4px`) is deliberate and the detail most
   likely to be "tidied": the avatar is a circle sitting inside a pill, so equal
   padding makes the left side read as too wide. 4px on the avatar side and 10px
   on the text side is what makes the two shapes look concentric.

   The avatar fill is `--tsnz-navy`, which means it re-colours with the theme
   along with everything else — it was already the platform navy in the app this
   came from, which is why it looked right there and nowhere else. */
.tsnz-account {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 32px;
  padding: 0 10px 0 4px;
  border: 1px solid var(--tsnz-line);
  border-radius: 999px;
  background: var(--tsnz-surface);
  color: var(--tsnz-ink);
  font: inherit;
  cursor: pointer;
  white-space: nowrap;
  transition: border-color var(--tsnz-motion), background var(--tsnz-motion);
}
.tsnz-account:hover { border-color: var(--tsnz-line-strong); }

.tsnz-account .tsnz-avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: var(--tsnz-action);
  color: var(--tsnz-on-action);
  font: 700 10px/1.45 var(--tsnz-font-ui);
  letter-spacing: .02em;
}

/* Name over role, tight. The role is the quiet half — it is context, not
   identity, and at 11px it must use `--tsnz-muted` or it competes. */
.tsnz-account .tsnz-who {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  line-height: 1.15;
  text-align: left;
}
.tsnz-account .tsnz-who b { font: 600 12px/1.25 var(--tsnz-font-ui); }
.tsnz-account .tsnz-who span {
  font: 400 11px/1.25 var(--tsnz-font-ui);
  color: var(--tsnz-muted);
}

/* The dropdown it opens. One surface, one radius, one shadow — an app that
   styles its own menu is the same drift as an app that styles its own button. */
.tsnz-menu {
  position: absolute;
  right: 0;
  min-width: 220px;
  padding: 6px;
  border: 1px solid var(--tsnz-line);
  border-radius: var(--tsnz-radius-modal);
  background: var(--tsnz-surface);
  box-shadow: 0 6px 20px rgba(0, 0, 0, .12), 0 1px 4px rgba(0, 0, 0, .08);
  z-index: 60;
}
.tsnz-menu a, .tsnz-menu button {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 8px 10px;
  border: 0;
  border-radius: 6px;
  background: none;
  color: var(--tsnz-ink);
  font: inherit;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}
.tsnz-menu a:hover, .tsnz-menu button:hover { background: var(--tsnz-surface-sunken); }
.tsnz-menu hr { margin: 6px 4px; border: 0; border-top: 1px solid var(--tsnz-line); }

@media (max-width: 640px) {
  .tsnz-app-title { font-size: 14px; }
  .tsnz-header { padding: 0 12px; gap: 12px; }
}
