/* The page chrome that holds everything else: the pinned header, the view tabs, the main column, the section
   frame, the collapsible panels, the pinned launch footer, and the toast stack.

   ONE COLUMN, THREE BANDS. The header, the content and the footer are three full-width bands, and each aligns
   its own contents to the same measure — so on a wide screen the title, the first card and the launch count
   all start on one line rather than the content being centred inside two bands that are not. The alignment is
   done with padding rather than with a wrapper element, because `main` is the only one of the three that can
   carry `margin-inline:auto` without the band's background stopping at the column's edge:

     padding-inline: calc(max(0px, 100% - var(--measure)) / 2 + var(--gutter))

   which is the gutter while the viewport is narrower than the measure, and half the overshoot plus the gutter
   once it is wider.

   The header LAYOUT is where the three width steps are most visible, and each step is the point at which the
   band's contents stop fitting the one before it:
   · phone      — four stacked rows (title, counts, auth zone, tabs). The auth zone alone carries four
                  controls and cannot share a 350px row with a title.
   · from 640px — the auth zone fits beside the title, so the header is three rows.
   · from 1024px— the counts fit between them, so it is two.
   Measured at 390x844 this takes the header from 187.6px to roughly 164px, and at 1280 from 104px to 105px:
   the phone is where the saving is, which is the point. */
header {
  position:sticky; top:0; z-index:20;
  background:var(--chrome);
  backdrop-filter:saturate(1.8) blur(16px);
  border-bottom:1px solid var(--line);
  padding-block:calc(var(--s-3) + env(safe-area-inset-top, 0px)) 0;
  padding-inline:calc(max(0px, 100% - var(--measure)) / 2 + var(--gutter));
  display:grid;
  grid-template-columns:minmax(0,1fr);
  row-gap:var(--s-2); column-gap:var(--s-4);
  align-items:center;
}
/* The bar is a grouping in the markup, not a box on screen: `display:contents` lifts its three children into
   the header's own grid so the whole band is laid out by ONE track list, which is what lets the auth zone
   move between rows at a breakpoint without a wrapper moving with it. */
.bar { display:contents; }
h1 { font-size:var(--t-lead); margin:0; font-weight:650; letter-spacing:-.02em; line-height:1.2;
      white-space:nowrap; }
.stats { display:flex; flex-wrap:wrap; gap:var(--s-1) var(--s-4); color:var(--muted);
         font-size:var(--t-meta); min-width:0; }
.stats b { color:var(--fg); font-weight:600; font-variant-numeric:tabular-nums; }
/* The auth zone carries FIVE controls — who is signed in, which Claude account is active, that account's
   usage pill (#229), Settings, Sign out — and on a phone that is the band's whole width budget. It is sized
   so the five fit ONE row at 390px, and the sizing is BOUNDED rather than hopeful: the two variable-length
   members — the operator's id and the account label — are capped in `ch` and truncate, and the other three
   are fixed-width, so the zone's widest possible state is a number rather than a hope.

   THE FIFTH CONTROL WAS PAID FOR, NOT ADDED. Measured at 390px with the pill and the four originals at their
   previous caps: opid 78.7 + host 143.1 (select 85.0 + pill 58.1) + Settings 80.0 + Sign out 79.5, plus
   3 x 4px of gap = 393.3px into 350px, and Sign out landed at the same x as Settings — a second row, and the
   207.8px header this block already once paid to remove. Three caps were tightened to buy it back, in
   increasing order of what they cost the operator:

   · the buttons' inline padding 12px -> 8px, which costs nothing at all: they are `--tap` tall, so the hit
     target is unchanged and only the ink moves.
   · the operator id 10ch -> 7ch. It is an identity, not a headline, and it is the one member here that
     already truncates by design.
   · the account select 9ch -> 6ch. This is the one that reads as a real loss and is not: the pill beside it
     now carries the active account's state as a NUMBER, and the account's full label is one tap away in the
     native picker, at full width, for every account at once. A truncated label next to an untruncated
     percentage is the trade this design was chosen for.

   At 320px it still wraps, and that is stated rather than papered over: five controls do not fit 280px
   without truncating a label past the point of use, and 320px is a width this page meets in a spec rather
   than in an operator's hand. */
/* NOWRAP, AND THE SHRINK PATH THAT MAKES NOWRAP SAFE — the two are one rule and must not be separated.

   This zone used to be `flex-wrap:wrap` with two width CAPS (`.opid` 7ch, `.foot-select` 6ch) sized so that
   five controls happened to add up to less than 390px. A cap in `ch` is a cap in the FONT'S units, and the
   font here is whatever the platform supplies: the stack is `-apple-system, BlinkMacSystemFont, "Segoe UI",
   Inter, system-ui, sans-serif`, of which a Linux box typically resolves NONE — `fc-match sans-serif` on the
   CI host answers DejaVu Sans, which is materially wider than SF Pro. So the arithmetic that held on macOS
   did not hold there, the zone wrapped, and the header went back to the two-row state this file had already
   paid to remove. A layout that fits only in one font is not a layout that fits.

   `nowrap` removes the escape, and the members below supply the give instead, in a declared order:
     · `.opid` shrinks FIRST and effectively without limit — `flex-shrink:999` against the host's 1. It is an
       identity that already truncates by design, so it is the correct thing to spend.
     · `#accountSelectorHost` shrinks next, and only the <select> inside it gives — the pill is `flex:none`
       (a number that truncates is a number that lies) and the select keeps a floor so it stays operable.
     · The buttons never shrink at all. They are `--tap` targets carrying fixed labels; squeezing one is
       taking away a thumb target, and their text would spill rather than ellipsise.
   The zone therefore stays one row at ANY font, and what a wider font costs is label length — visibly and in
   a fixed order — rather than a second row. */
.auth { display:flex; gap:var(--s-1); align-items:center; flex-wrap:nowrap; justify-content:flex-end;
        justify-self:end; min-width:0; }
.auth button.btn { padding-inline:var(--s-2); flex:none; }
/* The operator's id is an identity, not a headline: it truncates rather than pushing the controls beside it
   onto another line. It stays RENDERED at every width — the harness waits on it to know the page has settled,
   and hiding it at a breakpoint would hang every spec that opens at a phone width. */
/* `flex-shrink:999` is what orders the give: against `#accountSelectorHost`'s 1, this absorbs essentially all
   of any shortfall before the account selector loses a single character. `max-width` is a ceiling and says
   nothing about shrinking; `min-width:0` is what actually permits a flex item to go under its content width,
   and without it `nowrap` above would overflow the document instead of truncating here. */
.opid { color:var(--muted); font-size:var(--t-meta); min-width:0; max-width:7ch; flex:0 999 auto;
        overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.opid b { color:var(--fg); font-weight:600; }
/* The select and its pill are ONE flex row inside the host auth.js creates, rather than two inline boxes in a
   bare <span>. An inline span lets its children wrap against each other independently of `.auth`'s own
   wrapping, so the pill could drop under the select while the zone still reported one row — a wrap the
   height assertion in AccountUsagePillSpecs would not see. `min-width:0` is what lets the select actually
   honour a cap below its content width instead of pushing the row wider. */
#accountSelectorHost { display:flex; align-items:center; gap:var(--s-1); min-width:0; flex:0 1 auto; }
/* The pill is the one member of this row that must arrive whole or not at all: it is a PERCENTAGE, and a
   percentage clipped to its first digit reads as a different, much better number than the one measured. So it
   is `flex:none` — any shortfall is taken from the select beside it, never from here. */
#accountSelectorHost .usage-pill { flex:none; }
/* CAPPED, AND THEREFORE A DECLARED CLIPPER. The option text now reads `Label · 97 %` rather than `Label`, so
   the widest option is wider than a capped box — and ResponsiveSpecs' overflow detector draws exactly the
   right distinction about that: content wider than its box is a fault only where the box does not contain it,
   because `overflow-x:visible` leaves the excess loose in the layout to push something else off screen, while
   `overflow:hidden` is a DECISION that the content may be longer than the cap. `.opid` two rules up has always
   made that decision; this is the same one, for the same reason, and it is what the native picker exists to
   undo — the full label of every account, at full width, one tap away. Without this the select reported 45px
   of loose content at 390px and reddened a guard that was right to notice.

   A FLOOR as well as a cap, now that `.auth` is `nowrap`: `min-width` is what stops the last of the shortfall
   from collapsing the picker to nothing. 4ch leaves a readable stub plus the native arrow, so the control the
   operator taps to CHANGE account is still findable at the widest font on the narrowest phone — which is the
   whole point of having kept a native <select> here. */
.auth .foot-select { max-width:6ch; min-width:4ch; flex:0 1 auto;
                     overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }

/* The view tabs. The strip itself carries the AUTH gate (`hidden`, written by applyTabs from state.authed);
   `.tabs[hidden]` is spelled out because the `display:flex` below is an author rule and would otherwise
   outrank the UA sheet's `[hidden] { display:none }`.

   Each tab is a full `--tap` target — 44px, Apple's minimum — because a tab is one of the two things #63
   names as a thumb target, and because the strip is the only navigation this page has. It still WRAPS rather
   than scrolling: a scrolled tab is a view the operator cannot see is there, and there are only three. */
.tabs { display:flex; gap:var(--s-5); flex-wrap:wrap; }
.tabs[hidden] { display:none; }
.tab { display:inline-flex; align-items:center; gap:var(--s-2); min-height:var(--tap);
       border:0; border-bottom:2px solid transparent; background:none; color:var(--muted);
       padding:0; font:550 var(--t-ui)/1 inherit; font-family:inherit; cursor:pointer;
       transition:color var(--dur-1) var(--ease), border-color var(--dur-1) var(--ease); }
.tab:hover { color:var(--fg); }
.tab[aria-selected="true"] { color:var(--fg); border-bottom-color:var(--accent); }
/* The selected-issue count the Backlog tab carries. Same reader as the footer's, so the two cannot disagree
   — and this one stays on screen when the footer is scoped away on another tab. */
.tab-sel { color:var(--accent); font-weight:600; font-variant-numeric:tabular-nums; }

@media (min-width: 640px) {
  header { grid-template-columns:minmax(0,1fr) auto; }
  h1 { grid-column:1; grid-row:1; }
  .auth { grid-column:2; grid-row:1; gap:var(--s-2); }
  .auth button.btn { padding-inline:var(--s-4); }
  /* From 640px up the width budget is no longer the constraint, so both variable caps go back to a reading
     size — the button padding above already does the same, and this pair is why the phone-step tightening
     costs nothing on any wider surface. The pill's own width is unchanged at every step: it is a number, and
     a number does not need more room to be read. */
  .opid { max-width:24ch; }
  .auth .foot-select { max-width:16ch; }
  .stats { grid-column:1 / -1; grid-row:2; }
  .tabs { grid-column:1 / -1; grid-row:3; }
}

@media (min-width: 1024px) {
  header { grid-template-columns:auto minmax(0,1fr) auto; padding-block:var(--s-4) 0; }
  .stats { grid-column:2; grid-row:1; gap:var(--s-1) var(--s-5); }
  .auth { grid-column:3; grid-row:1; }
  .tabs { grid-column:1 / -1; grid-row:2; }
}

/* ---------------------------------------------------------------------------------------------------------
   The content column, and how much room it leaves for the band pinned under it.

   `main` reserves the footer's height, because the footer is `position:fixed` and therefore takes none of the
   flow. A single number for that reservation is wrong in both directions at once, and both were measured on
   the page as it stood: at 390x844 with two issues ticked the footer was 265.6px tall over a 120px
   reservation, so the last 140px of the board sat UNDER it and could not be scrolled to; on the Fleet and
   History tabs — which since the tab restructure carry nothing but the readiness hint — the same 120px stood
   over a 44.8px band, three quarters of it empty.

   So the reservation is COMPOSED from what the footer actually carries, out of the same tokens that size the
   footer itself: the band's own padding and hairline, one line of meta copy, and one control row. Change
   `--control-h` at a breakpoint and the reservation follows; it is not a magic number that has to be
   remeasured every time a control grows.

   WHICH of the three the footer carries is not something a stylesheet can work out — CSS cannot measure a
   fixed element, and "is the launch surface open" is a decision `refreshSelection` makes from the selection,
   the auth state and the active tab. So that one function publishes its own decision onto `<body>`, and this
   reads it. It is the same bit, not a second owner of it: one function decides, one selector reads. */
:root {
  --footer-pad:calc(var(--s-3) * 2 + 1px);            /* the band's padding, top and bottom, plus its hairline */
  --footer-line:20px;                                  /* one line of --t-meta copy */
  --footer-row:calc(var(--control-h) + var(--s-2));    /* one control row and the gap beneath it */
  --footer-note:calc(var(--footer-line) * 2);          /* the launch note wraps to two lines on a phone */
  --footer-stack:3;                                    /* control rows above the row Start sits on */
  /* The state the footer SHIPS in — a count and a note, no controls — so the first paint reserves correctly
     before any script has run. */
  --footer-reserve:calc(var(--footer-pad) + var(--footer-row) + var(--footer-note));
}
@media (min-width: 640px)  { :root { --footer-note:var(--footer-line); --footer-stack:2; } }
/* From 1024px the note sits on the SAME row as the count, so it costs no row of its own — hence zero rather
   than one line — and the launch surface is one row under it, hence a stack of one. */
@media (min-width: 1024px) { :root { --footer-note:0px; --footer-stack:1; } }

body[data-footer="hint"]   { --footer-reserve:calc(var(--footer-pad) + var(--footer-line)); }
body[data-footer="note"]   { --footer-reserve:calc(var(--footer-pad) + var(--footer-row) + var(--footer-note)); }
body[data-footer="launch"] { --footer-reserve:calc(var(--footer-pad) + var(--footer-row) * var(--footer-stack)
                                                   + var(--tap) + var(--s-2) + var(--footer-note)); }

/* The map panel bleeds to the viewport width (see css/map.css), and `100vw` includes the vertical
   scrollbar — so without this the page scrolls sideways by exactly the scrollbar's width whenever the map is
   on screen. `clip` and NOT `hidden`: `hidden` makes this element a scroll container, which would break the
   sticky header and the pinned launch footer. */
html, body { overflow-x: clip; }

main {
  padding-block:var(--s-5) calc(var(--footer-reserve) + env(safe-area-inset-bottom, 0px));
  padding-inline:calc(max(0px, 100% - var(--measure)) / 2 + var(--gutter));
}
.section { margin:0 0 var(--s-5); background:var(--panel); border:1px solid var(--line);
           border-radius:var(--radius); padding:var(--card-pad); box-shadow:var(--lift); }
.section h2 { font-size:var(--t-micro); letter-spacing:.09em; text-transform:uppercase; color:var(--faint);
              font-weight:650; margin:0 0 var(--s-3); display:flex; align-items:center; gap:var(--s-2); }
/* The AUTH gate: renderAuth toggles this class on the fleet and history panels on every auth transition. */
.section.hidden { display:none; }
/* The TAB gate, and it is a SECOND bit on the same panels rather than a second owner of the one above:
   applyTabs writes the `hidden` ATTRIBUTE, renderAuth writes the `.hidden` CLASS, and a panel shows iff
   neither hides it. Sharing one bit would mean signing in un-hides a panel whose tab is not selected.

   Written as an explicit author rule rather than left to the UA sheet, so a later `display:` on `.section`
   cannot silently outrank it and leave a panel on screen under a tab that reads unselected. And it is
   `display:none` rather than a transform or an opacity: Playwright considers a translated-off-screen element
   VISIBLE, which would make every visibility assertion in the browser suite vacuous. */
[role="tabpanel"][hidden] { display:none; }

/* Passkey auth panel */
.authpanel { display:none; }
.authpanel.open { display:block; }

/* ---------------------------------------------------------------------------------------------------------
   The launch footer. A GRID rather than a wrapping flex row, because what wraps where on a phone has to be a
   decision rather than an outcome: the count and the readiness state read together on one line, the two
   toggles get the full width of the bar so their labels stay at their reading size instead of being
   abbreviated to fit, and Start shares the last row with the substrate so the button that spends the
   subscription is never the thing that got pushed off the bottom.

   From 640px the two toggles sit side by side; from 1024px the whole launch surface is one row. */
footer {
  position:fixed; inset:auto 0 0 0; z-index:20;
  background:var(--chrome); backdrop-filter:saturate(1.8) blur(16px);
  border-top:1px solid var(--line);
  padding-block:var(--s-3) calc(var(--s-3) + env(safe-area-inset-bottom, 0px));
  padding-inline:calc(max(0px, 100% - var(--measure)) / 2 + var(--gutter));
  display:grid;
  grid-template-columns:auto minmax(0,1fr) auto;
  align-items:center;
  gap:var(--s-2) var(--s-3);
  /* DENSE, and it is load-bearing rather than a flourish. Every child below is placed by COLUMN only, and
     the readiness hint is the last of them in the markup while it belongs on the first row — sparse flow
     never walks the cursor backwards, so it would land under the note instead. Dense is also what lets the
     rows collapse cleanly as refreshSelection hides one control after another: nothing is pinned to a row
     number, so a hidden control leaves no empty track and no stray gap behind it. That is what keeps the
     reservation above EXACT rather than approximately right. */
  grid-auto-flow:row dense;
}
.sel { grid-column:1; font-weight:600; font-variant-numeric:tabular-nums; }
#readiness { grid-column:2; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
#selectAllBtn { grid-column:3; justify-self:end; }
.launch-controls { grid-column:1 / -1; display:grid; grid-template-columns:1fr 1fr; gap:var(--s-2);
                   align-items:center; }
/* The toggles take the whole bar on a phone: two segmented pairs side by side at 390px leaves 85px a half,
   and "Investigate" needs 73 of it before any padding — so they would either abbreviate or overflow. */
.launch-controls .seg { grid-column:1 / -1; }
.note { grid-column:1 / -1; color:var(--muted); font-size:var(--t-meta); }

@media (min-width: 640px) {
  .launch-controls .seg { grid-column:auto; }
}
/* From 1024px the first row carries everything that is a sentence — the count, "Select all", the readiness
   state and the note — and the launch surface gets the second row entire, so the four controls sit on one
   line at their natural widths instead of being squeezed into a track beside four other things. Two rows,
   109px measured, against 89.8px for the old wrapping flex row that had no touch target in it at all. */
@media (min-width: 1024px) {
  footer { grid-template-columns:auto auto auto minmax(0,1fr); }
  #selectAllBtn { grid-column:2; justify-self:start; }
  #readiness { grid-column:3; }
  .note { grid-column:4; }
  .launch-controls { grid-column:1 / -1;
                     grid-template-columns:repeat(4, auto); justify-content:start; gap:var(--s-3); }
}

/* The toast stack, which floats clear of whatever the footer currently is — the same reservation the content
   column uses, so a toast can never land on top of the launch controls it is reporting about. */
#toast { position:fixed; left:50%; z-index:60;
         bottom:calc(var(--footer-reserve) + env(safe-area-inset-bottom, 0px) + var(--s-3));
         transform:translateX(-50%); width:max-content;
         max-width:calc(100% - var(--gutter) * 2);
         display:flex; flex-direction:column; gap:var(--s-2); align-items:center; pointer-events:none; }
.toast { pointer-events:auto; max-width:min(520px, 100%); padding:var(--s-3) var(--s-4);
         border-radius:var(--radius-sm); font-size:var(--t-ui);
         border:1px solid var(--line); background:var(--panel); box-shadow:var(--lift-high);
         white-space:pre-wrap;
         /* The one thing on this page that ARRIVES. A toast that simply exists is indistinguishable from one
            that has been there a while, and this page stacks them: the entrance is what says "this is new".
            Eight pixels and a quarter of a second — enough to be seen moving, not enough to be waited for. */
         animation:toast-in var(--dur-2) var(--ease) both; }
@keyframes toast-in {
  from { opacity:0; transform:translateY(var(--s-2)) scale(.98); }
  to   { opacity:1; transform:none; }
}
.toast.ok    { border-color:color-mix(in srgb,var(--ready) 45%,var(--line)); }
.toast.err   { border-color:color-mix(in srgb,var(--block) 55%,var(--line)); color:var(--block); }
.toast.limbo { border-color:var(--limbo); background:var(--limbo-soft); color:var(--limbo); font-weight:600; }

/* Everything in this file that moves, disarmed together — AFTER every rule it has to beat, which is the rule
   rather than tidiness: `@media` adds no specificity, so a disarm written above the animation it names loses
   the cascade and disarms nothing while reading exactly as though it did. Moving `@keyframes toast-in` or the
   `.toast` rule below this block puts the animation back, silently. */
@media (prefers-reduced-motion: reduce) {
  .tab { transition:none; }
  .toast { animation:none; }
}
