/* superbot site tokens + base. One sheet for every human-facing page
   (/, /docs, /pair, /account, and the chat header). Pages keep their own
   layout CSS; anything two pages share lives here.

   Colour + brand: the duostorm kit (harness-notes/brand/duostorm/duostorm.css
   + duostorm-style-guide.html, chosen 2026-09-08) is the source for the
   surface ladder, the ink tones, the storm palette and every brand recipe
   below (.sb-tile, .sb-aurora, .sb-storm-outline, .sb-composer, the storm
   action). The storm ramp is brand chrome only: the mark's ghosts, the aura, the
   composer glow and the filled action ramp, never a surface or a text colour.
   The one solid accent everywhere else is storm-1 blue (#2b6bff).

   Scales (retrieved 2026-09-01, ledger: harness-notes/design-pass.md):
   - spacing: 4px base, the Tailwind v4 / Open Props step
   - type: 12/14/16/18/20 with paired line-heights, h1 fluid via clamp();
     UI sans (SF Pro Rounded / system-ui) for display and body, mono only
     for code, terminals, key caps and ledger figures
   - motion: Material 3 duration ladder, the expo-out curve bun/warp/zed all
     ship (cubic-bezier(.16,1,.3,1)) for entrances, M3 standard for the rest
   - focus: 2px accent outline offset 2px (8.4:1 on the page bg)
   - targets: every control ≥ 24px tall, primary ones 36px (WCAG 2.2 2.5.8);
     on a touch primary pointer every control lifts to 44px (Apple HIG 44×44pt,
     Material 48dp; retrieved 2026-09-02, ledger: harness-notes/mobile-e2e.md)
     through the --target tokens below, so no page carries its own touch rules
   - gutters: --gutter-l/--gutter-r are the page's side padding, never less
     than the device's safe-area inset (viewport-fit=cover on every page)

   Themes: the dark ladder below is the default and stays byte for byte what
   every page rendered before the light theme existed. The light ladder is the
   second block, `html[data-theme="light"]`, set by the inline prepaint before
   first paint; it overrides only what changes (surfaces, ink, accent, status,
   syntax, the provider pairs) and adds the tokens below that carry a value per
   theme (--face, --ghost-*, --shadow-*, --island-bg, --cta-*, --starfield-tint).
   `[data-theme="dark"]` re-declares the dark block on any element, so a dark
   island inside a light page (or the dark html itself) is the same ladder.
   Never theme with prefers-color-scheme: the attribute is the one switch. */

/* [data-theme="dark"] first, :root last: the beta pages' mirror test reads the
   first `:root {` block of this sheet, and a selector list ending in :root still
   starts one. A dark island (any element carrying the attribute) gets the whole
   block, so the three media tiers below name it too. */
[data-theme="dark"], :root {
  color-scheme: dark;

  /* ── surfaces + ink (duostorm neutral ladder = superbot-mobile's Dark theme, themes.ts, reached
     through duostorm.css; NOT the desktop's default palette, whose raised is #1a1a1c and line
     #1e1e21). Every value below is copied by hand; test/design-token-copies.test.mjs fails when
     one stops matching its source. ── */
  --bg: #000000;          /* canvas: mobile dark surface.canvas */
  --card: #0d0d0d;        /* sidebar / card: mobile dark surface.sidebar */
  --raised: #181818;      /* elevated: duostorm.css --elevated = mobile dark surface.elevated */
  --line: #262626;        /* duostorm.css --line = mobile dark line */
  --line-strong: #5c5c5c; /* mobile dark lineStrong (themes.ts), NOT the desktop's #3a3b40: the hairline of a pill or chip drawn on --raised. The RULE is the desktop's (superbot-desktop 239ed8f52: raised pills use the strong line; surfaces keep --line); the VALUE is mobile's */
  --fg: #ececec;          /* ink */
  --secondary: #cfcfcf;   /* the body tier: APCA Lc 77.5 on --bg, 77.2 on --card, 76.4 on --raised (floor 75) */
  --muted: #b3b3b3;       /* the content tier, mobile Dark text.muted (2026-10-19, was #9a9a9a at Lc 47.7): APCA Lc 61.2 on --bg, 60.9 on --card, 60.1 on --raised (floor 60) */

  /* ── the one solid accent: focus rings, links, chips, LEDs, selection ── */
  --accent: #4d8aff;      /* clears 4.5:1 on --card #0d0d0d (5.92:1, WCAG formula); storm-1 #2b6bff stays the brand ramp's start */
  --accent-ink: #0d0d0d;  /* 5.92:1 on the flat accent: the ink for text painted on filled accent */

  /* ── status coding, unchanged ── */
  --danger: #c96f6f; --danger-line: #5c3030; --warn: #d9b86b;

  /* ── per-theme tokens this sheet adds (each has a value in the light block below).
     Dark values reproduce what the pages paint today: no shadow, the storm ramp on the
     primary control, the white-alpha ghost ladder. A page paints a mascot face, a ghost
     ground, a card or island shadow, a CTA or the starfield through these, never a literal. ── */
  --face: #ffffff;        /* the mascot's face: desktop color.echo.face (dark = the brand white); the cyan and magenta echoes are scheme-invariant */
  /* the neutral-alpha ladder, mobile `ghost` (themes.ts): white-alpha on the dark grounds, black-alpha on paper */
  --ghost-3: rgba(255,255,255,.03); --ghost-6: rgba(255,255,255,.06); --ghost-8: rgba(255,255,255,.08);
  --ghost-12: rgba(255,255,255,.12); --ghost-14: rgba(255,255,255,.14);
  --shadow-island: none;  /* the floating header island and the menus that float with it: the dark page shows through on every side, no resting shadow */
  --shadow-card: none;    /* .sb-card */
  --island-bg: color-mix(in srgb, var(--card) 82%, transparent); /* the island's fill: translucent card over its own blur (light: opaque card) */
  --cta-bg: var(--storm-action);  /* the primary control's face, an <image>: the storm ramp (light: a solid --fg image, so every `background-image: var(--cta-bg)` still holds) */
  --cta-base: var(--accent);      /* the colour under that image (the ramp's antialiased rim); light: --fg, so no blue fringe shows under the ink */
  --cta-ink: var(--accent-ink);   /* the label on it */
  --starfield-tint: 236,236,236;  /* the starfield's "r,g,b" triple the canvas reads (ascii-bg.js data-color): the dark ink --fg #ececec */

  /* ── code syntax roles: the desktop app's palette, so code on the share page, the
     docs and the rooms panel reads in the colours it reads in the app. A COPIED
     block (no shared token package reaches the site): superbot-desktop
     packages/tokens/tokens/syntax.tokens.json (built as dist/tokens.css:327-336),
     the dark ground this sheet paints on; the light values are tokens-light/
     color.tokens.json syntax.* (built as dist/tokens.light.css:73-81), copied into
     the light block below and into frontends/rooms/hub-theme.css.
     Re-copy both when the app's tokens move; test/design-token-copies.test.mjs
     fails when a copy stops matching, and edge/test/highlight-theme.test.ts
     pins the names and the role map below. ── */
  --syntax-comment: #7d8799;
  --syntax-keyword: #c678dd;
  --syntax-string: #98c379;
  --syntax-number: #e5c07b;
  --syntax-function: #61afef;
  --syntax-variable: #e06c75;
  --syntax-operator: #56b6c2;
  --syntax-constant: #d19a66;
  --syntax-invalid: #ffffff;
  /* the shiki theme's roles (edge/src/highlight-theme.ts, a port of the app's
     packages/ui/src/lib/highlight-theme.ts): each reads its --syntax-* token;
     punctuation stays the foreground, as in the app's styles.css:915-926. A
     code box paints its own surface, so the theme's background is clear. */
  --shiki-foreground: var(--fg);
  --shiki-background: transparent;
  --shiki-token-comment: var(--syntax-comment);
  --shiki-token-keyword: var(--syntax-keyword);
  --shiki-token-string: var(--syntax-string);
  --shiki-token-number: var(--syntax-number);
  --shiki-token-function: var(--syntax-function);
  --shiki-token-variable: var(--syntax-variable);
  --shiki-token-constant: var(--syntax-constant);
  --shiki-token-operator: var(--syntax-operator);
  --shiki-token-punctuation: var(--fg);
  --shiki-token-invalid: var(--syntax-invalid);

  /* ── storm palette (brand chrome only) ── */
  --storm-0: #00e5c3; --storm-1: #2b6bff; --storm-2: #6a1fd8; --storm-3: #c026d3; --storm-4: #ff3d9a;
  --storm-gradient: linear-gradient(135deg, var(--storm-0) 0%, var(--storm-1) 25%, var(--storm-2) 50%, var(--storm-3) 75%, var(--storm-4) 100%);
  --storm-gradient-x: linear-gradient(90deg, var(--storm-0) 0%, var(--storm-1) 25%, var(--storm-2) 50%, var(--storm-3) 75%, var(--storm-4) 100%);
  --storm-action: linear-gradient(135deg, var(--storm-1) 0%, var(--storm-2) 55%, var(--storm-3) 100%);
  --action-glow: 0 6px 18px -6px rgb(43 107 255 / 0.55), 0 2px 10px -4px rgb(192 38 211 / 0.35);
  /* ── the control's borrowed aura: the kit's periods (duostorm.css:224-237),
     mirrored here because no page imports the kit. The button wakes the same
     two layers as the app tile (spectrum wheel + glint), on the same coprime
     clocks and the same settle, at a lower peak than the tile's 0.85. ── */
  --aura-blur: 26px;
  --aura-spin: 7s;
  --aura-sheen: 3.4s;
  --aura-settle: 480ms;
  --aura-foil-opacity: 0.55;
  --aura-glint-opacity: 0.35;

  /* ── composer glow ── */
  --composer-glow: var(--storm-gradient-x);
  --composer-glow-opacity: 0.38;
  --composer-glow-blur: 28px;

  /* ── aurora periods (incommensurate, so the composite never repeats) ── */
  --aurora-a: 9s; --aurora-b: 13s; --aurora-c: 17s;

  /* ── tile (the storm ring is retired 2026-10-06; --ring-width is read only by duostorm.css's flat .is-mono ring) ── */
  --tile-bg: #000000; --tile-radius: 22.5%; --ring-width: 2.73%;

  /* ── type: two families, no more ── */
  --font-ui: "SF Pro Rounded", "SF Pro Display", -apple-system, system-ui, "Segoe UI", sans-serif;
  --font-mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
  /* the one exception: the mobile app's new chat greeting is set in the system
     serif (New York on iOS), so the phone mocks that draw that screen quote it.
     The stack is the desktop kit's --font-serif (packages/tokens). Only the
     phone mocks' greeting uses it; site prose stays on the two families above. */
  --font-serif: ui-serif, "New York", "Iowan Old Style", Charter, Georgia, "Times New Roman", serif;

  /* ── two radii: 8px controls, 16px surfaces (pill + 50% are shapes, not
     values). The legacy aliases below let existing rules collapse onto
     the pair without touching every page. ── */
  --r-control: 8px; --r-surface: 16px; --r-pill: 9999px;
  --r-sm: 8px; --r-md: 8px; --r-lg: 16px; --r-xl: 16px;

  --sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px; --sp-5: 20px; --sp-6: 24px;
  --sp-8: 32px; --sp-10: 40px; --sp-12: 48px; --sp-16: 64px;
  --sp-section: clamp(48px, 8vw, 96px);

  --fs-xs: 12px; --lh-xs: 16px;
  --fs-sm: 14px; --lh-sm: 20px;
  --fs-base: 16px; --lh-base: 24px;
  --fs-lg: 18px; --lh-lg: 28px;
  --fs-xl: 20px; --lh-xl: 28px;
  --fs-h1: clamp(2rem, 1.2rem + 3.5vw, 3.25rem);
  --fs-h2: clamp(1.25rem, 1rem + 1vw, 1.5rem);
  --tracking-tight: -0.02em;
  --lh-h2: 1.2;                                 /* the step title's leading on the --fs-h2 clamp (download.html .dl-tf-title) */
  --tf-tile: calc(var(--sp-16) + var(--sp-6));  /* the TestFlight focus card's step tile, about 88px (download.html .dl-tf-tile) */
  --focus-w: 2px;                               /* the keyboard focus ring's width and offset (download.html .dl-tf-*) */

  --dur-1: 100ms; --dur-2: 150ms; --dur-3: 200ms; --dur-4: 300ms; --dur-5: 500ms;
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-std: cubic-bezier(0.2, 0, 0, 1);
  --ease-in: cubic-bezier(0.4, 0, 1, 1);

  --content: 720px;
  --container: 1120px;
  --measure: 62ch;
  /* the sticky header row = the floating island (10px top margin + the 48px
     island, border-box) + one --sp-2 breath; scroll-padding-top at :126 reads
     it so anchors clear the whole island */
  --nav-h: 58px;

  /* control heights: primary and small. A touch primary pointer lifts both
     to 44px below; a mouse keeps the tighter desktop scale. */
  --target: 36px;
  --target-sm: 28px;
  /* side padding for every page column: the spacing token, or the safe-area
     inset when the notch or home-indicator edge is wider (phones in landscape) */
  --gutter: var(--sp-6);
  --gutter-l: max(var(--gutter), env(safe-area-inset-left, 0px));
  --gutter-r: max(var(--gutter), env(safe-area-inset-right, 0px));
  /* ── hero module (#61/#54/#57) ── */
  --signin-w: 30rem;      /* the sign-in door's column cap (waitlist.css .wl); the value its old literal carried */

  /* ── sign-in surfaces (waitlist-form.js, the phone's sign-in sheet, /invite;
     edge/src/beta/pages.c1bff7a8.ts CSS_BASE mirrors every value here) ── */
  --control-h: 52px;      /* the sign-in stack's buttons and fields */
  --target-touch: 44px;   /* the touch floor as a value: phone landscape rows */
  /* brand-mandated neutrals, not accents: Apple HIG's white button on a dark
     page, and Google's dark theme (branding guidelines, 2026-07-07) with its
     Google Sans Medium 14/20 label, the G 12px in and 10px before the label */
  --apple-fill: #ffffff; --apple-ink: #000000;
  --google-fill: #131314; --google-line: #8e918f; --google-ink: #e3e3e3;
  --font-google: "Google Sans", Roboto, Arial, sans-serif;
  --google-pad: var(--sp-3); --google-gap: 10px;
  /* the hero's ask box and its one white door (hero-ask.css, 2026-10-19): Google's light-theme button kept white on the dark
     ground (branding guidelines: #ffffff fill, #1f1f1f ink, #747775 outline), its hover one step down; Rise's length (the
     desktop's send, packages/core/src/motion/feel.ts 'rise': 450 ms); the layer the hand-off's words ride over the page */
  --door-fill: #ffffff; --door-ink: #1f1f1f; --door-line: #747775; --door-hover: var(--fg);
  --rise-ms: 450ms;
  --z-handoff: 100;
  /* "Last used" (#54): the pill straddles the button's top edge, the tiny
     mascot beside it */
  --badge-h: var(--lh-sm); --mark-tiny: var(--sp-5);
  /* the done state (#57): the bare echo mark (2026-09-27, the desktop and
     mobile twins' echo restyle), the superbot.gg landing hero's own numbers
     (deploy/superbot-router/src/pages.ts .mark): the echoes rest at
     d*(3px,2px), throw to d*(7px,2px), kick to d*(5px,-1px) on the 2.8s
     `split` loop (d = -1 cyan, +1 magenta), and the whole mark floats -6px on
     a 5.6s ease-in-out loop */
  --mark-done: 96px;
  --echo-rest-x: 3px; --echo-rest-y: 2px; --echo-jump-x: 7px; --echo-kick-x: 5px; --echo-kick-y: -1px;
  --echo-split-dur: 2.8s; --echo-float-y: -6px; --echo-float-dur: 5.6s;
  /* the terms notice's quiet ink (owner, 2026-09-27: smaller, less
     prominent): a neutral a step under --muted that still clears 4.5:1 (4.9:1
     on --card, 5.3:1 on --bg), and its links' thin half-strength underline */
  --legal-ink: #808080; --legal-line: color-mix(in srgb, var(--legal-ink) 50%, transparent);
  /* ── the phone's sign-in sheet (#65) ── signin-sheet.css: the sticky CTA
     opens a <dialog>, a bottom sheet on a phone, a centred card wider up.
     --sheet-w is the card's width and the sheet's content column (the .wl
     stack's 30rem less the surface padding). --sheet-max-h is a percentage
     of the fixed containing block, not dvh: it follows the iOS toolbars and
     stays true under the 1.25 zoom tier below; 48px of page stays in view
     over the sheet, so it reads as over the page, not a new one. The scrim
     is the nav drawer's (.sb-scrim), no blur. The handle is the kit's pill.
     Entrance on --ease-out, the exit faster on --ease-in (design-motion:
     exits outpace entrances); the card scales from the .sb-btn press scale. */
  --sheet-w: 28rem;
  --sheet-max-h: calc(100% - var(--sp-12));
  --sheet-scrim: color-mix(in srgb, var(--bg) 72%, transparent);
  --sheet-handle-w: var(--sp-8); --sheet-handle-h: var(--sp-1);
  --sheet-in: var(--dur-4); --sheet-out: var(--dur-3);
  --sheet-scale-from: 0.96;

  /* ── the split install control (.sb-split, site-nav.ts): the values its group
     and its menu read, named once so the recipe and the menu share them ── */
  --storm-size: 200% 100%;  /* the storm ramp's width, twice the box, so the idle breathe has room to drift */
  --breathe: 6s;            /* the idle breathe's period (the .sb-btn recipe's own) */
  --press-scale: 0.96;      /* the press scale every .sb-btn and the split group share */
  --hairline: 1px;          /* a divider's or a menu's border */
  --focus-w: 2px;           /* the focus ring's width, and its offset (the site's :focus-visible recipe) */
  --split-line: color-mix(in srgb, var(--cta-ink) 25%, transparent);  /* the split divider: the button ink at a quarter, so it follows the synced accent */
  --split-state: color-mix(in srgb, var(--cta-ink) 10%, transparent); /* a chevron's hover and open fill: the button ink at a tenth */

  /* ── the phone tab bar (.sb-tabbar, site-nav.ts tabBar, shown at <= 600px only): the values its capsule,
     its slots, its sheet and the page's clearance read, named once so they never drift apart ── */
  --tab-slot-h: calc(var(--sp-12) + var(--sp-2));  /* a slot: --sp-2 + the 20px icon + --sp-1 + the 16px label + --sp-2 = 56px */
  --tabbar-pad: var(--sp-1);                       /* the capsule's inset around its slots */
  --tabbar-h: calc(var(--tab-slot-h) + var(--tabbar-pad) + var(--tabbar-pad) + var(--hairline) + var(--hairline)); /* the capsule's whole box: slot, inset and the two hairlines */
  --tabbar-max-w: calc(var(--sp-16) * 6);          /* its widest, 384px: wider screens keep a compact capsule */
  --tabbar-edge: max(var(--sp-2), calc((100% - var(--tabbar-max-w)) / 2)); /* the gap from a viewport side to the capsule (a fixed box's %, so left/right of the bar and the sheet's right read one value) */
  --tabbar-bottom: max(var(--sp-2), env(safe-area-inset-bottom, 0px)); /* the capsule floats this far off the bottom: the home indicator's inset when it is taller */
  --tabbar-top: calc(var(--tabbar-bottom) + var(--tabbar-h)); /* the capsule's top edge from the viewport bottom */
  --tabbar-clear: calc(var(--tabbar-top) + var(--sp-4)); /* what a page leaves under its last line so the footer is never beneath the bar */
  --tabbar-sheet-w: calc(var(--sp-16) * 3);        /* the more sheet's narrowest, 192px */
  --z-tabbar: 30;                                  /* over the page and its sticky strips (5 to 10), under the nav drawer's scrim (40) */
  --fw-tab: 500;                                   /* a tab's and a sheet row's label: 500 is for labels */
  --island-blur: 12px;                             /* the header island's own backdrop blur, the one the bar and its sheet share */
  --sb-tab-current: var(--accent);                 /* the current tab is solid superbot blue in both themes, not the storm ramp (owner, 2026-10-17; light since 2026-10-15) */
  --tab-ink: var(--accent-ink);                    /* the label and icon on the current tab's pill: #0d0d0d on the dark blue, 5.92:1 */
}

/* ── the light ladder (the second theme). Every value is a COPY of the app's light scheme:
   the neutral ladder and the status colours are superbot-mobile themes.ts `light` (warm
   paper) and superbot-desktop tokens-light/color.tokens.json, which mirrors it; the
   provider pairs are tokens-light color.provider.*; the syntax roles are tokens-light
   syntax.*. test/design-token-copies.test.mjs fails when one stops matching its source
   (a row per value there), and records the one place the two sources disagree:
   --warn is mobile light `warn` #8a6a1c, the desktop's color.warn is #806218 (a known
   divergence, pinned to mobile as the dark ladder is). What is not a copy is derived
   from a copied token: the shadows are the light ink rgb(20 19 16) at an alpha, the
   starfield tint is the --line-strong triple. The specificity (0,1,1) beats :root (0,1,0)
   whatever the sheet order, so duostorm.css loading after this sheet cannot undo it. ── */
html[data-theme="light"] {
  color-scheme: light;
  --bg: #faf8f3;          /* mobile light surface.canvas */
  --card: #ffffff;        /* mobile light surface.sidebar */
  --raised: #f0ede5;      /* mobile light surface.elevated */
  --line: #e3dfd6;        /* mobile light line */
  --line-strong: #8a8780; /* mobile light lineStrong = desktop tokens-light color.line-strong */
  --fg: #141310;          /* mobile light text.primary */
  --secondary: #3d3a33;   /* mobile light text.secondary */
  --muted: #5f5b52;       /* mobile light text.muted (6.37:1 on --bg; APCA Lc 79.2 on --bg, 83.3 on --card, 72.7 on --raised) */
  --accent: #141310;      /* mobile light accent: ink, not superbot blue (the owner, 2026-10-18: black and white on light); 18.0:1 on --bg; the focus ring, chips, selection */
  --accent-ink: #ffffff;  /* desktop color.accent-ink = mobile light accentInk: 18.6:1 on the ink accent */
  --danger: #a84f4f;      /* mobile light danger */
  --danger-line: color-mix(in srgb, var(--danger) 40%, transparent); /* derived: the dark #5c3030 is a deep red on black; on paper the border is the danger ink at 40% */
  --warn: #8a6a1c;        /* mobile light warn (the desktop's color.warn is #806218: KNOWN_DIVERGENCES) */
  --syntax-comment: #686c69;
  --syntax-keyword: #cf222e;
  --syntax-string: #0a3069;
  --syntax-number: #0550ae;
  --syntax-function: #7e4bda;
  --syntax-variable: #116329;
  --syntax-operator: #3d3a33;
  --syntax-constant: #0550ae;
  --syntax-invalid: #3d3a33;
  /* the sign-in provider buttons on a light page: tokens-light color.provider.* (Apple's black style, Google's light theme) */
  --apple-fill: #000000; --apple-ink: #ffffff;
  --google-fill: #ffffff; --google-line: #747775; --google-ink: #1f1f1f;
  --door-hover: var(--raised);
  --legal-ink: var(--muted); /* the terms notice: the dark ground's quiet #808080 is a step under --muted; on paper --muted itself is the quiet ink (6.37:1) */

  --face: #141310;        /* desktop tokens-light color.echo.face: the light fg ink, the white face would vanish on the paper */
  --ghost-3: rgba(0,0,0,.03); --ghost-6: rgba(0,0,0,.06); --ghost-8: rgba(0,0,0,.08);
  --ghost-12: rgba(0,0,0,.12); --ghost-14: rgba(0,0,0,.14);
  --shadow-island: 0 1px 2px rgb(20 19 16 / .06), 0 8px 24px rgb(20 19 16 / .08); /* ink rgb(20 19 16) = --fg */
  --shadow-card: 0 1px 2px rgb(20 19 16 / .05), 0 4px 12px rgb(20 19 16 / .05);
  --island-bg: var(--card); /* the white pill */
  --cta-bg: linear-gradient(var(--fg), var(--fg)); /* solid ink as an <image> */
  --cta-base: var(--fg);
  --cta-ink: var(--card); /* white (--card) on the solid ink: 18.58:1. Not --accent-ink: that follows the account's synced accent, and a label on the ink must stay white whatever the accent is */
  --action-glow: none;    /* no glow on a solid control */
  --aura-foil-opacity: 0; /* the screen-blended storm wheel would turn solid ink into a rainbow; the glint (a white band) stays as the hover sheen */
  --starfield-tint: 138,135,128; /* the --line-strong #8a8780 triple */
  --sb-tab-current: var(--accent); /* the current tab on paper is solid --accent, not the storm ramp (owner, 2026-10-15), and that accent is ink since 2026-10-18; a synced custom accent restates --accent */
  --tab-ink: var(--accent-ink);    /* white on the stock ink, 18.6:1; follows a synced accent's own ink */
}
@media (max-width: 480px) { [data-theme="dark"], :root { --gutter: var(--sp-4); } }

/* ---- touch: the primary pointer is a finger. Every control takes the 44px
   floor at once; fields hold 16px so iOS Safari stops zooming the page into
   them on focus; the grey tap flash goes, the :active states below stand in
   for hover, which a finger never has. ---- */
@media (pointer: coarse) {
  [data-theme="dark"], :root { --target: 44px; --target-sm: 44px; --nav-h: 62px; } /* the island grows to 52px here (below) so the 44px targets keep 4px of breathing room, and --nav-h follows: 10px top margin + 52px */
  /* !important because this is the iOS-zoom floor, not a style choice: a
     page-local shell rule (topup-page/onboarding-page/dashboard-page each
     carry `input{font-size:var(--fs-sm)}`, 0-0-1, and a class-scoped
     `.chat input`, 0-1-1) must not be able to drag a focused field under
     16px and make Safari zoom the phone into it. The shell copies of this
     rule are redundant with it (seam 4, 2026-09-17). */
  body input:not([type="hidden"]), body textarea, body select { font-size: max(16px, 1em) !important; }
  /* every <main> names the notch inset itself. A page whose own <main> carries
     a gutter (`.wrap`/`.sms`/`main#main`/`.features`, one class rule) outweighs
     this bare-element selector and keeps its padding; this is the floor for a
     page whose <main> has no horizontal padding of its own, which measured
     flush to both viewport edges (2026-09-17). Longhands because a checker
     that enumerates style[i] cannot see a var()/env() shorthand; env()
     resolves to 0px off a notch, so nothing moves on a phone without one. */
  main {
    padding-left: env(safe-area-inset-left, 0px);
    padding-right: env(safe-area-inset-right, 0px);
  }
}
/* touch-action: manipulation keeps pan and pinch and drops double-tap zoom on
   controls, so two fast taps on send or a tab never zoom the page */
a, button, summary, input, textarea, select, [role="button"], [role="tab"], [role="radio"] { -webkit-tap-highlight-color: transparent; touch-action: manipulation; }

*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
/* anchor targets land below the sticky header instead of under it */
html { scroll-padding-top: calc(var(--nav-h) + var(--sp-2)); }
body {
  background: var(--bg); color: var(--fg);
  font: var(--fs-base) / var(--lh-base) var(--font-ui);
  -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility;
  min-height: 100vh; min-height: 100dvh;
  /* the lander's original crosshair (owner call 2026-09-01: restored after
     the token-sheet pass cut it), scoped to chrome by the reset below.
     Controls keep their own cursor:pointer. */
  cursor: crosshair;
}
/* crosshair is chrome-only: prose, log lines, code and fields take the
   reading cursors back — the page-wide cursor killed the text I-beam. Links
   and buttons keep their UA / per-component pointers. */
p, li, dt, dd, blockquote, pre, code, kbd, figcaption, td, th,
input, textarea, select, label { cursor: auto; }
a { color: inherit; }
button, input, textarea, select { font: inherit; color: inherit; }
img, svg, canvas { display: block; max-width: 100%; }
h1, h2, h3 { font-weight: 650; letter-spacing: var(--tracking-tight); text-wrap: balance; }
p { text-wrap: pretty; }
/* mono is a content font, not a body font: code, key caps, terminal stages
   and figures carry it (pages scope their own .mono / .term rules) */
code, kbd, pre { font-family: var(--font-mono); }
/* the 0.94em step under a 12px parent (download's sha256 chips, 2026-09-17)
   resolved to 11.28px — under the iOS 11pt floor. The step keeps its intent
   on larger parents and floors at the caption size everywhere else. */
code, kbd { font-size: max(12px, 0.94em); }
/* the accent pair at full strength (the token block names selection a solid
   accent consumer): ink on solid accent is 5.92:1, where the dark ink over the
   old 35% tint was ~1.7:1 */
::selection { background: var(--accent); color: var(--accent-ink); }
/* code blocks get a visible scroll path of their own: a bare pre is
   white-space: pre with no overflow handling, so one long JSON line set a
   .more disclosure's content box past the 360px viewport (Q-09's residual) */
pre { overflow-x: auto; }
/* a scroller that is a tab stop (edge/src/focus-scrollers.ts: every pre, a docs copy row, a .tbl table wrapper) draws its ring INSIDE its
   own box. The card around a code block or a table usually clips (overflow: hidden), and the universal ring below sits outside the
   box, so on exactly the blocks a keyboard can reach it would be cut away. A page's own, more specific ring still wins. */
pre:focus-visible, .tbl:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* one focus ring for every control: outline follows border-radius, and
   the offset keeps it clear of the element's own border */
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* text fields already turn their border accent on focus; a soft ring beside
   it reads as one state instead of two nested boxes */
input:focus-visible, textarea:focus-visible, select:focus-visible {
  outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
}

/* cross-document view transitions: progressive enhancement, not a dependency —
   support is limited (Chrome 126, Safari 18.2, no Firefox; retrieved
   2026-09-03). Pages without it just navigate as they always did. */
@view-transition { navigation: auto; }
::view-transition-old(root), ::view-transition-new(root) { animation-duration: var(--dur-3); }

/* ---- skip link: the first Tab on any page with a header jumps past it
   (WCAG 2.4.1); visible only while focused ---- */
.sb-skip {
  position: absolute; left: var(--sp-4); top: var(--sp-2); z-index: 20;
  display: inline-flex; align-items: center; min-height: var(--target); padding: 0 var(--sp-4);
  background: var(--accent); color: var(--accent-ink); border-radius: var(--r-md);
  font-size: var(--fs-sm); font-weight: 600; text-decoration: none;
  box-shadow: 0 0 0 6px var(--bg);
  /* parked above the viewport by transform, not by top: transform is the only
     thing the transition animates (top would repaint every frame) */
  transform: translateY(calc(-100% - var(--sp-4)));
  transition: transform var(--dur-2) var(--ease-out);
}
.sb-skip:focus-visible { transform: none; outline-offset: 2px; }
/* the skip target is a landmark, not a control: no ring around the page.
   Checkers still flag the bare outline:none; the offset makes it explicit. */
main[tabindex="-1"]:focus-visible { outline: none; outline-offset: 0; }

/* ---- header: the floating island — a sticky 920px pill (10px off the top,
   48px tall), wordmark left, links right. The .sb-header row keeps only
   sticky/top/z-index/height: no full-width background, no blur, no bottom
   border — the page shows through on every side of the island. ---- */
.sb-header {
  position: sticky; top: 0; z-index: 10; height: var(--nav-h);
  /* the narrow ladder below (.sb-nav) keys on THIS box, not on the viewport:
     1280px at 200% text zoom lays the header out in 640 CSS px and a media
     query cannot see that, so the 200% axis used to get the full six-link
     desktop row in half the room (measured 2026-09-12, nav-narrow). Container
     queries are baseline for this project (mobile-web-design.mdc). */
  container-type: inline-size; container-name: sbnav;
  /* the notch: a phone on its side with viewport-fit=cover (iPhone 17 Pro,
     874x402, ~62px insets) ran the island edge to edge, the mark under the
     Dynamic Island and the CTA clipped by the rounded corner (iOS 26 sim,
     2026-09-26): the island's max-width only binds above --container, so
     below it nothing held the inset. The row pads by the insets, which also
     narrows the sbnav container the tiers below read. env() is 0px off a
     notch, so desktop, tablet and portrait phones do not move. Shorthand and
     longhands both, per mobile-web-design.mdc (two checkers, two readings). */
  padding: 0 env(safe-area-inset-right, 0px) 0 env(safe-area-inset-left, 0px);
  padding-left: env(safe-area-inset-left, 0px);
  padding-right: env(safe-area-inset-right, 0px);
}
.sb-header-in {
  /* .sb-header-in IS the island: hairline border, pill radius, translucent
     card fill with its own blur (--island-bg: the light theme's is the opaque
     white card) and the light theme's soft ink shadow (--shadow-island, none on
     dark). The 0 6px 0 14px padding leaves 6px between
     the Install Now pill and the rounded right end and 14px before the mark. */
  /* The island spans the page column: --container minus the two gutters, so
     its outer edges sit on the content's edges on every --container page
     (account, docs, developers) and frame the narrower --content pages. */
  max-width: calc(var(--container) - var(--gutter-l) - var(--gutter-r));
  height: 48px; margin: 10px auto 0; padding: 0 6px 0 14px;
  display: flex; align-items: center; gap: var(--sp-4);
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  background: var(--island-bg);
  box-shadow: var(--shadow-island);
  backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
}
/* coarse pointer: --target and --target-sm are already 44px (:115) and 44px
   fits the 48px island, but the island grows 4px so the 44px targets keep
   4px of breathing room instead of 2px; --nav-h lifts in step (:115). */
@media (pointer: coarse) {
  .sb-header-in { height: 52px; }
}
/* the wordmark: the duotone cat mark at 24px (brand/mark-duo-sm.svg, the OG
   card's teal/magenta offset cut for small sizes; the tile appears only where
   it can animate, and carries no ring) + lowercase text, the .gg suffix plain ink —
   no accent, no storm on the text */
.sb-word {
  /* own colour, not the page's `a` rule: /topup's a{color:accent} was
     tinting the whole wordmark while every other page read it as fg +
     plain .gg (2026-09-05) */
  color: var(--fg);
  font-size: var(--fs-sm); font-weight: 700; letter-spacing: 0; text-decoration: none;
  display: inline-flex; align-items: center; min-height: var(--target); padding: 0 var(--sp-2); margin-left: calc(-1 * var(--sp-2));
  border-radius: var(--r-md);
}
/* the spacing rides the mark, not the flex gap: a gap would also separate
   the "superbot" text node from the ".gg" suffix and split the word */
.sb-word .sb-word-mark { width: 24px; height: 24px; flex: none; margin-right: 8px; }
.sb-word .gg { color: var(--fg); }
.sb-nav { margin-left: auto; display: flex; align-items: center; gap: var(--sp-1); }
.sb-nav a:not(.sb-btn) {
  display: inline-flex; align-items: center; min-height: var(--target); padding: 0 var(--sp-3);
  border-radius: var(--r-md); color: var(--muted); text-decoration: none; font-size: var(--fs-sm);
  border: 1px solid transparent; white-space: nowrap;
  transition: color var(--dur-2) var(--ease-std), background-color var(--dur-2) var(--ease-std), border-color var(--dur-2) var(--ease-std);
}
.sb-nav a:not(.sb-btn):hover, .sb-nav a:not(.sb-btn):active { color: var(--fg); background: var(--raised); }
.sb-nav a:not(.sb-btn)[aria-current="page"] { color: var(--fg); }
/* the X account (site-nav.ts xLink, 2026-09-26): the 16px X mark alone, a
   square --target hit area (36px on a mouse, 44px under a coarse pointer) that
   wears the text links' muted ink, fg + raised hover and the shared focus ring.
   The handle inside it is for the phone menu's row (.sb-drawer-nav below); in
   the island the mark and the anchor's "@ezolabsinc on X" name carry it. */
.sb-nav a.sb-nav-x { padding: 0; min-width: var(--target); justify-content: center; }
.sb-nav-x svg { width: var(--sp-4); height: var(--sp-4); flex: none; }
.sb-nav-x-txt { display: none; }
/* the theme toggle (site-nav.ts THEME_TOGGLE, theme-toggle.js): one icon button
   right of the wordmark, outside .sb-nav (2026-10-20). It wears the text links'
   recipe (muted ink, fg + raised on hover, the shared :focus-visible ring) at the
   X link's size: a square --target hit area around the 16px glyph. A moon on the
   light theme and a sun on the dark one: .sb-only-light / .sb-only-dark (the
   utilities near the end of this sheet) show one off html[data-theme]. It is
   drawn only where the inline prepaint set that attribute (site-theme.ts): with
   JS off nothing could act on a click, and the dark :root block is the page. */
.sb-theme-toggle {
  display: inline-flex; align-items: center; justify-content: center; flex: none;
  min-height: var(--target); min-width: var(--target); padding: 0;
  /* box pinned so no page can move it: the dashboard's bare `button{margin-top:var(--sp-2)}` (dashboard-page.ts) lifted the
     margin box and, centred in the row, sank the glyph 4px under the links' baseline (2026-10-12). Explicit margin, row
     alignment and line box here win over any page-level `button` rule. */
  margin: 0; align-self: center; vertical-align: middle;
  border: 1px solid transparent; border-radius: var(--r-md); background: none;
  color: var(--muted); cursor: pointer; font: inherit; line-height: 1;
  transition: color var(--dur-2) var(--ease-std), background-color var(--dur-2) var(--ease-std), border-color var(--dur-2) var(--ease-std);
}
.sb-theme-toggle:hover, .sb-theme-toggle:active { color: var(--fg); background: var(--raised); }
.sb-theme-toggle svg { width: var(--sp-4); height: var(--sp-4); flex: none; }
html:not([data-theme]) .sb-theme-toggle { display: none; }
/* one frame with every transition off while the theme flips (the prepaint adds
   the class and takes it off the next frame): the palette changes at once, no
   element cross-fades between the two. */
html.sb-theme-switching *, html.sb-theme-switching *::before, html.sb-theme-switching *::after { transition: none !important; }
/* the hairline divider in front of the quiet log in / account slot: it pairs the
   ghost link with the split install control that follows it (site-nav.ts) */
.sb-nav a.sb-nav-login { border-left: var(--hairline) solid var(--line); }
/* the Chat door (site-nav.ts, 2026-10-19, the picked mockup A1 "Twin pills"): a solid --fg pill, the bubble and "Chat", right
   before the install control and as tall as it, so the header always names the web app beside the download. Inverse ink on
   either theme (a light pill on dark, an ink pill on paper), never the storm ramp: the install pill keeps the one gradient.
   0,3,1 so the text links' recipe and its narrow tiers (0,2,1) never reach it. Where the row is tight it keeps the bubble alone
   (the 720 tier below), and on a page with the phone tab bar it is the header's one dot (the tab bar block). */
.sb-header .sb-nav a.sb-nav-chat {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2); flex: none;
  min-height: var(--target); padding: 0 var(--sp-3); margin-right: var(--sp-1);
  border: var(--hairline) solid transparent; border-radius: var(--r-pill);
  background: var(--fg); color: var(--bg);
  font-size: var(--fs-xs); font-weight: 600; text-decoration: none; white-space: nowrap;
  transition: background-color var(--dur-2) var(--ease-std), transform var(--dur-2) var(--ease-std);
}
.sb-header .sb-nav a.sb-nav-chat:active { color: var(--bg); background: var(--secondary); transform: scale(var(--press-scale)); }
@media (hover: hover) { .sb-header .sb-nav a.sb-nav-chat:hover { color: var(--bg); background: var(--secondary); } }
.sb-nav-chat-ic { width: var(--sp-4); height: var(--sp-4); flex: none; }
@container sbnav (max-width: 720px) {
  /* the row's 12px step: the pill keeps its bubble and gives up the word (the label stays the accessible name) */
  .sb-header .sb-nav a.sb-nav-chat { min-width: var(--target); padding: 0; }
  /* the GitHub icon link (site-nav.ts ghLink) leaves the island here: the row below is measured without it; the drawer keeps it */
  .sb-header .sb-nav a.sb-nav-gh { display: none; }
  .sb-nav-chat-txt { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
}
/* the header's one filled control (Install Now): the shared .sb-btn recipe at
   the small pill size — fs-xs ink on the ramp — lifted to the full --target so
   it nests flush in the island's rounded end (36px pill in a 48px island = 6px
   inset top and bottom, matching the 6px right padding; the old --target-sm
   28px read as lost inside the capsule). The @420 tier below keeps the same
   lift through its .sb-btn.sm line. */
.sb-nav .sb-btn { min-height: var(--target); padding: 0 var(--sp-3); font-size: var(--fs-xs); border-radius: var(--r-pill); }
@container sbnav (max-width: 720px) {
  /* the row (wordmark + features + for teams + docs + benchmarks + log in +
     the Install Now pill) needs 673px at the 14px step, which an island of
     693 or less cannot give it, so below 720 it takes the 12px step, --sp-1
     link padding and no inter-item gap: 466px of row, which fits an island
     from 486 up. */
  .sb-header-in { gap: var(--sp-1); }
  .sb-nav { gap: 0; }
  .sb-word, .sb-nav a:not(.sb-btn) { font-size: var(--fs-xs); } /* :not(.sb-btn): the base recipe at :283 is 0,2,1 and beats a bare 0,1,1 here */
  .sb-nav a:not(.sb-btn) { padding: 0 var(--sp-1); }
  /* the notch padding (.sb-header) puts a landscape phone in this tier (812
     minus two 50px insets is 712): "docs" measured 37px wide there, under the
     44px touch floor. --target-sm is 28px on a mouse, under every label's own
     width, so a fine pointer does not move (2026-09-26). */
  .sb-nav a:not(.sb-btn) { min-width: var(--target-sm); justify-content: center; }
  .sb-nav .sb-btn.sm { padding: 0 var(--sp-2); } /* .sm: the shared .sb-btn.sm recipe at :451 is 0,2,0 and later, so the pill's own side padding is out-ranked here, as it is at every narrower tier */
}
@container sbnav (max-width: 560px) {
  /* for teams is still in the row here, so the links give up their side
     padding (their min-width holds the target the padding used to) and the
     island drops both gaps: 428px of row. The nav then takes whatever is
     left and spreads across it, so the labels keep their air (space-between
     inside the nav, not a gap between items: at 320 the five links and the
     pill have 256px of text in a 264px box, and a fixed gap would push the
     pill past the island). */
  .sb-header-in { gap: 0; }
  .sb-nav { flex: 1 1 auto; margin-left: var(--sp-2); justify-content: space-between; }
  .sb-nav a:not(.sb-btn) { padding: 0; min-width: var(--target-sm); justify-content: center; }
}
@container sbnav (max-width: 480px) {
  /* below 480 the island goes edge-to-edge with an 8px (--sp-2) side margin:
     max-width releases so it spans the width minus the margins; the 10px top
     margin and the narrower tiers below stand */
  .sb-header-in { max-width: none; margin: 10px var(--sp-2) 0; }
  /* the for-teams link (site-nav.ts, 2026-09-12) measured 56px at 360, on a
     row that was already over: it drops below 480, where /features' org block
     and the footer of every page that carries one keep the route. The api
     link (2026-10-03) drops with it: the 320 row had 20px to spare, and the
     phone drawer clones the whole nav, so the route stays one tap away */
  .sb-nav a[href$="/teams"], .sb-nav a[href$="/developers"] { display: none; }
}
@container sbnav (max-width: 440px) {
  /* the row is 384px with the wordmark text (110 + 8 + the 266px nav) while
     the island's content box is 405 at 441 and 385 at 421: below 440 the
     brand keeps its mark and gives up the text, 82px of the row. The link,
     its target and the anchor's "superbot.gg" name (site-nav.ts) all stay,
     so the brand is still announced and still goes home. */
  .sb-word-txt { display: none; }
  .sb-word { min-width: var(--target-sm); justify-content: center; }
  .sb-word .sb-word-mark { margin-right: 0; }
  .sb-nav .sb-btn.sm { min-height: var(--target); padding: 0 var(--sp-1); } /* min-height stays at the full --target so the pill keeps its flush 36px nest in the island at every width */
  /* the platform mark nav-store.js puts in the pill (16px + the button's
     --sp-2 gap) measured the header 23-27px over at 320 on every page
     (mobile matrix, 2026-09-09); the pill's text label stays, the mark
     goes below 420 */
  .sb-nav .sb-btn .sb-os-mark { display: none; }
}
@container sbnav (max-width: 360px) {
  /* 320px (WCAG 1.4.10's width, an iPhone SE): the island gives up its
     widest air instead of wrapping, so --nav-h and every anchor's
     scroll-padding stay true - 4px side margins, --sp-2 left padding,
     --sp-1 right. The 284px row fits the 304px content box at 320 with
     20px to spare. */
  .sb-header-in { margin-left: var(--sp-1); margin-right: var(--sp-1); padding-left: var(--sp-2); padding-right: var(--sp-1); }
}

/* ---- phone nav: the 44px menu button and its drawer (nav-drawer.js) ----
   Measured 2026-09-17 at 320 CSS px: the six-link row was 347px wide in a
   320px viewport — 23 to 27px of document overflow on every static page — and
   there was no menu button, so the row was the page's minimum width and the
   last link sat past the edge. The script clones the shell's own nav into the
   drawer, so labels, order, href and aria-current stay whatever the server
   emitted; these rules only dress it. */
.sb-navbtn {
  display: none; /* the script shows it only where the drawer installs */
  align-items: center; justify-content: center;
  min-height: var(--target); min-width: var(--target);
  margin-left: auto; padding: 0; border: 1px solid transparent;
  border-radius: var(--r-md); background: none; color: var(--fg); cursor: pointer;
}
.sb-navbtn:active { background: var(--raised); }
/* the links move into the drawer; the Install Now pill STAYS in the island —
   it is the page's one primary action and the drawer's own copy is its tap
   target there, so hiding the whole nav would have removed the CTA from the
   header entirely (2026-09-17). */
.sb-nav.sb-nav-has-btn a:not(.sb-btn) { display: none; }
/* the beta host's locked pill ("Get early access" / "Enter invite code",
   data-access, site-nav.ts) is too long for the phone island (owner,
   2026-09-25: "looks bad on that"): with the drawer installed it leaves the
   island and lives only as the drawer's CTA. Install Now stays as above. */
.sb-nav.sb-nav-has-btn .sb-btn[data-access] { display: none; }
.sb-nav.sb-nav-has-btn + .sb-navbtn { display: inline-flex; margin-left: var(--sp-1); }
/* the Chat door stays in the island with the install pill: it is a primary, not one of the links the drawer takes */
.sb-header .sb-nav.sb-nav-has-btn a.sb-nav-chat { display: inline-flex; }
/* with the links in the drawer the pill is alone in the nav: it docks beside
   the menu button (the phone convention, one thumb zone) instead of floating
   by the mark, and it takes back the side padding the ≤440 tier's --sp-1 took
   when the row still held five links — at 320 its label sat 4px from the
   rounded ends (2026-09-25). 0,4,0 outranks that tier's 0,3,0; the class
   only exists where the drawer installs, so the inline row is untouched. */
.sb-nav.sb-nav-has-btn { justify-content: flex-end; }
.sb-nav.sb-nav-has-btn .sb-btn.sm { padding: 0 var(--sp-3); }
/* the wordmark comes back. The ≤440 tier drops its text because the row
   there still held five links; with the drawer installed the island holds
   only the mark, the menu button and at most the Install Now pill, and the
   mark alone read as an empty pill (iOS 26 sim, 2026-09-26: "shows no
   links, looks weird"). The text returns at the 16px step: 262px of row at
   320 with the pill, inside a 298px island. .sb-drawer-dock is set on the
   dock by the script with .sb-nav-has-btn and cleared for its row
   measurement, so it never feeds back into the install decision. */
.sb-drawer-dock .sb-word-txt { display: inline; }
.sb-drawer-dock .sb-word { font-size: var(--fs-base); }
.sb-drawer-dock .sb-word .sb-word-mark { margin-right: var(--sp-2); }
/* the split install control (.sb-split) adds its chevron and divider to that
   row, which no longer fits the 298px island at 320: at 360 and below the dock
   gives the wordmark text up again, as the ≤440 inline tier does, and the mark,
   its target and the anchor's "superbot.gg" name stay (2026-10-08). */
@container sbnav (max-width: 360px) {
  .sb-drawer-dock .sb-word-txt { display: none; }
  .sb-drawer-dock .sb-word .sb-word-mark { margin-right: 0; }
}
/* the theme toggle (above, site-nav.ts THEME_TOGGLE) joins the phone header: the
   drawer's row is now the mark, the toggle, the Install pill and the menu button,
   44px wider than the row held before, and that row had no slack to give: the mark
   already poked out of the island from 361 to 440, and 320 had 9px too little. So
   up to 456 (where the text and the toggle first fit together) the dock gives the wordmark text up as the ≤360 tier above does for the
   split chevron (the mark, its target and the anchor's "superbot.gg" name stay), and
   at 336 and below the pill and the menu button take back 8px and 4px of side space,
   which keeps every target at its 44px. Scoped to html[data-theme]: only where the
   toggle is drawn. */
@container sbnav (max-width: 456px) {
  html[data-theme] .sb-drawer-dock .sb-word-txt { display: none; }
  html[data-theme] .sb-drawer-dock .sb-word .sb-word-mark { margin-right: 0; }
}
@container sbnav (max-width: 336px) {
  html[data-theme] .sb-nav.sb-nav-has-btn .sb-btn.sm { padding: 0 var(--sp-2); }
  html[data-theme] .sb-nav.sb-nav-has-btn + .sb-navbtn { margin-left: 0; }
}
.sb-navbtn-bars,
.sb-navbtn-bars::before,
.sb-navbtn-bars::after {
  display: block; width: 18px; height: 1.5px; border-radius: 1px; background: currentColor;
}
.sb-navbtn-bars { position: relative; }
.sb-navbtn-bars::before, .sb-navbtn-bars::after { content: ""; position: absolute; left: 0; }
.sb-navbtn-bars::before { top: -5px; }
.sb-navbtn-bars::after { top: 5px; }

/* the sheet (2026-09-26, the Discord mobile-web menu): full height from the
   right, a --sp-12 scrim strip on the left that a thumb can tap to dismiss,
   the top row docked on the island's own pixels, 56px rows, the CTA pinned in
   the thumb zone. The old panel was a 100dvh box that stopped above iOS 26
   Safari's floating toolbar with the page showing under it, its links 44px
   rows at 16px text in the top third and 60% of the panel empty. */
.sb-scrim {
  /* runs 128px past the bottom edge: iOS 26 Safari's floating toolbar sits
     over the page, and a scrim that stopped at the edge left a strip of live
     page under it */
  position: fixed; top: 0; left: 0; right: 0; bottom: calc(-2 * var(--sp-16)); z-index: 40;
  background: color-mix(in srgb, var(--bg) 72%, transparent);
  animation: sb-fade var(--dur-3) var(--ease-out);
}
.sb-drawer {
  position: fixed; top: 0; right: 0; z-index: 41;
  width: min(calc(100% - var(--sp-12)), calc(var(--sp-16) * 6));
  /* the content lays out in the visible height (the CTA must clear the
     toolbar); the fill below it runs on under the toolbar through ::after */
  height: 100vh; height: 100dvh;
  display: flex; flex-direction: column;
  padding-right: env(safe-area-inset-right, 0px);
  background: var(--card); border-left: 1px solid var(--line);
  animation: sb-drawer-in var(--dur-3) var(--ease-out);
}
/* display:flex above outranks the UA's [hidden] rule: the closed sheet must
   still leave the page */
.sb-drawer[hidden], .sb-scrim[hidden] { display: none; }
.sb-drawer::after {
  content: ""; position: absolute; top: 100%; left: -1px; right: 0; height: calc(2 * var(--sp-16));
  background: inherit; border-left: inherit;
}
/* focus lands on the dialog itself on open (nav-drawer.js): a landmark, not a
   control, so no ring, the way main[tabindex="-1"] carries none above */
.sb-drawer:focus, .sb-drawer:focus-visible { outline: none; outline-offset: 0; }
@keyframes sb-drawer-in { from { transform: translateX(var(--sp-4)); opacity: 0; } to { transform: none; opacity: 1; } }
body.sb-nav-open { overflow: hidden; }
/* the top row: the script measures the menu button and writes --sb-x-top,
   --sb-x-right and --sb-x-size, so the close control sits on the button's
   exact rect and the wordmark on the same band; the fallbacks are the
   island's own numbers (10px margin + 4px inset, a 44px target) */
.sb-drawer-head {
  flex: none; position: relative;
  height: calc(var(--sb-x-top, var(--sp-3)) + var(--sb-x-size, var(--target)) + var(--sp-4));
}
.sb-drawer-word.sb-word {
  position: absolute; top: var(--sb-x-top, var(--sp-3)); left: var(--sp-3);
  min-height: var(--sb-x-size, var(--target)); margin: 0; font-size: var(--fs-base);
}
.sb-drawer-close {
  position: absolute; top: var(--sb-x-top, var(--sp-3)); right: var(--sb-x-right, var(--sp-2));
  display: flex; align-items: center; justify-content: center;
  width: var(--sb-x-size, var(--target)); height: var(--sb-x-size, var(--target));
  min-height: var(--target); min-width: var(--target); padding: 0;
  border: 1px solid transparent; border-radius: var(--r-md); background: none; color: var(--fg); cursor: pointer;
}
.sb-drawer-close:active { background: var(--raised); }
@media (hover: hover) { .sb-drawer-close:hover { background: var(--raised); } }
.sb-drawer-close-x { position: relative; display: block; width: 18px; height: 18px; }
.sb-drawer-close-x::before, .sb-drawer-close-x::after {
  content: ""; position: absolute; left: 0; top: 50%; width: 18px; height: 1.5px; border-radius: 1px; background: currentColor;
}
.sb-drawer-close-x::before { transform: rotate(45deg); }
.sb-drawer-close-x::after { transform: rotate(-45deg); }
/* the rows: full width, 56px, 18px labels, hairlines between; the list
   scrolls on its own if a short screen cannot hold it, and the CTA rides its
   end, pushed to the bottom by margin-top:auto whenever there is room */
.sb-drawer-nav {
  flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  display: flex; flex-direction: column;
  padding-bottom: calc(var(--sp-4) + env(safe-area-inset-bottom, 0px));
}
.sb-drawer-nav a {
  display: flex; align-items: center; flex: none;
  color: var(--fg); text-decoration: none; white-space: nowrap;
}
.sb-drawer-nav a:not(.sb-drawer-cta) {
  min-height: calc(var(--sp-12) + var(--sp-2)); padding: 0 var(--sp-5);
  border-bottom: 1px solid var(--line);
  font-size: var(--fs-lg); line-height: var(--lh-lg); font-weight: 500;
  transition: background-color var(--dur-2) var(--ease-std);
}
.sb-drawer-nav a:not(.sb-drawer-cta):first-child { border-top: 1px solid var(--line); }
.sb-drawer-nav a:not(.sb-drawer-cta):active { background: var(--raised); }
@media (hover: hover) { .sb-drawer-nav a:not(.sb-drawer-cta):hover { background: var(--raised); } }
.sb-drawer-nav a[aria-current="page"]:not(.sb-drawer-cta) { color: var(--accent); }
/* the X row: the cloned header anchor shows its handle here, the mark ahead
   of it at the island's 16px */
.sb-drawer-nav a.sb-nav-x { gap: var(--sp-3); }
.sb-drawer-nav .sb-nav-x-txt { display: inline; }
.sb-drawer-nav a:not(.sb-drawer-cta):has(+ .sb-drawer-cta) { margin-bottom: var(--sp-6); }
.sb-drawer-nav a.sb-drawer-cta {
  margin: auto var(--sp-5) 0; min-height: calc(var(--sp-12) + var(--sp-1)); padding: 0 var(--sp-5);
  justify-content: center; border-radius: var(--r-pill);
  font-size: var(--fs-base); line-height: var(--lh-base);
  background: var(--cta-base); color: var(--cta-ink);
}
/* the clone keeps .sb-btn, so the CTA wears the same face as the pill it
   copies (the storm ramp on dark, solid ink on light); the flat --cta-base above
   was painting over it (2026-09-25) */
.sb-drawer-nav a.sb-btn.sb-drawer-cta { background-color: var(--cta-base); background-image: var(--cta-bg); background-size: 200% 100%; font-weight: 600; }

/* a phone on its side has 343px of height under the chrome: the island gives
   up 6px of its top margin and 2px of its own height so the sticky band is 56,
   not 62, and the 44px targets still fit inside the 46px island (2026-09-17,
   measured 62px on every coarse device; the landscape budget is 56). */
@media (pointer: coarse) and (orientation: landscape) and (max-height: 500px) {
  [data-theme="dark"], :root { --nav-h: 56px; }
  .sb-header-in { height: 46px; margin-top: 10px; }
}

/* ---- buttons: the filled action IS the brand's storm action (duostorm
   kit .sb-action recipe), and its hover is the brand's living aura (kit
   .sb-aura, duostorm.css:198-331) worn by a control: the ramp idles on a slow
   single-axis breathe that never stops, and a wake (hover, keyboard focus)
   fades in two compositor-only layers behind the label — the five-stop
   spectrum wheel turning on --aura-spin and a white glint crossing on
   --aura-sheen, coprime so they never phase-lock — while the glow lifts it.
   The press is a small spatial scale. Nothing snaps: every property hover
   changes rides a transition, and rest and hover share one geometry. Every
   other control stays quiet. Rework 2026-09-09; the two-frame swap it
   replaces was an unclosed comment swallowing this block's transition. ---- */
.sb-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2);
  min-height: var(--target); padding: 0 var(--sp-4); border-radius: var(--r-control);
  background-color: var(--cta-base); background-image: var(--cta-bg); background-size: 200% 100%; background-position: 0% 50%;
  color: var(--cta-ink); border: 1px solid transparent;
  font-size: var(--fs-sm); font-weight: 600; cursor: pointer; text-decoration: none; white-space: nowrap;
  /* overflow clips both pseudo layers to the rounded box and leaves the outer
     glow alone (clip-path would clip the glow too); isolation makes the
     negative-z layers paint above the control's own ramp, under the label,
     and scopes their blend to this face */
  position: relative; overflow: hidden; isolation: isolate;
  /* the wake: exactly the properties hover changes on the element itself */
  transition: box-shadow var(--dur-4) var(--ease-std), transform var(--dur-2) var(--ease-std);
}
/* nowrap is for the label: the download page's primary anchor (a.dl.sb-btn)
   stacks a mono sha256 <code> under it, and that code must keep its break
   opportunities or its 64-char min-content drags the whole grid out */
.sb-btn code { white-space: normal; overflow-wrap: anywhere; min-width: 0; }
/* the idle: the ramp drifts on one axis, slow and reversible, so the control
   feels alive at rest. It keeps breathing through hover, focus and press —
   the old hover kill snapped the ramp back to its start in one frame. */
.sb-btn:not([disabled]), .sb-split { animation: sb-storm-breathe var(--breathe) ease-in-out infinite; } /* deliberate */
@keyframes sb-storm-breathe { 0%, 100% { background-position: 0% 50%; } 60% { background-position: 60% 50%; } }
/* the foil: the kit's .sb-aura-spectrum on a control face. A conic wheel of
   the five storm stops, 1.5x the control's width so it covers every corner
   at every angle (radius 0.75w against a worst-case half-diagonal of 0.707w
   on any control wider than tall), rotating by transform only. Screen blend
   can only brighten the ramp, so the ink's contrast is never worse than at
   rest. Parked at opacity 0 and paused, so a sleeping control costs nothing. */
.sb-btn::before, .sb-split::before {
  content: ""; position: absolute; top: 50%; left: 50%;
  width: 150%; aspect-ratio: 1; border-radius: 50%;
  background: conic-gradient(from 0deg at 50% 50%,
    var(--storm-0) 0deg, var(--storm-1) 72deg, var(--storm-2) 144deg,
    var(--storm-3) 216deg, var(--storm-4) 288deg, var(--storm-0) 360deg);
  filter: blur(calc(var(--aura-blur) * 0.3));
  mix-blend-mode: screen;
  opacity: 0;
  transform: translate(-50%, -50%);
  transition: opacity var(--aura-settle) var(--ease-std);
  pointer-events: none; z-index: -2;
  animation: sb-btn-spin var(--aura-spin) linear infinite; /* deliberate ambient loop, runs only while awake */
  animation-play-state: paused;
}
@keyframes sb-btn-spin {
  from { transform: translate(-50%, -50%) rotate(0deg); }
  to   { transform: translate(-50%, -50%) rotate(360deg); }
}
/* the glint: the kit's .sb-aura-sheen. A white band crossing the face on
   --aura-sheen, transform only. It replaces the old one-shot sweep and the
   M3 state layer: the wake IS the feedback. */
.sb-btn::after, .sb-split::after {
  content: ""; position: absolute; inset: -40%;
  background: linear-gradient(115deg, transparent 42%, #fff 50%, transparent 58%);
  mix-blend-mode: screen;
  opacity: 0;
  transition: opacity var(--aura-settle) var(--ease-std);
  pointer-events: none; z-index: -1;
  animation: sb-btn-glint var(--aura-sheen) ease-in-out infinite; /* deliberate ambient loop, runs only while awake */
  animation-play-state: paused;
}
@keyframes sb-btn-glint { from { transform: translateX(-70%); } to { transform: translateX(70%); } }
/* the wake: pointer or keyboard, both layers fade in on the settle band and
   start moving; the glow lifts the control on the same clock */
/* hover is not an input (mobile-web-design.mdc): the pointer wake lives under
   (hover:hover) so a finger never matches it, and the keyboard wake is
   unguarded. A touch pointer keeps its feedback from :active, which is the
   event a finger actually sends (2026-09-17, U-site). */
.sb-btn:focus-visible:not([disabled])::before, .sb-split:has(:focus-visible)::before { opacity: var(--aura-foil-opacity); animation-play-state: running; }
.sb-btn:focus-visible:not([disabled])::after, .sb-split:has(:focus-visible)::after { opacity: var(--aura-glint-opacity); animation-play-state: running; }
.sb-btn:focus-visible:not([disabled]), .sb-split:has(:focus-visible) { box-shadow: var(--action-glow); }
@media (hover: hover) {
  .sb-btn:hover:not([disabled])::before, .sb-split:hover::before { opacity: var(--aura-foil-opacity); animation-play-state: running; }
  .sb-btn:hover:not([disabled])::after, .sb-split:hover::after { opacity: var(--aura-glint-opacity); animation-play-state: running; }
  .sb-btn:hover:not([disabled]), .sb-split:hover { box-shadow: var(--action-glow); }
}
@media (hover: none) {
  .sb-btn:active:not([disabled]), .sb-split:active { box-shadow: var(--action-glow); }
}
.sb-btn:active:not([disabled]), .sb-split:active { transform: scale(var(--press-scale)); }
/* ghost: the quiet variant — card surface, hairline border. It wakes the same
   wheel, but its ::after is the CUT: an opaque card fill one border-width
   inside, so the wheel reads only as a 1px rotating storm rim (the shimmer
   "backdrop cut"). The cut is restated at the wake's own specificity, or the
   wake rule (0,3,1) would out-rank it (0,2,1) and flood the face with foil.
   The rim is the feedback: border-color and shadow stay put. */
.sb-btn.ghost { background: var(--card); background-image: none; color: var(--fg); border-color: var(--line); animation: none; }
.sb-btn.ghost::after,
.sb-btn.ghost:hover:not([disabled])::after,
.sb-btn.ghost:focus-visible:not([disabled])::after {
  inset: 1px; border-radius: inherit; background: var(--card);
  mix-blend-mode: normal; opacity: 1; animation: none;
}
.sb-btn.ghost:hover:not([disabled]),
.sb-btn.ghost:focus-visible:not([disabled]) { box-shadow: none; }
.sb-btn.sm { min-height: var(--target-sm); padding: 0 var(--sp-3); font-size: var(--fs-xs); }
/* pill: the shape of the referent (the desktop app's Ask superbot control) —
   the nav CTA and anything cut that way */
.sb-btn.pill { border-radius: var(--r-pill); }
/* disabled drops the ramp and both aura layers for the elevated surface, the
   label settling on muted (≈5.2:1). Inert to the pointer entirely. */
.sb-btn[disabled] {
  pointer-events: none; cursor: default; transform: none; filter: none; animation: none;
  background-image: none; background: var(--raised); color: var(--muted);
  border-color: transparent; box-shadow: none;
}
.sb-btn[disabled]::before, .sb-btn[disabled]::after { content: none; }
.sb-btn.ghost[disabled] { background: var(--card); color: var(--muted); border-color: var(--line); }

/* ---- the split install control (site-nav.ts, the stock header) ----
   One rounded group that wears the .sb-btn recipe: the storm fill and ink, the
   idle breathe, the hover and focus aura (the ::before/::after and wake selectors
   above name .sb-split too) and the press scale. Two halves sit inside it: the
   primary link to /download (its own fill and aura are off here, the group paints
   them) and the chevron that opens the platform menu, a native popover. The
   divider is a hairline of the button's own ink, so it follows the signed-in
   accent. The keyboard ring belongs to the group, drawn when either half holds
   focus, so the group's clip never shaves a half's outline. ---- */
.sb-split {
  display: inline-flex; align-items: stretch; position: relative; isolation: isolate; overflow: hidden;
  min-height: var(--target); padding: 0; border: 0; border-radius: var(--r-pill);
  color: var(--cta-ink);
  background-color: var(--cta-base); background-image: var(--cta-bg); background-size: var(--storm-size);
  transition: box-shadow var(--dur-4) var(--ease-std), transform var(--dur-2) var(--ease-std);
}
.sb-split > .sb-btn,
.sb-split > .sb-btn:hover:not([disabled]),
.sb-split > .sb-btn:focus-visible:not([disabled]),
.sb-split > .sb-btn:active:not([disabled]) {
  background: none; background-image: none; box-shadow: none; animation: none; transform: none;
  border-color: transparent; border-radius: var(--r-pill);
}
.sb-split > .sb-btn::before, .sb-split > .sb-btn::after { content: none; }
.sb-split:has(:focus-visible) { outline: var(--focus-w) solid var(--accent); outline-offset: var(--focus-w); }
/* the focused half also says which half it is: an inset ring in the button's own
   ink, inside the group's clip, so the primary and the chevron read apart */
.sb-split :focus-visible { outline: var(--focus-w) solid var(--cta-ink); outline-offset: calc(var(--focus-w) * -2); }
.sb-split-chev {
  display: inline-flex; align-items: center; justify-content: center; flex: none;
  min-width: var(--target); min-height: var(--target); margin: 0; padding: 0;
  border: 0; border-left: var(--hairline) solid var(--split-line); border-radius: 0 var(--r-pill) var(--r-pill) 0;
  background: none; color: inherit; font: inherit; cursor: pointer;
  -webkit-appearance: none; appearance: none;
}
.sb-split-chev svg { display: block; width: var(--sp-4); height: var(--sp-4); flex: none; }
@media (hover: hover) { .sb-split-chev:hover { background: var(--split-state); } }
.sb-split-chev:active, .sb-split-chev[aria-expanded="true"] { background: var(--split-state); }

/* the platform menu: the site's raised surface, a hairline border, the control
   radius, rows on the spacing scale. It is a top-layer popover (outside .sb-nav,
   so nav-drawer.js does not clone its rows), with no resting shadow on dark (the
   island's soft ink shadow on light, --shadow-island). It enters
   by opacity and a small rise on --dur-2, and it leaves the same way: display
   and overlay take part in the transition (allow-discrete). */
.sb-platforms {
  inset: auto; margin: 0; width: max-content; max-width: calc(100vw - var(--sp-8));
  padding: var(--sp-1); color: var(--fg);
  background: var(--raised); border: var(--hairline) solid var(--line); border-radius: var(--r-control);
  box-shadow: var(--shadow-island);
  opacity: 1; transform: none;
  transition: opacity var(--dur-2) var(--ease-std), transform var(--dur-2) var(--ease-std),
    display var(--dur-2) allow-discrete, overlay var(--dur-2) allow-discrete;
}
.sb-platforms:not(:popover-open) { opacity: 0; transform: translateY(var(--sp-1)); }
@starting-style { .sb-platforms:popover-open { opacity: 0; transform: translateY(var(--sp-1)); } }
/* CSS anchor positioning: the menu's top edge sits one --sp-2 below the chevron
   and its right edge is the chevron's. Where it is missing, nav-store.js places
   the menu from the chevron's rect. */
@supports (anchor-name: --sb-split) {
  .sb-split-chev { anchor-name: --sb-split; }
  .sb-platforms { position-anchor: --sb-split; top: calc(anchor(bottom) + var(--sp-2)); right: anchor(right); }
}
.sb-platforms ul { list-style: none; margin: 0; padding: 0; }
.sb-platforms a, .sb-platforms-soon {
  display: flex; align-items: center; min-height: var(--target); padding: 0 var(--sp-3);
  border-radius: var(--r-control); color: var(--fg); font-size: var(--fs-sm); line-height: var(--lh-sm);
  text-decoration: none; white-space: nowrap;
}
.sb-platforms-soon { color: var(--muted); }
@media (hover: hover) { .sb-platforms a:hover { background: var(--line); } }
.sb-platforms a:active { background: var(--line); }

/* ---- the phone tab bar (2026-10-15, the picked mockup v5) ----
   site-nav.ts tabBar renders it after the header on every page that wears the site
   chrome, the account pages included (their own sections are the account slot's hub,
   /account/menu). A phone (<= 600px, the width nav-drawer.js tests too) navigates by it:
   a floating capsule along the bottom, icon over label, five equal slots (home,
   features, docs, account, more), and a small sheet over it for the rest. Above
   600px it is display: none and nothing else on this sheet changes.
   Look plan: the capsule is the header island's own fill and hairline
   (--island-bg, --line, the island's blur and light shadow), so the two read as
   one family; resting slots --muted, the current slot a --r-pill pill painted
   by --sb-tab-current (the storm action on dark, --accent on light, unless a shell repoints it) with
   --tab-ink on it; labels --fs-xs in the
   site's one sans; radii --r-pill (bar, pill) and --r-surface (sheet) with
   --r-control on a sheet row. Targets: a slot is --tab-slot-h tall (56px) and a
   row --target-touch (44px). Motion: the sheet opening is the one entrance
   (opacity + a small rise on --dur-3, --ease-out); a slot or row only changes
   colour on hover and press. */
.sb-tabbar { display: none; }
@media (max-width: 600px) {
  .sb-tabbar {
    display: flex; align-items: stretch;
    position: fixed; z-index: var(--z-tabbar);
    left: var(--tabbar-edge); right: var(--tabbar-edge); bottom: var(--tabbar-bottom);
    padding: var(--tabbar-pad);
    background: var(--island-bg); border: var(--hairline) solid var(--line); border-radius: var(--r-pill);
    box-shadow: var(--shadow-island);
    backdrop-filter: blur(var(--island-blur)); -webkit-backdrop-filter: blur(var(--island-blur));
  }
  .sb-tab {
    flex: 1 1 0; min-width: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--sp-1);
    min-height: var(--tab-slot-h); margin: 0; padding: var(--sp-2) var(--sp-1);
    border: 0; border-radius: var(--r-pill); background: none; color: var(--muted);
    font: inherit; font-size: var(--fs-xs); line-height: var(--lh-xs); font-weight: var(--fw-tab);
    text-decoration: none; white-space: nowrap; cursor: pointer;
    -webkit-appearance: none; appearance: none;
    transition: color var(--dur-2) var(--ease-std), background-color var(--dur-2) var(--ease-std);
  }
  .sb-tab-ic { width: var(--sp-5); height: var(--sp-5); flex: none; }
  .sb-tab-label { display: block; }
  /* hover is not an input (mobile-web-design.mdc): the pointer states ride (hover: hover), the press rides :active */
  .sb-tab:active { color: var(--fg); background-color: var(--ghost-8); }
  @media (hover: hover) { .sb-tab:hover { color: var(--fg); background-color: var(--ghost-8); } }
  /* the current slot: the more slot wears the pill when the page is one of the sheet's rows (data-current) */
  .sb-tab[aria-current="page"], .sb-tab[data-current],
  .sb-tab[aria-current="page"]:active, .sb-tab[data-current]:active { color: var(--tab-ink); background: var(--sb-tab-current, var(--storm-action)); }
  @media (hover: hover) { .sb-tab[aria-current="page"]:hover, .sb-tab[data-current]:hover { color: var(--tab-ink); background: var(--sb-tab-current, var(--storm-action)); } }

  /* the sheet: fixed over the bar, its right edge on the capsule's, a hairline surface on the island's fill */
  .sb-more-sheet {
    inset: auto; margin: 0; right: var(--tabbar-edge); bottom: calc(var(--tabbar-top) + var(--sp-2));
    width: max-content; min-width: var(--tabbar-sheet-w); max-width: calc(100% - var(--sp-4));
    padding: var(--sp-1); color: var(--fg); overflow: visible;
    background: var(--island-bg); border: var(--hairline) solid var(--line); border-radius: var(--r-surface);
    box-shadow: var(--shadow-island);
    backdrop-filter: blur(var(--island-blur)); -webkit-backdrop-filter: blur(var(--island-blur));
    opacity: 1; transform: none;
    transition: opacity var(--dur-3) var(--ease-out), transform var(--dur-3) var(--ease-out),
      display var(--dur-3) allow-discrete, overlay var(--dur-3) allow-discrete;
  }
  /* closed: the exit is quicker than the entrance (--dur-2, --ease-in) */
  .sb-more-sheet:not(:popover-open) {
    opacity: 0; transform: translateY(var(--sp-2));
    transition: opacity var(--dur-2) var(--ease-in), transform var(--dur-2) var(--ease-in),
      display var(--dur-2) allow-discrete, overlay var(--dur-2) allow-discrete;
  }
  @starting-style { .sb-more-sheet:popover-open { opacity: 0; transform: translateY(var(--sp-2)); } }
  .sb-more-list { list-style: none; margin: 0; padding: 0; }
  .sb-more-row {
    display: flex; align-items: center; gap: var(--sp-3); width: 100%;
    min-height: var(--target-touch); margin: 0; padding: 0 var(--sp-3);
    border: 0; border-radius: var(--r-control); background: none; color: var(--fg);
    font: inherit; font-size: var(--fs-sm); line-height: var(--lh-sm); font-weight: var(--fw-tab);
    text-align: left; text-decoration: none; white-space: nowrap; cursor: pointer;
    -webkit-appearance: none; appearance: none;
    transition: background-color var(--dur-2) var(--ease-std);
  }
  .sb-more-ic { width: var(--sp-4); height: var(--sp-4); flex: none; color: var(--muted); }
  .sb-more-row:active { background-color: var(--ghost-8); }
  @media (hover: hover) { .sb-more-row:hover { background-color: var(--ghost-8); } }
  .sb-more-row[aria-current="page"] { background-color: var(--ghost-12); }
  .sb-more-row[aria-current="page"] .sb-more-ic { color: var(--fg); }
  .sb-more-sep { height: var(--hairline); margin: var(--sp-1) var(--sp-3); padding: 0; background: var(--line); }

  /* a page that has the bar: the header keeps its wordmark, theme toggle and install control and drops the
     inline links (the bar carries them), and the page gets room under its last line for the capsule. :has() lets
     one stylesheet serve every shell, with no class on <body> to forget. */
  body:has(.sb-tabbar) .sb-nav a:not(.sb-btn) { display: none; }
  body:has(.sb-tabbar) .sb-nav { justify-content: flex-end; }
  /* the install control on that header (2026-10-15, the picked mockup v2 "White dot"): the storm split pill, full
     height and most of the island's width, gives way to one solid --fg dot with lucide's download arrow knocked out of
     it (mask-composite: exclude), so the island's own fill shows through the arrow on either theme: a light dot with a
     dark arrow on dark, an ink dot with a paper arrow on light. The link stays a square --target hit area (44px under a
     coarse pointer) and the dot is that square less 4px; the island's right padding drops to --sp-1 so the dot sits as
     far from the rounded end as from the top. Its label stays in the DOM at font-size 0 as the accessible name
     (nav-store.js still writes "Get superbot" on a phone). The group keeps its focus ring and press scale and drops the
     fill, breathe, aura and glow; the chevron and its platform menu leave the phone header, where /download and the
     bar's more sheet carry the same routes. The beta host's locked pill (data-access: "Get early access", "Enter invite
     code") takes the same dot below; it has no group to carry the focus ring, so its dot is a masked ::before and the
     link itself stays unmasked, ring and all. Its label is the accessible name, as the more sheet's invite row pairs it
     with the same download glyph. */
  body:has(.sb-tabbar) .sb-header-in { padding-right: var(--sp-1); }
  body:has(.sb-tabbar) .sb-split { background: none; animation: none; box-shadow: none; }
  body:has(.sb-tabbar) .sb-split::before,
  body:has(.sb-tabbar) .sb-split::after { content: none; }
  body:has(.sb-tabbar) .sb-split-chev { display: none; }
  body:has(.sb-tabbar) .sb-nav .sb-split > .sb-btn {
    width: var(--target); min-width: var(--target); height: var(--target); min-height: var(--target); padding: 0;
    font-size: 0; color: transparent; background: var(--fg);
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 15V3m9 12v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5l5-5'/%3E%3C/svg%3E") center / 45% no-repeat,
      radial-gradient(circle closest-side, black calc(100% - 2.5px), transparent calc(100% - 2px));
    -webkit-mask-composite: xor;
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 15V3m9 12v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5l5-5'/%3E%3C/svg%3E") center / 45% no-repeat,
      radial-gradient(circle closest-side, black calc(100% - 2.5px), transparent calc(100% - 2px));
    mask-composite: exclude;
  }
  body:has(.sb-tabbar) .sb-nav > .sb-btn[data-access] {
    position: relative; width: var(--target); min-width: var(--target); height: var(--target); min-height: var(--target); padding: 0;
    font-size: 0; color: transparent; background: none; border-color: transparent; box-shadow: none; animation: none;
  }
  body:has(.sb-tabbar) .sb-nav > .sb-btn[data-access]::after { content: none; }
  body:has(.sb-tabbar) .sb-nav > .sb-btn[data-access]::before,
  body:has(.sb-tabbar) .sb-nav > .sb-btn[data-access]:is(:hover, :focus-visible, :active)::before {
    inset: 2px; width: auto; height: auto; aspect-ratio: auto; border-radius: 50%;
    background: var(--fg); filter: none; mix-blend-mode: normal; opacity: 1; transform: none; animation: none; z-index: 0;
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 15V3m9 12v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5l5-5'/%3E%3C/svg%3E") center / 50% no-repeat,
      linear-gradient(black 0 0);
    -webkit-mask-composite: xor;
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 15V3m9 12v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5l5-5'/%3E%3C/svg%3E") center / 50% no-repeat,
      linear-gradient(black 0 0);
    mask-composite: exclude;
  }
  /* the Chat door on that header (2026-10-19, the picked phone mockup v1 "Swap circle"): where the page has it, it takes the
     install control's place as the same --fg dot (the square less 4px, a radial fill) with the bubble in --bg, and the install
     control leaves the header: the bar's download tab carries it, invite target and all. A page without the door (the env's web
     app not live) keeps the download dot above. */
  body:has(.sb-tabbar) .sb-header .sb-nav a.sb-nav-chat {
    display: inline-flex; width: var(--target); min-width: var(--target); height: var(--target); padding: 0; margin: 0;
    border-color: transparent; color: var(--bg);
    background: radial-gradient(circle closest-side, var(--fg) calc(100% - 2.5px), transparent calc(100% - 2px));
  }
  body:has(.sb-tabbar) .sb-header .sb-nav a.sb-nav-chat .sb-nav-chat-ic { width: 45%; height: 45%; }
  body:has(.sb-tabbar):has(.sb-nav-chat) .sb-nav .sb-split,
  body:has(.sb-tabbar):has(.sb-nav-chat) .sb-nav > .sb-btn[data-access] { display: none; }
  body:has(.sb-tabbar) { padding-bottom: var(--tabbar-clear); }
  /* a focused link or an anchor jump scrolls clear of the capsule (WCAG 2.4.11) */
  html:has(.sb-tabbar) { scroll-padding-bottom: var(--tabbar-clear); }
  /* the join-the-beta bar on / and /features (signin-sheet.css) is fixed to the bottom edge: it rides on top of the capsule,
     and its page padding (signup-bar.js sets --sb-cta-h) adds to the capsule's */
  body:has(.sb-tabbar) .sb-cta-bar { bottom: calc(var(--tabbar-top) + var(--sp-2)); padding-bottom: var(--sp-2); }
  html.sb-cta-pad body:has(.sb-tabbar) { padding-bottom: calc(var(--sb-cta-h, 0px) + var(--tabbar-clear)); }
}

/* ---- OS marks (os-detect.js) -------------------------------------------
   A platform glyph inside a control, left of its label, at currentColor.
   Two shared recipes: .sb-os-mark for the mark inside any .sb-btn (the
   header pill via nav-store.js, the download primary, the lander hero), and
   .dl-mark for the download row's fixed-width mark column. The svg carries
   its own width and height from os-detect.js osMark(); an empty .dl-mark
   renders nothing, so a no-JS row is unchanged. ---- */
.sb-os-mark { display: inline-flex; flex: none; align-items: center; justify-content: center; }
.sb-os-mark svg { display: block; }
.dl-mark { display: none; flex: none; width: 20px; height: var(--lh-base); align-items: center; justify-content: center; }
.dl-mark:not(:empty) { display: inline-flex; }
.dl-mark svg { display: block; }

/* ---- brand recipes (ported from the duostorm kit) ---- */

/* A brand tile: any square element with the mark image inside. It is a bare
   black rounded square holding the duotone face. The storm-gradient ring that
   used to be painted around it (a masked ::before) is retired 2026-10-06 (owner:
   "we don't use that anywhere anymore"); nothing on this sheet draws a stroke
   around a tile. .is-ringless is kept as a no-op class so old markup stays
   valid. The one ring the kit still has is .is-mono's flat --mono-ring, which
   lives in duostorm.css and is not the storm gradient. */
.sb-tile {
  position: relative;
  display: inline-grid;
  place-items: center;
  aspect-ratio: 1;
  background: var(--tile-bg);
  border-radius: var(--tile-radius);
  overflow: hidden;
}
.sb-tile > img { width: 72%; height: 72%; }

/* The storm aurora: three blurred storm blobs over the electric-blue ground,
   each centre a registered <percentage> pair animated alternate ease-in-out
   on its own incommensurate period. */
@property --sa-x1 { syntax: "<percentage>"; inherits: true; initial-value: 18%; }
@property --sa-y1 { syntax: "<percentage>"; inherits: true; initial-value: 22%; }
@property --sa-x2 { syntax: "<percentage>"; inherits: true; initial-value: 84%; }
@property --sa-y2 { syntax: "<percentage>"; inherits: true; initial-value: 30%; }
@property --sa-x3 { syntax: "<percentage>"; inherits: true; initial-value: 50%; }
@property --sa-y3 { syntax: "<percentage>"; inherits: true; initial-value: 88%; }
.sb-aurora {
  background:
    radial-gradient(circle at var(--sa-x1) var(--sa-y1), var(--storm-0) 0%, transparent 62%),
    radial-gradient(circle at var(--sa-x2) var(--sa-y2), var(--storm-4) 0%, transparent 62%),
    radial-gradient(circle at var(--sa-x3) var(--sa-y3), var(--storm-2) 0%, transparent 66%),
    linear-gradient(var(--storm-1), var(--storm-1));
  animation:
    sb-aurora-a var(--aurora-a) ease-in-out infinite alternate,
    sb-aurora-b var(--aurora-b) ease-in-out infinite alternate,
    sb-aurora-c var(--aurora-c) ease-in-out infinite alternate;
}
@keyframes sb-aurora-a { from { --sa-x1: 18%; --sa-y1: 22%; } to { --sa-x1: 78%; --sa-y1: 74%; } }
@keyframes sb-aurora-b { from { --sa-x2: 84%; --sa-y2: 30%; } to { --sa-x2: 16%; --sa-y2: 82%; } }
@keyframes sb-aurora-c { from { --sa-x3: 50%; --sa-y3: 88%; } to { --sa-x3: 56%; --sa-y3: 10%; } }

/* A storm-gradient outline on any control (focus ring, selected rail item). */
.sb-storm-outline {
  border: 2px solid transparent;
  background:
    linear-gradient(var(--raised), var(--raised)) padding-box,
    var(--storm-gradient) border-box;
}

/* The composer pill's glow: a blurred storm band under the pill. */
.sb-composer { position: relative; }
.sb-composer::before {
  content: "";
  position: absolute; inset: -12px -24px;
  background: var(--composer-glow);
  filter: blur(var(--composer-glow-blur));
  opacity: var(--composer-glow-opacity);
  border-radius: var(--r-pill);
  z-index: -1;
}

/* ---- surfaces + inline code ---- */
.sb-card { background: var(--card); border: 1px solid var(--line); border-radius: var(--r-surface); box-shadow: var(--shadow-card); }
p code, li code, .sb-code {
  background: var(--card); border: 1px solid var(--line); border-radius: var(--r-sm); padding: 1px 6px; color: var(--fg);
  /* a chip holding a URL has no break opportunity of its own: at 320px the
     uninstall commands on /docs ran 45px past the viewport (2026-09-02) */
  overflow-wrap: anywhere;
}
/* a flag-shaped chip (--uninstall) must not break at its own hyphen into two
   chips on a phone; long commands keep their wrap and do not take this class */
.sb-nowrap { white-space: nowrap; }

/* ---- no-JS install fallback ----
   edge/src/app.ts landerWithFallback renders bare <pre>/<p>/<a> into the
   #oneliner mount; install-snippet.js replaces them once it runs. These
   rules only ever paint for a reader without JavaScript. */
#oneliner:not(.sb-oneliner) { max-width: var(--content); margin-left: auto; margin-right: auto; text-align: left; }
#oneliner:not(.sb-oneliner) > pre {
  background: var(--card); border: 1px solid var(--line); border-radius: var(--r-surface);
  padding: var(--sp-3) var(--sp-4); overflow-x: auto; font-size: var(--fs-sm); line-height: var(--lh-sm); color: var(--fg);
}
#oneliner:not(.sb-oneliner) > p { color: var(--muted); font-size: var(--fs-sm); line-height: var(--lh-sm); margin-top: var(--sp-2); overflow-wrap: anywhere; }
#oneliner:not(.sb-oneliner) > p code { background: var(--card); border: 1px solid var(--line); border-radius: var(--r-sm); padding: 1px 6px; color: var(--fg); }
#oneliner:not(.sb-oneliner) > p a { color: var(--accent); }

/* ---- footer ---- */
.sb-footer {
  margin-top: var(--sp-section); border-top: 1px solid var(--line);
  color: var(--muted); font-size: var(--fs-xs); line-height: var(--lh-xs);
}
.sb-footer-in {
  max-width: var(--container); margin: 0 auto;
  padding: var(--sp-6) var(--gutter-r) max(var(--sp-6), env(safe-area-inset-bottom, 0px)) var(--gutter-l);
  display: flex; flex-wrap: wrap; gap: var(--sp-3) var(--sp-6); align-items: center; justify-content: space-between;
}
/* The same edges again, as longhands, and this rule is not redundant: a
   shorthand whose value carries var() is not expanded into longhands that the
   CSSOM can enumerate, so a checker reading `style[i]` saw an EMPTY padding
   on exactly the declarations that carry the notch (2026-09-17). The other
   checker reads the declaration TEXT, where only the shorthand line above
   shows the inset. Two readings of one computed padding — the shorthand is
   what the browser actually lays out; these longhands only make the floor
   legible to a checker that enumerates properties. Values are identical. */
@media (pointer: coarse) {
  .sb-footer-in { padding-left: var(--gutter-l); padding-right: var(--gutter-r); }
}
/* footer links: 24px is the WCAG floor a mouse gets; a finger gets the 44px
   token, and the row spacing shrinks so the taller rows do not read as a gap */
.sb-footer a { color: var(--muted); text-decoration: none; display: inline-flex; align-items: center; justify-content: center; min-height: max(24px, var(--target-sm)); }
.sb-footer a:hover, .sb-footer a:active { color: var(--fg); }
.sb-footer .links { display: flex; gap: var(--sp-4); flex-wrap: wrap; }
/* the X link: its mark sits before the handle (siteFooter, X_MARK) */
.sb-footer .sb-x { gap: 6px; }
/* 44 wide as well as tall: "home" is 29px of text, so the padding makes up
   the rest and the gap shrinks by the same amount */
@media (pointer: coarse) {
  .sb-footer a { min-width: var(--target-sm); padding: 0 var(--sp-2); }
  .sb-footer .links { gap: 0; margin: 0 calc(-1 * var(--sp-2)); }
  /* the -8px pull lets the padded tap boxes use the plain 16px gutter, but on a
     notched phone on its side the gutter IS the inset (--gutter-l/r = 62), so
     the last box sat 8px under the rounded corner (874x402, 2026-09-26). Each
     side pulls only as far as the gutter exceeds its inset: -8 without a
     notch, 0 beside one. */
  .sb-footer .links {
    margin-left: max(calc(-1 * var(--sp-2)), calc(env(safe-area-inset-left, 0px) - var(--gutter-l)));
    margin-right: max(calc(-1 * var(--sp-2)), calc(env(safe-area-inset-right, 0px) - var(--gutter-r)));
  }
}
.sb-footer .creed { color: var(--accent); }
/* the trademark line is a sentence a phone reads, not a caption: 14px (the
   mobile matrix flags it as running copy at the footer's 12px) */
.sb-footer .tm { font-size: var(--fs-sm); line-height: var(--lh-sm); }
/* phones: the footer measured 197px tall at 375 (241 at 320) under a 48px
   margin, "unnecessarily fat" (owner, 2026-09-25). The 44px link rows already
   carry 14px of air above and below their 16px text, so the stacked 12px gaps
   and the 24px padding were air on top of air: the gaps go, the padding and
   the margin above take one step down. Targets stay 44px (156px at 375). */
@media (max-width: 480px) {
  .sb-footer { margin-top: var(--sp-8); }
  .sb-footer-in {
    row-gap: 0;
    padding-top: var(--sp-4);
    padding-bottom: max(var(--sp-4), env(safe-area-inset-bottom, 0px));
  }
}

/* ---- entrance: fade-up, staggered by --i, one token per step ---- */
/* from .01, never 0: an LCP candidate first painted under opacity 0 is not counted (ga-audit 2026-10-01) */
@keyframes sb-rise { from { opacity: .01; transform: translateY(8px); } to { opacity: 1; transform: none; } }
@keyframes sb-fade { from { opacity: .01; } to { opacity: 1; } }
.sb-reveal {
  animation: sb-rise var(--dur-4) var(--ease-out) both;
  animation-delay: calc(var(--i, 0) * 60ms);
}

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

/* ---- theme-twin art: an element drawn once per theme (a dark mascot and its
   -light twin, a chart) carries one of these and the other theme hides it. They
   hide, never show, so the element keeps its own display; !important is the
   point of a utility, a page's `.hero-img { display: grid }` must not beat it.
   Dark is the default (no attribute, no script, the dark html), so only light
   hides .sb-only-dark and everything that is not light hides .sb-only-light.
   A dark island (an element carrying data-theme="dark") inside a light page is
   read as dark: the :not() leaves its .sb-only-dark art showing and hides its
   .sb-only-light art. ---- */
html[data-theme="light"] .sb-only-dark:not([data-theme="dark"] *) { display: none !important; }
html:not([data-theme="light"]) .sb-only-light, [data-theme="dark"] .sb-only-light { display: none !important; }
/* a dark island's text: the island re-declares the ladder, but `color` is inherited
   as a computed value, so without this it would carry the light page's ink onto
   its dark ground. The island's background is its own (a card, a mock, a hero). */
[data-theme="dark"]:not(html) { color: var(--fg); }

/* ---- the whole page reads a quarter larger on wide screens (owner,
   2026-09-01): a plain zoom rather than a second token sheet. Phones and
   small laptops stay at 1x so the 390px layouts keep their exact fit.
   zoom inflates dvh measurements non-uniformly — inside this tier a 100dvh
   probe resolves 1.25× the real viewport, so any 100dvh consumer overflows
   by exactly the zoom factor (measured 2026-09-04, zoom, resolved
   2026-09-08). The pin now lives here instead of per page: the body's own
   dvh min-height (above) drops to a percentage inside the tier, because
   percentages still resolve against the real viewport under zoom — the same
   idiom chat.html proved on its shell. The clean replacement — a rem-based
   root scale (html font-size 125% with the token sheet in rem) — would
   rewrite every page's px-token consumption, including sheets this file does
   not own; until that lands, each new 100dvh consumer needs the same
   percentage pin. ---- */
@media (min-width: 1200px) {
  html { zoom: 1.25; height: 100%; }
  body { min-height: 100%; }
  .sb-drawer { height: 100%; } /* the same pin for the drawer's 100dvh (a zoomed 200% text axis can still install it) */
}
