@import url('../omni-ui.css?v=3328c747');

/* OmniStream shell — ported from mockups/omni-mock.css.
   Only shell/nav/topbar/cmdk/universal-component blocks are included here.
   Feature-section blocks (.sd*, .qa*, .preview, .connect-row, etc.) live
   in their own section stylesheets (later tickets). */

/* ---- Fonts (graceful fallback if offline) ---- */
@import url('https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap');

*{box-sizing:border-box}
html,body{margin:0;height:100%}
body{
  background:var(--bg); color:var(--text); font-family:var(--font-display);
  font-size:13px; line-height:1.45; -webkit-font-smoothing:antialiased;
}
a{color:inherit;text-decoration:none}

/* ============ ACCESSIBILITY ============ */
.skip-link{position:absolute;left:-999px;top:0;background:var(--accent);color:var(--on-strong);padding:8px 14px;z-index:200;border-radius:0 0 var(--r) 0}
.skip-link:focus{left:0}
/* Visually-hidden utility. The shell EMITS .sr-only markup (the nav's "(coming
   soon)" hint), so the shell sheet must define it rather than inheriting it from
   sections.css — otherwise the chrome depends on a feature stylesheet's load order
   and the hint paints as visible text. Same standard inset-rect pattern. */
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
:focus-visible{outline:2px solid var(--accent-soft); outline-offset:2px; border-radius:var(--r-sm)}
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}

/* ============ APP SHELL ============ */
/* Auth-gate flash protection (P1-T2). auth-gate.js sets html[data-auth]
   ('ok' once /api/account confirms a session, 'error' on a transient 5xx /
   network blip). Until it resolves, the shell must NOT paint — otherwise an
   unauthenticated visitor sees the full dashboard chrome (nav, channel label,
   Go Live) flash before being redirected to /login. The default-hidden state
   below is in <head>-linked CSS so it is in force from first paint; auth-gate
   is `defer` (runs after parse) but the attribute is absent until then, so the
   shell stays hidden by default and is revealed only on a resolved state. On
   'error' we reveal (with the section's own quiet banner) rather than trap a
   logged-in user behind a blank screen on a transient failure. */
html[data-js]:not([data-auth='ok']):not([data-auth='error']) .app-shell{visibility:hidden}
html[data-auth='ok'] .app-shell,
html[data-auth='error'] .app-shell{visibility:visible}
/* Three rows, not two: `notice` is the full-width banner strip between the topbar
   and the content (see .shell-notices below). It is `auto`, so with no banner in
   it the row is zero-height and the shell renders exactly as before. */
.app-shell{--sidebar-w:248px; display:grid; grid-template-columns:var(--sidebar-w) 1fr; grid-template-rows:auto auto 1fr; grid-template-areas:"sidebar topbar" "sidebar notice" "sidebar content"; height:100vh; height:100dvh; overflow:hidden}
.app-shell.collapsed{--sidebar-w:64px}

/* Sidebar */
.sidebar{grid-area:sidebar; background:linear-gradient(180deg,var(--surface),var(--surface-inset)); border-right:1px solid var(--border); display:flex; flex-direction:column; overflow-y:auto; overflow-x:hidden}
.brand{display:flex; align-items:center; gap:var(--space-3); padding:var(--space-4); font-weight:700; font-size:15px; letter-spacing:.3px; position:sticky; top:0; background:var(--surface); z-index:2}
.brand .logo{width:26px;height:26px;border-radius:7px;object-fit:contain;flex:none;display:block}
.brand .logo svg{width:15px;height:15px}
/* Wide PYRE·Stream wordmark lockup (mark + text in one SVG, ~5-6:1). Scale by
   height so the width follows the art. The mark-only fallback is hidden until the
   rail collapses (see the .collapsed block below). */
.brand-lockup{display:block;height:24px;width:auto}
.brand-mark{display:none}
/* ── Collapsed icon rail ──────────────────────────────────────────────────────
   The rail must lose the LABELS, never the ACCESSIBLE NAMES. `display:none` on a
   label span would delete it from the accessibility tree too, leaving screen-reader
   users with a column of unnamed links, so every text span is CLIPPED instead (the
   standard visually-hidden pattern): the row keeps its name, keeps its place in the
   tab order, and sighted users get the label back via the title tooltip that
   shell.js sets while the rail is on. The icon span (.ico) is never clipped — it IS
   the rail. */
.collapsed .brand-name,
.collapsed .nav-item span:not(.ico),
.collapsed .nav-group-name,
.collapsed .nav-upsell-label,
.collapsed .nav-soon-cat,
.collapsed .rail-toggle-label{
  position:absolute;width:1px;height:1px;padding:0;margin:0;
  overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0;
}
/* Rail rows centre on their icon; the group header keeps its vertical BOX (so the
   icons below don't shift up when collapsing) and reads as a quiet divider. */
.collapsed .nav-item,
.collapsed .nav-group-label,
.collapsed .nav-upsell{justify-content:center;padding-left:0;padding-right:0}
.collapsed .nav-group-label .chev,.collapsed .nav-upsell .chev,.collapsed .nav-soon-dot{display:none}
.collapsed .nav-group-label::after{content:"";width:var(--space-4);height:1px;background:var(--border-strong)}
.collapsed .nav-upsell{margin-left:var(--space-2);margin-right:var(--space-2)}
.collapsed .brand,.collapsed .rail-toggle{justify-content:center;padding-left:0;padding-right:0}
/* Collapsed rail (64px) is narrower than the wide lockup — swap the full lockup for
   the mark-only glyph, centered in the rail. */
.collapsed .brand-lockup{display:none}
.collapsed .brand-mark{display:block;height:26px;width:26px}

.nav{padding:var(--space-2) var(--space-2) var(--space-6)}
.nav-group{margin-top:var(--space-4)}
.nav-group-label{display:flex;align-items:center;gap:var(--space-2);padding:var(--space-2) var(--space-3);font-family:var(--font-mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--text-faint);cursor:pointer;user-select:none}
.nav-group-label .chev{margin-left:auto;transition:transform var(--dur) var(--ease);font-size:12px;line-height:1;color:var(--text-dim)}
.nav-group.closed .chev{transform:rotate(-90deg)}
.nav-group.closed .nav-item,.nav-group.closed .nav-soon-cat{display:none}
.nav-item{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-2) var(--space-3);border-radius:var(--r-sm);color:var(--text-dim);cursor:pointer;font-size:13px;font-weight:500;position:relative;transition:background var(--dur-fast) var(--ease),color var(--dur-fast) var(--ease)}
.nav-item .ico{width:18px;text-align:center;flex:none;font-size:15px;filter:grayscale(.3)}
/* :hover is decorative-only and lives in @media(hover:hover) (S2-shell) so it
   never sticks on touch — the unconditional rule was removed to avoid sticky hover. */
.nav-item.is-active{background:var(--accent-wash);color:var(--text)}
.nav-item.is-active::before{content:"";position:absolute;left:-2px;top:var(--space-2);bottom:var(--space-2);width:3px;border-radius:3px;background:var(--accent)} /* shell rail indicator (allowed: nav affordance, not a card stripe) */
.nav-item .tag{margin-left:auto;font-family:var(--font-mono);font-size:9px;padding:1px 6px;border-radius:var(--r-pill);background:var(--surface-2);color:var(--text-dim)}
.nav-item .lock{margin-left:auto;opacity:.5;font-size:11px}
/* ── Coming-soon nav affordances (data-driven via nav-config comingSoon). ── */
.soon-pill{font-family:var(--font-mono);font-size:9px;letter-spacing:.08em;text-transform:uppercase;padding:2px 6px;border-radius:var(--r-pill);background:var(--accent-wash);border:1px solid var(--border-accent);color:var(--accent-2)}
.nav-group-label .soon-pill{margin-left:auto} /* push the tag right, ahead of the chevron */
.nav-group-label .soon-pill + .chev{margin-left:var(--space-2)} /* chev sits just after the tag, not auto-pushed */
.nav-item--soon{color:var(--text-faint)}
.nav-item--soon .ico{opacity:.6}
.nav-soon-dot{margin-left:auto;width:6px;height:6px;border-radius:50%;background:var(--border-accent)}
/* ── The ONE consolidated upsell row (IA-LAYOUT §2) ──────────────────────────
   A single quiet disclosure that replaces ~14 permanently-disabled rows. Deliberately
   the LOWEST-contrast interactive row in the sidebar (dashed hairline, faint text,
   no fill) so it reads as a footnote next to the things that work — it must not
   compete with a real destination. Expanding it reveals the locked groups in place,
   group labels intact, so the taxonomy is folded, not flattened. */
.nav-upsell{display:flex;align-items:center;gap:var(--space-3);width:100%;margin:var(--space-4) 0 0;padding:var(--space-2) var(--space-3);border:1px dashed var(--border);border-radius:var(--r-sm);background:transparent;color:var(--text-faint);font-family:var(--font-display);font-size:12px;font-weight:500;text-align:left;cursor:pointer}
.nav-upsell .ico{width:18px;text-align:center;flex:none;font-size:13px}
.nav-upsell .chev{margin-left:auto;transition:transform var(--dur) var(--ease);font-size:12px;line-height:1;color:var(--text-dim)}
.nav-group--soon{margin-top:0}
.nav-group--soon.closed .nav-upsell .chev{transform:rotate(-90deg)}
/* The locked group's own name, inside the expanded panel — a label, not a control
   (the whole panel is one disclosure), so it is a plain div at group-label weight. */
.nav-soon-cat{padding:var(--space-3) var(--space-3) var(--space-1);font-family:var(--font-mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--text-faint);opacity:.75}

/* ── Sidebar footer: the icon-rail toggle (persisted). ── */
.sidebar-foot{margin-top:auto;padding:var(--space-3);border-top:1px solid var(--border);display:flex;gap:var(--space-2);position:sticky;bottom:0;background:var(--surface-inset)}
.rail-toggle{display:flex;align-items:center;gap:var(--space-3);width:100%;padding:var(--space-2) var(--space-3);border:1px solid transparent;border-radius:var(--r-sm);background:transparent;color:var(--text-faint);font-family:var(--font-display);font-size:12px;font-weight:500;text-align:left;cursor:pointer}
.rail-toggle .ico{width:18px;text-align:center;flex:none;font-size:13px}

/* Topbar */
/* NOTE: no backdrop-filter. A blurred sticky bar is one of the most expensive
   per-frame compositor ops, and it's ALWAYS in the viewport — when a preview video
   drives the compositor (20fps) it re-blurs every frame, which showed up as scroll/
   input sluggishness (worst on the dense Stream Manager page). Solid bar instead. */
/* 🔴 min-width:0 IS LOAD-BEARING. A grid item's automatic minimum size is its
   MIN-CONTENT width, so anything that overflows the topbar (a chrome enhancer
   injecting a sentence, say) does not just clip itself — it widens the `1fr`
   shell column past the viewport, and every pane below then lays its auto-fit
   tracks out against that too-wide container. That is exactly how the v11
   reconnect nudge broke #schedule on a phone: two summary cards side by side
   with the second one off-screen. Pinning the floor at 0 keeps an overflowing
   bar's damage inside the bar. */
.topbar{grid-area:topbar;display:flex;align-items:center;gap:var(--space-3);padding:0 var(--space-4);height:56px;min-width:0;border-bottom:1px solid var(--border);background:var(--surface);position:sticky;top:0;z-index:10}
.icon-btn{width:34px;height:34px;display:grid;place-items:center;border-radius:var(--r-sm);background:transparent;border:1px solid transparent;color:var(--text-dim);cursor:pointer;font-size:16px}
/* :hover decorative-only → @media(hover:hover) (S2-shell), no sticky hover on touch. */
/* (.chan-switch retired in desktop-overhaul §02 — the channel dropdown folded into
   the destination cards; see sec-destinations.js.) */
.cmdk-trigger{display:flex;align-items:center;gap:var(--space-2);min-width:220px;padding:7px var(--space-3);border-radius:var(--r-sm);background:var(--surface-inset);border:1px solid var(--border);color:var(--text-faint);cursor:text;font-size:12px}
.cmdk-trigger .kbd{margin-left:auto}
.kbd{font-family:var(--font-mono);font-size:10px;padding:1px 6px;border-radius:4px;background:var(--surface-2);border:1px solid var(--border);color:var(--text-dim)}
.topbar-spacer{flex:1}
/* NAV DIET (dashboard-redesign R1a): destinations flagged `chrome` in nav-config
   (Import today) render as a permanent topbar button instead of a sidebar row.
   Plain .btn rhythm plus an icon; the label collapses on mobile (below), where the
   icon + title/aria-label still name it. */
.topbar-chrome-nav{display:inline-flex;align-items:center;gap:var(--space-2)}
/* P4 low-balance nudge: a polite status message paired with a warn action link
   (NOT a .pill — §3.3 reserves .pill for transient LIVE/ERROR). Inline group with
   topbar rhythm; the link itself uses the .btn system (.btn--warn .btn--sm). */
.billing-nudge{display:inline-flex;align-items:center;gap:var(--space-2)}
.billing-nudge-msg{font-family:var(--font-display);font-weight:600;font-size:12px}

/* ── THE NOTICE STRIP (grid-area:notice) ─────────────────────────────────────
   The home for full-width shell banners: the LIVE banner and the scope-v11
   reconnect nudge today. It is a column, so two banners stack rather than fight,
   and it lives in its own grid row so a banner PUSHES the content down instead of
   overlapping the chrome. `min-width:0` for the same reason the topbar carries it
   (a long banner must not widen the shell column).
   🔴 A SENTENCE-LENGTH BANNER MUST NOT GO IN `.topbar` — that bar is a 56px
   nowrap flex row, and the v11 nudge in it is precisely what broke mobile. */
.shell-notices{grid-area:notice;display:flex;flex-direction:column;min-width:0}

/* Scope v11 stale-credential nudge (reconnect-banner.js) — a warn-toned strip:
   a polite status message, a real action link (.btn, NOT a .pill: §3.3 reserves
   .pill for transient LIVE/ERROR) and a dismiss control. The message takes a
   `20rem` basis so it holds the line beside its buttons on a wide shell and drops
   them onto their OWN line on a phone; everything wraps by gap, no block margins.
   Tone is a token wash + hairline, never a raw hex or a colored edge > 1px. */
.reconnect-nudge{
  display:flex;flex-wrap:wrap;align-items:center;gap:var(--space-3);
  padding:var(--space-3) var(--space-4);min-width:0;
  border-bottom:1px solid color-mix(in srgb, var(--warn) 35%, var(--border));
  background:color-mix(in srgb, var(--warn) 9%, transparent);
}
.reconnect-nudge-msg{
  flex:1 1 20rem;min-width:0;overflow-wrap:anywhere;
  font-family:var(--font-display);font-weight:600;font-size:12px;line-height:1.4;
}
.reconnect-nudge > .btn{flex:none}
/* The dismiss X rides the far edge of whichever line it lands on. */
.reconnect-nudge > .icon-btn{flex:none;margin-left:auto}

/* Content */
.content{grid-area:content;overflow-y:auto;padding:var(--space-6);min-width:0}
/* No margin-bottom: the .section gap column owns the space under the header
   (SPACING PRIMITIVES, sections.css). It used to carry space-5 while every
   other block gap was space-4, so the header sat on a different rhythm to the
   cards under it — one of the "spacing is inconsistent between pages" reports. */
.page-header{display:flex;align-items:flex-end;gap:var(--space-4);flex-wrap:wrap}
.page-header h1{margin:0;font-size:24px;font-weight:700;letter-spacing:.2px}
.page-header .sub{color:var(--text-dim);font-size:13px;margin-top:2px}
.page-header .ph-actions{margin-left:auto;display:flex;gap:var(--space-2)}
.section{display:none}
/* Vertical rhythm is NOT set here. The `.section` gap column, the [data-pane]
   stack and the child-margin guard are the SPACING PRIMITIVES block at the top
   of sections.css (loaded after this file, index.html), which upgrades this
   display:block to a flex column. This rule stays as the no-CSS-yet fallback.
   The former owl here — `.section > * { margin-bottom: var(--space-4) }` — was
   the second of three competing spacing models and is gone: a margin on a block
   that ALSO sits in a gap container renders the sum of both, which is how pages
   ended up at 28px next to 12px. See that block for the full rationale. */
.section.is-active{display:block;animation:rise var(--dur-slow) var(--ease)}
@keyframes rise{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}

/* Live banner (persistent when live) */
.live-banner{display:none;align-items:center;gap:var(--space-3);padding:8px var(--space-4);background:linear-gradient(90deg,color-mix(in srgb, var(--danger) 16%, transparent),transparent);border-bottom:1px solid var(--border)}
body.is-live .live-banner{display:flex}
.live-banner .rec{display:flex;align-items:center;gap:7px;font-family:var(--font-mono);font-size:11px;color:var(--text)}
.live-banner .rec .dot{background:var(--danger);animation:live-glow-d 2.8s ease-in-out infinite}
@keyframes pulse{50%{opacity:.4}}
/* Soft, slow glow for the LIVE indicators — a calm breathing halo rather than a
   hard opacity blink (intentionally low-key, not distracting). Two colour variants:
   red-on-dark for the banner dot, white-on-red for the Go-Live button dot. */
@keyframes live-glow-d{0%,100%{box-shadow:0 0 4px 0 color-mix(in srgb, var(--danger) 50%, transparent)}50%{box-shadow:0 0 11px 1px color-mix(in srgb, var(--danger) 95%, transparent)}}
@keyframes live-glow-w{0%,100%{box-shadow:0 0 4px 0 rgba(255,255,255,.4)}50%{box-shadow:0 0 10px 1px rgba(255,255,255,.9)}}
@media (prefers-reduced-motion: reduce){.live-banner .rec .dot,.btn--live .live-dot{animation:none}}

/* ============ COMPONENTS ============ */

/* Card */
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--r);box-shadow:var(--shadow-card);padding:var(--space-4)}
.card--raised{background:var(--surface-raised);border-color:var(--border-accent)}

/* Buttons — one system */
/* line-height is EXPLICIT so a .btn is the same height whether it is a <button>
   or an <a>: <button> takes its line-height from the UA form-control default,
   <a> inherits the body's, so a mixed action row (Public Profile's Published /
   View page / Save) rendered buttons 1px apart and visibly misaligned. */
.btn{font-family:var(--font-display);font-weight:600;font-size:13px;line-height:1.45;display:inline-flex;align-items:center;gap:7px;padding:12px 20px;border-radius:var(--r-sm);border:1px solid var(--border);background:var(--surface-inset);color:var(--text);cursor:pointer;transition:transform var(--dur-fast) var(--ease),background var(--dur-fast) var(--ease),border-color var(--dur-fast)}
/* .btn / .btn--primary :hover decorative-only → @media(hover:hover) (S2-shell). */
.btn:active{transform:translateY(1px)}
/* SOLID tokens only in this gradient: --accent-soft is a 14% TRANSLUCENT wash
   since the ember re-skin, and using it here made primary buttons semi-see-
   through at rest and unreadable on hover (reported live 2026-08-09). */
.btn--primary{background:linear-gradient(135deg,var(--accent-2),var(--accent));border-color:transparent;color:var(--on-strong);box-shadow:0 6px 18px -8px var(--accent)}
/* Live Go-Live button (topbar + Home): solid red with a soft-glowing dot. Shared
   so both entry points reflect the broadcast state identically. */
/* Foreground on the danger fill uses --text (the palette's off-white): there is
   no --on-danger token, and DESIGN-GUIDELINES §5 bans pure white. */
.btn--live{background:var(--danger);border-color:transparent;color:var(--text);box-shadow:0 6px 18px -8px var(--danger)}
.btn--live .live-dot{display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--text);animation:live-glow-w 2.8s ease-in-out infinite}
.btn--ghost{background:transparent}
.btn--warn{border-color:var(--warn);color:var(--warn)} .btn--danger{border-color:var(--danger);color:var(--danger)}
.btn--sm{padding:9px 16px;font-size:12px} .btn--lg{padding:14px 24px;font-size:14px}
.btn[disabled]{opacity:.45;cursor:not-allowed}

/* Status: pill (transient) vs badge (persistent) */
.pill{font-family:var(--font-mono);font-size:10px;letter-spacing:.06em;text-transform:uppercase;padding:3px 9px;border-radius:var(--r-pill);background:var(--surface-2);color:var(--text-dim);display:inline-flex;align-items:center;gap:6px}
.pill--ok{background:color-mix(in srgb, var(--ok) 14%, transparent);color:var(--ok-soft)} .pill--warn{background:color-mix(in srgb, var(--warn) 15%, transparent);color:var(--warn)} .pill--danger{background:color-mix(in srgb, var(--danger) 15%, transparent);color:var(--danger)}
.badge{font-family:var(--font-mono);font-size:9px;letter-spacing:.08em;text-transform:uppercase;font-weight:700;padding:2px 7px;border-radius:var(--r-sm);background:var(--accent-wash);color:var(--accent-2);border:1px solid var(--border-accent)}
.badge--beta{background:color-mix(in srgb, var(--warn) 12%, transparent);color:var(--warn);border-color:color-mix(in srgb, var(--warn) 35%, transparent)}
.dot{width:8px;height:8px;border-radius:50%;background:var(--text-faint);flex:none}
.dot--ok{background:var(--ok);box-shadow:0 0 8px color-mix(in srgb, var(--ok) 60%, transparent)} .dot--warn{background:var(--warn)} .dot--danger{background:var(--danger);box-shadow:0 0 8px color-mix(in srgb, var(--danger) 60%, transparent)}

/* Command palette */
/* Scrim = the app ground at ~72% (no dedicated scrim token; --bg keeps it themed). */
.cmdk{position:fixed;inset:0;background:color-mix(in srgb, var(--bg) 72%, transparent);display:none;justify-content:center;align-items:flex-start;padding-top:12vh;z-index:130;backdrop-filter:blur(3px)}
.cmdk.open{display:flex}
.cmdk-box{width:min(560px,92vw);background:var(--surface);border:1px solid var(--border-accent);border-radius:var(--r-md);box-shadow:var(--shadow-modal);overflow:hidden}
.cmdk-box input{width:100%;border:0;background:transparent;color:var(--text);font-family:var(--font-display);font-size:16px;padding:16px 18px;border-bottom:1px solid var(--border)}
.cmdk-box input:focus{outline:none}
.cmdk-list{max-height:340px;overflow-y:auto;padding:6px}
.cmdk-item{display:flex;align-items:center;gap:var(--space-3);padding:10px 12px;border-radius:var(--r-sm);cursor:pointer;color:var(--text-dim);width:100%;text-align:left;background:transparent;border:0;font:inherit}
/* .sel (keyboard/programmatic selection) stays unconditional; :hover is
   decorative-only and lives in @media(hover:hover) (S2-shell) — no sticky hover on touch. */
.cmdk-item.sel{background:var(--accent-wash);color:var(--text)}
.cmdk-item .grp{margin-left:auto;font-family:var(--font-mono);font-size:9px;text-transform:uppercase;color:var(--text-faint)}

/* Empty / loading states */
.empty{display:grid;place-items:center;text-align:center;padding:var(--space-10) var(--space-4);color:var(--text-dim)}
.empty .big{font-size:30px;margin-bottom:8px;opacity:.6}

/* Utility */
.mono{font-family:var(--font-mono)}
.dim{color:var(--text-dim)} .faint{color:var(--text-faint)}
/* Semantic text-color utilities (token-only). Pair with role="alert" so an
   error state is both ANNOUNCED and visibly colored (DESIGN-GUIDELINES §5:
   error = cause + retry, visually signalled). Used by inline form errors, list
   error states and the script lastError row in the bot/loyalty sections. */
.danger{color:var(--danger)} .ok{color:var(--ok)} .warn{color:var(--warn)}
.hide{display:none!important}

/* ============ DROPDOWN MENU (account / channel switcher) ============ */
/* Token-only floating menu. `--shadow-2` from the ticket has no token in
   omni-ui.css; nearest existing elevation token is --shadow-modal (used here)
   and --shadow-drawer. */
.menu{
  position:absolute; top:calc(100% + var(--space-2)); right:0; z-index:120;
  min-width:200px;
  background:var(--surface); border:1px solid var(--border);
  border-radius:var(--r-md); box-shadow:var(--shadow-modal);
  padding:var(--space-1); overflow:hidden;
}
.menu[hidden]{display:none}
/* The account menu's trigger is the right-most topbar child, so the default
   right:0 anchors it correctly. (The left-anchored channel-switcher popover —
   .chan-switch-anchor/.menu--channels — was retired in desktop-overhaul §02.) */
.menu-head{
  padding:var(--space-2) var(--space-3);
  display:flex; flex-direction:column; gap:2px;
  border-bottom:1px solid var(--border);
}
.menu-name{font-weight:600;color:var(--text)}
.menu-list{padding-top:var(--space-1)}
.menu-item{
  display:flex; align-items:center; gap:var(--space-2);
  width:100%; text-align:left;
  padding:var(--space-2) var(--space-3);
  border:0; background:transparent;
  border-radius:var(--r-sm);
  color:var(--text-dim); cursor:pointer;
  font:inherit; font-size:13px;
}
/* :focus stays unconditional (keyboard); :hover is decorative-only and lives in
   @media(hover:hover) (S2-shell) — no sticky hover on touch. */
.menu-item:focus{background:var(--surface-inset);color:var(--text)}

/* ============================================================================
   S2-shell — MOBILE LAYOUT (additive, scoped). Everything below is either
   inside @media(max-width:860px), @media(hover:*) or body.is-embedded so the
   desktop grid/topbar/sidebar render byte-identical to before. Breakpoint 860px.
   ============================================================================ */

/* dvh: keep the shell pinned to the *visible* viewport on mobile browsers whose
   URL bar collapses (100vh overshoots; 100dvh tracks the live height). Desktops
   that don't support dvh keep the 100vh value. */
.app-shell{height:100vh;height:100dvh}

/* Drawer scrim — hidden by default everywhere; only painted on mobile when the
   nav is open. The [hidden] attr (toggled by shell.js) is the inert default. */
.nav-scrim{display:none}

/* Body scroll-lock while the drawer is open (set by shell.js with the drawer).
   NOTE: body is not the scroll container (.app-shell is overflow:hidden and the
   real scroller is .content{overflow-y:auto}), so this body rule is belt-and-
   braces only. The actual lock is `.app-shell.nav-open .content{overflow:hidden}`
   in the @media(max-width:860px) block below, which pins the real scroller so
   keyboard/trackpad/programmatic scroll can't reach the content behind the drawer. */
body.nav-locked{overflow:hidden}

/* Drawer close (X) affordance lives in the brand row; desktop never shows it. */
.nav-close{display:none}

/* Bottom tab-bar — built by shell.js, hidden on desktop; shown only <=860px. */
.tabbar{display:none}

/* nav-group-label is now a real <button> (was a div) — strip native button
   chrome so the desktop look is byte-identical to the old div. Additive +
   unconditional; the .nav-group-label class rule above supplies layout/color. */
button.nav-group-label{width:100%;margin:0;border:0;background:transparent;text-align:left;font-family:var(--font-mono);font-size:10px;line-height:1.45;color:var(--text-faint)}

@media (hover:hover){
  /* Pure-decoration hovers — only on devices that truly hover (not touch). These
     are the SOLE home for these :hover declarations (the unconditional duplicates
     were removed) so a tap on touch falls through to :active with no sticky hover.
     Desktop always matches (hover:hover) so its look is byte-identical. */
  .nav-item:hover{background:var(--surface-3);color:var(--text)}
  .nav-group-label:hover,.rail-toggle:hover{color:var(--text-dim)}
  .rail-toggle:hover{background:var(--surface-3)}
  .nav-upsell:hover{border-color:var(--border-strong);color:var(--text-dim)}
  .icon-btn:hover{background:var(--surface-3);color:var(--text)}
  .btn:hover{border-color:var(--border-accent);background:var(--surface-3)}
  .btn--primary:hover{background:linear-gradient(135deg,var(--accent-2),var(--accent));filter:brightness(1.08)}
  .cmdk-item:hover{background:var(--accent-wash);color:var(--text)}
  .menu-item:hover{background:var(--surface-inset);color:var(--text)}
}
/* :active feedback for touch — no hover to lean on, so press-state matters. */
.nav-item:active{background:var(--accent-wash)}
.nav-upsell:active,.rail-toggle:active,.nav-group-label:active{background:var(--surface-2)}
.icon-btn:active{background:var(--surface-2);transform:translateY(1px)}

@media (max-width:860px){
  html,body{overflow-x:hidden}

  /* Single-column: topbar, the notice strip, content, tab-bar; the sidebar leaves
     the grid (it goes fixed). The `notice` row is `auto` — zero-height until a
     banner lands in it, and a full-width strip that pushes the page down when one
     does (it must never overlap the topbar chrome). */
  .app-shell{grid-template-columns:1fr;grid-template-rows:auto auto 1fr auto;grid-template-areas:"topbar" "notice" "content" "tabbar"}
  /* Neutralize the desktop collapsed rail — on mobile the drawer is the nav. */
  .app-shell.collapsed{--sidebar-w:248px}

  /* Off-canvas drawer */
  .sidebar{
    position:fixed; inset:0 auto 0 0; width:min(86vw,320px); z-index:140;
    transform:translateX(-100%); transition:transform var(--dur) var(--ease);
    box-shadow:var(--shadow-drawer);
    padding-top:var(--safe-top); padding-bottom:var(--safe-bottom);
    padding-left:max(0px,var(--safe-left));
  }
  .app-shell.nav-open .sidebar{transform:none}
  /* Lock the REAL scroller while the drawer is open (body overflow is a no-op
     here — see the .nav-locked note above). Stops keyboard/trackpad/programmatic
     scroll of the content behind the open drawer, not just scrim interception. */
  .app-shell.nav-open .content{overflow:hidden}
  /* The desktop rail CLIPS its labels (see the .collapsed block above). The mobile
     drawer is never a rail, so un-clip everything in case a persisted .collapsed
     lingers from a desktop session, and restore the row layout it re-centred. */
  .app-shell.collapsed .brand-name,
  .app-shell.collapsed .nav-item span:not(.ico),
  .app-shell.collapsed .nav-group-name,
  .app-shell.collapsed .nav-upsell-label,
  .app-shell.collapsed .nav-soon-cat,
  .app-shell.collapsed .rail-toggle-label{
    position:static;width:auto;height:auto;overflow:visible;
    clip:auto;clip-path:none;white-space:normal;
  }
  /* The drawer is 248px, not the 64px rail — show the full lockup, not the mark,
     even if a persisted .collapsed lingers from a desktop session. */
  .app-shell.collapsed .brand-lockup{display:block}
  .app-shell.collapsed .brand-mark{display:none}
  .app-shell.collapsed .nav-item,
  .app-shell.collapsed .nav-group-label,
  .app-shell.collapsed .nav-upsell{justify-content:flex-start;padding-left:var(--space-3);padding-right:var(--space-3)}
  .app-shell.collapsed .nav-group-label .chev,
  .app-shell.collapsed .nav-upsell .chev,
  .app-shell.collapsed .nav-soon-dot{display:block}
  .app-shell.collapsed .nav-group-label::after{content:none}

  /* The rail toggle is a desktop affordance — on mobile the drawer IS the nav. */
  .sidebar-foot{display:none}

  .nav-scrim{
    display:block; position:fixed; inset:0; z-index:130;
    background:rgba(0,0,0,.5); border:0; opacity:0; pointer-events:none;
    transition:opacity var(--dur) var(--ease);
  }
  .app-shell.nav-open .nav-scrim{opacity:1;pointer-events:auto}
  .nav-scrim[hidden]{display:none}

  /* Drawer close (X) in the brand row */
  .nav-close{
    display:grid; place-items:center; margin-left:auto;
    width:var(--touch-min); height:var(--touch-min);
    border:1px solid transparent; border-radius:var(--r-sm);
    background:transparent; color:var(--text-dim); cursor:pointer; font-size:18px;
  }
  .nav-close:active{background:var(--surface-2)}

  /* ---- Topbar reflow ---- */
  .topbar{
    gap:var(--space-2);
    padding-left:max(var(--space-4),var(--safe-left));
    padding-right:max(var(--space-4),var(--safe-right));
    padding-top:var(--safe-top);
    height:calc(56px + var(--safe-top));
  }
  /* cmdk-trigger collapses to a compact search affordance (still opens cmdk). */
  .cmdk-trigger{min-width:0;flex:none;width:var(--touch-min);justify-content:center;padding:0;height:var(--touch-min)}
  .cmdk-trigger .kbd,.cmdk-trigger .cmdk-label{display:none}
  /* Header chrome nav (Import): icon-only on mobile — the accessible name rides
     the button's title/aria surface, so nothing is lost with the label hidden. */
  .topbar-chrome-nav .topbar-chrome-label{display:none}
  .topbar-chrome-nav{padding-left:var(--space-3);padding-right:var(--space-3)}

  /* ---- Touch targets ---- */
  .icon-btn{width:var(--touch-min);height:var(--touch-min);font-size:18px}
  .btn{min-height:var(--touch-min)}
  .nav-item{min-height:48px;padding:var(--space-3)}
  .nav-group-label,.nav-upsell{min-height:var(--touch-min)}

  /* ---- Content: pad for safe-area + leave room for the bottom tab-bar ---- */
  .content{
    padding-left:max(var(--space-6),var(--safe-left));
    padding-right:max(var(--space-6),var(--safe-right));
    padding-bottom:var(--space-4);
  }
  /* Notice-strip banners clear the notch/rounded corners on their own edges (the
     strip itself is a bare column, so the padding belongs on each banner). */
  .live-banner,
  .reconnect-nudge{
    padding-left:max(var(--space-4),var(--safe-left));
    padding-right:max(var(--space-4),var(--safe-right));
  }
  /* On a phone the sentence always owns its own line and the controls follow on
     the next one — a 20rem basis would otherwise let a short two-word CTA squeeze
     up beside a wrapped paragraph. */
  .reconnect-nudge-msg{flex:1 1 100%}

  /* ---- Bottom tab-bar (primary mobile nav ergonomics) ---- */
  .tabbar{
    display:flex; grid-area:tabbar; z-index:120;
    background:var(--surface); /* solid (no backdrop-filter — same per-frame compositor cost as the topbar) */
    border-top:1px solid var(--border);
    padding-bottom:var(--safe-bottom);
  }
  body.is-embedded .tabbar{display:none}
  .tabbar-item{
    flex:1; min-width:0; height:56px;
    display:grid; place-items:center; gap:2px;
    border:0; background:transparent; color:var(--text-dim);
    cursor:pointer; font:inherit; font-family:var(--font-display);
  }
  .tabbar-item .tb-ico{font-size:18px;line-height:1}
  .tabbar-item .tb-label{font-size:10px;letter-spacing:.02em;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .tabbar-item.is-active{color:var(--accent-2)}
  .tabbar-item:active{background:var(--surface-3)}
}

/* ============ EMBEDDED MODE (T7) — chrome-less single column ============ */
/* When ?embedded is present shell.js sets body.is-embedded: strip the topbar,
   sidebar and tab-bar so the content fills an iframe/webview surface. */
body.is-embedded .topbar,
body.is-embedded .sidebar,
body.is-embedded .shell-notices,
body.is-embedded .tabbar{display:none}
body.is-embedded .app-shell{grid-template-columns:1fr;grid-template-rows:1fr;grid-template-areas:"content"}
body.is-embedded .content{padding-bottom:var(--space-6)}
