/* rainyseason.cr — line. Fira Code throughout (the house font),
   terminal ink, Swiss and square: no curved edges anywhere. Fixed frame;
   when the mobile keyboard opens, the composer holds and messages move. */

@font-face { font-family: "Fira Code"; src: url("/fonts/FiraCode-Regular.woff2") format("woff2"); font-weight: 400; font-display: swap; }
@font-face { font-family: "Fira Code"; src: url("/fonts/FiraCode-Medium.woff2") format("woff2"); font-weight: 500; font-display: swap; }
@font-face { font-family: "Fira Code"; src: url("/fonts/FiraCode-SemiBold.woff2") format("woff2"); font-weight: 600; font-display: swap; }

:root {
  /* The skin IS the shared tokens now (tokens.css, loaded first): the ground,
     the one white, the ink ladder, the hairlines, the rail, the CTA floor and
     the locked six senses (plus the --accent/--spark/--storm/--gold shims) all
     resolve there. The retired 2026-07 palette this file used to re-pin over
     them — the #0007cc blue, the orange sun, the purple hairlines, the grey
     swatches, the second white — is gone (de-drifted 2026-08-06); only names
     tokens.css does not know stay local, and each one points at a token. */
  --ground: var(--g);        /* the one ground — vanta black */
  --panel:  var(--g-elev);   /* raised panels — the OFF tone */
  --dim:    var(--ink-3);    /* the secondary voice — the 50% rung */
  --faint:  var(--ink-4);    /* the quietest meta — the 30% rung */
  --accent-ink: var(--lit);  /* ink that sits on the accent — the one white */
  /* The refusal voice. The old palette spoke alarms in the sun-orange; under
     the six-sense lock the sun is a dark green, so refusals move to the one
     hot red-family member of the six — the scent hibiscus. */
  --alarm: var(--hibiscus);
  /* Fira Code everywhere — the design-system font. Both local names point at
     the house token so every element inherits the one typeface. */
  --mono: var(--font-code);
  --sans: var(--font-code);
}

* { margin: 0; padding: 0; box-sizing: border-box; }
[hidden] { display: none !important; }

html, body {
  overscroll-behavior-x: none; /* a horizontal trackpad swipe is OURS (reply), never history navigation */
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  background: var(--ground);
  color: var(--ink);
  font: 16px/1.5 var(--mono);
  -webkit-font-smoothing: antialiased;
  overflow: hidden;
  overscroll-behavior: none;
}
button, input, textarea { font: inherit; color: inherit; }
::selection { background: color-mix(in srgb, var(--accent) 45%, transparent); }

/* The frame. JS sizes it to the visible viewport so the keyboard never
   covers the composer and the page itself never scrolls. */
.app {
  position: fixed;
  top: 0; left: 0; right: 0;
  height: 100dvh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  transform: none;
}

.view { flex: 1; display: flex; flex-direction: column; min-height: 0; }
.view.center {
  align-items: center; justify-content: center; text-align: center;
  padding: 32px 28px; gap: 22px;
}

/* The centre marks are the FULL all-segments component now (the one-component
   law) — taller than the old square crops (viewBox 74.8×94.3), sized so the
   lit cloud holds near its old optical width. */
.mark { width: 96px; height: auto; }
.wordmark { letter-spacing: 0.16em; font-size: 13px; color: var(--dim); text-transform: uppercase; }
.soft { color: var(--ink-2); } /* the screen's one message — reading copy reads at the 72% rung */
/* The estate's name at the foot of the standalone screens and the privacy
   sheet, so people know whose domain they are at (William, 2026-08-07).
   Lowercase, quiet, the caption size. */
.foot-domain {
  font-family: var(--mono); font-size: var(--t-caption); letter-spacing: var(--track-micro);
  color: var(--ink-3); text-align: center;
}
.view.center { position: relative; }
.view.center .foot-domain {
  position: absolute; left: 0; right: 0;
  bottom: max(18px, env(safe-area-inset-bottom));
}
.rain-lines { font-size: 15px; font-weight: 400; letter-spacing: 0.12em; line-height: 2.4; }

/* The gate runs the site's stage-hero--center zone grid (shell.css): three rows
   1fr / auto / 1fr, so the H1 pins to the EXACT vertical centre of the screen —
   level with /services and /experiences. The bar + mark fill the top
   zone (mark hugging the H1 from above); the statement, promises and CTAs fill
   the bottom zone, the CTAs holding the floor. */
/* The grid is inset top/bottom exactly as the site's stage-hero (padding 24 /
   --btn-floor), so the hero centres on the same line as /experiences AND the
   CTAs floor at the same height. The bottom pad carries the CTA floor, so the
   form's own floor padding drops (below). */
/* Nothing sits below a primary button on any step, so the floor is just this
   padding: the button base rides at --btn-floor from the frame's bottom edge,
   or above the home indicator where a safe-area inset is larger. */
.gate { position: relative; display: grid; grid-template-rows: 1fr auto 1fr; grid-template-columns: minmax(0, 1fr); min-height: 0; padding: max(24px, env(safe-area-inset-top)) 0 max(var(--btn-floor), env(safe-area-inset-bottom)); }
/* The bar OVERLAYS the top exactly as the site's fixed bar does, so it takes
   NO grid space — the hero grid (mark / title / body) spans the full screen
   and the title centres at the identical height to /experiences. The encryption
   strip lives in the conversation only (William, 2026-08-07). min-width: 0 lets
   the zones shrink to the viewport (grid items default to min-content) so
   nothing overflows. */
.gate-chrome { position: absolute; top: 0; left: 0; right: 0; z-index: 5; }
.gate-lockup {
  grid-row: 2; align-self: center; width: 100%; min-width: 0;
  display: flex; flex-direction: column; align-items: center;
  text-align: center; padding: 0 24px;
  /* The title rides the shared lift MINUS the text drop (tokens.css),
     matching the site's /path titles exactly (William, 2026-08-05). */
  transform: translateY(calc(-1 * var(--hero-lift, 55px) + var(--hero-title-drop, 22px)));
}
.gate-bottom {
  grid-row: 3; align-self: stretch; min-height: 0; min-width: 0; width: 100%;
  display: flex; flex-direction: column; align-items: center;
  text-align: center; padding: 0 24px;
}
/* The body copy rides WITH the title (the site's centre-zone law): tagline and
   trust lines take the same lift-minus-drop; the gate body (CTAs, steps) holds
   the floor (William, 2026-08-05). */
.gate-bottom > .gate-tagline,
.gate-bottom > .gate-trust {
  transform: translateY(calc(-1 * var(--hero-lift, 55px) + var(--hero-title-drop, 22px)));
}
/* The live 3D hero. The mount div wears the shared .hero-symbol (cloud.css) for
   its seat, size and short-viewport ladder — THE ONE cloud spec, byte-shared
   with the site + wallet so the gate cloud can never drift. .gate-3d now carries
   ONLY the gate's own grid placement of that mount. */
.gate-3d {
  position: relative;
  grid-row: 1; align-self: start; min-width: 0;
}
/* The left pad on the name matches its tracking so the word sits optically
   centred — the trailing letter-space would pull it off-axis. */
.gate-3d .g2d {
  position: absolute; inset: 0; margin: auto; height: 67%;   /* 86 x 0.78: the 2D still shrinks 22% with the live mark (William, 2026-08-06) */ width: auto;
  opacity: 1; transition: opacity 700ms ease;
}
.gate-3d.live .g2d { opacity: 0; }
/* The private line title is a small strong lowercase eyebrow (William, 2026-08-01):
   the statement and the rainyseason.cr wordmark below it carry the hero now, so
   the title sits above them as a quiet mono label, not a big hero word. All
   truly centred (no optical padding nudge; the whole column shares one axis). */
/* The title runs a touch smaller than the site hero word (William, 2026-08-10,
   "it doesn't need to be this big") — its own min(26px, 4.4vw) rather than the
   shared --t-hero clamp, held under the old 5.35vw so the 24-character domain
   line.rainyseason.cr still fits narrow phones with no break point (the
   string can't wrap; smaller only fits easier). The alt slugs still override to
   --t-path below. */
.gate-h1 { font-family: var(--mono); font-size: min(26px, 4.4vw); font-weight: 500; letter-spacing: var(--track-wordmark); line-height: 1.15; text-transform: lowercase; color: var(--ink); text-wrap: balance; }
.gate-lead { margin-top: 9px; font-family: var(--mono); font-size: 11px; font-weight: 500; letter-spacing: 0.14em; line-height: 1.2; text-transform: uppercase; color: var(--ink-3); }
/* The statement runs the site's hero body measure (42ch) so it sets to two
   balanced lines, the same width as every other body-copy container. */
/* Byline-to-statement gap matches the site's stage hero exactly: 24px cluster
   gap + 16px on the lead = 40px, so the body sits on the same line as
   /experiences. */
/* The statement sits --hero-copy-gap under the word — the same token as every
   site hero, so the two origins share one word-to-copy rhythm. It wears the
   site's LEAD CHIP dress (hero.css .page-hero-loc--lead — the chip-led hero
   grammar, aligned 2026-08-06): black chip, --t-body mono, zero tracking,
   1.5 leading, the 34ch chip measure. No text-wrap:balance — the tagline
   carries a hand-set br, and hand-set lines beat the balancer (the type.css
   law). */
.gate-tagline { margin-top: var(--hero-copy-gap, 12px); padding: 2px 6px; background: #000; font-family: var(--mono); font-size: var(--t-body); font-weight: 400; letter-spacing: 0; line-height: 1.5; color: var(--ink-2); /* the soft ceiling, matched to the site chip (William, 2026-08-06) */ max-width: 34ch; text-align: center; }
/* In the bottom zone the promises float centred between the statement above and
   the floored CTAs below (a margin-top:auto here and on .gate-body split the
   free air evenly). Two by two, and every promise holds one line beside its
   icon (nowrap) — sized so both columns fit the narrowest supported phone
   (375px; the app needs iOS 17). Fira Code advances 0.615em/char: the English
   rows are 38ch ≤ 11px works; the Spanish rows are 49ch, so es reads smaller
   and tighter. The block centres as a whole; each column's icons share an edge. */
.gate-trust {
  list-style: none; margin: auto auto 0; width: 100%;
  display: grid; grid-template-columns: auto auto;
  justify-content: center; justify-items: start; align-items: center;
  column-gap: 20px; row-gap: 12px; /* a breath more air between the four promises (William, 2026-08-06); still two columns inside 375px in both languages */
}
.gate-trust li {
  display: flex; align-items: center; justify-content: flex-start; gap: 5px;
  font-family: var(--mono); font-size: 11px; letter-spacing: 0.04em; color: var(--ink-2); /* matched to the site follow line (William, 2026-08-06) */
  white-space: nowrap;
}
html[lang="es"] .gate-trust li { font-size: 8.5px; letter-spacing: 0.02em; }
.gate-trust .t-ic { width: 12px; height: 12px; color: inherit; flex: none; }
.gate-trust .t-link { display: inline-flex; align-items: center; gap: 6px; color: inherit; text-decoration: none; transition: color 0.15s ease; }
.gate-trust .t-link span { text-underline-offset: 3px; }
.gate-trust .t-link--github .t-ic { color: var(--accent-ink); }
.gate-trust .t-link:hover { color: var(--ink); }
.gate-trust .t-link:hover span, .gate-trust .t-link:active span { text-decoration: underline; text-decoration-color: var(--accent); }
.gate-body {
  flex: none; width: 100%; max-width: 460px; margin: auto auto 0;
  /* Floor comes from .gate's bottom padding now, so no bottom pad here. No side
     padding either: .gate-bottom's 24px inset is the one edge, so the CTAs run
     the full column and line up with the statement above them. */
  padding: 0;
  display: flex; flex-direction: column; gap: 11px;
}
@keyframes rise { from { opacity: 0; transform: translateY(26px); } to { opacity: 1; transform: none; } }
#stepStart > * { animation: rise 0.55s cubic-bezier(0.22, 1, 0.36, 1) both; }
#stepStart > *:nth-child(1) { animation-delay: 0.04s; }
#stepStart > *:nth-child(2) { animation-delay: 0.11s; }
#stepStart > *:nth-child(3) { animation-delay: 0.18s; }
#stepStart > *:nth-child(4) { animation-delay: 0.25s; }
@media (prefers-reduced-motion: reduce) { #stepStart > *, .gate-step { animation: none; } }
.gate-step { display: flex; flex-direction: column; gap: 12px; }
/* The primary button is the last row on every step — one shared floor, no
   reserves. Secondary links (Resend code) sit above the button, never below. */
/* The consent gate sits with the form: a filled-circle mark that fills accent
   when read, and holds both the email field and the CONTINUE button until it
   is ticked. */
/* One quiet line, not a form: the sentence never wraps (10.5px holds it whole
   at 375px in both languages). The mark wears NO resting ring (William,
   2026-08-07) — it is only the dot, dormant in the off tone like an unlit
   segment, lighting to the accent when ticked. */
.gate-consent { display: flex; align-items: center; gap: 10px; font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.02em; line-height: 1.4; color: var(--ink-2); cursor: pointer; user-select: none; white-space: nowrap; }
.gate-consent input { appearance: none; -webkit-appearance: none; flex: none; width: 18px; height: 18px; margin: 0; border: 0; border-radius: 50%; background: transparent; -webkit-tap-highlight-color: transparent; display: grid; place-content: center; cursor: pointer; }
.gate-consent input::before { content: ""; width: 10px; height: 10px; border-radius: 50%; background: var(--wx-off, #101010); transition: background 0.12s ease; }
.gate-consent input:checked::before { background: var(--accent); }
.gate-consent input:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.gate-step .btn:disabled { opacity: 0.4; cursor: not-allowed; }
/* Only the DOORS wear a mark (an exception to the text-only button law in
   button.css): the cloud on START A CONVERSATION, the blue stars on the
   client journey's passkey doors. Action buttons (CONTINUE, the returns)
   are words alone. The stars mark is sized like the cloud marks; the cloud
   is un-hidden past the law with an id-scoped rule. */
/* The gate's glyphs are the all-segments master now (the symbol-button
   standard, William 2026-08-05): button.css (synced) sizes, shows and
   states them; the light keys sit on the buttons (cloud / conc). */
/* The client-journey doors wear the accent stroke at rest — the blue ring.
   The ids outrank every shared state rule, so the ring holds through hover
   and focus; .primary elsewhere keeps its hover-only accent. #openKey left
   this list 2026-08-13: it wears the PRIMARY rank now (.btn--primary,
   button.css) — dark chip, the full set ring, the strike — and an id-pinned
   border or colour here would fight that dress. */
#createPasskeyBtn,
#returnKeyBtn { border-color: var(--accent); }
/* A short viewport (a small phone with browser chrome) compresses the same
   column further instead of ever growing a scroll. These are scoped under
   .gate.signup so they outrank the base component rules that appear LATER in
   this file (equal-specificity, later-wins would otherwise beat a plain
   .trust-body here). */
@media (max-height: 700px) {
  /* .gate-3d's 33vh gate step now lives in the shared cloud.css (.hero-symbol
     GATE profile) — the mount wears .hero-symbol, so the step comes from there. */
  .gate.signup .gate-lockup { padding-top: 2px; }
  .gate.signup .gate-body { gap: 5px; }
  /* The point faces compress for BOTH four-point screens (.gate scope, not
     .signup, so signup and passkey shrink identically); the cluster geometry
     compresses through its tokens in the media block beside them. */
  .gate .trust-title, .gate .passkey-copy__title { font-size: 12.5px; margin-bottom: 1px; }
  .gate .trust-body, .gate .passkey-copy__text { font-size: 11px; line-height: 1.3; }
  .gate.signup .signup-form { gap: 7px; }
}
#stepInvite:not([hidden]), #stepEmail:not([hidden]), #stepCode:not([hidden]), #stepPasskey:not([hidden]) { animation: step-fade 0.28s ease both; }
@keyframes step-fade { from { opacity: 0; } to { opacity: 1; } }
/* Home-screen banner — closeable, above the composer in chat. The icon
   wears the iOS rounded-square: it depicts the OS icon. */
.a2hs-banner {
  flex: none; display: flex; align-items: center; gap: 15px;
  margin: 0 18px 14px; padding: 2px 0;
}
.a2hs-banner:first-of-type { margin-top: 6px; }
#notifyBanner { cursor: pointer; }
.a2hs-ic { position: relative; flex: none; display: inline-flex; }
.a2hs-banner img { width: 62px; height: 62px; border-radius: 15px; display: block; box-shadow: 0 5px 16px rgba(0, 0, 0, 0.55); }
#passkeyBanner .a2hs-ic svg { box-sizing: border-box; width: 62px; height: 62px; padding: 15px; border-radius: 15px; display: block; background: color-mix(in srgb, var(--ink) 6%, transparent); box-shadow: 0 5px 16px rgba(0, 0, 0, 0.55); }
/* The badge is OS chrome and can't be recoloured from a web app — it
   stays the system red. */
.a2hs-badge {
  position: absolute; top: -4px; right: -4px;
  width: 14px; height: 14px; border-radius: 50%; background: #ff3b30;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.45);
}
.a2hs-b-txt { flex: 1; min-width: 0; line-height: 1.45; }
.a2hs-b-head { display: flex; align-items: center; gap: 10px; }
.a2hs-b-txt strong { flex: 1; min-width: 0; font-size: 14px; color: var(--ink); }
.a2hs-b-txt span { display: flex; align-items: center; gap: 5px; font-size: 11px; color: var(--dim); }
.a2hs-b-txt .share-ic { color: var(--accent); flex: none; }
/* The × overflows the row without stretching it (negative margins) so the
   two lines keep their rhythm. */
.a2hs-x { background: none; border: 0; color: var(--ink); font-size: 30px; font-weight: 500; line-height: 1; cursor: pointer; flex: none; padding: 0 2px; margin: -8px -4px -8px 0; }
.a2hs-x:active { color: var(--dim); }
/* The button — .btn and every variant — is the shared component in button.css
   (linked in the head, synced from packages/shell/ by tools/sync-door-menu.sh).
   Only the gate's ROW layout stays local: full-width, centred. */
.gate-step .btn { width: 100%; justify-content: center; }
/* The access-key field joins the button's ONE full-row width law: the shared
   .btn caps at --btn-w-full (420) while the gate step tracks 460, so the field
   rendered 40 wider than the CONTINUE beneath it (William, 2026-08-08: the
   pair must always hold the same width). Same cap, same auto centring — the
   two edges align at every viewport. */
.gate-step .field-box { width: 100%; max-width: var(--btn-w-full, 420px); margin-inline: auto; }
/* The email step is the data-security detail view. The cloud, the 2x2 bullets
   and the landing tagline give way to those same four promises written out in
   full, so the heading, the copy, the field and the button read as one column
   that never overlaps. */
/* The email and passkey detail steps collapse the centred landing grid to a
   simple top-anchored column: the topic, its explanation and one action. */
.gate.signup,
.gate.passkey-setup,
.gate.gate--account-deleted { display: flex; flex-direction: column; overflow-y: auto; overscroll-behavior: contain; }
.gate.signup .gate-3d,
.gate.signup .gate-trust,
.gate.signup .gate-tagline,
.gate.signup .gate-lead,
.gate.passkey-setup .gate-3d,
.gate.passkey-setup .gate-trust,
.gate.passkey-setup .gate-tagline,
.gate.passkey-setup .gate-lead,
.gate.gate--account-deleted .gate-3d,
.gate.gate--account-deleted .gate-trust,
.gate.gate--account-deleted .gate-tagline,
.gate.gate--account-deleted .gate-lead { display: none; }
/* The gate's particle field — the accent blue scatter riding behind the
   components (William, 2026-08-07; every step since 2026-08-13). The layer
   sits at z-index -1 inside the relative gate: above the vanta ground, under
   every component — and the components are glass (transparent buttons and
   fields) so the stars read through them. It used to show on the signup step
   only, wary of a second WebGL context beside the landing's 3D cloud mark —
   but the site seats this same field beside the 3D mark behind every hero,
   so the pair is the proven house grammar; the one-context caution belonged
   to the six stacked never-released contexts that crashed the WKWebView. */
.gate-particles { position: absolute; inset: 0; z-index: -1; pointer-events: none; }
.gate-particles .sense-ph { position: absolute; inset: 0; }
/* The colour token cloud-particles.js reads (--starN on the page root):
   star 1 = the sound blue, the private line's accent scatter (William 2026-08-10). */
:root { --star1: var(--thunder); }

/* THE line accent is the sound blue (William, 2026-08-10): buttons, form
   fields, carets, selection, links, the ring beams. Set on body, not :root, on
   purpose — the 3D weather mark ALONE reads --accent off
   document.documentElement (html), so the html rung is the mark's own dial
   while every UI surface under body reads sound. Since 2026-08-13 that dial
   turns the mark's hard rain down to the darker chrome intuition, deepened
   the same day to #3029E7 x0.18 = #090729 (below the OFF tone's darkness,
   a shade above the vanta cloud): still the blue, but fully receded, so the
   permanently lit bolts (data-wx-lit on the mount) carry the mark. */
:root { --accent: #090729; }
body { --accent: var(--thunder); }
/* The chrome rejoins the flow, so the detail sits below it rather than under
   the overlay. The gate's top padding drops to ZERO in every static-chrome
   state (enrolled included): the bar carries its own safe-area padding, so
   its flow seat matches the landing's overlay seat exactly — ONE bar line on
   every step. It used to drop by the 24px/safe-area stage inset on top of
   the bar's own inset (William, 2026-08-07). */
.gate.signup,
.gate.passkey-setup,
.gate.gate--account-deleted,
.gate.enrolled { padding-top: 0; }
.gate.signup .gate-chrome,
.gate.passkey-setup .gate-chrome,
.gate.gate--account-deleted .gate-chrome { position: static; }
/* In these flex-column detail steps the title is a heading in the flow, NOT a
   floating hero: drop the centre-zone lift (translateY -33px) so it sits under
   the bar instead of colliding with it (fixed 2026-08-06). */
.gate.signup .gate-lockup,
.gate.passkey-setup .gate-lockup,
.gate.gate--account-deleted .gate-lockup { transform: none; }
/* The email step carries the most content of any screen (four assurances,
   consent, field, button); its headroom runs tighter so the whole column fits
   the frame with the button on the floor: no scroll, ever. */
/* The detail-step titles sit ON the column: the lockup takes the step's own
   40ch measure (all resolve ch against the same inherited size — none sets
   its own) and the slug reads LEFT on it, lined up with the copy below
   (William, 2026-08-07). Account-deleted joined the same dress — no more
   centred exception (William, 2026-08-07). */
/* One title seat for the whole flow: /create-passkey and /account-deleted hold
   the exact line /private-messaging holds (same headroom clamp). */
.gate.signup .gate-lockup,
.gate.passkey-setup .gate-lockup,
.gate.gate--account-deleted .gate-lockup { padding-top: clamp(10px, 2.5vh, 28px); }
.gate.signup .gate-lockup,
.gate.passkey-setup .gate-lockup,
.gate.gate--account-deleted .gate-lockup { max-width: 40ch; margin-inline: auto; align-items: flex-start; }
.gate.signup .gate-h1,
.gate.passkey-setup .gate-h1,
.gate.gate--account-deleted .gate-h1 { text-align: left; }
/* Detail-step titles (/private-messaging, /create-passkey, /account-deleted)
   run one size DOWN from the hero — the site's long-slug path scale (--t-path,
   the /adventures-for-the-senses dress), not --t-hero and not a hand-typed
   22px (William, 2026-08-07). Only the landing /line keeps the hero word. */
.gate.signup .gate-h1,
.gate.passkey-setup .gate-h1,
.gate.gate--account-deleted .gate-h1 { font-size: var(--t-path); }
.gate.signup .gate-bottom,
.gate.passkey-setup .gate-bottom,
.gate.gate--account-deleted .gate-bottom { flex: 1; min-height: 0; }
.gate.signup .gate-err:empty { display: none; }
/* The return step is a detail step too: the hero and name stay, but the
   landing's statement and promises give way to the one ask — sign in — so
   its button holds the same floor as every other step's. */
.gate.gate--return .gate-tagline,
.gate.gate--return .gate-trust { display: none; }
/* The body fills the space beneath the heading. The signup column (the email
   AND code steps — one dressing, William 2026-08-07) lives on .gate-body now:
   the shared four-point cluster holds the free middle, #gateErr and the active
   step's form group sit under it, the form flooring so the button lands on
   the same line as every other step's. The cluster is hoisted out of the
   steps (one list, both steps); order -1 leads it past the DOM's gateErr, so
   errors surface just above the form — the consent line's slot. */
.gate.signup .gate-body,
.gate.passkey-setup .gate-body {
  flex: 1; min-height: 0; width: 100%; max-width: 40ch; margin: 0 auto; padding-inline: 0;
  display: flex; flex-direction: column; gap: 0; /* the air lives in the list's stretch; the short-viewport block below restores a minimum */
  text-align: left; /* the whole detail column reads left, under the left-set title */
}
.gate-body > .trust-list { display: none; }
.gate.signup .gate-body > .trust-list { display: flex; order: -1; }
/* Four identical assurances: a clean icon-and-text row each, the icon white
   (structural, not one of the six colours), the body wrapping naturally at the
   column measure. No boxes — hierarchy from the icon, the title weight and the
   ink step. The icon holds the TITLE row only and centres with it — never
   baseline- or top-aligned (William, 2026-08-07); the body hangs in its own
   row under the title. */
/* THE GATE CLUSTER TOKENS — one geometry for every four-point screen
   (/private-messaging AND /create-passkey), so the rows land on IDENTICAL
   lines by construction, not by copy length (William, 2026-08-07): the
   cluster anchors at --cluster-top under the title, every point occupies one
   --point-slot regardless of how far its body wraps, and --point-gap is the
   one air between slots. Change a token, both screens move together. */
:root {
  --cluster-top: clamp(14px, 5vh, 44px); /* title to first point */
  /* One point row: the 20px icon rules the title row, then a three-line body.
     MEASURED at 80 (tools/cluster-measure.mjs, 390x720: the signup points
     render 79px; the old hand-mathed 74 let them overflow and the drift
     accumulated 5/9/13px down the column — 2026-08-07). */
  --point-slot: 80px;
  --point-gap: clamp(12px, 2.2vh, 19px); /* between points */
}
/* Short screens compress the cluster through its tokens — after the
   definitions so the override wins; both screens shrink together. The slot
   again fits the measured compressed point (title row 20 + three 14.3px
   lines), not wishful arithmetic. */
@media (max-height: 700px) {
  :root { --cluster-top: 6px; --point-slot: 66px; --point-gap: 6px; }
}
.trust-list { list-style: none; margin: 0; padding: var(--cluster-top) 0 0; flex: 1; min-height: 0; display: flex; flex-direction: column; justify-content: flex-start; gap: var(--point-gap); }
/* HONEST GRID (found 2026-08-07): the old template named "icon title"/"body"
   areas, but the title and body are NOT grid items — they live inside
   .trust-text, which had no area, so the text block was AUTO-placed and the
   icon floated against the whole block (mid-body in Chrome, differently in
   the desk's WebKit — auto-placement is engine soup). Two columns, ONE row,
   two real items: the icon column and the text column, both top-aligned; the
   20px glyph sits on the ~19px title line, so it reads centred with the
   title by construction, in every engine. */
.trust-point { display: grid; grid-template-columns: 20px minmax(0, 1fr); column-gap: 13px; align-items: start; align-content: start; min-height: var(--point-slot); }
.trust-ic { width: 20px; height: 20px; color: var(--ink); }
/* ONE face for these gate paragraphs: the passkey points carry the trust
   classes directly now; the grouped __title/__text selectors keep the return
   step's welcome in the same dress (William, 2026-08-07). These are normal
   flow inside .trust-text — the 2px title margin is the title-to-body air. */
.trust-title, .passkey-copy__title { margin: 0 0 2px; font-family: var(--mono); font-size: 14px; font-weight: 500; line-height: 1.35; color: var(--ink); }
/* 1.4 leading — the paragraphs read a notch tighter than body prose (was 1.5,
   a little too open in the four-point stack, William, 2026-08-07). */
.trust-body, .passkey-copy__text { margin: 0; font-family: var(--mono); font-size: 12.5px; line-height: 1.4; color: var(--ink-2); }
/* The fourth point opens the source through one word in the sentence —
   "public" wears the gate's inline link dress (.pn-link, below) — so no
   full-cover link and no trailing "View the code" tag (William, 2026-08-07). */
/* The sign-in group floors as a unit. */
.signup-form { margin-top: auto; display: flex; flex-direction: column; gap: 10px; }
/* The wordmark carries the active topic. */
.gate-h1 .gh-alt,
.gate-h1 .gh-passkey,
.gate-h1 .gh-deleted { display: none; }
.gate.signup .gate-h1 .gh-main { display: none; }
.gate.signup .gate-h1 .gh-alt { display: inline; }
.gate.passkey-setup .gate-h1 .gh-main { display: none; }
.gate.passkey-setup .gate-h1 .gh-passkey { display: inline; }
.gate.gate--account-deleted .gate-h1 .gh-main { display: none; }
.gate.gate--account-deleted .gate-h1 .gh-deleted { display: inline; }
/* Passkey creation answers only the three questions needed to make the choice.
   The action holds the established button floor beneath the explanation. */
.gate.passkey-setup .gate-err:empty { display: none; }
/* The passkey step mirrors the signup column exactly: the step fills the
   body, its four points hold the centred cluster (the trust-list treatment),
   the button floors — one geometry across the whole flow (William,
   2026-08-07). :not([hidden]) keeps the display:flex from defeating the
   [hidden] swap. */
.gate.passkey-setup #stepPasskey:not([hidden]) { flex: 1; min-height: 0; display: flex; flex-direction: column; }
/* The four points ride the SAME cluster tokens as the signup screen: same
   top anchor, same fixed slots, same gap — identical lines by construction
   (William, 2026-08-07). */
.gate.passkey-setup .passkey-copy { flex: 1; min-height: 0; justify-content: flex-start; padding-top: var(--cluster-top); }
/* The passkey copy holds the signup column's measure (40ch) and the shared
   cluster tokens, so the two screens read as one family; the faces themselves
   are grouped with .trust-title / .trust-body above (William, 2026-08-07). */
.passkey-copy { width: 100%; max-width: 40ch; margin: 0 auto; display: flex; flex-direction: column; gap: var(--point-gap); text-align: left; }
.passkey-copy__title { margin: 0 0 2px; }
.gate.passkey-setup #createPasskeyBtn { margin-top: auto; }
/* /create-passkey centres as ONE group (William, 2026-08-08): the title and
   the four points ride together at the vertical centre of the room instead of
   anchoring top-down. The wrappers between the .gate column and the group
   (.gate-bottom, .gate-body, the step) flatten away, so the lockup, the
   cluster and the button share ONE flex context — the two auto margins (above
   the title, above the button) split the free space evenly BY CONSTRUCTION,
   and the button keeps the shared floor. Each freed child re-states the exact
   horizontal seat the wrappers used to hand it: min(40ch, 100% - 48px),
   centred — the same box the old chain (gate-bottom's 24px inset + the 40ch
   body) produced, verified like-for-like at 390x720. When the room runs out
   the autos collapse to zero together and .gate's overflow-y takes over. */
.gate.passkey-setup .gate-bottom,
.gate.passkey-setup .gate-body,
.gate.passkey-setup #stepPasskey:not([hidden]) { display: contents; }
.gate.passkey-setup .gate-lockup { margin-top: auto; padding-top: 0; }
.gate.passkey-setup .passkey-copy { flex: none; min-height: 0; width: calc(100% - 48px); max-width: 40ch; }
.gate.passkey-setup .gate-err,
.gate.passkey-setup #createPasskeyBtn { width: calc(100% - 48px); max-width: 40ch; margin-inline: auto; }
/* The error speaks at the FLOOR on this step (William, 2026-08-09): riding
   just above the button, never between the title and the points. Flex order
   seats it after the cluster; when it speaks it takes the floor auto margin
   and hands the button a fixed 12px gap, so the centred group above holds
   its seat. */
.gate.passkey-setup .gate-err { order: 1; }
.gate.passkey-setup #createPasskeyBtn { order: 2; }
.gate.passkey-setup .gate-err:not(:empty) { margin-top: auto; }
.gate.passkey-setup .gate-err:not(:empty) ~ .gate-step #createPasskeyBtn { margin-top: 12px; }
/* Account deletion wears the passkey step's dress EXACTLY: the step fills the
   body, one trust-point (trash octicon at 20 / title / body) on the shared
   cluster tokens, the button floors. It confirms what left, then names the
   narrow record-retention exception only when true (William, 2026-08-07). */
.gate.gate--account-deleted .gate-body { flex: 1; min-height: 0; margin: 0 auto; }
.gate.gate--account-deleted .gate-err { display: none; }
.gate.gate--account-deleted #stepAccountDeleted { flex: 1; min-height: 0; }
.gate.gate--account-deleted .passkey-copy { flex: 1; min-height: 0; justify-content: flex-start; padding-top: var(--cluster-top); }
.account-deletion__note { margin: 0; font-family: var(--mono); font-size: 12.5px; line-height: 1.6; color: var(--ink-2); }
.gate.gate--account-deleted #returnToFrontDesk { margin-top: auto; }
/* .primary / .quiet variants + the flashlight beam live in button.css. */
.linklike {
  background: none; border: 0; color: var(--dim);
  font-size: 13px; line-height: 17px; cursor: pointer; padding: 6px; text-decoration: underline;
}
/* The code step's resend is the consent line's TWIN — same left seat, same
   18px row height (the checkbox's), the privacy-notice link face — so the
   field box above the floor sits at the identical line on both steps and the
   geometry never jumps (William, 2026-08-07). */
#stepCode .linklike {
  align-self: flex-start; margin: 0; padding: 0; min-height: 18px;
  display: inline-flex; align-items: center;
  font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.02em; line-height: 1.4;
  color: var(--ink); text-decoration: underline; text-underline-offset: 2px;
}
.gate-title { font-weight: 600; font-size: 18px; letter-spacing: 0.02em; }
.gate-copy { font-size: 13px; line-height: 1.6; color: var(--dim); }
/* Enrolled: keep the bar, drop the hero, centre the confirmation. */
.gate.enrolled { display: flex; flex-direction: column; }
.gate.enrolled .gate-3d,
.gate.enrolled .gate-lockup,
.gate.enrolled .gate-tagline,
.gate.enrolled .gate-trust { display: none; }
.gate.enrolled .gate-chrome { position: static; }
.gate.enrolled .gate-bottom { flex: 1; min-height: 0; }
.gate.enrolled .gate-body { flex: 1; justify-content: center; gap: 0; margin: 0 auto; }
.gate-enrolled { gap: 11px; }
.enrolled-head { text-align: center; margin-bottom: 10px; }
.enrolled-title { font-family: var(--mono); font-size: 26px; font-weight: 500; letter-spacing: 0.06em; color: var(--ink); }
.enrolled-sub { margin-top: 12px; font-size: 14px; line-height: 1.55; letter-spacing: 0.01em; color: var(--dim); max-width: 32ch; margin-left: auto; margin-right: auto; }
.step-n { display: inline-block; margin-right: 9px; }
/* .btn.done lives in button.css. */
.enrolled-hint {
  margin: 2px 0 0; font-size: 12px; line-height: 1.7; letter-spacing: 0.02em;
  color: var(--dim); text-align: center;
}
.enrolled-skip { margin-top: 6px; align-self: center; }
.field {
  /* THE one control height — 44px, same as every button (tokens --btn-h,
     "buttons + inputs", William, 2026-08-07); was a hand-typed 54px. Vertical
     padding drops so the 16px text centres itself in the fixed box (16px holds
     — under it iOS zooms the page on focus). */
  width: 100%; height: var(--btn-h, 44px); background: var(--panel); border: 1px solid var(--line-2);
  border-radius: 0; appearance: none; -webkit-appearance: none;
  color: var(--ink); padding: 0 20px; outline: none; text-align: center;
  font-family: var(--sans); font-size: 16px; letter-spacing: 0.01em;
  caret-color: var(--accent); /* the caret writes in the private line blue — every field, one rule */
}
.field:focus { border-color: var(--accent); }
.field::placeholder { color: var(--dim); }
/* Autofill keeps the house ground — no browser yellow. */
.field:-webkit-autofill, .field:-webkit-autofill:hover, .field:-webkit-autofill:focus {
  -webkit-box-shadow: 0 0 0 1000px var(--panel) inset;
  -webkit-text-fill-color: var(--ink);
  caret-color: var(--accent);
  transition: background-color 999999s ease-out;
}
/* The email field takes the beam, not a solid stroke: a box wraps the input, the
   input goes borderless, and on focus the encrypted-blue flashlight sweeps the
   edge — the same beam as the read-docs strip. */
.field-box { position: relative; isolation: isolate; }
.field-box > .field { position: relative; z-index: 1; }
.field-box::before {
  content: ""; position: absolute; inset: -1px; padding: 1px;
  background: conic-gradient(from var(--btn-beam),
    var(--beam-a) 0deg, var(--beam-b) 30deg,
    color-mix(in srgb, var(--beam-b) 30%, transparent) 52deg,
    transparent 72deg, transparent 360deg);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask-composite: exclude;
  pointer-events: none; z-index: 0; opacity: 0; transition: opacity 0.3s ease;
  animation: btnBeam 4.6s linear infinite;
}
.field-box:focus-within::before { opacity: 1; }
.field-box:focus-within > .field { border-color: transparent; }
@media (prefers-reduced-motion: reduce) { .field-box::before { animation: none; } }
/* Every gate field wears the box: transparent over the ground (no panel wash),
   a smaller, fainter placeholder, the beam on focus. One field, one grammar. */
.field-box > .field { background: transparent; }
.field-box > .field::placeholder { font-size: 14px; color: color-mix(in srgb, var(--dim) 48%, transparent); }
.field-box > .field:-webkit-autofill { -webkit-box-shadow: 0 0 0 1000px var(--ground) inset; }
/* The key and the code type like what they are: grouped serial characters,
   uppercased as you write, the caret in the accent. The placeholder shows the
   exact shape the desk mints (XXXX five times; six digits). Both centre in
   their slot like the button labels around them (William, 2026-08-06) — the
   .field base carries the centring; nothing re-pins it left. */
#invite, #code { text-transform: uppercase; letter-spacing: 0.08em; }
/* The verdict strokes. Blue is verified: the moment a field's content is the
   right shape, its stroke turns accent and the step's button lights with it.
   Hibiscus is refused: a failed submit turns the WHOLE entry alarm — stroke,
   the refused characters and the caret — beside its message, until the person
   types again. A verdict outranks the focus beam — the stroke IS the answer,
   so the sweep rests while a verdict shows. */
.field-box.is-valid > .field,
.field-box.is-valid:focus-within > .field { border-color: var(--accent); }
.field-box.is-error > .field,
.field-box.is-error:focus-within > .field { border-color: var(--alarm); color: var(--alarm); caret-color: var(--alarm); }
/* iOS autofills the code straight from the mail; autofilled text takes the
   fill-color, so the refusal must speak there too. */
.field-box.is-error > .field:-webkit-autofill { -webkit-text-fill-color: var(--alarm); caret-color: var(--alarm); }
.field-box.is-valid:focus-within::before,
.field-box.is-error:focus-within::before { opacity: 0; }
/* A lit step button wears the verified blue too: enabled means ready. */
#inviteContinue:not(:disabled),
#continueBtn:not(:disabled),
#enterBtn:not(:disabled) { border-color: var(--accent); }

/* The gate's ONE inline text link — the consent's privacy notice (a button)
   and the open-source point's "public" (an anchor) both wear it. */
.pn-link { appearance: none; background: none; border: 0; padding: 0; margin: 0; font: inherit; color: var(--ink); text-decoration: underline; text-underline-offset: 2px; cursor: pointer; }
.pn-link:hover, .pn-link:focus-visible { text-decoration-color: var(--accent); }
/* The email field is held until consent is given (it carries the disabled
   attribute): dimmed and inert, so no one types before agreeing to the notice.
   The box's beam can't fire while it can't take focus, so nothing to silence. */
.field:disabled { opacity: 0.4; cursor: not-allowed; }
.field-box:has(> .field:disabled) { cursor: not-allowed; }
/* The beam stays the passkey's signature alone — no beam on CONTINUE. */
#stepEmail .btn.primary::before, #stepCode .btn.primary::before { display: none; }

.key-open {
  width: 100%; max-width: 300px; background: transparent; color: var(--ink);
  border: 1px solid var(--line-2); padding: 14px 16px; cursor: pointer;
  letter-spacing: 0.02em;
}
.key-open:active { border-color: var(--faint); }
/* Refusals speak in the alarm voice, the same attention colour as a suspect
   message or a failed send. */
.gate-err { color: var(--alarm); min-height: 1.4em; font-size: 14px; }
/* The in-app browser notice: a quiet mono line in the ink step under the
   voice, never the alarm — guidance, not an error. The copy-the-link action
   wears the house inline link (.pn-link, font inherited). line.js shows
   it on the landing and key steps only; belt: the passkey step's flattened
   column never seats it. */
.gate-inapp { margin: 0 0 4px; font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.02em; line-height: 1.5; color: var(--ink-2); text-align: left; }
.gate-inapp .pn-link { display: inline-block; margin-top: 6px; }
.gate.passkey-setup .gate-inapp { display: none; }
.gate-hint { font-size: 13px; max-width: 300px; }

.chat { background: var(--ground); }

.bar {
  flex: none; display: grid; grid-template-columns: 1fr auto 1fr; align-items: center;
  /* Left content starts on the rail (so the mark lands there by structure, no
     nudge); the right keeps the 8px so the hamburger sits over the menu's X. */
  padding: 4px 8px 4px var(--rail); border-bottom: 1px solid var(--line);
  padding-top: max(4px, env(safe-area-inset-top));
}
.bar-brand { display: inline-flex; align-items: center; padding: 0; text-decoration: none; }
.bar-lead { justify-self: start; display: inline-flex; align-items: center; gap: 6px; min-width: 0; }
.lang-switch { display: inline-flex; align-items: center; gap: 10px; font-family: var(--mono); font-size: 12px; letter-spacing: 0.1em; }
.lang-opt {
  appearance: none; background: none; border: 0; padding: 3px 4px; margin: 0;
  font: inherit; font-weight: 500; letter-spacing: inherit; color: var(--faint); cursor: pointer;
  line-height: 1; transition: color 140ms ease;
}
.lang-opt:hover, .lang-opt:focus-visible { color: var(--dim); outline: none; }
.lang-opt.is-on { color: var(--ink); }
.bar > .lang-switch { justify-self: center; }
/* The chat bar's centre carries the domain (William, 2026-08-07), in the same
   quiet dress as .foot-domain: caption size, micro tracking, third-rung ink. */
.bar-domain {
  grid-column: 2; justify-self: center; white-space: nowrap;
  font-family: var(--mono); font-size: var(--t-caption);
  letter-spacing: var(--track-micro); color: var(--ink-3);
}
/* Pinned to the last column BY NAME: the chat bar's centre cell is empty (no
   clock, no switch — William, 2026-08-07), so auto-placement would otherwise
   seat the hamburger in the middle cell. */
.bar > .menu-toggle { grid-column: 3; justify-self: end; }

/* The bar mark is the site's master weather mark (menu-weather-mark.svg),
   rendered at the site's exact size (52px tall) so the private line menu icon is
   identical to the experiences one. ONE size for the whole app — the gate,
   chat and site-menu bars and the docs sheet all wear it at the same scale. */
.bar-glyph { display: inline-flex; }
.bar-glyph img { display: block; height: 52px; width: auto; }

.menu-toggle {
  appearance: none; display: inline-flex; align-items: center; justify-content: center;
  width: 46px; min-width: 46px; height: 46px; margin: 0; padding: 0; border: 0;
  background: transparent; color: var(--dim); cursor: pointer; flex-shrink: 0;
  transition: color 140ms ease;
}
.menu-toggle:hover, .menu-toggle:focus-visible { color: var(--ink); outline: none; }
.ham-bars { position: relative; display: block; width: 22px; height: 12px; }
.ham-bar { position: absolute; left: 0; width: 100%; height: 1.5px; background: currentColor; border-radius: 2px; transition: transform 200ms ease, opacity 200ms ease; }
.ham-bar:nth-child(1) { top: 2px; }
.ham-bar:nth-child(2) { bottom: 2px; }
body.menu-open .ham-bar:nth-child(1) { transform: translateY(3.5px) rotate(45deg); }
body.menu-open .ham-bar:nth-child(2) { transform: translateY(-3.5px) rotate(-45deg); }
@media (prefers-reduced-motion: reduce) { .ham-bar { transition: none; } }


/* Keyboard focus is visible on every control on the sign-in path. */
.btn:focus-visible,
.linklike:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
/* Fields would double the ring under :focus-visible — their focus voice is
   the accent border alone. */
.field:focus-visible { outline: none; }

.enc-notice {
  flex: none; width: 100%; display: flex; align-items: center; justify-content: center;
  gap: 6px; padding: 8px 16px; background: transparent; border: 0;
  /* No border-top: the .bar above already draws a 1px hairline, and a second
     here would stack into a 2px line. One 1px top (the bar's) + one 1px bottom
     (ours) keeps the strip even top-and-bottom, matching the buttons' 1px stroke. */
  border-bottom: 1px solid var(--line); cursor: pointer; text-align: center;
  color: var(--dim); font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.04em; line-height: 1.4;
  white-space: nowrap; /* the notice stays on one line in both languages */
}
.enc-notice .enc-lock { color: var(--accent); flex: none; }
/* At 375px (the narrowest supported device) the one-line notice needs a step
   down in both languages, or the lock and the link fall off the edges. */
@media (max-width: 385px) {
  .enc-notice { font-size: 10px; letter-spacing: 0.02em; padding: 8px 10px; gap: 5px; }
}
.enc-more { color: var(--ink); text-decoration: underline; text-underline-offset: 2px; }
.enc-notice:active { background: color-mix(in srgb, var(--ink) 3%, transparent); }

.messages {
  flex: 1; overflow-y: auto; overflow-x: hidden; -webkit-overflow-scrolling: touch;
  padding: 14px 14px 6px; display: flex; flex-direction: column; gap: 8px;
  overscroll-behavior: contain;
}
.msg { max-width: 80%; display: flex; flex-direction: column; gap: 3px; }
.msg.them { align-self: flex-start; align-items: flex-start; }
.msg.me { align-self: flex-end; align-items: flex-end; }
/* The bubble itself — dress, voice grounds, links, estate cards — lives in
   bubbles.css, the ONE spec the desk links too, so the studio sees exactly
   what the guest sees (William, 2026-08-07). This file keeps only the guest
   app's message LAYOUT (sides, widths, alarm strokes) and the swipe. */
/* Swipe to reply: the row follows the finger, mouse or trackpad (JS sets the
   transform), a reply arrow fades in off the row's left edge, and the release
   snaps home. pan-y keeps vertical touch scrolling native while horizontal
   drags belong to the gesture; swipe-lock holds text selection still under a
   mouse drag. */
.msg { position: relative; touch-action: pan-y; }
.messages.swipe-lock, .messages.swipe-lock * { user-select: none; -webkit-user-select: none; }
.msg.swiping { transition: none; }
.msg:not(.swiping) { transition: transform 0.28s cubic-bezier(0.22, 1, 0.36, 1); } /* the house ease — the row settles, never snaps */
.msg::before {
  content: "↩"; /* the reply arrow — in the gap the leftward pull opens */
  position: absolute; right: -34px; top: 50%; transform: translateY(-50%);
  font-size: 15px; color: var(--dim);
  /* The arrow fades in WITH the pull (--swipe is the drag's progress, JS-set)
     and rests invisible. */
  opacity: min(1, var(--swipe, 0));
  pointer-events: none;
}
.msg.will-reply::before { color: var(--ink); }
/* The jump-to-quote flash — one soft pulse of the page primary's ring. */
.msg.flash .bubble { animation: quoteFlash 1.1s ease; }
@keyframes quoteFlash {
  0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 66%, transparent); }
  55% { box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 40%, transparent); }
  100% { box-shadow: 0 0 0 0 transparent; }
}
/* The composer's reply chip — the swiped quote held above the field. The
   quote IS a miniature of the actual message's bubble (the shared voice
   classes), so the chip matches what it answers — no accent bar, no second
   dress (William, 2026-08-07). */
.reply-chip { display: flex; align-items: center; gap: 10px; margin: 0; }
.reply-chip .bubble { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; padding: 6px 10px; }
.reply-chip__who { font-size: 10px; letter-spacing: 0.08em; color: inherit; opacity: 0.72; }
.reply-chip__text {
  font-size: 11px; line-height: 1.4; color: inherit; opacity: 0.85;
  overflow: hidden; display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical;
  overflow-wrap: anywhere;
}
.reply-chip__x {
  appearance: none; background: none; border: 0; padding: 2px 6px;
  font-size: 16px; line-height: 1; color: var(--dim); cursor: pointer;
}
.reply-chip__x:hover, .reply-chip__x:focus-visible { color: var(--ink); outline: none; }

/* The attachment chip — a staged picture or PDF, held above the field until a
   send carries it. Same shape as the reply chip. */
.attach-chip {
  display: flex; align-items: center; gap: 10px; margin: 0;
  padding: 6px 8px; border: 1px solid var(--line-strong);
}
.attach-chip__preview {
  flex: none; width: 34px; height: 34px; border-radius: 6px; overflow: hidden;
  display: flex; align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--ink) 8%, transparent);
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.06em; color: var(--dim);
}
.attach-chip__preview img { width: 100%; height: 100%; object-fit: cover; }
.attach-chip__name {
  flex: 1; min-width: 0; font-family: var(--mono); font-size: 12px; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.attach-chip.is-error .attach-chip__name { color: var(--alarm); }
.attach-chip__x {
  flex: none; appearance: none; background: none; border: 0; cursor: pointer;
  color: var(--dim); font-size: 18px; line-height: 1; padding: 0 4px;
}
.attach-chip__x:hover, .attach-chip__x:focus-visible { color: var(--ink); outline: none; }
/* The plus on the field's left, mirroring the send bolt on the right. A <label
   for> the file input, so the picker opens natively on tap. */
.attach-btn {
  flex: none; align-self: stretch; min-width: 46px; padding: 0 12px;
  background: transparent; border: 0; border-right: 1px solid var(--line-strong);
  color: var(--ink); cursor: pointer; user-select: none;
  display: flex; align-items: center; justify-content: center;
  transition: opacity 0.12s;
}
.attach-btn svg { display: block; opacity: 0.4; transition: opacity 0.15s ease; }
.attach-btn:hover svg, .attach-btn:focus-visible svg { opacity: 1; }
.attach-btn:focus-visible { outline: none; }
/* A desk reply whose signature does not verify against the pinned desk key —
   forged, tampered, or from before signing. Flagged, never silently trusted. */
.key-alarm {
  align-self: stretch; margin: 2px 0 8px; padding: 10px 12px;
  border: 1px solid var(--alarm); background: color-mix(in srgb, var(--alarm) 12%, transparent);
  color: var(--ink); font-family: var(--mono); font-size: 11px; line-height: 1.5; letter-spacing: 0.02em;
}
/* The bubble's own 12px inset already clears the stroke — no padding override
   now that the bubbles carry real padding again. */
.msg.suspect .bubble { border-left: 2px solid var(--alarm); }
.msg.suspect .bubble::after {
  clear: both;
  content: "unverified: not proven to be from the studio";
  display: block; margin-top: 5px; font-family: var(--mono); font-size: 10px;
  letter-spacing: 0.08em; color: var(--alarm); text-align: left;
}
/* A send the server refused or the network dropped. The bubble stays — never
   silently gone — marked in the same attention grammar as suspect, and the
   whole row is a tap target to re-queue it. */
.msg.failed { cursor: pointer; }
.msg.failed .bubble { border-right: 2px solid var(--alarm); opacity: 0.85; }
.send-fail {
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.08em;
  color: var(--alarm); padding: 0 3px;
}
/* One tick under your message: grey while it flies, turning neon only once the
   studio's machine holds it and the send beam has settled. */
.ack { display: inline-flex; color: var(--faint); transition: color 0.45s ease, opacity 0.45s ease; }
.ack svg { display: block; }
.ack.is-delivered { color: var(--accent); }
.ack.is-delivered svg { filter: drop-shadow(0 0 3px color-mix(in srgb, var(--accent) 55%, transparent)); }
/* The @RAINYSEASON.CR handle rides one size down the ramp — the eyebrow rung,
   not the hand-typed 11 (William, 2026-08-07). */
.msg .sender { font-family: var(--sans); font-size: var(--t-eyebrow); color: var(--dim); letter-spacing: 0.18em; padding: 2px 3px 1px; }

.composer {
  flex: none; display: flex; flex-direction: column; gap: 8px;
  padding: 8px 12px max(8px, env(safe-area-inset-bottom));
  border-top: 1px solid var(--line);
  background: var(--ground);
}
/* The field box carries the border and the beam; the textarea and the bolt
   button sit inside it, borderless. Hairlines ride the ink ladder (the one
   white at rest --line-strong, the 50% rung on focus) — never a second white. */
.composer-field {
  flex: 1; display: flex; align-items: flex-end;
  border: 1px solid var(--line-strong); background: transparent;
  transition: border-color 0.12s;
}
.composer-field:focus-within { border-color: var(--ink-3); }
.input {
  flex: 1; background: transparent; border: 0; border-radius: 0;
  appearance: none; -webkit-appearance: none;
  color: var(--ink); padding: 11px 14px; outline: none; resize: none;
  min-height: 46px; max-height: 140px; line-height: 1.4;
  font-family: var(--mono); font-size: 16px;
  caret-color: var(--accent); /* the composer writes in the same blue as the gate fields */
}
.input::placeholder { color: var(--dim); }
.input:disabled { opacity: 0.5; }
/* Send — same height as the input, joined to its right edge by a divider, the
   weather mark's bolts inside. */
.send-btn {
  flex: none; align-self: stretch; min-width: 54px; padding: 0 14px;
  background: transparent; border: 0; border-left: 1px solid var(--line-strong);
  color: var(--ink); cursor: pointer; display: flex; align-items: center; justify-content: center;
  transition: background 0.12s, opacity 0.12s;
}
.send-btn:focus-visible { outline: none; }
.send-btn:disabled { opacity: 0.4; cursor: default; }
.send-btn svg { display: block; color: var(--spark); opacity: 0.3; transition: opacity 0.15s ease; }
/* No washed square on the button — it is the bolts that light. They sit low
   until you reach for them (hover/focus) or fire (press, Enter, mid-send). */
.send-btn:hover svg,
.send-btn:focus-visible svg,
.send-btn:active svg,
body.beam-sending .send-btn svg { opacity: 1; }

/* The beam — a blue flashlight sweeping the read-docs strip and the message
   field on one clock, so they move as one. All blue: no white, no white tip. */
.enc-notice, .composer-field { position: relative; isolation: isolate; }
.enc-notice > *, .composer-field > * { position: relative; z-index: 1; }
.enc-notice::before, .composer-field::before {
  content: ""; position: absolute; inset: -1px; padding: 1px; border-radius: inherit;
  background: conic-gradient(from var(--btn-beam),
    var(--beam-a) 0deg,
    var(--beam-b) 30deg,
    color-mix(in srgb, var(--beam-b) 30%, transparent) 52deg,
    transparent 72deg, transparent 360deg);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask-composite: exclude;
  pointer-events: none; z-index: 0;
  transition: --beam-a 0.5s ease, --beam-b 0.5s ease;
  animation: btnBeam 4.6s linear infinite;
}
@media (prefers-reduced-motion: reduce) { .enc-notice::before, .composer-field::before { animation: none; } }

/* The beam has three moods. At rest it is the encrypted blue. On send it runs
   blue → lime while the sealed message flies; on delivery it holds all lime for
   3.33s, then falls back to blue. Registered so the shift is a fade, not a jump. */
/* @property takes literals only, so the rest value is the --thunder hex
   (tokens.css, the sound blue) written out; the body moods below re-point it. */
@property --beam-a { syntax: "<color>"; inherits: true; initial-value: #046AEF; }
@property --beam-b { syntax: "<color>"; inherits: true; initial-value: #046AEF; }
body.beam-sending   { --beam-a: var(--accent); --beam-b: var(--spark); }
body.beam-delivered { --beam-a: var(--spark);  --beam-b: var(--spark); }

.menu {
  position: fixed; inset: 0; z-index: 20;
  display: flex; flex-direction: column; justify-content: flex-end;
  background: color-mix(in srgb, var(--g) 62%, transparent);
  animation: menu-fade 0.2s ease both;
}
.menu-sheet {
  position: relative;
  background: var(--ground); border-top: 1px solid var(--line-strong);
  padding-bottom: env(safe-area-inset-bottom);
  animation: sheet-rise 0.3s cubic-bezier(0.22, 1, 0.36, 1) both;
}
.sheet-x {
  position: absolute; top: 8px; right: 10px; z-index: 2;
  background: none; border: 0; color: var(--dim);
  font-family: var(--mono); font-size: 26px; line-height: 1; cursor: pointer;
  padding: 4px 10px;
}
.sheet-x:active { color: var(--ink); }
@keyframes menu-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes sheet-rise { from { transform: translateY(100%); } to { transform: none; } }
@media (prefers-reduced-motion: reduce) { .menu, .menu-sheet { animation: none; } }
.menu-item {
  display: flex; align-items: center; gap: 10px;
  width: 100%; text-align: left; background: transparent;
  border: 0; border-top: 1px solid var(--line); color: var(--ink);
  padding: 17px 20px; cursor: pointer; text-decoration: none;
  font-family: var(--mono); font-size: 12px; font-weight: 500;
  letter-spacing: 0.14em; text-transform: uppercase;
  transition: background 0.12s;
}
.menu-item .oc { flex: none; color: var(--dim); }
.menu-item:active { background: color-mix(in srgb, var(--ink) 5%, transparent); }
/* The privacy blocks wear the gate's ONE trust-point grammar now (trust
   classes, line.html) — the bespoke .menu-explain caps dress is gone
   (William, 2026-08-07). .docs-copy just seats the shared column in the
   sheet's scroll. */
.docs-copy { padding: 24px 20px 26px; }
.info-delete { display: none; }
body.in-chat .info-delete { display: block; }
.acct-link.danger, .menu-item.danger { color: color-mix(in srgb, var(--alarm) 72%, var(--dim)); }
.acct-link.danger:hover, .acct-link.danger:active,
.menu-item.danger:hover, .menu-item.danger:active { color: var(--alarm); }

.site-menu {
  position: fixed; inset: 0; z-index: 40; background: var(--ground);
  display: flex; flex-direction: column; animation: menu-fade 0.2s ease both;
}
.site-menu[hidden] { display: none !important; }
/* The docs grow OUT of the strip (or lock) you tapped: its on-screen rectangle,
   handed in as the --x-* insets, expands to fill the screen — and collapses back
   into it on close. Reveal only (clip-path), so the content never distorts. */
.site-menu.from-strip {
  animation: none;
  clip-path: inset(0 0 0 0);
  transition: clip-path 0.34s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.28s ease;
  will-change: clip-path;
}
.site-menu.from-strip.collapsed {
  clip-path: inset(var(--x-top, 0px) var(--x-right, 0px) var(--x-bottom, 0px) var(--x-left, 0px));
  opacity: 0.5;
}
@media (prefers-reduced-motion: reduce) {
  .site-menu.from-strip { transition: none; }
  .site-menu.from-strip.collapsed { clip-path: inset(0 0 0 0); opacity: 1; }
}
.site-menu-head {
  flex: none; display: grid; grid-template-columns: 1fr auto 1fr; align-items: center;
  /* Left starts on the rail exactly like .bar, so the mark holds the same
     x-position when a takeover opens — it never slides. */
  padding: 4px 8px 4px var(--rail); border-bottom: 1px solid var(--line);
  padding-top: max(4px, env(safe-area-inset-top));
}
.site-menu-head .bar-brand { justify-self: start; }
/* Force the close X into the third column so it sits exactly where the bar's
   hamburger is (the head has no centre child, so auto-placement would drop it
   into the middle column and read as centred). */
.site-menu-head .menu-toggle { justify-self: end; grid-column: 3; }
/* The docs-logo glyph inherits the one bar-glyph size; no local override or
   inset, so with the head's rail padding it holds the exact same x-position
   as the bars — it never shifts when the docs open. */
.site-menu-head .docs-logo { justify-self: start; grid-column: 1; display: inline-flex; align-items: center; gap: 8px; }
.docs-name { font-family: var(--mono); font-size: 13px; font-weight: 600; letter-spacing: 0.2em; color: var(--ink); }
.site-nav {
  /* The scroll container shared by the mounted door menu (#siteNav, styled by
     the canonical door-menu.css .rs-doors) and the privacy sheet. The right
     inset matches the head's 8px; the account links inherit it. */
  --menu-x-r: 8px;
  flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden; -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  display: flex; flex-direction: column; padding: 6px 0 0;
}

.site-menu-account { display: none; margin: 0; }
body.in-chat .site-menu-account { display: block; }
.acct-link {
  display: flex; align-items: center; gap: 14px; width: 100%; text-align: left;
  background: transparent; border: 0;
  border-top: 1px solid color-mix(in srgb, var(--ink) 6%, transparent); color: var(--dim); cursor: pointer;
  /* Sentence case, no caps tracking — the sheet reads in the gate's own voice
     (William, 2026-08-07: not capitals). */
  font-family: var(--mono); font-weight: 500; font-size: 13px;
  padding: 14px var(--menu-x-r) 14px 20px; transition: color 0.12s;
}
.site-menu-account .acct-link:first-child { border-top: 0; }
.acct-link .oc { width: 15px; height: 15px; }
.acct-link .oc { flex: none; color: var(--faint); transition: color 0.12s; }
.acct-link:hover, .acct-link:focus-visible { color: var(--ink); outline: none; }
.acct-link:hover .oc, .acct-link:focus-visible .oc { color: var(--ink); }

/* Source link in the docs. Always shown, styled like the account rows. */
.docs-source {
  display: flex; align-items: center; gap: 8px; width: 100%;
  border-top: 1px solid color-mix(in srgb, var(--ink) 6%, transparent); color: var(--dim);
  /* Sentence case like the account rows — no caps in the sheet. */
  font-family: var(--mono); font-weight: 500; font-size: 13px; text-decoration: none;
  padding: 14px var(--menu-x-r) 14px 20px; transition: color 0.12s;
}
.docs-source .gh { width: 15px; height: 15px; flex: none; color: var(--accent-ink); transition: color 0.12s; }
.docs-source:hover, .docs-source:focus-visible { color: var(--ink); outline: none; }
.docs-source:hover .gh, .docs-source:focus-visible .gh { color: var(--accent-ink); }
/* The sheet's own domain foot: floors on tall screens (margin-top auto in the
   .site-nav column), clears the home indicator. */
.site-nav .foot-domain { margin-top: auto; padding: 26px 0 max(18px, env(safe-area-inset-bottom)); }
