/* ════════════════════════════════════════════════════════════════════
   thesis.trade — app shell (shared chrome)
   ────────────────────────────────────────────────────────────────────
   The pieces every app screen repeats: the dark rail, the top crumb bar,
   the white focus plane, and the floating composer field. Reference
   implementation: research/Trade - Crisp Landing.html.

   Consume it per screen with:
     <link rel="stylesheet" href="../system/tokens.css">
     <link rel="stylesheet" href="../system/app-shell.css">
   …then write ONLY page-specific styles inline. Structural + color
   decisions live here so one edit moves every consumer. All values are
   tokens from tokens.css — never hardcode a shell color/shadow here.

   RESPONSIVE — every decision about how the FRAME behaves lives here, once.
   A screen must never write its own media query to make the chrome work.
     · Desktop (≥860px): two-column grid, full rail (240px).
     · Collapsed:  .app[data-rail="collapsed"] → 64px icons-only rail.
     · Mobile (<860px): rail leaves the grid and becomes a left DRAWER
       overlay; .top gains a menu button; .app[data-rail="open"] slides
       it in over a scrim. The drawer IS the rail — one implementation.
   The collapsed/open states are attribute-driven (CSS here); the toggle
   is ~6 lines of JS — see the reference implementation + README.

   MARKUP CONTRACT (so collapse can hide labels cleanly):
     .rail-word  → <span class="mark">t<span class="dotb">.</span></span>
                   <span class="full">thesis<span class="dotb">.</span></span>
     .rail-item  → <svg>…</svg><span class="lbl">Trade</span>
     .top        → <button class="menu">…</button> (mobile-only) + .crumb + .status
     .top .status→ consumes the CHIP system: <span class="chip live">LIVE</span>
     .stage      → top-aligned & scrolling by DEFAULT; add .focus for the
                   single-question vertically-centered surfaces (Trade landing).
   ════════════════════════════════════════════════════════════════════ */

/* ── Reset / base ───────────────────────────────── */
*{box-sizing:border-box;}
html,body{margin:0;height:100%;}
body{
  font-family:var(--font-sans);
  color:var(--ink-1);
  background:var(--bg);
  -webkit-font-smoothing:antialiased;
}
a{color:var(--accent-on-light);text-decoration:none;}
a:hover{text-decoration:underline;}

/* ── Keyboard focus ─────────────────────────────── */
/* Measured 2026-08-25: a shell page loads exactly two stylesheets
   (tokens.css + app-shell.css) and NEITHER contained a single
   `:focus-visible` rule, so a keyboard user tabbing the rail, the menu
   button, the mode chip or Sign out got only the UA default — which
   several of these controls suppress, because they are <button>s
   restyled to `background:transparent;border:none` further down.
   site.css has carried this block since 2026-08-05; the app had no
   equivalent. Same shape, same tokens.

   ════ THE POLICY: ONE INDICATOR PER FOCUSED CONTROL ════
   This block is the shell's FALLBACK. It is deliberately at type-plus-
   pseudo-class specificity (0,1,1), which means it outranks every bare
   `.thing{outline:none}` (0,1,0) in every page kit, at every width,
   regardless of source order. That is the point — a kit that suppresses
   an outline without supplying a replacement leaves a control with NO
   keyboard indicator, and the fallback is what catches that. It also
   means the fallback REACHES controls whose kit did supply a
   replacement, and there it would paint a second ring. So the fallback
   is suppressed again, per control, and ONLY where all three of these
   were measured true:

     1. the control suppresses its own outline, so it has nothing of
        its own to fall back on;
     2. an ancestor paints a ring driven by `:focus-within` — driven by
        FOCUS, not by an `.open`/`.active` state class, which cannot say
        which control has focus and so is not a focus indicator at all;
     3. that ancestor holds exactly ONE tab stop. A ring around a
        wrapper with two or more tab stops is in the same place for all
        of them and identifies none of them.

   Failure direction is safe in both axes: a kit that renames its
   wrapper loses the suppression and gets the fallback ring back
   (doubled, never bare), and a kit that adds a second tab stop to a
   wrapper likewise restores the per-element ring.

   ════ WHAT WAS MEASURED ════
   Real Tab navigation (NOT element.focus() — :focus-visible's heuristic
   resolves differently for programmatic focus), Chromium 1.61.1, at
   1280x800 AND 390x844, reading computed styles after the kits'
   `transition: box-shadow .15s` had settled. Identical at both widths.

     .size-control.stepper  (trade_ticket_kit.css:250) — THREE tab stops
       (−, input, +). Wrapper ring is focus-driven but cannot identify
       which. Criterion 3 FAILS → keeps the fallback. Measured without
       app-shell.css, the two buttons ALREADY showed the UA `outline 1px
       auto` alongside that wrapper ring, so per-element marking is this
       kit's own existing pattern; the input was the one member missing
       it. On a control where − and + move contract count in opposite
       directions on a real-money screen, "which one is focused" has to
       be answerable.
     .size-control.numeric  (same kit) — ONE tab stop; the `.affix`
       sibling is a <span>. All three criteria hold → suppressed below.
     .cv-composer .rw-ask-form  (research_workspace.css:94) — TWO tab
       stops (textarea + `.rw-ask-send`). Criterion 3 FAILS → keeps the
       fallback. And the form NODE RELOCATES: research_workspace.js
       placeForm() moves it between `.rw-ask-bar` and `.cv-composer`, and
       in the bar state that `:focus-within` rule does not match at all —
       measured without app-shell.css, `.rw-ask-input` there was BARE, no
       indicator of any kind. `.rw-ask.open .rw-ask-bar` is not a
       substitute: it is keyed on the `.open` CLASS, and measured
       identical with focus on the textarea and on the send button.
       Suppressing this input would have restored a blind control.
     .pub-claim / .pub-arg / .pub-tags  (library.css:90-91) — the
       reviewer flagged that no wrapper was established for these. There
       is none. There is something better: library.css:91 gives each
       field its OWN focus-driven ring (border → blue-500 plus a 3px
       halo). Measured with this block active, each field carried BOTH
       that halo and a 2px outline 2px outside it — the clearest true
       double of the four. All three fields are live markup
       (trade_construction.html:248, 251, 255), none is a dead selector.
       Suppressed below. (library.css uses `:focus`, not
       `:focus-visible`, so its ring also paints on click — a superset of
       keyboard focus, strictly more visible, not less.)

   The shell's OWN composer, `.field`, is judged by the same three
   criteria and FAILS criterion 3: `.field-foot` — and the `.go` submit
   button inside it — are children of `.field`
   (thesis_landing.html:63-69), so the field holds two tab stops. An
   earlier draft of this comment asserted `.go` was outside the field.
   It is not; the markup was checked. `.field textarea{outline:none}` in
   the Composer block therefore does NOT stand as policy-compliant on
   source order, and the ring is restored for it explicitly below at
   (0,2,1). The two indicators do not collide: `.field` has
   `padding:15px 16px 12px`, so a +2px outline on the textarea lands
   ~13px inside the field's own ring, which is separation, not a
   concentric pair. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible,
[role="button"]:focus-visible{
  outline:2px solid var(--accent-on-light);
  outline-offset:2px;
  /* NO border-radius here, deliberately. A `border-radius` in this block
     would have specificity (0,1,1) — a type plus a pseudo-class — and so
     would BEAT `.chip{border-radius:var(--r-pill)}` (0,1,0), `.go{7px}`
     and `.rail-item{6px}`: every one of those controls would change SHAPE
     for as long as it held focus. It is also unnecessary — an outline
     follows its element's own border-radius in every current engine. */
}
/* On the dark rail --accent-on-light (blue-600) is nearly invisible.
   --accent-on-navy is the system's on-navy accent (blue-300, AA ≥5.7:1
   on this ground per tokens.css). */
.rail a:focus-visible,
.rail button:focus-visible{
  outline-color:var(--accent-on-navy);
}
/* The scrim is a full-viewport button; a ring at +2px offset would draw
   OUTSIDE the viewport and read as no ring at all. Pull it inside. */
.rail-scrim:focus-visible{outline-offset:-4px;}

/* ── The two proven suppressions ─────────────────
   Both satisfy all three criteria above; both are scoped as narrowly as
   the measurement justifies and no wider. They name selectors owned by
   other lanes' kits, which is a coupling worth stating plainly: the
   SHELL owns this policy because the shell is what introduced the
   fallback, and neither kit can suppress it from its own file without
   re-deriving this specificity argument. The standing handoff is that
   each kit adopts its own line and this pair is deleted; until then a
   renamed class in either kit costs a doubled ring, never a bare
   control. */
/* .size-control.numeric: one tab stop (input + a <span class="affix">),
   wrapper ring focus-driven at trade_ticket_kit.css:260. NOT .stepper,
   which holds three. */
.size-control.numeric .size-input:focus-visible{outline:none;}
/* library.css:91 already paints each of these fields its own
   focus-driven border-accent + 3px halo. No wrapper is involved and none
   is needed — but the halo sits ON the element, so the fallback outline
   at +2px was a true concentric double. Unscoped, so it also suppresses
   on a page that used `.pub-claim` WITHOUT loading library.css. That is
   not silent: such a page would also be missing the class's border,
   radius and padding, so it announces itself as an unstyled form rather
   than as a quiet a11y regression. */
.pub-claim:focus-visible,
.pub-arg:focus-visible,
.pub-tags:focus-visible{outline:none;}

/* ── Restored for the shell's own composer ───────
   `.field textarea{outline:none}` in the Composer block is (0,1,1) and
   declared later, so it beats the bare `textarea:focus-visible`. `.field`
   holds two tab stops, so the textarea needs its own mark; (0,2,1) here
   is what takes it back. */
.field textarea:focus-visible{
  outline:2px solid var(--accent-on-light);
  outline-offset:2px;
}

/* ── App frame ──────────────────────────────────── */
/* THE vh/dvh PAIR IS NOT OPTIONAL — it is a restored regression.
   100vh on iOS Safari is the STATIC "URL-bar-collapsed" viewport, so a
   100vh frame is taller than what is actually visible and its bottom row
   sits underneath the browser chrome. 100dvh tracks the live visible
   viewport. Dual declaration with vh FIRST as the fallback for browsers
   without dvh (< iOS Safari 15.4 / < Chrome 108).

   This is the same idiom, carried forward verbatim rather than reinvented,
   from redesign.css `.phone` / `.phone.app` (`min-height:100vh;
   min-height:100dvh`), where the comment records the iPhone verification
   of 2026-05-26: nav labels were rendering UNDER the Safari URL bar at the
   static 100vh size. The shell shipped with 100vh alone, which put every
   converted screen back on the wrong side of that verification.

   redesign.css is PROVENANCE here and nothing more — no template loads
   it, so nothing it declares can reach a screen. The idiom had to be
   re-declared on this sheet precisely because inheriting it was never an
   option. */
.app{display:grid;grid-template-columns:var(--sidebar-width) 1fr;height:100vh;height:100dvh;}

/* ── Rail (dark chrome) ─────────────────────────── */
.rail{background:var(--rail-bg);border-right:1px solid var(--rail-line);display:flex;flex-direction:column;padding:0 0 14px;min-width:0;}
.rail-acct{display:flex;align-items:center;gap:10px;padding:0 14px 0 16px;height:56px;border-bottom:1px solid var(--rail-line);}
/* Type metrics below diverge from the handoff copy (operator alignment
   2026-07-14): the rail carries the OLD sidebar's type treatment —
   .sb-avatar / .sb-username / .sb-item in redesign.css — on the new
   dark chrome. Same families both chromes always shared; the handoff's
   smaller/lighter cuts read as a different font on the dark ground.
   Flag to Design before the next sweep. */
.rail-acct .av{width:30px;height:30px;border-radius:50%;background:#2a3566;color:#cdd6f5;font-size:11px;font-weight:600;letter-spacing:0.02em;display:flex;align-items:center;justify-content:center;flex:0 0 auto;}
/* username in the SANS face (operator 2026-07-16: "doesn't seem like the
   right font" — it was mono; handles read as identity, not data tokens) */
.rail-acct .nm{font-family:var(--font-sans);font-size:15px;font-weight:600;letter-spacing:-0.01em;color:var(--rail-ink);white-space:nowrap;overflow:hidden;}
/* the mode chip rides the account row, right side; dot-only when collapsed */
.rail-acct .rail-mode{margin-left:auto;flex:0 0 auto;text-decoration:none;}
[data-rail="collapsed"] .rail-acct .rail-mode{position:absolute;top:8px;right:8px;padding:0;border:none;background:none;}
[data-rail="collapsed"] .rail-acct .rail-mode .lbl{display:none;}
[data-rail="collapsed"] .rail-acct{position:relative;}
.rail-word{padding:20px 18px 14px;}
.rail-word .mark,.rail-word .full{font-size:20px;font-weight:700;letter-spacing:-0.02em;color:#fff;}
.rail-word .mark{display:none;}
.rail-word .dotb{color:var(--brand-dot);}
.rail-nav{display:flex;flex-direction:column;gap:2px;padding:0 10px;}
.rail-item{display:flex;align-items:center;gap:11px;padding:9px 10px;border-radius:6px;font-size:16px;font-weight:500;letter-spacing:-0.008em;color:var(--rail-ink-2);cursor:pointer;white-space:nowrap;}
.rail-item svg{width:17px;height:17px;stroke:currentColor;fill:none;stroke-width:1.7;flex:0 0 auto;}
.rail-item .lbl{overflow:hidden;text-overflow:ellipsis;}
.rail-item:hover{color:var(--rail-ink);}
.rail-item.active{background:var(--rail-active);color:#fff;font-weight:600;}
.rail-collapse{margin-top:auto;display:flex;align-items:center;gap:9px;padding:12px 20px 0;border-top:1px solid var(--rail-line);color:var(--rail-ink-2);font-size:16px;font-weight:500;letter-spacing:-0.008em;cursor:pointer;white-space:nowrap;}
.rail-collapse svg{width:13px;height:13px;stroke:currentColor;fill:none;stroke-width:2;flex:0 0 auto;transition:transform .18s ease;}
.rail-collapse:hover{color:var(--rail-ink);}
/* Sign out — the rail's last row, under Collapse (added 2026-07-16;
   the shell shipped without any logout control). */
.rail-signout{display:flex;align-items:center;gap:9px;padding:10px 20px 0;color:var(--rail-ink-2);font-size:16px;font-weight:500;letter-spacing:-0.008em;cursor:pointer;white-space:nowrap;}
.rail-signout svg{width:15px;height:15px;stroke:currentColor;fill:none;stroke-width:1.8;flex:0 0 auto;}
.rail-signout:hover{color:var(--rail-ink);}

/* Scrim — only rendered on mobile when the drawer is open */
.rail-scrim{display:none;position:fixed;inset:0;background:rgba(10,15,28,.44);z-index:40;border:none;padding:0;}

/* ── Collapsed rail (desktop/tablet): 64px, icons only ── */
.app[data-rail="collapsed"]{grid-template-columns:var(--sidebar-width-rail) 1fr;}
[data-rail="collapsed"] .rail-acct{padding:0;justify-content:center;}
[data-rail="collapsed"] .rail-acct .nm{display:none;}
[data-rail="collapsed"] .rail-word{padding:20px 0 14px;text-align:center;}
[data-rail="collapsed"] .rail-word .full{display:none;}
[data-rail="collapsed"] .rail-word .mark{display:inline;}
[data-rail="collapsed"] .rail-nav{padding:0 8px;}
[data-rail="collapsed"] .rail-item{justify-content:center;gap:0;padding:9px 0;}
[data-rail="collapsed"] .rail-item .lbl{display:none;}
[data-rail="collapsed"] .rail-collapse{justify-content:center;padding:12px 0 0;}
[data-rail="collapsed"] .rail-collapse .lbl{display:none;}
[data-rail="collapsed"] .rail-signout{justify-content:center;padding:10px 0 0;}
[data-rail="collapsed"] .rail-signout .lbl{display:none;}
[data-rail="collapsed"] .rail-collapse svg{transform:rotate(180deg);}

/* ── Main column + top bar ──────────────────────── */
/* min-height:0 is load-bearing: without it this grid item's min-size
   is content-height, the 100vh track overflows, and the BODY scrolls —
   the rail + topbar ride away on any long page (operator QA 2026-07-16,
   Research). With it, .stage scrolls internally and chrome stays put. */
.main{display:flex;flex-direction:column;min-width:0;min-height:0;}
/* Desktop: NO topbar (operator 2026-07-16) — the stage starts at the
   window edge and every screen breathes. Mobile keeps the slim bar as
   the drawer trigger (display flipped in the <860px block below). */
.top{display:none;height:var(--topbar-h);flex:0 0 var(--topbar-h);align-items:center;padding:0 14px;border-bottom:1px solid var(--border);}
.top .menu{display:none;align-items:center;justify-content:center;width:34px;height:34px;margin:0 12px 0 -6px;border-radius:7px;color:var(--ink-2);cursor:pointer;flex:0 0 auto;position:relative;}
.top .menu svg{width:18px;height:18px;stroke:currentColor;fill:none;stroke-width:1.8;}
.top .menu:hover{background:var(--surface-2);color:var(--ink-1);}
/* TOUCH TARGET. The visual box is 34×34 and this button is the ONLY way
   to open navigation on a phone — the rail is off-canvas and there is no
   other trigger. 34px is under every published minimum (44pt iOS / 48dp
   Android / 24px WCAG 2.2 AA).

   The HIT AREA grows, not the icon: a transparent ::before expander,
   centred on the button, takes the tappable region to 44×44 while the
   34px rounded hover plate and the 18px glyph render exactly as before.
   Pointer events on a pseudo-element are dispatched to its originating
   element, so this needs no JS and no markup change. `.top` is 48px tall
   on mobile, so 44px fits without touching the bar. */
.top .menu::before{
  content:"";
  position:absolute;
  top:50%;left:50%;
  transform:translate(-50%,-50%);
  width:44px;height:44px;
}
.top .crumb{font-size:14px;font-weight:600;letter-spacing:-0.01em;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.top .crumb .up{color:var(--ink-3);font-weight:500;}
/* The topbar consumes the CHIP system — it does NOT re-implement live state.
   Use <span class="chip live">LIVE</span> / .chip.paper / .chip.halt here. */
.top .status{margin-left:auto;display:inline-flex;align-items:center;gap:12px;flex:0 0 auto;}

/* ── Focus plane ────────────────────────────────── */
/* The clean white working surface. DEFAULT: top-aligned and scrolling —
   safe for every surface whose content can exceed the viewport (Library,
   Portfolio blotter, Research conversation). Add .focus for the single-
   question, vertically-centered surfaces (Trade landing). */
.stage{flex:1;min-height:0;display:flex;flex-direction:column;overflow-y:auto;background:var(--plane);}
.stage.focus{justify-content:center;}
.wrap{width:100%;max-width:var(--content-narrow);margin:0 auto;padding:32px;}

/* ── Composer field (floats off the plane) ──────── */
.field{background:var(--surface);border:1px solid var(--field-border);border-radius:9px;padding:15px 16px 12px;box-shadow:var(--field-shadow);}
/* This IS the composer's focus indicator, and it is why the textarea
   below carries `outline:none`: the whole field lights up (accent border
   + ring) rather than the bare textarea drawing a rectangle inside it.
   The rule was left in place rather than deleted when the :focus-visible
   block was added up top — deleting it would have removed the
   replacement along with the suppression. */
.field:focus-within{border-color:var(--accent-on-light);box-shadow:var(--field-focus-ring);}
.field textarea{width:100%;border:none;outline:none;resize:none;font-family:var(--font-sans);font-size:15px;line-height:1.55;color:var(--ink-1);background:transparent;min-height:70px;padding:0;margin:0 0 10px;}
.field textarea::placeholder{color:var(--ink-3);}
.field-foot{display:flex;align-items:center;gap:12px;border-top:1px solid var(--border);padding-top:11px;}
.field-hint{font-family:var(--font-mono);font-size:10.5px;color:var(--ink-3);}
.go{margin-left:auto;display:inline-flex;align-items:center;gap:8px;height:38px;padding:0 16px;border-radius:7px;background:var(--blue-700);color:#fff;border:none;font-family:var(--font-sans);font-size:13.5px;font-weight:600;cursor:pointer;}
.go svg{width:15px;height:15px;stroke:#fff;fill:none;stroke-width:2;}

/* ════════════════════════════════════════════════════════════════════
   MOBILE (<860px) — the frame changes, not just the widths.
   Rail leaves the grid, becomes an off-canvas left drawer over a scrim.
   ════════════════════════════════════════════════════════════════════ */
@media (max-width: 859px){
  .top{display:flex;}
  .app{grid-template-columns:1fr;}
  /* the rail is now a fixed drawer, closed by default (slid off-canvas) */
  .rail{position:fixed;top:0;left:0;bottom:0;width:min(82vw, var(--sidebar-width));z-index:50;transform:translateX(-100%);transition:transform .22s ease;box-shadow:0 0 40px rgba(10,15,28,.28);}
  .app[data-rail="open"] .rail{transform:none;}
  .app[data-rail="open"] .rail-scrim{display:block;}
  /* collapse control is meaningless on mobile — the drawer just closes.
     Sign out survives and inherits the bottom pin the hidden collapse
     control no longer provides. */
  .rail-collapse{display:none;}
  .rail-signout{margin-top:auto;}
  .rail-signout .lbl{display:inline;}
  /* mobile always uses the full labelled rail even if data-rail was "collapsed" */
  .rail-word .full{display:inline;}
  .rail-word .mark{display:none;}
  .rail-item .lbl{display:inline;}
  .rail-acct .nm{display:inline;}

  .top{height:var(--topbar-h-mobile);flex-basis:var(--topbar-h-mobile);padding:0 16px;}
  .top .menu{display:inline-flex;}
  .wrap{padding:24px 18px;}

  /* ── SAFE AREA ────────────────────────────────────────────────────
     _base_shell.html gained `viewport-fit=cover` in the same change, so
     the app frame now extends UNDER the notch/status bar and the home
     indicator instead of being letterboxed inside them. That is the only
     way `env(safe-area-inset-*)` reports anything but 0, and it obliges
     the chrome to pad itself back out. Cover + no padding would put the
     menu button under the status bar.

     CORRECTION (2026-08-25, second pass). An earlier version of this
     comment counted "six such rules already existed across the kits."
     That count was wrong and the word "kits" was doing the damage: four
     of the six live in redesign.css, and NO TEMPLATE LOADS
     redesign.css. `git grep -n redesign.css -- dashboard/templates`
     returns eleven hits and every one is a Jinja comment; the <head> of
     this shell says so itself. Those four rules cannot fire whatever
     the viewport meta says, so they were never evidence for anything.
     Only TWO pre-existing rules are on a loaded sheet —
     trade_ticket_kit.css:153 and trade_confirmed_kit.css:41 — and both
     spell the fallback `0` rather than `0px`, which is a LATENT defect
     handed off to those kits' owners. Latent, not active, and the
     difference was measured rather than assumed: Chromium DEFINES
     safe-area-inset-*, so the fallback is never consulted here and both
     spellings compute 20px. Force the name undefined and they diverge —
     `calc(20px + env(name, 0))` computes 0px (invalid, declaration
     dropped, base padding lost) against 20px for the `0px` spelling.
     Everything on THIS sheet uses `0px`.

     The four dead ones were audited for behaviour that needed a home on
     a live sheet:
       redesign.css:234 `.topbar`      → the deleted chrome's top bar;
                                         succeeded by `.top` below.
       redesign.css:870 `.scroll`      → the old scroller's bottom
                                         inset; succeeded by `.wrap`
                                         below (and `.wrap.bleed`, which
                                         was missing it — added).
       redesign.css:823 `.bottom-nav`  → the shell has no bottom nav; the
                                         rail drawer's own inset is here.
       redesign.css:716 `.body`        → dead twice over: even inside
                                         redesign.css the `padding-bottom`
                                         is overridden two lines later by
                                         the `padding:` shorthand.

     Two-argument env() throughout, `env(x, 0px)`: with the one-argument
     form, a browser that does not support env() drops the ENTIRE
     declaration, taking the base padding with it. The fallback keeps
     these identical to the pre-change values everywhere else.

     Vertical insets only appear on notched hardware in portrait;
     left/right only in landscape. Both are handled — a phone held
     sideways is where the rail drawer and the plane meet the notch. */
  .top{
    height:calc(var(--topbar-h-mobile) + env(safe-area-inset-top, 0px));
    flex-basis:calc(var(--topbar-h-mobile) + env(safe-area-inset-top, 0px));
    padding-top:env(safe-area-inset-top, 0px);
    padding-bottom:0;
    padding-left:calc(16px + env(safe-area-inset-left, 0px));
    padding-right:calc(16px + env(safe-area-inset-right, 0px));
  }
  /* The drawer is position:fixed to all four edges, so it owns the full
     screen height including both insets: its account row would sit under
     the status bar and Sign out — pinned to the bottom by margin-top:auto
     — under the home indicator. */
  .rail{
    padding-top:env(safe-area-inset-top, 0px);
    padding-bottom:calc(14px + env(safe-area-inset-bottom, 0px));
    padding-left:env(safe-area-inset-left, 0px);
  }
  /* The stage is the scroller, so its last child is what lands on the
     home indicator — a Submit button or the composer, on most screens.
     `.wrap.bleed{padding:0}` still wins for full-bleed data surfaces
     (0-2-0 beats 0-1-0 at any source position); those own their gutters. */
  .wrap{
    padding-bottom:calc(24px + env(safe-area-inset-bottom, 0px));
    padding-left:calc(18px + env(safe-area-inset-left, 0px));
    padding-right:calc(18px + env(safe-area-inset-right, 0px));
  }
}
/* ════════════════════════════════════════════════════════════════════
   PHONE (<600px) — ergonomics, not frame. The <860px block above changes
   the FRAME (rail becomes a drawer); this one changes what a thumb and an
   iOS keyboard need, and deliberately does NOT reach tablets, where the
   desktop type density is still right.

   600px is the codebase's existing phone threshold. The LIVE precedent
   is tokens.css, whose --page-gutter override uses the same number;
   redesign.css uses it too for `.phone`, but that file is not loaded by
   any template and is cited here only as where the number came from,
   never as evidence that anything in it runs. Written as a literal in
   both live files because a media query cannot read a custom property.
   ════════════════════════════════════════════════════════════════════ */
@media (max-width: 599px){
  /* iOS Safari ZOOMS THE PAGE when a text field smaller than 16px takes
     focus, and does not zoom back out. This textarea is the thesis
     composer — the single field a user types into most on this platform —
     and it was 15px. One pixel is the whole bug. Scoped to phones so
     desktop density is untouched. */
  .field textarea{font-size:16px;}

  /* Drawer rows are the phone's navigation. min-height (not padding) so
     the target is guaranteed at 44px regardless of what the resolved
     line-height of --font-sans turns out to be; both rows are already
     display:flex + align-items:center, so nothing moves but the box. */
  .rail-item{min-height:44px;}
  .rail-signout{min-height:44px;}
}
@media (prefers-reduced-motion: reduce){
  .rail{transition:none;}
  .rail-collapse svg{transition:none;}
}

/* ── Repo integration (2026-07-14, adoption commit) ─────────────────
   Design's reference marks rail items as <span>; in the app they are
   real <a>/<button> elements (navigation + a11y). Neutralize the base
   anchor/button styling INSIDE the chrome so the semantic upgrade
   renders byte-identically to the reference. Chrome behavior lives
   here, never per screen. */
a.rail-item, a.rail-item:hover { text-decoration: none; }
a.chip, a.chip:hover { text-decoration: none; }
button.rail-collapse, button.rail-signout, button.menu {
  background: transparent; border: none; font: inherit; text-align: left;
}

/* ── Stage width variants (adoption 2026-07-14) ─────────────────────
   The token file's three surface widths, expressed as chrome: narrow
   is the .wrap default; screens opt into the wider surfaces via the
   stage_width consumer var — never with their own media queries.
   Placed AFTER the mobile block so .bleed's padding:0 wins the
   cascade at every viewport (the band/blotter own their gutters). */
.wrap.decision{max-width:var(--content-decision);}
/* bleed fills the stage as a flex column so a data surface can pin a
   band and scroll internally (Research mk-split), same as the old
   chrome's content column. Surfaces that just flow still work: the
   stage remains the scroller when content exceeds the viewport. */
.wrap.bleed{max-width:none;padding:0;flex:1;min-height:0;display:flex;flex-direction:column;}
/* …and that `padding:0` is exactly the hole. It is (0,2,0) and declared
   after the mobile block, so it beat the safe-area `padding-bottom` that
   the <860px `.wrap` rule adds — which means the two surfaces that
   declare `stage_width='bleed'`, Research (research_workspace.html:36)
   and the Portfolio blotter, got NO bottom inset at all and put their
   last row under the home indicator. Bleed owns its horizontal gutters;
   it does not own the notch. Only the bottom is restored, at matching
   specificity and after, so `padding:0` still governs left/right/top.
   Bare `env()` with no added constant: a bleed surface has no base
   bottom padding to preserve, so this is 0px on every non-notched
   device. */
@media (max-width: 859px){
  .wrap.bleed{padding-bottom:env(safe-area-inset-bottom, 0px);}
}

/* Nav loading beat (operator 2026-07-23): a thin top progress bar showing
   the click registered and the next page is coming. Fixed to the viewport
   top, above all chrome; adds zero layout to any view. app-shell.js drives
   the width (creep-and-hold); the new page paints over it. */
.nav-progress{
  /* top rides the safe-area inset: with viewport-fit=cover, `top:0` is
     UNDER the status bar on a notched phone in portrait, and a progress
     bar nobody can see is the same as no progress bar. 0px fallback keeps
     desktop and every non-notched device exactly where it was. */
  position:fixed; top:env(safe-area-inset-top, 0px); left:0; height:3px; width:0;
  z-index:2000; pointer-events:none;
  background:var(--blue-600,#4456d1);
  box-shadow:0 0 8px rgba(68,86,209,.55);
  opacity:0;
  transition:width .18s ease-out, opacity .25s ease;
  border-top-right-radius:2px; border-bottom-right-radius:2px;
}
.nav-progress.active{ opacity:1; }
@media (prefers-reduced-motion: reduce){
  .nav-progress{ box-shadow:none; transition:opacity .25s ease; }
}
