/* ===========================================================================
   THE PUBLIC SHELL'S CONTROL LAYER.

   ⚠️ WHY THIS IS ITS OWN FILE AND NOT A FEW LINES APPENDED TO `orbit-public.css`.
   `orbit-tokens.css` + `orbit-base.css` + `orbit.css` + `orbit-public.css` are a
   PARTITION of the frozen design reference (`mockups/linear/orbit.css`), and
   `src/orbit-tokens.test.mjs` asserts they reconstruct it byte-for-byte. That
   assertion is what proves the split was a cut and not a rewrite, so appending to any
   of the four destroys the only evidence the cascade is faithful. `web/pub/carried.css`
   is the same shape for the gated app, and exists for the same reason.

   ── the defect this closes ─────────────────────────────────────────────────────
   `.btn` is declared in `orbit.css` — the INSTRUMENT part — and the public pages
   deliberately do not link that sheet (a document is not the app shell, and
   `orbit-tokens.test.mjs` asserts they never link it). But `orbit-public.css:80`
   carries `.site .btn { height: 38px; … }`, which is a GEOMETRY OVERRIDE presupposing
   a base that public pages never received. Measured live on 2026-08-07:
   `/login`'s "Sign in with GitHub" resolved to `display:inline` with a transparent
   background — the primary call to action on the sign-in page was plain text.

   So the base paint lives here, copied from `web/app/orbit.css` rather than
   re-authored, and the public pages link this after `orbit-public.css`.

   Everything here obeys the same two rules as the sheets above it: every colour is a
   token from `orbit-tokens.css` (no literal hex, ever), and nothing carrying a verdict
   glyph wears the accent — a setup handshake and a sign-in have no verdict.
   =========================================================================== */

/* ── the button ─────────────────────────────────────────────────────────────
   Declarations copied verbatim from `web/app/orbit.css`'s `.btn` block so the two
   surfaces cannot drift into two different buttons. `.site .btn` in orbit-public.css
   then raises the height 30px → 38px for document-scale pages; that override still
   wins because it is a more specific selector, not because of link order. */
.btn {
  height: 30px; display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  padding: 0 12px; border-radius: var(--r-md);
  border: 1px solid var(--border); background: var(--panel);
  color: var(--text); font-size: 12.5px; font-weight: 500;
  transition: background var(--t-fast) var(--ease), border-color var(--t-fast) var(--ease);
  white-space: nowrap;
}
.btn:hover:not(:disabled) { background: var(--elev-2); }
.btn .ic { width: 14px; height: 14px; opacity: .8; }
.btn.primary { background: var(--accent); border-color: transparent; color: var(--accent-fg); }
.btn.primary:hover:not(:disabled) { background: var(--accent-hi); }
.btn.primary .ic { opacity: .95; }
.btn:disabled, .btn[aria-disabled="true"] { opacity: .45; cursor: not-allowed; }
/* `.site .btn` raises every button to 38px, and the landing page's one small CTA — "See how
   reviews look" (`class="btn sm"`, index.html) — asks to opt back out. Without this it is
   simply a 38px button, which is the same class of miss as the base rule itself: a modifier
   the markup renders and the sheet never received. */
.btn.sm { height: 26px; padding: 0 9px; font-size: 12px; }

/* ── the icon button ────────────────────────────────────────────────────────
   Same story as `.btn`, one control over: every public page ends with a theme toggle
   (`<button class="iconbtn" data-theme-toggle>`) and the 30×30 box it sits in is declared
   only in the instrument. Copied verbatim, for the same no-drift reason as above. */
.iconbtn {
  width: 30px; height: 30px; display: grid; place-items: center;
  border-radius: var(--r-md); color: var(--text-2);
  border: 1px solid transparent;
  transition: background var(--t-fast) var(--ease), color var(--t-fast) var(--ease);
}
.iconbtn:hover { background: var(--hover); color: var(--text); }
.iconbtn.bordered { background: var(--panel); border-color: var(--border); box-shadow: var(--shadow-sm); }

/* ── the verdict and status glyphs ──────────────────────────────────────────
   ⚠️ THIS ONE IS NOT CHROME. The landing page renders its verdict mark from the SAME
   generator the product uses — `site.mjs` mounts `[data-verdict]` through `icons.mjs`, so the
   front door cannot show a different "safe to ship" mark than the app does. It was showing a
   different COLOUR: `.verdict-ic.v-safe` lives in the instrument, so the mark inherited body
   slate and the one saturated signal in the whole product was monochrome on the page whose
   job is to explain it. The classes reach the markup through the generator rather than any
   `class="…"` in an .html file, which is why a scan of the pages alone would not find them.

   These are the ONLY saturated colours this sheet carries, and they are verdict tokens on a
   verdict glyph — the licensed case, not an exception to it. */
.verdict-ic { width: 16px; height: 16px; }
.verdict-ic.v-safe   { color: var(--v-safe); }
.verdict-ic.v-review { color: var(--v-review); }
.verdict-ic.v-block  { color: var(--v-block); }
.verdict-ic.none     { color: var(--text-3); }

/* the status glyph, coloured only by category — never by verdict */
.status-ic { width: 15px; height: 15px; color: var(--text-2); }
.status-ic.cat-backlog   { color: var(--text-3); }
.status-ic.cat-unstarted { color: var(--text-3); }
.status-ic.cat-started   { color: var(--text-2); }
.status-ic.cat-gated     { color: var(--text); }
.status-ic.cat-done      { color: var(--text-3); }
.status-ic.cat-cancelled { color: var(--text-3); }

/* ── form controls ──────────────────────────────────────────────────────────
   `orbit-public.css` styles `.field input` and `.field select` at the instrument's
   32px. These two pages are FORMS a stranger fills in once, often on a phone, so they
   get a 44px target — the platform minimum — rather than the dense-list height. That
   is a size decision, not a second palette: colour, border and radius all still come
   from the rules above it. */
.site .field input, .site .field select, .site .field textarea { min-height: 44px; }
.site .field { max-width: none; }
/* A select needs its own arrow once it stops being the platform control's own colour. */
.site .field select { appearance: none; -webkit-appearance: none; padding-right: 30px;
  background-image: linear-gradient(45deg, transparent 50%, var(--text-3) 50%),
                    linear-gradient(135deg, var(--text-3) 50%, transparent 50%);
  background-size: 5px 5px, 5px 5px;
  background-position: calc(100% - 15px) calc(50% + 1px), calc(100% - 10px) calc(50% + 1px);
  background-repeat: no-repeat;
}
.field .opt { color: var(--text-3); font-weight: 400; }

/* ── a status/error row ─────────────────────────────────────────────────────
   DESIGN.md: every status signal is icon + text, never colour alone — which is why
   this row is slate in BOTH states and the only thing that changes is the glyph and
   the text weight. A form that failed is not a verdict. */
.auth-err {
  display: flex; align-items: flex-start; gap: 7px;
  margin: 12px 0 0; min-height: 18px;
  font-size: 13px; line-height: 1.45; color: var(--text-2);
}
.auth-err .ic { width: 15px; height: 15px; margin-top: 1px; display: none; }
.auth-err[data-bad="1"] { color: var(--text); }
.auth-err[data-bad="1"] .ic { display: block; }

/* ── links inside a card ────────────────────────────────────────────────────
   `--link`, not `--accent` (#978, D2). The accent is licensed on link chrome (Decisions Log
   2026-08-07) and nothing on these pages carries a verdict glyph — but as TEXT it measures
   4.31:1 on light `--bg` and 4.24 / 4.05 / 3.89 on the three dark surfaces, all below AA.
   `--link` is the same family one step away from the surface, and clears AA on every one. */
.auth-card .link { color: var(--link); text-decoration: none; }
.auth-card .link:hover { text-decoration: underline; text-underline-offset: 2px; }

/* ── the signed-in account menu (landing nav) ───────────────────────────────
   The nav used to lay the whole signed-in state out flat: a primary "Go to Runs",
   then the login as bare text, then a "Sign out" button of equal visual weight to
   the CTA. Three peers, one of which is the thing you came for and one of which is
   the thing you least often want — and sign-out is the only destructive-ish action
   on the page, sitting one mis-click from the button beside it.

   The first pass folded sign-out in and KEPT the primary CTA beside the trigger, so
   "Go to Runs" then appeared twice in one nav — the same href, eight pixels apart,
   which reads as two choices and is one. The menu item is now the only one.

   A native `<details>` rather than a scripted popover: the summary is focusable and
   Enter/Space toggles it with no JS, which is the same reasoning the modal primitive
   uses for `<dialog>` (2026-07-16). The page script adds only the two behaviours the
   element does not supply — Esc, and click-outside.

   Slate throughout, including the trigger: the accent is licensed on primary-button
   chrome and this is not one, it is chrome around an identity. */
.acctmenu { position: relative; display: inline-block; }
.acctmenu > summary {
  display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
  height: 34px; padding: 0 10px; border-radius: var(--r-md);
  border: 1px solid var(--border); background: var(--panel); color: var(--text);
  font-size: 13px; list-style: none; user-select: none;
}
.acctmenu > summary::-webkit-details-marker { display: none; }
.acctmenu > summary:hover { background: var(--hover); }
.acctmenu > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.acctmenu .caret { width: 12px; height: 12px; color: var(--text-3); transition: transform var(--t-fast) var(--ease); }
.acctmenu[open] .caret { transform: rotate(180deg); }
.acctmenu-panel {
  position: absolute; right: 0; top: calc(100% + 6px); z-index: 60;
  min-width: 200px; padding: 4px;
  background: var(--elev); border: 1px solid var(--border-2);
  border-radius: var(--r-md); box-shadow: var(--shadow-sm);
}
.acctmenu-who {
  display: block; padding: 7px 9px 8px; font-size: 11.5px; color: var(--text-3);
  border-bottom: 1px solid var(--border); margin-bottom: 4px;
}
.acctmenu-item {
  display: flex; align-items: center; gap: 8px; width: 100%;
  padding: 7px 9px; border: 0; border-radius: var(--r-sm);
  background: none; color: var(--text); font: inherit; font-size: 12.5px;
  text-align: left; text-decoration: none; cursor: pointer;
}
.acctmenu-item:hover { background: var(--hover); }
.acctmenu-item:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.acctmenu-item .ic { width: 15px; height: 15px; color: var(--text-3); flex: none; }

/* ── the sign-in form's own vocabulary ──────────────────────────────────────
   🚨 THESE FOUR WERE A FILED, KNOWN DEFECT. `src/public-page-vocabulary.test.mjs` carried
   them in its `UNBACKED` allowlist with an honest reason: login.html renders `.auth-label`,
   `.auth-input`, `.auth-alt` and `.err-msg`, no stylesheet in the repo declared any of them,
   "so that form renders at browser defaults" — and, unlike `.btn`, there was nowhere to copy
   the rules FROM, so #866 filed them rather than guessing at new visual vocabulary. This is
   the design pass that entry was waiting for; the allowlist entries are deleted with it.

   Measured before, on the dev token form: the label ended at x=642 and the field began at
   x=646 — a 4px gap where the spacing scale's half-step is reserved for dense card internals
   — and the input stopped 61px short of the full-width button beneath it. A ragged row on the
   break-glass path an operator only reaches when something is already wrong.

   `.auth-alt` is NOT dev-only and never was: it wraps "Reviewing your agent's PRs? Sign in →",
   which every visitor to /login sees. Unstyled, it rendered as body text with no link
   affordance at all, one line under a full-width primary button — a partner's entire route
   into the product, looking like a sentence. That is the production half of this defect.

   Nothing new is invented: sizes come from the public scale, colour from the tokens, and the
   44px target matches the `.field` rule above for the same reason. */
.auth-label {
  display: block; margin: 0 0 6px;
  font-size: 12.5px; font-weight: 500; color: var(--text-2);
}
.auth-input {
  display: block; width: 100%; box-sizing: border-box;
  min-height: 44px; padding: 0 12px; margin: 0 0 10px;
  font: inherit; font-size: 13.5px; color: var(--text);
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--r-md);
}
.auth-input::placeholder { color: var(--text-3); }
.auth-input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-color: transparent; }

/* The secondary route in. A real link affordance — `--link`, for the reason above: the
   accent's licence on link chrome is about which vocabulary may appear here, and AA is about
   whether it can be read. Both have to hold. */
.auth-alt { margin: 14px 0 12px; font-size: 13.5px; text-align: center; }
.auth-alt a { color: var(--link); text-decoration: none; }
.auth-alt a:hover { text-decoration: underline; text-underline-offset: 2px; }

/* The message half of `.auth-err`. Slate in both states, like the row that holds it. */
.err-msg { min-width: 0; overflow-wrap: anywhere; }

/* ═══ THE PHONE AND THE TABLET ════════════════════════════════════════════════════════════
 *
 * ⚠️ EVERYTHING BELOW IS AN OVERRIDE, AND IT IS HERE BECAUSE IT CANNOT BE ANYWHERE ELSE.
 * `orbit-public.css` is a frozen partition member — `src/orbit-tokens.test.mjs` asserts the
 * four sheets reconstruct `mockups/linear/orbit.css` byte for byte — so a media query added
 * there destroys the only evidence the cascade is a faithful cut. The 720/900/1000/620/560/860
 * queries already in that file came ACROSS from the mockup; ours cannot join them. Same reason
 * this file exists at all (see the header).
 *
 * `web/app/orbit.css` grew the app's narrow layout and touch density on 2026-08-12 (#933). The
 * funnel got neither. The claim in that sheet — "the funnel pages have honoured 44px since
 * #114" — is half true and the missing half is the half a stranger meets first: #114 raised
 * `.field input` and `.auth-input` to a 44px BOX and never touched anything else, so every
 * BUTTON on every public page is still 38px, the theme toggle is 30px, and the sign-in form's
 * one text entry is 13.5px.
 * ════════════════════════════════════════════════════════════════════════════════════════ */

/* ── the full-height auth shell ───────────────────────────────────────────────────────────
   `100vh` on a phone is the LARGEST viewport height — the one with the address bar retracted —
   so a box pinned to it reaches under the bar that is actually on screen. #933 fixed exactly
   this on `.app` and `.settings`; `.auth-page` is the same declaration on the funnel, and it
   centres the sign-in card, the sign-out confirmation and the activation ladder. `vh` stays
   first as the fallback: a browser without `dvh` must get something rather than nothing. */
.auth-page { min-height: 100vh; min-height: 100dvh; }

/* ── the theme toggle keeps the size it declares ──────────────────────────────────────────
   NOT a density question — a defect at every pointer type. `.site-nav .site-wrap` is a flex
   row and `.iconbtn` inherits `flex-shrink: 1`, so the toggle is the thing that gives when the
   row runs out of width. Measured at 375px: a control declared 30px wide rendered at 18px, and
   the nav "fitted" only because it had been squeezed. A button that silently shrinks is also
   the reason the row's real overflow was invisible. */
.site-nav .iconbtn { flex: none; }

/* ── the narrow nav: the section links stand down ─────────────────────────────────────────
 * A WIDTH rule, not a density one, because the row runs out of room before it runs out of
 * finger. Measured signed-in at 375px: the nav needed 427px and overflowed by 52 — and the
 * account menu's panel is `position: absolute; right: 0`, so it hung 40px off the right edge
 * of the page with no way to scroll to it. Raising the controls to 44px below only widens it.
 *
 * The two that go are `How it works` and `Reviews`, and they are the right two: both are
 * `#anchors` INTO THE PAGE YOU ARE ALREADY SCROLLING, so on a phone they duplicate the scroll
 * gesture. What stays is everything that goes somewhere the scroll cannot — home, the theme,
 * and sign-in-or-your-account. Nothing is unreachable at any width, which is the whole
 * difference between this and the `.sb { display: none }` the app sheet exists to warn about.
 *
 * 560px because the row's own arithmetic asks for it, not because it is a round number: the
 * six children plus their gaps and the wrap's 24px gutters need 451px with the account chip
 * in, and a breakpoint has to clear the widest state of the thing it is measuring, not the
 * narrowest. It lands on the same 560 the mockup's own sheet already uses for `.beats`.
 *
 * ⚠️ `.site-wrap` IS IN THE SELECTOR ON PURPOSE, and dropping it silently un-does this rule.
 * The density layer below sets `display: inline-flex` on `.site-nav a.navlink` to give it a
 * 44px box, at the identical specificity and later in the file — so the shorter selector loses
 * the cascade to it and the links come back, on exactly the devices this rule is for. Measured
 * that way once: `matchMedia('(max-width: 560px)')` true, `display` computed `flex`. Written
 * one class higher, the outcome no longer depends on which block a future edit appends after
 * the other. */
@media (max-width: 560px) {
  .site-nav .site-wrap a.navlink { display: none; }
}

/* ── the iOS focus-zoom threshold ─────────────────────────────────────────────────────────
   A MECHANISM, not a readability preference, and the same one `web/app/orbit.css` documents:
   mobile Safari zooms the viewport when a focused input's font-size is under 16px and does not
   zoom back out. #114 gave these fields a 44px box and left the type at 13–13.5px, so the two
   forms a stranger actually fills in — the sign-in token box and the activation ladder's repo,
   volume and contact fields — magnify the page on first tap and leave it there.

   Under `pointer: coarse` only, so the dense funnel a mouse sees is unchanged, exactly as the
   instrument's own density layer is scoped. */
@media (pointer: coarse) {
  .auth-input,
  .site .field input,
  .site .field select,
  .site .field textarea { font-size: 16px; }
}

/* ── the funnel's touch density ───────────────────────────────────────────────────────────
 *
 * The instrument's density layer (`web/app/orbit.css`), arriving on the pages an operator
 * meets BEFORE the instrument. Same axis and same reasoning: `pointer: coarse` and not width,
 * because a 1280px tablet is a finger and a 900px desktop window is a mouse, and width answers
 * that question wrongly in both directions. Nothing here changes a colour, a typeface or any
 * geometry a mouse ever sees.
 *
 * ⚠️ SPECIFICITY IS LOAD-BEARING HERE IN A WAY IT IS NOT IN THE APP SHEET. A media query adds
 * none, so `.btn { height: 44px }` inside this block still LOSES to `orbit-public.css`'s
 * `.site .btn { height: 38px }` — the same trap that made the primary CTA render as plain text
 * in the first place (this file's header). Every subject below is written at or above the
 * specificity of the base rule it is overriding, and `responsive-public.test.mjs` checks that
 * mechanically rather than trusting the reading.
 *
 * `.btn.sm` — "See how reviews look" on the landing page — comes up to 44 with the rest. It is
 * 26px on a desktop to sit UNDER the primary beside it, and that subordination survives: it is
 * still the one without `.primary`, so it is still slate against a filled accent button. A
 * hierarchy carried by a target too small to hit is not a hierarchy, it is a miss.
 * ──────────────────────────────────────────────────────────────────────────────────────── */
@media (pointer: coarse) {
  /* `.site .btn`, not `.btn` — see above. `.site .btn.sm` for the same reason one level up. */
  .site .btn, .site .btn.sm { height: 44px; }
  .iconbtn { width: 44px; height: 44px; }

  /* Links and a summary rather than buttons, so the target is a min-height plus the box that
     lets it apply — an inline `<a>` ignores height entirely.

     `a.wordmark`, NOT `.site-nav .wordmark`: the same class is a link in the nav and on the
     three auth cards, and a plain `<div>` on `/activate` and `/workspace/setup`. Keying off
     the element is what tells the control apart from the label — and scoping it to the nav
     would have left the sign-in card's own way home at 25px, on the page where it is the only
     way home. */
  a.wordmark,
  .site-nav a.navlink { display: inline-flex; align-items: center; min-height: 44px; }
  .acctmenu > summary { height: 44px; }
  .acctmenu-item { min-height: 44px; }

  /* Standalone destinations, not links inside a sentence. The footer row is Runs / Reviews /
     Sign in; `.auth-back` is "Back to the home page", which on `/retired` is the only way out
     that is not one of the two buttons above it.

     ⚠️ `.auth-alt a` IS ON THIS LIST AND IT LOOKS LIKE IT BELONGS ON THE OTHER ONE. It reads
     as prose — "Reviewing your agent's PRs? Sign in →" — and the first pass exempted it on
     that basis. Wrong, and this file already says why 60 lines up: it is centred on its own
     line, wearing the accent as a link affordance, and it is A PARTNER'S ENTIRE ROUTE INTO
     THE PRODUCT. A miss there costs the visit. What stays exempt is `.link` and
     `.auth-note a`, which really are three words mid-sentence — a 44px box around those
     pushes the paragraph apart to nobody's benefit. The distinction is standalone-vs-inline,
     not classed-vs-unclassed. */
  .site-foot a { display: inline-flex; align-items: center; min-height: 44px; }
  .auth-back,
  .auth-alt a { display: inline-flex; align-items: center; min-height: 44px; }
}

/* ── the way back off a dead-end page ─────────────────────────────────────────────────────
   `/login` and `/retired` end with "Back to the home page" on its own centred line — a
   standalone destination, not a link in a sentence, and the only way out of `/retired` other
   than the two buttons above it. It carried its colour and underline in a `style=` attribute
   on both pages, which is why it had no target rule and could not have one: the density layer
   above is keyed off classes, and an unclassed `<a>` is invisible to it and to the guard. The
   declarations are the ones the attributes already held, moved somewhere a media query can
   reach them. */
.auth-back { color: var(--text-2); text-decoration: underline; text-underline-offset: 2px; }
