/* ══ The app shell — header, footer, instruments. THE APP OWNS THIS. ═══════
 *
 * nominate.css and skin.css reskin Loom's WIDGETS; they deliberately do not
 * touch its CONTAINERS, and an app that leaves the containers to loom.css
 * looks assembled rather than designed (cbprospect's first review, verbatim:
 * "spacing not correct; page body not reaching the edges; no hero headings").
 * So the shell is authored here, once, and the three rules it encodes are:
 *
 *   - ONE LEFT EDGE: --shell-gutter is the gutter, and the header, the main
 *     column and the footer all use it.
 *   - EVERY VIEW OPENS WITH A HERO (the .hero classes below; hero() in the
 *     page script builds one in four lines).
 *   - THE FOOTER IS FIXED and carries the instruments: the live-log pill and
 *     the Feedback button.
 */

:root {
  --shell-gutter: clamp(1rem, 4vw, 2.5rem);
  /* ⚠️ ONE WIDTH DIAL FOR ALL THE CHROME BOXES (cbapex#112). Above ~1232px
     the header/controls (full-bleed, gutter-padded) and the main/footer
     columns (max-width, centered) used to disagree about the left edge by
     100-150px — every app's h1 sat right of its wordmark, the family-wide
     "heading-background offset". Main and footer cap to --shell-max; the
     full-bleed bars (header, controls, crumb, subnav) pad to the SAME column
     edge via --shell-align, whose % resolves per box against #app's width —
     the same base .shell-main centers in. Below the cap the max() collapses
     to the bare gutter, so phones are untouched. An app that wants a wider
     page (cbprospect: 1680px) overrides ONE token and every box follows. */
  --shell-max: 72rem;
  --shell-align: calc(max(0px, (100% - var(--shell-max)) / 2) + var(--shell-gutter));
  --shell-foot-h: 2.6rem;
  --shell-head-h: 3.4rem;
}

/* ── Header — slim sticky bar: mark, name, nav, identity ─────────────────── */
.shell-head {
  position: sticky;
  top: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  gap: 1.25rem;
  height: var(--shell-head-h);
  /* --shell-align, not the bare gutter: the header's content shares the main
     column's left edge at every width (cbapex#112). */
  padding: 0 var(--shell-align);
  border-bottom: var(--ax-line);
  background: var(--surface);
}
.shell-mark {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  /* The family wordmark, the aligned recipe (2026-08-18): mono 800 at
     0.82rem, tracked 0.28em, uppercase, in the app's own metal — PROSPECT ·
     ELECTION NIGHT · PUBLIC, one voice. This used to be a heavy tight-set
     name that each app re-voiced locally; the recipe lives here now. */
  font-family: var(--ax-mono, ui-monospace, monospace);
  font-weight: 800;
  font-size: var(--ax-fs-sm);
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--gold, var(--accent));
  text-decoration: none;
}
/* ⚠️ THE WORDMARK MUST BE ABLE TO SHRINK, AND IT COULD NOT ═══════════════════
   `.shell-mark` is a flex item of `.shell-head`, so its `min-width: auto`
   floors it at its own min-content — which for tracked uppercase mono is the
   LONGEST WORD, not the longest line. "Conservative" alone measures 138px at
   0.82rem/0.28em. The mark then refuses to shrink, the header's last control
   is pushed past the viewport, and the page scrolls sideways.

   Measured at 390px, all ten apps, both shared screens (2026-08-26, cbapex#56):
   cbconnector overflowed 26px with the MENU BUTTON off the right edge — the
   disclosure that holds the nav and sign-out at phone width — and cbnight,
   cbvmod and cbconnector were all silently WRAPPING the wordmark onto two
   lines inside a 3.4rem header.

   ⚠️ The overflow is therefore a function of `--app-name`, which is per-app —
   so "the file is shared, so every app is broken" is exactly wrong here, and so
   is "one app is broken". Nine were one long client name away from it. The
   floor belongs in the kit; the name that trips it does not exist yet.

   `min-width: 0` on both boxes lets it shrink; the ellipsis is what it degrades
   TO, rather than clipping mid-glyph. Nothing changes for a name that already
   fits — verified: only the two overflowing pages and the three wrapping
   wordmarks moved. */
/* ⚠️ THE FLOOR IS THE IMAGE, NOT ZERO (cbapex#132). This said `min-width: 0`,
   which let the ANCHOR collapse to nothing while the <img> kept its intrinsic
   width — so the logo overflowed its own box and the next nav control rendered
   on top of it. Measured on cbconnector, whose lockup is the family's widest:
   anchor 0px, image 84px, spill 84px, overlapping a control by 64px at 1100px
   and above.

   ⚠️ The reported cause was a missing `flex-shrink: 0` on the image, and the
   measurement says otherwise: the image was already at its full 84px and not
   shrinking at all. Removing the anchor's floor is what allows the collapse,
   so restoring it is the fix. The span keeps its own `min-width: 0` below, so
   the anchor's automatic minimum is now `image + 0` — the wordmark still
   ellipsises away to nothing under pressure (which is all cbapex#56 needed),
   and the image can no longer be squeezed out from under itself. */
/* ⚠️ AND THAT LAST SENTENCE WAS A GUESS, AND IT WAS WRONG (2026-09-07, the
   family's first Assay sweep). A flex item's automatic minimum is its
   min-content size, and a child's `min-width: 0` does not lower the child's
   min-content CONTRIBUTION — it only lets the child be shrunk once the parent
   is being laid out. So `min-width: auto` here floored the anchor at
   image + gap + THE WHOLE NAME: measured on the kit's own Feedback screen at
   390px, cbvmod's anchor 238px (69 image + 160 "VICTORY MODELING") pushed the
   Menu button to x=402, cbsitrep's 188px pushed it to 396 — exactly the
   cbapex#56 symptom, back, on the three shared screens, in the two apps
   whose name + lockup is widest. The floor is stated in pixels now: the
   image's own floor (2rem, square), so the anchor can shrink to a square
   logo and no further — the #132 collapse stays fixed — and the span below
   carries a large `flex-shrink`, so the NAME yields first and the logo only
   once the name is gone. ⚠️ Scoped to the NARROW header (the `64rem` block
   below, where the nav is hidden and the row is mark + three buttons): at
   desktop the nav is what yields, as it always has — measured unchanged at
   1100 and 1440 under four brands, and a first cut that floored the anchor
   at every width shrank cbconnector's lockup from 84px to 54px at 1100 with
   nothing overflowing. Fix the width you measured. */
.shell-mark img {
  /* ⚠️ It may SHRINK, down to a floor — `0 0 auto` fixed the overlap and made
     the sideways scroll worse, because the anchor then claimed the full 84px it
     had previously given up (cbconnector 119px → 203px of document overflow at
     1100). Both symptoms are the same row being over-full; the question is only
     what yields. A logo that scales down is a better answer than a logo with
     text on top of it OR a page that scrolls sideways, so it yields — to 2rem,
     which is its own height, i.e. it never goes below square. */
  height: 2rem;
  width: auto;
  flex: 0 1 auto;
  min-width: 2rem;
  object-fit: contain;
}
.shell-mark > span {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* ⚠️ IF THE MARK FAILS TO LOAD, THE NAME TAKES ITS PLACE — and for two apps
   that is the difference between a degraded header and an EMPTY one.
   `onerror` on every shared screen's <img> hides the image, which is right (a
   broken-image glyph in the chrome is worse than no glyph). It is only
   sufficient where the span beside it is visible text. cbconnector and cbvmod
   ship a LOCKUP — the wordmark is IN the artwork — so their `brand.css`
   visually hides that span deliberately, and hiding the image too leaves
   `<a class="shell-mark" href="/">` with nothing in it at all: the way home,
   blank.
   ⚠️ Both apps already solved this ON THEIR OWN PAGES (connector.html and
   vmod.html each un-hide the span in their own `onerror`) and neither fix could
   reach HERE, because these five screens are the kit's. That is the shape this
   family keeps paying for: fixed in the app, still broken in the shared file.
   Specificity (0,2,1) beats `brand.css`'s (0,1,1) whatever the load order, so
   this wins without `!important` and without brand.css needing to know. */
.shell-mark.is-markless > span {
  position: static;
  width: auto;
  height: auto;
  clip: auto;
  clip-path: none;
  overflow: visible;
  margin: 0;
}
/* ══ THE APEX SWITCHER — moved out (cbapex#23) ══════════════════════════════
   Its styling now lives in `apex-switcher.css`, because the switcher travels to
   pages that do not wear this frame at all — cbnight's Loom-descended pages,
   whose chrome is cbloom's `#hdr`. What stays HERE is the one thing that is
   genuinely this header's business: where Apps sits among its neighbours, in
   the narrow `order` block near the bottom of this file.

   ⚠️ Do not restate an `.apex-*` rule in this file. A shadow copy that loads
   later WINS, and a fix in the original is then silently overridden in exactly
   one place — the failure written up at length in cbnight's night.css. */

.shell-nav {
  display: flex;
  gap: 0.9rem;
  margin-left: auto;
  /* ⚠️ IT MUST BE ABLE TO YIELD, AND IT COULD NOT (cbapex#132). With neither
     `flex-wrap` nor `min-width`, this nav can only push: measured on
     cbconnector at 1100px its five children came to 954px of a 1203px header,
     and the overspill went to the DOCUMENT as sideways scroll rather than to
     the nav as a second line. Wrapping trades a horizontal scrollbar for a
     taller header, which is the better failure — and it is what the reporter
     was already seeing on his machine ("COPY TOKEN wrapped to two lines"),
     where a wider mono face pushed the row over sooner. */
  flex-wrap: wrap;
  min-width: 0;
  row-gap: 0.35rem;
}
/* ⚠️ GROUPED, NOT COPIED. `.shell-nav-token` is a <button> because it performs
   an action rather than navigating, but it is a nav-level control and must
   read as one. Two declaration lists that are meant to match
   will not stay matching — that is the drift this file keeps arguing against —
   so they share this one and add only what a <button> needs on top.
   ⚠️ A new kit-mounted nav BUTTON must be added to all four groups below, or it
   renders as a raw browser widget in the menu — the family's "a control outside
   `.ax-field` gets nothing" trap, in the chrome. `shell-nav-controls.test.ts`
   fails when a class shell-controls.js mounts is missing from them. */
.shell-nav a,
.shell-nav .shell-nav-token {
  font-family: var(--ax-mono);
  font-size: var(--ax-fs-micro);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--subdued);
  text-decoration: none;
  padding: 0.25rem 0.55rem;
  border: 1px solid transparent;
  border-radius: var(--ax-r-tight, 4px);
}
/* The <button> reset lives here, never inline: an inline `background:none;
   border:none` on a button is what files.test.ts flags, and it would also beat
   the open-menu rules below. */
.shell-nav .shell-nav-token {
  background: none;
  cursor: pointer;
  text-align: left;
}
.shell-nav a:hover,
.shell-nav .shell-nav-token:hover:hover {
  color: var(--text);
}
.shell-nav a[aria-current] {
  color: var(--accent);
  border-color: var(--accent);
}
/* ⚠️ State the [hidden] rule NEXT TO the display rule (family trap, paid for
   three times): gated links (Invite, staff sections, channel views) carry
   `hidden` until /api/me says otherwise, and the authored displays here and
   in the open-menu block below would beat the UA's [hidden]{display:none}. */
.shell-nav a[hidden] {
  display: none;
}
/* A nav entry that ACTS ("+ New Segment") and the app's front door. Both
   classes were used in three apps and defined NOWHERE (cbapex#54): the
   elements fell through to the plain entry while the suffix promised styling
   that was not there. The CTA wears the accent and weight in the closed head
   and again in the open sheet (the sheet's own rule below out-specifies this
   one); Home reads heavier than its siblings and nothing more. Any other
   shell-* class an app page names that no stylesheet defines is reported by
   shell-classes.test.ts. */
.shell-nav a.shell-nav-cta {
  color: var(--accent);
  font-weight: 700;
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
}
.shell-nav a.shell-nav-home {
  color: var(--text);
  font-weight: 700;
}
.shell-ident {
  font-family: var(--ax-mono);
  font-size: var(--ax-fs-micro);
  color: var(--subdued);
  white-space: nowrap;
  /* The one flexible child. Without these three, a signed-in identity at
   * phone width pushes past the viewport and the whole page gains ~28px of
   * sideways scroll — found on cbnight's Florida map view, but it affects
   * every view of every app spawned from this shell. (.shell-nav already
   * carries the margin-left:auto.) */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ── Main column ─────────────────────────────────────────────────────────── */
.shell-main {
  max-width: var(--shell-max);
  /* width:100% is load-bearing: #app is a column flexbox (see the claim
     below), and on a flex item the auto margins absorb cross-axis space —
     without an explicit width, a page whose content is narrower than the
     max-width SHRINKS to it, and the hero stops sharing the chrome's left
     edge. Caught by prospect's layout harness on the frame migration. */
  width: 100%;
  margin: 0 auto;
  padding: 1.5rem var(--shell-gutter) calc(var(--shell-foot-h) + 3rem);
}

/* The hero: eyebrow, name at display size, one measured line. A view that
   opens with a bare 1.5rem section label has no top. */
.hero {
  margin: 0.5rem 0 2rem;
}
.hero .eyebrow {
  font-family: var(--ax-mono);
  font-size: var(--ax-label-size, 0.7rem);
  letter-spacing: var(--ax-label-track, 0.1em);
  text-transform: uppercase;
  color: var(--accent);
  margin: 0 0 0.35rem;
}
.hero h1 {
  margin: 0;
  font-size: var(--ax-fs-display);
  font-weight: 900;
  letter-spacing: -0.02em;
  line-height: 1.05;
  /* A hero whose headline IS a number ("7 pending items") must not jitter
     between refreshes; two-line headlines break at the sensible place. */
  font-variant-numeric: tabular-nums;
  text-wrap: balance;
}
.hero .lede {
  margin: 0.6rem 0 0;
  max-width: 46rem;
  color: var(--subdued);
}
/* The meta row: when the numbers were read, and the one quiet action that
   re-reads them. Mono and subdued, and any button in it is compact — a 46px
   form button beside a 12px timestamp is the toolbar this row replaces (the
   HR queue's `.ax-head` + bare `.ax-btn`, 2026-08-27). The same contextual
   sizing `.data-table .ax-btn` already has. */
.hero-meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.35rem 0.75rem;
  margin-top: 0.75rem;
  font-family: var(--ax-mono);
  font-size: var(--ax-fs-micro);
  letter-spacing: 0.04em;
  color: var(--subdued);
}
.hero-meta[hidden] {
  display: none;
}
.hero-meta .ax-btn {
  min-height: 1.75rem;
  padding: 0.25rem 0.7rem;
  font-size: var(--ax-fs-xs);
}

/* ── Footer — fixed, with the instruments ────────────────────────────────── */
.shell-foot {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  z-index: 10;
  height: var(--shell-foot-h);
  display: flex;
  align-items: center;
  gap: 1rem;
  /* NO gutter here — .foot-main carries it, so the instruments land on the
     same left and right edges as the content column above them. The bar
     itself still spans the viewport; only its CONTENTS are the page's width.
     (Operator, 2026-08-18: the Feedback pill must line up with the page
     edge. Full-bleed padding put it ~100px outside the content column on a
     wide screen, which reads as a misalignment because it is one.) */
  padding: 0;
  border-top: var(--ax-line);
  background: var(--surface);
  font-family: var(--ax-mono);
  font-size: var(--ax-fs-micro);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--subdued);
}
/* Row one of the footer. The wrapper exists because build-line.js flips
   #footer itself to a column for the build line — the instruments keep
   their row in here. Without it they stack into full-width rows. */
.shell-foot .foot-main {
  display: flex;
  align-items: center;
  gap: 1rem;
  min-height: 2.2rem;
  min-width: 0;
  /* The same box as .shell-main: one max-width, one gutter, one pair of
     edges for the whole page. */
  width: 100%;
  max-width: var(--shell-max);
  margin: 0 auto;
  padding: 0 var(--shell-gutter);
}
/* ── The instrument strip (operator, 2026-09-11; CENTRED 2026-10-12) ───────
   The instruments cluster in one order at every width — You · Feedback ·
   Live log · Record — so nothing about them depends on the viewport. They
   held the RIGHT edge until 2026-10-12 (operator: "center the pills at the
   bottom … version info is centered"): the build line under them is
   centred, and a right-held row over a centred line read as misaligned.
   The name still holds the left: it is a flex:1 box, and the ::after is a
   second flex:1 box on the right, so the cluster sits at the true centre
   whatever the name's length (it ellipsises inside its box rather than
   pushing). Under 540px the name is hidden and the spacer with it, and
   `justify-content: center` alone does the job — the same centre when a
   page ships no name at all. The identity chip is a pill like the rest: it
   has opened You since 2026-09-07 and read as a label beside the one
   control that looked like one ("not apparent it's a link; offset").
   GROUPED with `.foot-btn`, not copied — two lists meant to match will not
   stay matching. */
.shell-foot .foot-main { justify-content: center; }
.shell-foot .foot-main > .foot-name {
  flex: 1 1 0;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.shell-foot .foot-main::after {
  content: "";
  flex: 1 1 0;
}
.shell-foot .foot-btn,
.shell-foot .shell-ident[role="button"] {
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  color: var(--subdued);
  background: transparent;
  border: var(--ax-line);
  border-radius: 999px;
  padding: 0.15rem 0.6rem;
  cursor: pointer;
  white-space: nowrap;
}
.shell-foot .foot-btn:hover,
.shell-foot .shell-ident[role="button"]:hover {
  color: var(--text);
  border-color: var(--accent);
}
/* A long name yields before the pills beside it do. */
.shell-foot .shell-ident[role="button"] {
  max-width: 14rem;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* A door whose thing is OPEN, or has something WAITING, says so beyond its word. */
.shell-foot .foot-log[aria-pressed="true"],
.shell-foot .foot-rec.has-n {
  color: var(--text);
  border-color: var(--accent);
}
/* The live pill: id + inner <span> are vendored loom.js's contract —
   setWsStatus() swaps connecting/live/down classes, rewrites the label and
   sets --hot-bg. State is never color alone; the label changes with it. */
.shell-foot .foot-live {
  /* No auto margin: the strip is centred (above), and an auto margin here
     would tear the live pill away from the cluster. */
  margin-left: 0;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
}
/* ⚠️ The lamp is the ::AFTER, ordered first. It was the ::before, and so is
   the pills' 44px touch extender below (`.shell-foot .foot-btn::before`:
   absolute, full width, 2.75rem tall) — the live pill is a .foot-btn, both
   rules drew on one pseudo-element, and the later one won position and
   size while this one kept the colour: a 2.75rem amber bar over the whole
   pill, measured 7×44px on public.campaignbrain.dev (hidden there only
   because `connecting` is display:none). The zoo's Frame row found it on
   2026-10-12, the first time the pill was ever drawn in a specimen. */
.shell-foot .foot-live::after {
  content: "";
  order: -1;
  width: 0.45rem;
  height: 0.45rem;
  border-radius: 50%;
  background: var(--hot-bg, var(--amber, #d4a024));
}
.shell-foot .foot-live.connecting::after,
.shell-foot .foot-live.down::after {
  animation: shell-live-pulse 1s ease-in-out infinite;
}
@keyframes shell-live-pulse {
  50% {
    opacity: 0.25;
  }
}
@media (max-width: 540px) {
  /* The header already names the product; the instruments and the version
     are what a thumb needs down here. */
  .shell-foot .foot-name,
  .shell-foot .foot-main::after {
    /* The name goes, and so must its balancing spacer, or the cluster is
       pushed left of centre by one flex:1 box with nothing opposite it. */
    display: none;
  }
  /* Four pills in 358px: tighter gaps, and the name pill gives first. */
  .shell-foot .foot-main { gap: 0.4rem; }
  .shell-foot .foot-btn,
  .shell-foot .shell-ident[role="button"] { padding: 0.15rem 0.5rem; }
  .shell-foot .shell-ident[role="button"] { max-width: 7.5rem; }
}

/* The live log overlay (markup + base styles are vendored from loom.css,
   which anchors it above cbloom's own 58px bar). This footer is the anchor
   here, and the head row wraps so all five buttons stay reachable at phone
   width. */
#loom-log-panel {
  bottom: var(--shell-foot-h);
}
#loom-log-panel .loom-log-head {
  flex-wrap: wrap;
  row-gap: 4px;
}
/* ⚠️ THE WAY OUT (operator, 2026-09-11). loom's head row ships pause · copy ·
   token · clear · harness and NO close — in cbloom the door was the footer's
   socket pill, which is dead here and was stripped from every app page, leaving
   Ctrl/Cmd+` as the only control and none at all on a phone. shell-controls.js
   appends this one on every page; `order` keeps it last however the row wraps,
   and the accent border marks it as the one that ends the thing rather than
   another verb applied to it. */
#loom-log-panel .shell-log-close {
  order: 99;
  color: var(--text);
  border-color: var(--accent);
  /* ⚠️ TALLER THAN ITS FIVE SIBLINGS, DELIBERATELY. loom sizes every button in
     this row at 21px, measured — fine for five verbs you reach for with a
     mouse, and this is the one a THUMB has to find to get its screen back. 32px
     is still under the 44px target; the row is loom-s own and a blanket height
     would break its rhythm for no gain (the .ctrl-select ruling), so the one
     control that ENDS the thing is the one that grows. */
  min-height: 32px;
  padding: 8px 14px;
}
#loom-log-panel .shell-log-close:hover {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}

/* ── Doors ───────────────────────────────────────────────────────────────
   A card that goes somewhere must SAY so — an iPad has no hover to discover
   an onclick with. Scoped to [onclick] so an informational card never wears
   a false chevron. Pair with doorAttrs() in the page script for keyboard
   and screen-reader semantics. */
.shell-card {
  padding: 1rem 1.15rem;
  border: var(--ax-line);
  border-radius: var(--ax-r, 8px);
  background: var(--surface);
  box-shadow: var(--ax-lift, none);
}
.shell-card[onclick] {
  position: relative;
  padding-right: 2.1rem;
  cursor: pointer;
}
.shell-card[onclick]::after {
  content: "›";
  position: absolute;
  top: 50%;
  right: 0.9rem;
  transform: translateY(-50%);
  font-family: var(--ax-mono);
  font-size: var(--ax-fs-lg);
  line-height: 1;
  color: var(--subdued);
}
.shell-card[onclick]:hover {
  border-color: var(--accent);
}
.shell-card[onclick]:hover::after,
.shell-card[onclick]:focus-visible::after {
  color: var(--accent);
}
.shell-card[onclick]:active {
  transform: translate(1px, 1px);
  box-shadow: none;
}
.shell-card[onclick]:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Sign out — outline, never solid: the one consequential control in the bar
   should be reachable without looking like the thing you are meant to press.
   It turns red on approach and asks before it acts. */
.shell-signout {
  font-family: var(--ax-mono);
  font-size: var(--ax-fs-micro);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--subdued);
  background: transparent;
  border: var(--ax-line);
  border-radius: var(--ax-r-tight, 4px);
  padding: 0.4rem 0.7rem;
  min-height: 2.2rem;
  cursor: pointer;
  white-space: nowrap;
}
.shell-signout:hover,
.shell-signout:focus-visible {
  color: var(--red, #d92b2b);
  border-color: var(--red, #d92b2b);
}
@media (max-width: 40rem) {
  /* Mobile is P1 for this family: a phone header holds a mark and one
     control. Identity moves to the footer; keep the way out. */
  .shell-head .shell-ident { display: none; }
  /* Tighter top on a phone (operator, 2026-08-27, measured on /app/hr: 24px of
     dead air above the crumb was most of the "tall band" — the strip and the
     crumb already separate content from chrome, so the padding was doing no
     work). 1.5rem → 1rem AT MOBILE ONLY; desktop keeps its air. A family
     decision made in the kit, not fought per-app with negative margins. */
  .shell-main { padding-top: 1rem; }
}

/* ══ Density ═══════════════════════════════════════════════════════════════
   How much air the repeated surfaces get. One attribute on <html> moves these
   properties; components read them instead of hardcoding padding, so a new
   row type joins the system by using the token rather than by being added to
   a list somewhere.

   Cozy is the middle and the default. The scale is not linear — the step is
   mostly VERTICAL, because that is what decides how many rows land on a phone
   screen, while horizontal padding barely moves: text touching the edge of its
   own card reads as broken at every size. */
:root,
:root[data-density="cozy"] {
  --shell-row-y: 0.7rem;
  --shell-row-x: 0.9rem;
  --shell-row-gap: 0.5rem;
  --shell-box-y: 0.85rem;
  --shell-box-x: 1rem;
  --shell-cell-y: 0.55rem;
}
:root[data-density="compact"] {
  --shell-row-y: 0.4rem;
  --shell-row-x: 0.75rem;
  --shell-row-gap: 0.3rem;
  --shell-box-y: 0.55rem;
  --shell-box-x: 0.85rem;
  --shell-cell-y: 0.3rem;
}
:root[data-density="comfortable"] {
  --shell-row-y: 1rem;
  --shell-row-x: 1.15rem;
  --shell-row-gap: 0.75rem;
  --shell-box-y: 1.15rem;
  --shell-box-x: 1.25rem;
  --shell-cell-y: 0.8rem;
}

/* Wire your repeated surfaces to the tokens, not to numbers. The two the
   template ships are here; add rows as the app grows them. */
.shell-card { padding: var(--shell-box-y) var(--shell-box-x); }
.data-table td,
.data-table th {
  padding-top: var(--shell-cell-y);
  padding-bottom: var(--shell-cell-y);
}

/* ── The control strip ─────────────────────────────────────────────────────
   Chrome, not content: it joins the sticky stack under the header and stays
   put while the view scrolls beneath it.

   Plain text, no box, right-aligned. The bordered segmented control this
   replaced took ~250px of the 390 available on a phone for something read
   once a week — a control that big has to earn it. */
.shell-controls {
  position: sticky;
  top: var(--shell-head-h);
  z-index: 4; /* under the sticky header */
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 0.85rem;
  padding: 0.3rem var(--shell-align); /* the main column's edge — cbapex#112 */
  /* Opaque, or the view scrolls through it and the labels become unreadable
     over their own content. */
  background: var(--bg, #171326);
  border-bottom: 2px solid var(--border, #5f4c66);
}

/* ── Loom's socket button is DEAD on Apex — retired in the seam ──────────────
   `<button id="loom-ws-status" class="foot-btn foot-live connecting">` came
   with every page promoted out of cbloom (55 pages across eleven apps, the
   kit's own screens included). It reports Loom's WebSocket, which no Apex
   page opens (loomInit is deliberately not called), so it pulses "connecting"
   forever — the "little spinner still running" the operator saw on 2026-08-28.
   Hidden here so all eleven apps stop showing it on the next vendor; the
   markup should still be removed page by page (cbapex issue filed). */
#loom-ws-status,
.foot-live.connecting {
  display: none;
}

/* ── The page-name slot and the sub-menu strip — CHROME, not content ────────
 * (operator, 2026-08-27, the de-iframe pass: full-bleed siblings of .shell-main,
 * not inset boxes; and 2026-08-28: "put the page name top-right where density
 * is — density only on the home page; remove the empty strip; lock the
 * sub-menu in place.")
 *
 * THE PAGE NAME rides in the controls strip's right-hand slot, where the home
 * page shows the density group — one occupant at a time, and a page with a
 * sub-menu gives the slot to its NAME:
 *   <span class="shell-page">HR Operations</span>
 * That retires the crumb strip on such pages (an empty strip is what was left
 * once the name moved). `.shell-crumb` stays for a page that wants a way-back
 * line and has no sub-menu.
 *
 * THE SUB-MENU is sticky: it joins the stack under the header and the strip, so
 * the section switcher is reachable while reading row 40 — the operator's
 * "lock the sub-menu in place". Its `top` is the strip's RENDERED height
 * (`--shell-controls-h`, set by shell-controls.js), not a design token: the
 * strip's height follows its content. Markup:
 *   <div class="shell-subnav"><div class="ax-tabs" role="tablist">…</div></div> */
.shell-page {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--ax-fs-sm);
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--text);
  text-decoration: none;
}
.shell-crumb {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.3rem var(--shell-align); /* cbapex#112 */
  background: var(--bg, #171326);
  font-size: var(--ax-fs-sm);
}
.shell-crumb a {
  color: var(--muted);
  text-decoration: none;
  font-weight: 700;
}
.shell-crumb a:hover {
  color: var(--text);
}
.shell-subnav {
  position: sticky;
  top: calc(var(--shell-head-h) + var(--shell-controls-h, 2.4rem));
  z-index: 3; /* under the strip (4) and the header */
  background: var(--bg, #171326);
}
.shell-subnav .ax-tabs {
  margin: 0; /* the strip IS the spacing; .ax-tabs' own 1.25rem top is for in-content use */
  padding: 0 var(--shell-align); /* cbapex#112 */
}
/* The sub-menu, same treatment (2026-09-07): the strip is a flex row so an
   ALL button can join its right edge when the tabs overflow — measured by
   shell-controls.js (`is-overflow`), never assumed from the viewport. The
   button shares the tabs' baseline rule so the strip stays one line. */
.shell-subnav {
  display: flex;
  align-items: stretch;
}
.shell-subnav .ax-tabs {
  flex: 1 1 auto;
  min-width: 0;
}
.shell-subnav-all {
  display: none;
  flex: none;
  align-items: center;
  gap: 0.3rem;
  margin: 0 var(--shell-align) 0 0;
  padding: 0.6rem 0.9rem 0.5rem;
  border: 0;
  border-bottom: var(--ax-line);
  background: none;
  color: var(--accent);
  font: inherit;
  font-size: var(--ax-fs-sm);
  font-weight: 700;
  white-space: nowrap;
  cursor: pointer;
}
.shell-subnav-all::after {
  content: "";
  width: 0.45em;
  height: 0.45em;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: translateY(-0.12em) rotate(45deg);
}
.shell-subnav.is-overflow .shell-subnav-all {
  display: inline-flex;
}
.shell-subnav.is-overflow .ax-tabs {
  padding-right: 0.5rem;
}
.shell-subnav-all:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* ⚠️ A SHORT VIEWPORT GETS ITS CHROME BACK (cbapex#110). A real reader at
   354×374 (Chrome on iOS 26, keyboard up) had 159px of sticky chrome — 43%
   of the screen — and filed "the sizing and scrolling is off". Nothing
   overflowed; the app simply handed them two table rows of room. Below 560px
   of HEIGHT the header and control strips stop sticking and scroll away with
   the page: nothing is removed, it just yields. The FOOTER deliberately does
   NOT shrink — at 62px it is the biggest band, and it carries the Feedback
   button, which is how this report reached us in the first place. 390×844 is
   untouched; height is the trigger, because no width rule can see this. */
@media (max-height: 560px) {
  .shell-head,
  .shell-controls,
  .shell-subnav {
    position: static;
  }
}


.shell-controls-label {
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--ax-fs-micro);
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--muted, #8b7a92);
  opacity: 0.75;
}
/* The density CONTROL is retired (cbapex#104) — each app authors its own
   `data-density` on <html>; the tokens below shell.css's density scale are
   the mechanism and they stay. */

/* ══ The ticker ════════════════════════════════════════════════════════════
   A view's headline numbers, riding in the control strip.

   WHY IT EXISTS. The instinct on a data app is to open every page with three
   or four bordered stat cards. cbprospect did, and the operator's verdict
   (2026-08-17) was "incredibly intrusive ... a sales person needs to see
   everything important up top" — which is the same complaint from both sides:
   the cards WERE up top, and by being up top they pushed the work below the
   fold. The strip was already there, already sticky, and half empty.

   THE CONSTRAINT IS THE HEIGHT. This bar sits above every view, so a second
   line here costs a line of the page everywhere in the app, forever. Hence
   the fixed height, the hard nowrap, and the marquee: content wider than the
   bar scrolls through it rather than wrapping. Nothing in this block may be
   allowed to grow the strip.

   Pair it with the ticker half of shellTicker() in example.html. */
.shell-ticker {
  --shell-tick-h: 1.15rem;
  /* Sized to content, allowed to shrink, never to grow — the search button's
     `margin-right: auto` is what holds the two ends of this row apart. */
  flex: 0 1 auto;
  /* min-width:0 is what stops a long ticker from shoving Search off the left
     edge: a flex item's default min-width is its content. */
  min-width: 0;
  height: var(--shell-tick-h);
  display: flex;
  align-items: center;
  justify-content: flex-end;
}
.shell-ticker[hidden] {
  display: none;
}
/* The strip is a button (2026-09-07): a tap opens the Headline dialog. */
.shell-ticker[role="button"] {
  cursor: pointer;
  border-radius: var(--ax-r);
}
.shell-ticker[role="button"]:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
/* The strip says it opens (2026-09-07): the sub-menu's chevron, at the end,
   outside the rolling window. */
.shell-ticker-more {
  flex: none;
  width: 0.45em;
  height: 0.45em;
  margin: 0 0.35rem 0 0.55rem;
  border-right: 2px solid var(--accent);
  border-bottom: 2px solid var(--accent);
  transform: translateY(-0.12em) rotate(45deg);
}
/* A rolling ticker beside a page name at phone width crushed the name to
   "H.." (measured on the fixture, 2026-09-07): both shrink, and the ticker's
   basis is its whole content. The NAME keeps a share; the ticker rolls more. */
@media (max-width: 40rem) {
  .shell-controls:has(.shell-ticker:not([hidden])) .shell-page {
    flex: 0 0 auto;
    max-width: 42%;
  }
}
.shell-ticker-dlg .shell-ticker-deck {
  --ax-deck-min: 8rem;
  --ax-deck-gap: 0.5rem;
  margin: 0.3rem 0 0.9rem;
}
.shell-ticker-win {
  max-width: 100%;
  overflow-x: auto;
  /* Swipeable even when the animation is off (see the reduced-motion block).
     The scrollbar is suppressed because this is chrome, not content. */
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.shell-ticker-win::-webkit-scrollbar {
  display: none;
}
.shell-ticker-track {
  display: flex;
  align-items: center;
  gap: 1.5rem;
  white-space: nowrap;
  width: max-content;
}
/* Rolling only when the content is genuinely wider than the bar — the JS
   measures before adding this class, because motion with nothing to reveal is
   just noise. The duplicated copy plus -50% is what makes the loop seamless;
   animating one copy leaves the bar empty for half of every cycle. */
.shell-ticker-win:has(.is-rolling) {
  /* The fade exists to say "this continues" — on a bar that FITS it said
     "this is cut off" instead (operator's collision report). Rolling only. */
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 1.4rem, #000 calc(100% - 1.4rem), transparent);
  mask-image: linear-gradient(90deg, transparent, #000 1.4rem, #000 calc(100% - 1.4rem), transparent);
}
.shell-ticker-track.is-rolling {
  animation: shell-tick-roll var(--shell-tick-dur, 24s) linear infinite;
}
.shell-ticker-win:hover .shell-ticker-track.is-rolling,
.shell-ticker-win:focus-within .shell-ticker-track.is-rolling {
  animation-play-state: paused;
}
@keyframes shell-tick-roll {
  from { transform: translateX(0); }
  /* Half, because the track holds the content twice; the trailing gap is
     inside the first copy, so -50% minus half a gap lands on the seam. */
  to { transform: translateX(calc(-50% - 0.75rem)); }
}
.shell-tick {
  display: inline-flex;
  align-items: baseline;
  gap: 0.4rem;
  line-height: 1;
}
.shell-tick b {
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--ax-fs-xs);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--text, #fff7ec);
}
.shell-tick span {
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--ax-fs-micro);
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted, #8b7a92);
}
/* Semantic colour, never on its own: the tone rides on the NUMBER, which
   already carries weight and a mono face, and the label stays put. */
.shell-tick.is-ok b { color: var(--green, #2fbf71); }
.shell-tick.is-warn b { color: var(--amber, #ffb347); }
.shell-tick.is-bad b { color: var(--red, #ff5c5c); }
@media (prefers-reduced-motion: reduce) {
  .shell-ticker-win:has(.is-rolling) {
  /* The fade exists to say "this continues" — on a bar that FITS it said
     "this is cut off" instead (operator's collision report). Rolling only. */
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 1.4rem, #000 calc(100% - 1.4rem), transparent);
  mask-image: linear-gradient(90deg, transparent, #000 1.4rem, #000 calc(100% - 1.4rem), transparent);
}
.shell-ticker-track.is-rolling { animation: none; }
}
/* ══ The busy portal ═══════════════════════════════════════════════════════
   The app's ONE loading state: covers everything, blurs what is underneath,
   and holds the previous view on screen rather than blanking it.

   ⚠️ The blur is `backdrop-filter` HERE, never `filter` on #app — a filter on
   an ancestor makes it the containing block for `position: fixed` children,
   which tears the header and footer off the viewport for as long as the app is
   loading. The amount comes from skin.css's `[data-glass]` opt-in via
   `--ax-glass-blur`; the design language bans blur everywhere else. */
.shell-busy {
  position: fixed;
  inset: 0;
  z-index: 100; /* above the sticky header and the fixed footer */
  display: grid;
  place-items: center;
  align-content: center;
  gap: 0.9rem;
  background: color-mix(in srgb, var(--bg, #171326) 38%, transparent);
  --ax-glass-blur: 3px;
  animation: shell-busy-in 0.14s ease-out;
}
/* A `hidden` attribute LOSES to any authored display. State the rule next to
   the display rule, or the overlay never goes away. */
.shell-busy[hidden] { display: none; }
.shell-busy-label {
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--ax-fs-micro);
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--muted, #8b7a92);
}
/* The portal's spinner is the WEAVE — nine cells lighting on a diagonal, from
   cbloom's spinner lab via cbprospect (which replaced the template's old
   three-dot spinner when the portal became the app's one loading state).
   skin/shell.js injects the portal markup and carries the mechanism:
   reference-counted busyStart()/busyEnd(), a 180ms delay so sub-150ms calls
   never flash it, and withBusy(promise) which ALWAYS settles — a rejection
   that left #app inert would lock the page with no way back.

   ⚠️ ONE loading state. Never draw a spinner into #content next to this —
   that blanks the view being read and puts two spinners on screen, a mistake
   this platform has already paid for twice (loom.js's scrim, and the frame's
   own first cut). */
.shell-weave {
  --shell-weave-size: 3rem;
  --shell-weave-speed: 1.1s;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: calc(var(--shell-weave-size) * 0.12);
  width: var(--shell-weave-size);
  height: var(--shell-weave-size);
}
.shell-weave i {
  border-radius: 3px;
  background: var(--accent, #ff8100);
  animation: shell-weave-cell var(--shell-weave-speed) ease-in-out infinite;
}
.shell-weave i:nth-child(1) { animation-delay: 0s; }
.shell-weave i:nth-child(2),
.shell-weave i:nth-child(4) { animation-delay: calc(var(--shell-weave-speed) * 0.12); }
.shell-weave i:nth-child(3),
.shell-weave i:nth-child(5),
.shell-weave i:nth-child(7) { animation-delay: calc(var(--shell-weave-speed) * 0.24); }
.shell-weave i:nth-child(6),
.shell-weave i:nth-child(8) { animation-delay: calc(var(--shell-weave-speed) * 0.36); }
.shell-weave i:nth-child(9) { animation-delay: calc(var(--shell-weave-speed) * 0.48); }
@keyframes shell-weave-cell {
  0%, 70%, 100% { opacity: 0.18; }
  35% { opacity: 1; }
}

/* ⚠️ #app IS THE FRAME'S ELEMENT, AND loom.css THINKS IT IS ITS OWN.
   loom.css pins #app to the viewport (height: 100dvh, overflow-y: hidden) —
   cbloom's app shell scrolls INSIDE its own main, ours scrolls the page. Left
   unclaimed, any page that adds the wrapper for the busy portal stops
   scrolling entirely: fourteen invite rows clipped at the fold with nothing
   to say they continue (operator, 2026-08-18: "unable to scrolldown screen").
   cbprospect carries this same override in its own layer; the frame owns it
   here so no page has to remember. The lift (z-index + isolation) is what
   keeps the ground's starfield behind the content. */
#app {
  position: relative;
  z-index: 1;
  isolation: isolate;
  display: flex;
  flex-direction: column;
  height: auto;
  min-height: 100vh;
  min-height: 100dvh;
  overflow: visible;
}

/* Pointer-events fallback for anything without `inert`: a stale list must not
   be clickable while it is being replaced. */
#app.is-busy {
  pointer-events: none;
  user-select: none;
}

@keyframes shell-busy-in {
  from { opacity: 0; }
  to { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .shell-busy { animation: none; }
  .shell-busy .shell-weave i { animation-duration: 2.4s; }
}

/* ══ Search ════════════════════════════════════════════════════════════════
   The launcher sits at the strip's left edge, opposite Density: one is what
   you are looking FOR, the other is how it is laid out. */
.shell-searchbtn {
  margin-right: auto; /* the only thing holding the two apart */
  /* Never shrinks. The ticker shares this row and can be arbitrarily long;
     a flex item's default `shrink: 1` is exactly how it would eat search. */
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.15rem 0;
  border: 0;
  background: transparent;
  color: var(--muted, #8b7a92);
  cursor: pointer;
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--ax-fs-micro);
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}
.shell-searchbtn:hover { color: var(--accent, #ff8100); }
/* The magnifier, drawn rather than fetched: one glyph is not worth a request
   or a font, and this way it takes the button's colour in every state. */
.shell-searchbtn-i {
  position: relative;
  width: 0.72rem;
  height: 0.72rem;
  border: 2px solid currentColor;
  border-radius: 50%;
}
.shell-searchbtn-i::after {
  content: "";
  position: absolute;
  right: -0.18rem;
  bottom: -0.18rem;
  width: 0.3rem;
  height: 2px;
  background: currentColor;
  transform: rotate(45deg);
  transform-origin: right center;
}
.shell-searchbtn kbd {
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--ax-fs-micro);
  padding: 0 0.28rem;
  border: 2px solid var(--border, #5f4c66);
  border-radius: 4px;
}

/* The modal — same portal treatment as the busy overlay: outside #app, blurred
   through skin.css's `[data-glass]` opt-in. */
.shell-search {
  position: fixed;
  inset: 0;
  z-index: 110; /* above the busy portal: search can be opened while idle */
  display: grid;
  justify-items: center;
  align-content: start;
  padding: max(8vh, 3rem) 1rem 1rem;
  background: color-mix(in srgb, var(--bg, #171326) 45%, transparent);
  --ax-glass-blur: 3px;
}
.shell-search[hidden] { display: none; }
.shell-search-box {
  width: min(36rem, 100%);
  border: 2px solid var(--border, #5f4c66);
  border-radius: 7px;
  background: var(--surface, #231831);
  box-shadow: 8px 8px 0 rgba(7, 5, 14, 0.34);
  overflow: hidden;
}
.shell-search-top {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 0.6rem 0.5rem 0.9rem;
  border-bottom: 2px solid var(--border, #5f4c66);
}
.shell-search-top input {
  flex: 1;
  min-width: 0;
  border: 0;
  background: transparent;
  color: var(--text, #fff7ec);
  /* 16px minimum: anything smaller and iOS zooms the whole page on focus,
     which on a phone reads as the app lurching sideways. */
  font-size: var(--ax-fs-md);
  padding: 0.45rem 0;
}
.shell-search-top input:focus { outline: none; }
.shell-search-esc {
  border: 2px solid var(--border, #5f4c66);
  border-radius: 4px;
  background: transparent;
  color: var(--muted, #8b7a92);
  cursor: pointer;
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--ax-fs-micro);
  letter-spacing: 0.1em;
  padding: 0.2rem 0.4rem;
}
.shell-search-results {
  max-height: min(60vh, 26rem);
  overflow-y: auto;
}
/* The region heading over each kind of result (2026-09-06) — the menu's and
   the switcher's recipe. It replaced the per-row kind badge, so a row is one
   column: the label, its sub-line under it. */
/* The heading a `here` search row scrolled to — a ring for a beat, then gone. */
.shell-search-found {
  outline: 2px solid var(--accent);
  outline-offset: 0.25rem;
  border-radius: 0.3rem;
  transition: outline-color 0.3s;
}
.shell-search-more {
  float: right;
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  opacity: 0.7;
}
.shell-search-h {
  margin: 0;
  padding: 0.6rem 0.9rem 0.2rem;
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--ax-fs-micro);
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--subdued, #a598b8);
}
.shell-search-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  align-items: baseline;
  gap: 0.15rem 0.6rem;
  padding: 0.5rem 0.9rem;
  cursor: pointer;
  border-bottom: 1px solid color-mix(in srgb, var(--border, #5f4c66) 40%, transparent);
}
.shell-search-row:last-child { border-bottom: 0; }
/* Selected is a FILL plus a left edge, not colour alone — this row is chosen
   by keyboard as often as by pointer, and the keyboard user has no cursor to
   tell them where they are. */
.shell-search-row.sel {
  background: color-mix(in srgb, var(--accent, #ff8100) 14%, transparent);
  box-shadow: inset 3px 0 0 var(--accent, #ff8100);
}
.shell-search-kind {
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--ax-fs-micro);
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent, #ff8100);
}
.shell-search-label {
  font-weight: 700;
  font-size: var(--ax-fs-sm);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.shell-search-sub {
  grid-column: 1;
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--ax-fs-micro);
  color: var(--muted, #8b7a92);
}
.shell-search-hint {
  padding: 0.7rem 0.9rem;
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--ax-fs-micro);
  color: var(--muted, #8b7a92);
}

/* ══ Fields — ONE border, ONE ring, and the chevron is ours ════════════════
   Backported from cbprospect, where the operator caught it (2026-08-17): "on
   Invite a Team Member and other text fields and drop downs, we have two
   borders. I think we make one and the platform makes one."

   Neither was the platform's. Measured on the focused field, the two rings
   were `outline: 2px solid #ffba55` and `border: 1px solid #ff8100`:

     1. skin.css draws the app's one focus ring on `:focus-visible` — gold at
        3px offset, deliberately never removed, because loom.css sets
        `outline: none` on inputs and swaps a border colour instead, and a
        focus indicator made of border colour disappears against a themed
        border.
     2. loom.css ALSO turns the field's own border accent on focus.

   So the fix is to stop drawing the second one, not to weaken the first: the
   border holds still and the ring is the whole signal. A ring appearing is a
   shape change rather than a colour swap, so "colour is never the only
   signal" still holds.

   ⚠️ `.loom-form-row` is what loomFormModal actually emits — not `.modal`,
   not `.field`. cbprospect had both of those in the selector and neither
   matched, so the one screen the operator was looking at fell through to a
   generic 1px treatment while every other field in the app wore 2px. */
.ctrl-input,
.filter-bar select,
.filter-bar input,
.dt-filter input,
.loom-form-row input:not([type="checkbox"]):not([type="radio"]):not([type="file"]),
.loom-form-row select,
.loom-form-row textarea,
.field input:not([type="checkbox"]):not([type="radio"]):not([type="file"]),
.field select,
.field textarea {
  border: 2px solid var(--border, #5f4c66) !important;
  border-radius: 5px !important;
  /* background-COLOR, not the shorthand: the select's chevron below is a
     background-image and the shorthand would wipe it. */
  background-color: var(--bg, #171326) !important;
  color: var(--text, #fff7ec);
  font: inherit;
  font-size: var(--ax-fs-sm);
  padding: 0.5rem 0.7rem;
  /* The closed control is ours — no system bevel, no system arrow. The OPEN
     list stays the OS's on purpose: on a phone it is a full-height sheet a
     thumb can already use, and no hand-built listbox would beat it. */
  appearance: none;
  -webkit-appearance: none;
}
/* ⚠️ Plain `:focus` as well as `:focus-visible`. A <select> opened with a
   mouse does not match `:focus-visible` in every browser, so killing the
   border change without this line leaves a mouse-focused dropdown with no
   focus indication at all — a worse bug than the one being fixed, and an
   invisible one. */
.ctrl-input:focus,
.filter-bar select:focus,
.filter-bar input:focus,
.dt-filter input:focus,
.loom-form-row input:focus,
.loom-form-row select:focus,
.loom-form-row textarea:focus,
.field input:focus,
.field select:focus,
.field textarea:focus {
  border-color: var(--border, #5f4c66) !important;
  outline: 2px solid var(--n-gold, #ffba55);
  outline-offset: 3px;
}

/* The chevron, drawn rather than fetched. Two gradients meeting at a point —
   a triangle, in tokens. A data-URI SVG would be shorter and would bake a hex
   inside a URL where no theme can reach it, which is the one thing that
   breaks a theme switch. */
.filter-bar select,
.loom-form-row select,
.field select,
select.ctrl-input {
  padding-right: 1.9rem;
  background-image:
    linear-gradient(45deg, transparent 50%, var(--muted, #8b7a92) 50%),
    linear-gradient(135deg, var(--muted, #8b7a92) 50%, transparent 50%);
  background-position: calc(100% - 1.05rem) calc(50% + 1px), calc(100% - 0.72rem) calc(50% + 1px);
  background-size: 0.33rem 0.33rem;
  background-repeat: no-repeat;
}
.filter-bar select:hover,
.loom-form-row select:hover,
.field select:hover,
select.ctrl-input:hover {
  background-image:
    linear-gradient(45deg, transparent 50%, var(--accent, #ff8100) 50%),
    linear-gradient(135deg, var(--accent, #ff8100) 50%, transparent 50%);
}

/* ⚠️ THE LINE FOLLOWS THE ACCENT TOO, AND ITS LIGHTNESS DOES NOT.
   Operator, 2026-09-04: "do the line too." A straight `color-mix` of the accent
   fails the same way one shared ink value fails thirteen accents: measured at a
   fixed 30%, cbconnector's ruby yields a 1.38:1 border that disappears while
   cbnight's silver yields 3.02:1. A border has to stay a border in every app.

   So the HUE comes from the accent and the LIGHTNESS from a fixed grey: 22%
   accent into #57505f. Measured across the eleven undeclared apps, that lands
   2.07:1 – 3.30:1 on `--n-surface`, against the 2.18:1 the family's own default
   `#5f4c66` already ships — so the quietest derived line is the one the design
   has been accepting all along, and the brightest belongs to silver, which is
   an intrinsically light accent and reads correctly a little stronger.

   It is set on `:root` rather than on `html` because this is a TOKEN, not
   atmosphere — and `brand.css` loads after this file, so the two apps that
   declared their own line keep it with no dial required. */
:root {
  --n-line: color-mix(in srgb, var(--accent, #ff8100) 22%, #57505f);
}

/* ══ The ground ══════════════════════════════════════════════════════════════
   The family's atmosphere: warm radial washes over the plum gradient, with
   the starfield behind everything. cbprospect carried this in its own app
   layer since 2026-08-16 and the other apps quietly lost it — a flat var(--bg)
   page next to Prospect's is what "missing certain backgrounds" looks like.

   The layering is deliberately simpler than prospect's original: the washes
   live on <html>, the starfield on body::before at z-index:-1, and body goes
   transparent — so the dots sit under ALL content with no stacking-context
   surgery on the page's own elements. (A ::before at z-index:0 paints OVER
   in-flow content; prospect solves that with an #app lift, which every page
   would otherwise have to remember.) Static hexes for the same reason they
   ⚠️ THE HEXES WERE cbprospect'S BRAND, AND THEY SHIPPED TO THIRTEEN APPS.
   This block said the static hexes were deliberate — "atmosphere mixed against
   the palette, not tokens of it" — and that was true of the app it was written
   in and false of everywhere it landed. `#ff8100` is not a warm wash, it is
   `tokens.css`'s DEFAULT ACCENT, which is to say cbprospect's; `#171326` is
   `--n-space`'s default value written out longhand. So every other app wore
   Prospect's plum ground under Prospect's orange glow, whatever its own brand:
   garnet on plum, emerald on plum, teal on plum. Operator, 2026-09-04: "fix the
   plum ground - make it read the app accent."

   It was invisible for a reason worth keeping: `body[data-device]` and any page
   painting its own ground cover it, so the atmosphere only shows on the
   KIT-OWNED screens — profile, invites, feedback — which are the three screens
   an app's own agent never designs and rarely opens.

   The mapping is exact where a token exists. `#171326` IS `--n-space`; the
   accent wash was `#ff8100` at alpha 0x22, which is 13%. The end stop `#1b1030`
   is the one hand-tuned value with no token behind it, so it is derived instead
   — `--n-space` carried 20% toward the app's own `--n-surface`, which reproduces
   the original's "deepen slightly down the page" without inventing a hue.

   `--shell-wash` keeps the cool counter-glow a dial rather than a decision: it
   is a deliberate TEMPERATURE CONTRAST against the accent, so deriving it from
   the brand too would flatten exactly what it is there to do. An app that wants
   a different atmosphere sets one property. */
html {
  /* ⚠️ THE GROUND IS DERIVED FROM THE ACCENT, NOT FROM `--n-space`, and that is
     the second half of the same fix. Making the GLOW read the accent left the
     plum in place for eleven of thirteen apps, because the plum IS `--n-space`'s
     default and only cbsitrep and cbsvi ever declared their own. Operator,
     2026-09-04: "make the ground follow the accent for the other eleven too."

     A near-black carrying a little of the app's own accent, so every app's page
     is in its own hue: emerald reads #0f1716, garnet #210e17, silver #1c1b22,
     mint #0b1b1a. Measured across all thirteen before choosing the ratios —
     `--n-foreground` lands between 13.2:1 and 17.0:1 on the derived grounds, so
     no app is anywhere near a floor.

     ⚠️ AN APP THAT HAS ALREADY CHOSEN ITS GROUND KEEPS IT. These are dials, not
     a decision: cbsitrep and cbsvi pin `--shell-ground` to their own `--n-space`
     in brand.css, because a considered ground beats a derived one and theirs
     were picked with the palette. Everything else derives. */
  /* The dial's default, declared on `html` so a brand's `:root { --shell-wash }`
     (higher specificity, loads last) wins over it; it used to exist only as a
     `var()` fallback, which no test could see (cbapex#176). */
  --shell-wash: #7ca8c41c;
  --shell-ground: color-mix(in srgb, var(--accent, #ff8100) 9%, #0b0910);
  --shell-ground-deep: color-mix(in srgb, var(--accent, #ff8100) 15%, #0b0910);
  background: radial-gradient(circle at 12% 6%,
      color-mix(in srgb, var(--accent, #ff8100) 13%, transparent), transparent 30rem),
    radial-gradient(circle at 88% 8%, var(--shell-wash), transparent 28rem),
    linear-gradient(180deg, var(--shell-ground), var(--shell-ground-deep));
  background-attachment: fixed;
}
body {
  /* skin.css paints body var(--bg); the ground on <html> is the page's
     colour now, and an opaque body is how the atmosphere gets lost. */
  background: transparent;
}
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: 0.5;
  background-image: radial-gradient(circle, #fff7eca6 0 1.5px, transparent 2px),
    radial-gradient(circle, #ffba5580 0 1px, transparent 1.5px);
  background-size: 140px 128px, 180px 176px;
  background-position: 0 0, 42px 58px;
}

/* ══ Header buttons — Card and Menu ══════════════════════════════════════════
   cbnight's pattern (nt-hbtn), generalized 2026-08-18 when Public asked for
   the same pair. One box shared by Card and Menu — any drift between them
   reads as a misalignment, not a style difference. */
.shell-hbtn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  min-height: 2.5rem;
  padding: 0.4rem 0.7rem;
  border: 2px solid color-mix(in srgb, var(--line) 80%, transparent);
  border-radius: var(--ax-r);
  background: transparent;
  color: var(--subdued);
  cursor: pointer;
  font-family: var(--ax-mono);
  font-size: var(--ax-fs-micro);
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  transition: 0.15s;
}
.shell-hbtn:hover {
  border-color: var(--accent);
  color: var(--accent);
}

/* ══ Touch targets on the chrome (Assay QA at 390, 2026-09-06) ═════════════
   The chrome stays exactly as dense as it is — cbapex#41 keeps that on
   purpose, and a 44px search button would make the strip a toolbar. What
   grows is the HIT AREA, not the paint: a transparent ::before on each small
   control extends it to 2.75rem (44px) tall, full width, centred on the
   control; the header buttons, already 40px tall, simply get the 44px box.
   Measured before: mark 32px, search 19px, footer feedback 21px, header
   buttons 35×40 — on every page, at every phone viewport, in fourteen apps.
   On a phone or any coarse pointer; a mouse never needed it. ⚠️ This block
   sits BELOW `.shell-hbtn`'s own rule on purpose: same specificity, and the
   later declaration wins — above it, min-height stayed 2.5rem (measured 44×40
   on the first live crawl after it shipped). Assay's
   tap-target check reads the ::before, so it measures what a thumb gets. */
/* The footer pills' hit area holds at EVERY width (APEX-COMPOSE Q0.5): they
   sit alone on their row, so a 2.75rem target overlaps nothing, and a cursor
   deserves the same floor as a thumb. The ink does not change. */
.shell-foot .foot-btn, .shell-foot .shell-ident[role="button"] { position: relative; }
.shell-foot .foot-btn::before, .shell-foot .shell-ident[role="button"]::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 2.75rem;
  transform: translateY(-50%);
}
/* Under a MOUSE these four still sat below WCAG 2.5.8's 24px (search 22,
   ticker 18, the crumb's current level and the card's Edit 13): a 24px hit
   area at every width, which overlaps nothing. Touch takes 44 below. */
.shell-searchbtn, .shell-ticker[role="button"], .ax-crumb b[role="button"], .shell-crumb b[role="button"] { position: relative; }
.shell-searchbtn::before, .shell-ticker[role="button"]::before, .ax-crumb b[role="button"]::before, .shell-crumb b[role="button"]::before, .shell-bc-edit::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 1.5rem;
  transform: translateY(-50%);
}
@media (max-width: 40rem), (pointer: coarse) {
  .shell-hbtn { min-width: 2.75rem; min-height: 2.75rem; }
  /* APEX-COMPOSE Q0.6 (2026-10-09) adds four that OPEN something and were
     measured under the floor at 390: the ticker strip (18px, opens the
     Headline dialog), the crumb's current level (13px, opens where-you-are),
     the hero meta row's compact button (28px), the business card's Edit link
     (13px; already absolute, so it needs no position of its own). */
  .shell-searchbtn, .shell-foot .foot-btn, .shell-foot .shell-ident[role="button"], .shell-page[role="button"], .shell-subnav-all, .shell-mark,
  .shell-ticker[role="button"], .ax-crumb b[role="button"], .shell-crumb b[role="button"], .hero-meta .ax-btn,
  .ax-group-head .ax-btn, .ax-person-foot .ax-btn, .ax-sect-e .ax-btn, .ax-notice-act .ax-btn, .ax-secret .ax-btn { position: relative; }
  .shell-searchbtn::before, .shell-foot .foot-btn::before, .shell-foot .shell-ident[role="button"]::before, .shell-page[role="button"]::before, .shell-subnav-all::before, .shell-mark::before,
  .shell-ticker[role="button"]::before, .ax-crumb b[role="button"]::before, .shell-crumb b[role="button"]::before, .hero-meta .ax-btn::before, .shell-bc-edit::before,
  .ax-group-head .ax-btn::before, .ax-person-foot .ax-btn::before, .ax-sect-e .ax-btn::before, .ax-notice-act .ax-btn::before, .ax-secret .ax-btn::before {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: 50%;
    height: 2.75rem;
    transform: translateY(-50%);
  }
}

/* ⚠️ THIS USED TO BE `margin-left: 0`, and the comment on it read: "NO
   margin-left:auto here at wide widths: .shell-nav and .shell-signout each
   carry one, and flex DIVIDES free space between every auto margin — a third
   would strand this button in open space (prospect paid for it)." Both of
   those auto margins are gone from the wide header now that the nav is a
   disclosure at every width and the in-header Sign out is hidden, so this is
   the only push left and it has to be here — on the FIRST of the button
   group, so Card · Apps · Menu travel together. The hazard the old comment
   names is real and unchanged: if a THIRD auto margin ever returns to this
   row, they divide the space and this button strands. */
.shell-card-btn { margin-left: auto; }
/* The QR glyph off cbcdn, painted through a MASK so it takes the button's
   colour in every state. Safari still needs the -webkit- form. If the CDN is
   unreachable the box paints solid — small, beside a button that still reads
   CARD and still works. */
.shell-card-btn .ci {
  --ci-mask: url("https://cdn.campaignbrain.dev/svg/icons/qr.svg");
  display: block;
  width: 1rem;
  height: 1rem;
  background: currentColor;
  -webkit-mask: var(--ci-mask) center / contain no-repeat;
  mask: var(--ci-mask) center / contain no-repeat;
}

/* The disclosure button — hamburger to X, at EVERY width since 2026-09-24
   (see the block below the dialog-lock rules). It used to be hidden wide and
   turned on by a ≤64rem block — a TABLET, not just a phone, because 52rem
   stranded the iPad. */
.shell-menu { display: inline-flex; }
.shell-menu .mi {
  display: grid;
  gap: 3px;
  width: 0.9rem;
}
.shell-menu .mi i {
  display: block;
  height: 2px;
  background: currentColor;
  border-radius: 2px;
  transition: 0.15s;
}
.shell-menu[aria-expanded="true"] {
  border-color: var(--accent);
  color: var(--text);
  background: color-mix(in srgb, var(--accent) 16%, transparent);
}
.shell-menu[aria-expanded="true"] .mi i:nth-child(1) { transform: translateY(5px) rotate(45deg); }
.shell-menu[aria-expanded="true"] .mi i:nth-child(2) { opacity: 0; }
.shell-menu[aria-expanded="true"] .mi i:nth-child(3) { transform: translateY(-5px) rotate(-45deg); }

/* The nav-panel copies of identity and sign-out exist only for the narrow
   disclosure layout. */
.shell-who-nav,
.shell-signout-nav { display: none; }

/* Region headings exist at every width and SHOW only in the open sheet —
   wide, the nav is an inline row and the regions are its order. */
.shell-nav-h { display: none; }
/* The glass behind the open menu: the busy portal's own recipe (`data-glass`,
   `--ax-glass-blur` — skin.css's sanctioned exception), below the header
   (z 20) and above everything else. */
.shell-scrim {
  position: fixed;
  inset: 0;
  z-index: 15;
  background: color-mix(in srgb, var(--bg, #171326) 38%, transparent);
  --ax-glass-blur: 3px;
  touch-action: none;
}
.shell-scrim[hidden] { display: none; }
/* The document does not scroll while the menu or search sheet is open; the
   sheet does. shell.js sets these two on their own open/close. */
html.shell-menu-open,
html.shell-menu-open body,
html.shell-search-open,
html.shell-search-open body {
  overflow: hidden;
  overscroll-behavior: none;
}
/* ⚠️ AND THIS ALONE SCROLLS THE PAGE TO THE TOP, THE SAME WAY THE DIALOG DID
   (cbapex#147, after cbapex#146). `overflow: hidden` on `<html>` collapses
   its scrollable range to zero ONLY because `lib/loom.css` gives it an
   explicit `height: 100%` (for iOS Safari's URL bar, nothing to do with
   scrolling) — measured against the real, fully-vendored page: opening the
   menu or the search overlay from deep in a long screen landed `scrollY` at
   0 every time; a from-scratch page missing that one loom.css rule
   reproduced neither this nor the dialog's bug. openMenu/openSearch in
   shell-controls.js capture the real position through shell.js's shared
   `shellScrollLock` BEFORE adding this class, and restore it with `scrollTo`
   AFTER removing it.

   ⚠️ NO `position: fixed` ON `<body>` HERE, unlike the dialog below — see
   `shellScrollLock`'s own comment for why giving these two the same pin
   would have shipped a worse bug: it drags the header (`position: sticky`)
   and the nav sheet off-screen with it, and neither the menu nor the search
   sheet has a full-viewport `::backdrop` to hide that behind. */
/* The dialog gets its own rule, not folded into the one above, because it
   needs one more property AND a different comment — see the note just above
   and `shellScrollLock`'s in shell.js. shell.js sets `shell-dialog-open` for
   every shellDialog — the Card included (2026-09-06). `dialogLock()` pins
   `<body>` at its current scroll offset with `top`, and this rule turns that
   pin into an actual visual lock: safe here because a `<dialog>` shown
   modally is top-layer (viewport-anchored, independent of `<body>`) and its
   own `::backdrop` covers whatever `<body>` is doing underneath. Removed
   again on the last close, where shell.js restores the real scroll
   position. */
html.shell-dialog-open,
html.shell-dialog-open body {
  overflow: hidden;
  overscroll-behavior: none;
}
html.shell-dialog-open body {
  position: fixed;
  inset-inline: 0;
  width: 100%;
}

/* ══ THE DISCLOSURE IS THE NAV AT EVERY WIDTH (operator, 2026-09-24) ═══════
   "Can we work on cleaning up the top menu on Desktop? I'd rather keep
   everything under a Hamburger. We have three lines of options, overrunning
   the bounds."

   Everything below this comment used to live in `@media (max-width: 64rem)`,
   and the wide header carried the nav as an inline row. That row had become
   a WRAPPING row and then an overflowing one. cbapex#132 gave `.shell-nav`
   `flex-wrap` deliberately — wrapping is the better failure than sideways
   document scroll — but the header is a fixed `--shell-head-h`, so rows two
   and three spill out of it and land on the controls strip. Measured at
   1440×900 before this change, against the real vendored pages: cbconnector
   13 entries → 2 rows, nav 55px inside a 54px header, the second row sitting
   on the search strip; cbhq the same, and 3 rows for a superadmin, who is
   who reported it.

   ⚠️ GROWING THE HEADER IS NOT THE FIX, and it is the tempting one. It trades
   the spill for a chrome band whose height is the app's section count — a
   cost paid by every screen, in an app family where adding a section is a
   one-line edit an app agent makes without ever seeing the header rewrap.
   The sheet phones and tablets already had has no such cost and is already
   built, tested and understood, so it becomes the layout at every width and
   `.shell-nav` is never an inline row again.

   What that buys back, and what it costs: the wordmark, Card, Apps, Menu and
   the identity now have the whole header, and every destination is one tap
   behind a button that is in the same place on every device — at the cost of
   that tap, which a wide inline row did not charge. The operator asked for
   the trade with the overrun in front of them.

   The rules here are the ≤64rem sheet's, unchanged. What is WIDTH-dependent
   is only the sheet's geometry — full-bleed on a phone, a right-anchored
   panel on a desktop — and that is the `min-width: 64.01rem` block after the
   narrow one below. */
.shell-head > .shell-signout { display: none; }
.shell-head .shell-nav { display: none; }
/* ══ The menu is a SHEET with regions (operator, 2026-09-06) ═══════════════
   Laid out like the Apps switcher: a heading opens each region — Sections,
   Account, Session — and seven or more sections go two-up so the panel
   does not become the scroll it was opened to avoid. The document behind
   it is locked (`html.shell-menu-open`), glassed (`.shell-scrim`, the busy
   portal's blur) and inert (shell-controls.js); the sheet scrolls on its
   own if it must, and only it. */
.shell-head.open .shell-nav {
  display: grid;
  grid-template-columns: 1fr;
  align-content: start;
  gap: 0 0.75rem;
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  z-index: 40;
  margin-left: 0;
  padding: 0.35rem var(--shell-gutter) 1rem;
  max-height: calc(100dvh - var(--shell-head-h, 3.4rem) - 1rem);
  overflow-y: auto;
  overscroll-behavior: contain;
  background: color-mix(in srgb, var(--space, #171326) 96%, transparent);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border-bottom: var(--ax-line);
  box-shadow: 0 14px 24px rgba(7, 5, 14, 0.45);
}
.shell-head.open .shell-nav.is-many { grid-template-columns: 1fr 1fr; }
.shell-head.open .shell-nav > :not([data-region="sections"]) { grid-column: 1 / -1; }
/* ⚠️ THE HEADINGS LEAD (operator, 2026-09-11: "the menu is still hard to
   comprehend — make the headings the large bold font and the items a bit
   smaller"). They were 0.62rem mono caps in the subdued ink — the same
   treatment as an eyebrow — under 0.8rem entries in the same face, so the
   three regions read as one list with three odd rows. Now the heading is
   the page's own heading face, bold, in the text ink, and the entries are
   a step smaller in the chrome's mono caps: the shape of a section, not a
   list with labels in it. */
.shell-head.open .shell-nav-h {
  display: block;
  grid-column: 1 / -1;
  margin: 1.1rem 0 0.2rem;
  padding: 0 0.25rem;
  font-family: var(--font-body, var(--font-sans, system-ui, sans-serif));
  font-size: var(--ax-fs-md);
  font-weight: 700;
  letter-spacing: 0;
  text-transform: none;
  color: var(--text);
}
.shell-head.open .shell-nav-h:first-child { margin-top: 0.4rem; }
.shell-head.open .shell-nav a,
.shell-head.open .shell-nav .shell-nav-token {
  /* ⚠️ TIGHTER, AND CENTRED (operator, 2026-09-27: "heads are OK; the menu
     items are too far apart"). 0.7rem of padding and a 2.6rem floor made each
     entry 42px under a 0.7rem label, the label sitting at the TOP of its box
     with the air beneath it — so the gap read as belonging to the next entry.
     34px with the label centred: ten entries give back 80px of a phone. The
     headings are untouched. */
  display: flex;
  align-items: center;
  width: 100%;
  padding: 0.3rem 0.25rem;
  min-height: 2.1rem;
  border-radius: 0;
  border: 0;
  border-bottom: 1px solid color-mix(in srgb, var(--line) 35%, transparent);
  color: var(--text);
  font-size: var(--ax-fs-micro);
}
/* Where you are, said the way the kit's list says it (`.ax-row[aria-current]`):
   the accent AND a bar, because colour alone is not a state. */
.shell-head.open .shell-nav a[aria-current] {
  color: var(--accent);
  font-weight: 700;
  box-shadow: inset 3px 0 0 var(--accent);
  padding-left: 0.65rem;
}

.shell-head.open .shell-nav a.shell-nav-cta { color: var(--accent); font-weight: 700; }
/* The open menu's display:block above would un-hide gated links. */
.shell-head.open .shell-nav a[hidden] { display: none; }
.shell-head.open .shell-who-nav {
  display: block;
  padding: 0.5rem 0.25rem 0.25rem;
  max-width: none;
  font-family: var(--ax-mono);
  font-size: var(--ax-fs-micro);
  color: var(--subdued);
}
/* ⚠️ THE SWITCHER IS NOT IN THIS PANEL. It is a child of the header and
   the same dropdown at every width — see the block near the top of this
   file. It lived here as a second column briefly, and two things were wrong
   with that: it inherited `.shell-nav`'s visibility, so on a phone you had
   to open the menu to reach it; and a two-column block of long names beside
   the app's own sections read as a split screen rather than one menu.

   What remains here is only where its BUTTON sits in the row. */
/* ⚠️ ORDER EVERY DIRECT-CHILD BUTTON, not just the ones wearing kit classes.
   Ordering only `.shell-card-btn` / `.shell-menu` left cbnight — whose header
   predates the kit and uses `nt-hbtn` / `nt-card-btn` / `nt-menu` — with
   unordered siblings at the default `order: 0`, which sort BEFORE the Apps
   button's `order: 2`. The rule meant to place Apps in the middle was the
   very thing pushing it last.
   ⚠️ `[aria-controls]` is the discriminator, not a class and not a position.
   The menu button is by definition the one that controls the nav, and both
   the kit's (`aria-controls="app-nav"`) and cbnight's pre-kit
   (`aria-controls="nt-nav"`) say so, while neither Card nor Apps carries it.

   The obvious `button:last-child` does NOT work, and the reason is worth
   keeping: the switcher appends its own `<div class="apex-apps">` to the
   header, so the header's last child is never a button again. That selector
   matched nothing, cbnight's menu kept the default order, and Apps stayed
   stranded on the end — the very bug this block exists to fix. */
.shell-head > button { order: 1; }
.shell-head > .apex-appsbtn { order: 2; }
.shell-head > .shell-menu,
.shell-head > button[aria-controls] { order: 3; }

.shell-head.open .shell-signout-nav {
  display: block;
  width: 100%;
  margin-top: 0.5rem;
  text-align: left;
}

@media (max-width: 64rem) {
  /* What is genuinely narrow-only: the wordmark's floor, and the short-
     viewport condition below it. Everything else this block used to hold is
     now above, at every width. */
  /* The wordmark can shrink here — see the note above `.shell-mark img`. The
     floor is the image's own (2rem, square); the name yields first because
     negative space is shared by flex-shrink × basis, and 1000 puts all of it
     on the name until the name is an ellipsis. */
  .shell-mark { min-width: 2rem; }
  .shell-mark > span { flex: 0 1000 auto; }
  /* ⚠️ NOT WHEN THE VIEWPORT IS ALSO SHORT (cbapex#118). This bare rule sat
     later in the file than cbapex#110's `@media (max-height: 560px)` block and
     matched at equal specificity, so it silently undid it — and it undid it at
     precisely the screen #110 was written from: 354×374, Chrome on iOS with the
     keyboard up. 354px is inside `max-width: 64rem`, so the width rule won and
     the header re-stuck. #110 was returning 65px of the 119px it was meant to.

     Stated as a CONDITION rather than fixed by moving the block, so a later
     edit that reorders this file cannot quietly re-break it. Nothing changes at
     390×844: height is the trigger, and no width rule can see this. */
  @media (min-height: 561px) {
    .shell-head { position: sticky; }
  }
}

/* ══ Wide: the sheet is a PANEL under its own button ═══════════════════════
   Edge-to-edge is right on a phone, where the header IS the viewport; at 1440
   it is a 1440px-wide menu holding one column of 0.7rem entries. So it
   anchors to `--shell-align` — the same column edge the header, the controls
   strip and the main column all pad to — and takes a measured width instead
   of the viewport's. ⚠️ The `%` inside `--shell-align` resolves against this
   box's containing block (the header's padding box) rather than against #app
   as it does on the header itself; the two agree because the header spans
   #app, and the rendered right edges were measured flush at 1100 and 1440
   rather than assumed. */
@media (min-width: 64.01rem) {
  .shell-head.open .shell-nav {
    left: auto;
    right: var(--shell-align);
    width: 21rem;
    padding: 0.35rem 1rem 1rem;
    border: var(--ax-line);
    border-top: 0;
    border-radius: 0 0 var(--ax-r, 10px) var(--ax-r, 10px);
  }
  /* Seven or more sections go two-up here as they do in the sheet — with the
     panel widened to suit, or the two columns are 9rem each and every entry
     wraps. */
  .shell-head.open .shell-nav.is-many { width: 34rem; }
}

/* ══ The US map — the family navigator, with the locationdex pin layer ══════
   Prospect's choropleth idiom (five accent bands, sqrt-bucketed — see
   usmap.ts for why sqrt), shared since cbapex#8. Colour is never the only
   signal: every state and pin carries its number in a <title>, the legend
   names the bands, and consumers repeat the numbers in text beside the map. */
.shell-usmap {
  margin: 1rem 0 0;
}
.shell-usmap-svg {
  width: 100%;
  height: auto;
  display: block;
}
.shell-us-st {
  stroke: var(--surface);
  stroke-width: 0.8;
}
.shell-us-st.q0 { fill: color-mix(in srgb, var(--subdued) 12%, transparent); }
.shell-us-st.q1 { fill: color-mix(in srgb, var(--accent) 18%, transparent); }
.shell-us-st.q2 { fill: color-mix(in srgb, var(--accent) 36%, transparent); }
.shell-us-st.q3 { fill: color-mix(in srgb, var(--accent) 58%, transparent); }
.shell-us-st.q4 { fill: color-mix(in srgb, var(--accent) 82%, transparent); }
/* Interactive only where something made it a door. */
[onclick] > .shell-us-st,
.shell-us-st[onclick],
a > .shell-us-st {
  cursor: pointer;
}
.shell-us-st:hover,
.shell-us-st:focus-visible,
a:hover > .shell-us-st,
a:focus-visible > .shell-us-st {
  stroke: var(--accent);
  stroke-width: 1.6;
  outline: none;
}
.shell-us-label {
  fill: var(--text);
  font: 700 var(--ax-fs-micro) var(--ax-mono, ui-monospace, monospace);
  letter-spacing: 0.03em;
}
/* The pins: the metal's dot over the choropleth, dark-ringed so a pin over
   the deepest band still reads as a pin and not a brighter fill. */
.shell-us-pin {
  fill: color-mix(in srgb, var(--gold, var(--accent)) 88%, white);
  stroke: var(--space, #171326);
  stroke-width: 0.6;
}
a:hover > .shell-us-pin,
a:focus-visible > .shell-us-pin {
  stroke: var(--accent);
  stroke-width: 1.2;
  outline: none;
}
.shell-usmap-legend {
  display: flex;
  gap: 0.4rem;
  align-items: center;
  color: var(--subdued);
  font: 600 var(--ax-fs-micro)/1 var(--ax-mono, ui-monospace, monospace);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  margin-top: 0.5rem;
}
.shell-us-swatch {
  width: 0.9rem;
  height: 0.9rem;
  border-radius: 2px;
  display: inline-block;
}
.shell-us-swatch.q0 { background: color-mix(in srgb, var(--subdued) 12%, transparent); }
.shell-us-swatch.q1 { background: color-mix(in srgb, var(--accent) 18%, transparent); }
.shell-us-swatch.q4 { background: color-mix(in srgb, var(--accent) 82%, transparent); }
.shell-us-swatch.pin {
  border-radius: 50%;
  background: color-mix(in srgb, var(--gold, var(--accent)) 88%, white);
  border: 1px solid var(--space, #171326);
}

/* ══ The business card ═══════════════════════════════════════════════════════
   The person's card — photo, name, role, tel:/mailto: lines that are real
   actions on the device this is read on, and the QR a stranger scans off the
   screen. Rendered by skin/card.js from /api/profile; the Card header button
   opens it in a modal (press one thing, turn the phone around). Extracted
   from cbnight, shell-bc-* for nt-bc-*. */
.shell-bizcard {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: 0.9rem;
  align-items: center;
  position: relative;
  /* The extra top padding is a RESERVED BAND for the absolutely-positioned
     Edit link (prospect's lesson: without it, Edit lands on the QR block's
     corner whenever the card is short). */
  padding: 1.85rem 1.15rem 1rem;
  border: var(--ax-line);
  border-radius: var(--ax-r);
  background: var(--surface);
  box-shadow: var(--ax-lift);
  min-width: 19rem;
}
.shell-bc-photo {
  width: 3.6rem;
  height: 3.6rem;
  border-radius: 50%;
  object-fit: cover;
  /* Grayscale on the way in AND here: consistency across photos taken in a
     dozen different rooms is the whole point of the treatment. */
  filter: grayscale(1) contrast(1.05);
  border: 2px solid var(--line, #5f4c66);
  display: grid;
  place-items: center;
}
.shell-bc-initials {
  font-family: var(--ax-mono);
  font-weight: 800;
  font-size: var(--ax-fs-lg);
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  filter: none;
}
.shell-bc-body {
  display: grid;
  /* ⚠️ An IMPLICIT auto column is sized from its items' min-content, so one
     unbreakable token — a person whose display name is their email, a role
     pasted from a URL — set the column at 288px inside a 151px body, and the
     Profile screen scrolled 6px sideways in every app at 390 (measured
     2026-09-07, the family's first Assay sweep re-checked under a throwaway
     whose name IS its address). The column is the body's width, no more; the
     name and role break inside a token only when nothing else will. */
  grid-template-columns: minmax(0, 1fr);
  gap: 0.1rem;
  min-width: 0;
}
.shell-bc-name {
  font-size: var(--ax-fs-md);
  font-weight: 800;
  letter-spacing: -0.01em;
  min-width: 0;
  overflow-wrap: anywhere;
}
.shell-bc-role {
  font-family: var(--ax-mono);
  font-size: var(--ax-fs-micro);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 0.15rem;
  min-width: 0;
  overflow-wrap: anywhere;
}
.shell-bc-line {
  font-family: var(--ax-mono);
  font-size: var(--ax-fs-micro);
  color: var(--subdued);
  text-decoration: none;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  /* line-height, NOT flex centring: flex puts the text in an anonymous flex
     item and text-overflow never reaches it (prospect paid for this). */
  display: block;
  min-height: 1.6rem;
  line-height: 1.6rem;
}
.shell-bc-line:hover { color: var(--accent); }
.shell-bc-missing {
  /* Amber, not the accent: "this is a gap you can close" is the warning
     tone's job, and the QR-empty slot below speaks the same language. */
  color: var(--amber, #d4a024);
  border-bottom: 1px dashed currentColor;
  align-self: start;
}
.shell-bc-qr { display: grid; justify-items: center; gap: 0.25rem; }
.shell-bc-qr canvas {
  width: 4.6rem;
  height: 4.6rem;
  border-radius: 4px;
  background: var(--paper, #fff7ec);
}
.shell-bc-qrcap {
  font-family: var(--ax-mono);
  font-size: var(--ax-fs-micro);
  letter-spacing: 0.06em;
  color: var(--subdued);
  max-width: 5.5rem;
  /* Three lines, broken anywhere: a claimed link name is one long token and the
     ellipsis cut every one in the family to "bradley-isenb…" (cbapex#168). */
  line-height: 1.3;
  max-height: 3.9em;
  overflow: hidden;
  overflow-wrap: anywhere;
  text-align: center;
}
/* The unclaimed slot — exactly the canvas's footprint, so claiming a link
   name does not reflow the card. Dashed and amber: an offer, not a hole. */
.shell-bc-qr-empty { text-decoration: none; }
.shell-bc-qrslot {
  display: grid;
  place-items: center;
  width: 4.6rem;
  height: 4.6rem;
  border: 2px dashed var(--amber, #d4a024);
  border-radius: 4px;
  font-family: var(--ax-mono);
  font-size: var(--ax-fs-micro);
  font-weight: 700;
  letter-spacing: 0.14em;
  color: var(--amber, #d4a024);
}
/* Token-pure. The fallback here used to be a literal `rgba(255, 186, 85, 0.09)`
   — an ORANGE tint, wrong on every app whose accent is not orange, and exactly
   the hardcoded value the design language forbids. cbnight's copy had it right. */
.shell-bc-qr-empty:hover .shell-bc-qrslot,
.shell-bc-qr-empty:focus-visible .shell-bc-qrslot {
  background: color-mix(in srgb, var(--amber, #d4a024) 9%, transparent);
}
.shell-bc-qr-empty:hover .shell-bc-qrcap { color: var(--amber, #d4a024); }
.shell-bc-edit {
  position: absolute;
  top: 0.5rem;
  right: 0.6rem;
  font-family: var(--ax-mono);
  font-size: var(--ax-fs-micro);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--subdued);
  text-decoration: none;
}
.shell-bc-edit:hover { color: var(--accent); }

/* ── The Card POPUP ─────────────────────────────────────────────────────────
   The modal version, read across a table with the phone turned around. It is
   a different object from the inline card and needs the whole treatment, not
   just a bigger QR — the extraction kept only the canvas rule, and
   prospect / public / rural all shipped a broken popup as a result while
   cbnight looked right, because cbnight still had this block under its own
   nt-* names (2026-08-19). Ported back verbatim; cbnight's copy is the
   original.

   The load-bearing lines are the first three: the modal supplies its own
   frame, and `min-width: 19rem` inherited from .shell-bizcard overflows a
   narrow dialog, so the card must be free to shrink inside it. */
.shell-bizcard-big {
  /* Two columns, so the QR drops to a full-width row of its own below the
     details rather than fighting them for horizontal space. */
  grid-template-columns: auto minmax(0, 1fr);
  width: 100%;
  min-width: 0;
  border: 0;
  background: transparent;
  box-shadow: none;
  padding: 0.25rem 0.25rem 0;
}
/* The popup is the version a stranger reads — an edit affordance there is
   furniture. */
.shell-bizcard-big .shell-bc-edit { display: none; }
.shell-bizcard-big .shell-bc-qr,
.shell-bizcard-big .shell-bc-qr-empty {
  grid-column: 1 / -1;
  justify-self: center;
  margin-top: 0.4rem;
}
.shell-bizcard-big .shell-bc-qr canvas {
  width: min(56vw, 12.5rem);
  height: auto;
  image-rendering: pixelated; /* a QR module is a square, not a gradient */
}
.shell-bizcard-big .shell-bc-qrslot {
  width: min(56vw, 12.5rem);
  height: min(56vw, 12.5rem);
  font-size: var(--ax-fs-md);
}
.shell-bizcard-big .shell-bc-qrcap { max-width: min(56vw, 12.5rem); }

/* ⚠️ THIS BLOCK CAME BACK FROM cbnight, which was AHEAD of the kit here. The
   whole card was a parallel implementation over there (`nt-bizcard` / `nt-bc-*`,
   byte-identical to this file in 24 of 27 rules) and every rule that differed
   was an improvement this copy was missing. Same shape as the `stamp.ts` drift:
   agreement between copies is not evidence of correctness, and the odd one out
   was the one that was right. */
.shell-cardshow {
  display: grid;
  justify-items: center;
  gap: 0.75rem;
  padding: 0.35rem 0 0.15rem;
}
.shell-cardshow .shell-bizcard { box-shadow: none; }
/* The card dialog's second region (2026-09-06): the Links heading wears the
   menu's region heading, shown here regardless of width. */
.shell-carddlg .shell-cardshow-h {
  display: block;
  margin: 1rem 0 0.35rem;
  font-family: var(--ax-mono, ui-monospace, monospace);
  font-size: var(--ax-fs-micro);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--subdued);
}
/* The feedback result dialog (2026-09-07): the capture at the dialog's width,
   the link as a field. */
.shell-fb-thumb { display: block; border: var(--ax-line); border-radius: var(--ax-r); overflow: hidden; }
.shell-fb-thumb img { display: block; width: 100%; max-height: 18rem; object-fit: cover; object-position: top; }
.shell-fb-url { width: 100%; font-family: var(--ax-mono); font-size: var(--ax-fs-xs); }
/* The breadcrumb's current level opens Where you are (2026-09-07): the strip's
   page name and the in-page trail's <b> are buttons. */
.shell-page[role="button"],
.ax-crumb b[role="button"],
.shell-crumb b[role="button"] {
  cursor: pointer;
  border-radius: 4px;
}
.shell-page[role="button"]:focus-visible,
.ax-crumb b[role="button"]:focus-visible,
.shell-crumb b[role="button"]:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.shell-where-list .ax-row {
  padding-inline: 0.5rem;
  font: inherit;
  background: none;
  border-inline: 0;
  border-bottom: 0;
  width: 100%;
  text-align: left;
  color: var(--text);
  cursor: pointer;
}
.shell-where-list .ax-row:first-child {
  border-top: 0;
}
.shell-where-dlg .ax-dialog-actions,
.shell-sections-dlg .ax-dialog-actions {
  justify-content: flex-end;
  gap: 0.5rem;
}
/* The header's wordmark opens About (2026-09-07): the rows are the app's
   cards; the build line keeps its mono voice. */
.shell-about-dlg .shell-about-list .ax-row {
  padding-inline: 0.5rem;
}
.shell-about-dlg .shell-so-who {
  margin-bottom: 0.9rem;
}
.shell-about-dlg .shell-about-build {
  font-family: var(--ax-mono);
  font-size: var(--ax-fs-xs);
  overflow-wrap: anywhere;
}
.shell-about-dlg .ax-dialog-actions {
  justify-content: flex-end;
  gap: 0.5rem;
}
/* The footer's identity chip is a button (2026-09-07): a tap opens You. It is
   dressed as a pill with the other instruments, up in the footer block. */
.shell-foot .shell-ident[role="button"]:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
/* The notice (2026-09-11, server/notice.ts): the banner sits between the
   sticky stack and the page; the dialog's countdown is the loud thing in it. */
.shell-notice-bar {
  margin: 0.75rem var(--shell-gutter, 1rem) 0;
}
.shell-notice-bar .shell-notice-left,
.shell-notice-count {
  font-family: var(--ax-mono);
  font-variant-numeric: tabular-nums;
}
.shell-notice-bar .shell-notice-left { color: var(--text); font-weight: 600; }
.shell-notice-dlg .shell-notice-text { margin: 0 0 0.6rem; white-space: pre-wrap; }
.shell-notice-count { font-size: var(--ax-fs-xl); margin: 0 0 0.6rem; color: var(--text); }
/* The recorder dialog (2026-09-11): numbered steps, plain, tight. */
.shell-rec-steps {
  margin: 0.3rem 0 0;
  padding-left: 1.3rem;
  font-size: var(--ax-fs-sm);
  line-height: 1.45;
}
.shell-rec-steps li + li { margin-top: 0.35rem; }
.shell-rec-steps code { font-family: var(--ax-mono); font-size: var(--ax-fs-xs); }
/* The Display dialog (2026-09-07): the density chips in a row, the note quiet. */
.shell-display-density { margin: 0.4rem 0 0.2rem; }
.shell-display-dlg .ax-setting { padding-inline: 0; }
.shell-display-say { margin: 0.6rem 0 0; }
/* The copy-token dialog (2026-09-07): the mask and the usage line, as code. */
.shell-tok-mask,
.shell-tok-use {
  display: block;
  margin: 0.35rem 0 0;
  padding: 0.45rem 0.6rem;
  border: var(--ax-line);
  border-radius: var(--ax-r);
  background: var(--n-subtle, var(--surface3));
  font-family: var(--ax-mono);
  font-size: var(--ax-fs-xs);
  overflow-wrap: anywhere;
}
/* The sign-out dialog (2026-09-07): who, in two lines. */
.shell-so-who { margin: 0; line-height: 1.35; }
.shell-so-who small { font-family: var(--ax-mono); font-size: var(--ax-fs-micro); color: var(--subdued); }
.shell-so-never { display: inline-flex; align-items: center; gap: 0.6rem; }
.shell-so-never-l { font-size: var(--ax-fs-sm); }
.shell-cardshow-links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}
/* The card dialog's regions (2026-09-07): the link line stands in its own
   region now; the QR slot can say why it is blank. */
.shell-carddlg .shell-cardshow-link {
  margin: 0;
  overflow-wrap: anywhere;
}
.shell-bc-qrfail {
  display: block;
  max-width: min(56vw, 14rem);
  font-family: var(--ax-mono, ui-monospace, monospace);
  font-size: var(--ax-fs-micro);
  line-height: 1.4;
  text-align: center;
  color: var(--amber, #d4a024);
  overflow-wrap: anywhere;
}
.shell-bc-qrfail a {
  color: inherit;
}
/* ⚠️ ON THE SMALL CARD THE MESSAGE TAKES ITS OWN ROW. It was sized for the
   dialog (14rem) and the inline card's QR column is `auto`, so a failed draw
   made that column 218px and left the person 12px: the name stacked one
   letter per line, 510px tall (rendered at 390, 2026-09-27). cbhq draws no
   code at all while cbcdn refuses its origin, so this is a live state. */
.shell-bizcard:not(.shell-bizcard-big) .shell-bc-qr:has(.shell-bc-qrfail) {
  grid-column: 1 / -1;
}
.shell-bizcard:not(.shell-bizcard-big) .shell-bc-qrfail {
  max-width: none;
}
.shell-cardshow-url {
  font-family: var(--ax-mono);
  font-size: var(--ax-fs-micro);
  color: var(--subdued);
  text-decoration: none;
  /* A claimed slug can be long and this sits inside a modal — without this the
     URL pushes the card's own width out. */
  overflow-wrap: anywhere;
  text-align: center;
}
/* The LINK form only — the no-page-yet fallback is a sentence, and a sentence
   in tracked caps reads as shouting. The kit used to style both the same. */
a.shell-cardshow-url {
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  text-decoration: underline;
  text-underline-offset: 3px;
}
a.shell-cardshow-url:hover { color: var(--accent); }

/* ══ THE PHONE HEADER RUNS OUT OF ROOM ═════════════════════════════════════
   A wordmark plus Card, Apps and Menu is four controls, and with their text
   labels they measured 451px against a 390px viewport — the Menu button hung
   61px off the right edge and gave the whole page a horizontal scrollbar.
   Named by walking the DOM for elements whose right edge passed the viewport,
   not by eyeballing it.

   Below 30rem the labels go and the glyphs stay. Every one of these buttons
   already carries an `aria-label`, so nothing is lost to a screen reader, and
   an icon-only header is what a phone expects anyway. The glyph spans are
   `aria-hidden`, which is what distinguishes them from the text. */
@media (max-width: 30rem) {
  .shell-hbtn > span:not([aria-hidden]) {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  .shell-hbtn {
    padding-left: 0.55rem;
    padding-right: 0.55rem;
  }
  /* The wordmark tightens rather than disappearing. 0.28em is a wide-screen
     typographic choice and it is expensive here: measured at 390px, the name
     DEMANDS 162px in cbnight and 93px in cbprospect against ~148px and ~92px
     of room, so both were losing their last characters to the ellipsis above.
     At 0.16em both fit whole; cbvmod and cbconnector still truncate, and that
     is what the ellipsis is for.

     ⚠️ Tracking, not size. These three shared screens are IDENTICAL in ten
     apps, so the wordmark is the only thing on them that says which app you
     are in — hiding it the way the button labels are hidden two rules up would
     be the one label on this page that cannot be spared, and shrinking the
     type re-voices the mark where tightening it does not. */
  .shell-mark { letter-spacing: 0.16em; }
}

/* ══ Hidden wins — the shell- namespace (cbapex#154) ═══════════════════════
   `hidden`'s `display: none` is the UA's, so ANY author display beats it: this
   file authors ~50 of them and only a handful had a `[hidden]` neighbour.
   `<div class="shell-subnav" hidden>` stayed on screen, and cbprospect had to
   carry `.shell-subnav[hidden] { display: none }` app-side. Same guard as
   components.css's `ax-` one, same (0,2,0), LAST in the file so it wins every
   tie; a rule that out-specifies it states its own `[hidden]` next to it.
   server/components-hidden.test.ts holds this file to that too. */
/* The seven rules above that out-specify (0,2,0), each with its neighbour.
   The token's is `.open.open` — (0,5,0) beats its (0,4,0) rule without writing
   the `.shell-nav .shell-nav-token` string shell-nav-controls.test.ts counts. */
.shell-subnav.is-overflow .shell-subnav-all[hidden],
.shell-menu .mi i[hidden],
.shell-head.open .shell-nav[hidden],
.shell-head.open .shell-nav-h[hidden],
.shell-head.open.open .shell-nav-token[hidden],
.shell-head.open .shell-who-nav[hidden],
.shell-head.open .shell-signout-nav[hidden] {
  display: none;
}
[class^="shell-"][hidden],
[class*=" shell-"][hidden],
[class^="shell-"] [hidden],
[class*=" shell-"] [hidden] {
  display: none;
}
