/* =========================================================================
   The shared component layer — task 5 of `tasks/todo.md`, Finding X1 of
   `docs/audits/craft/00-design-system.md`.

   Nine components that seven tabs depend on used to be defined INSIDE
   `upload.css` and `dashboard.css`, and worked only because of the order the
   `<link>` tags happen to be in: `.head`, the section heading used by eight
   files across 27 rules, rendered on the Calendar tab because `upload.css`
   was loaded third. Anyone reordering those links — or anyone deleting the
   Upload tab — silently broke the section heading on seven other tabs, and
   nothing in the code said so. `.p`, `.btn`, `.lab` and `.mark` were in
   `tokens.css` for the same lack of anywhere better; that file's contract is
   that colour, type and spacing are DECIDED there, and a component is not a
   token.

   This file loads between `tokens.css` and `app.css` on BOTH `app.html` and
   `login.html`, so a component is defined before the shell and every tab
   stylesheet that modifies it. It is in `PUBLIC_ASSETS` in `src/index.ts`,
   not the guarded set, because the sign-in screen needs `.p`, `.btn` and
   `.lab` before there is a session to check.

   TASK 5 MOVED RULES AND CHANGED NONE OF THEM. Every block below was verbatim
   from where it came from, in the order it was in, so that anything that broke
   was a load-order problem rather than a taste one. TASK 6 then rebuilt the
   four controls among them — `.btn`, `.seg`, `details.dx` — and added `.in`,
   and those four now live in their own section at the top of this file rather
   than under the tab they came from. Tasks 10 to 13 restyle the rest. Where a
   rule looks wrong here, it has a task already.

   WHERE EACH BLOCK CAME FROM
     .btn, .in, .seg, details.dx      THE CONTROL LIBRARY, below: rebuilt to
                                      Appendix D in task 6, not moved
     .lab, .p, .mark                  tokens.css
     .chip, .issues/.issue,
     .head, .overlay                  upload.css
     .head > .btn                     dashboard.css (a `.head` modifier, so it
                                      travels with `.head`)
     .alert, .verdict, .kv            dashboard.css
     .btn.dgr, .selwrap               todo.css (the only copy of each, and
                                      neither is the To-do tab's own)

   WHAT STAYED BEHIND, AND WHY
     A tab-specific MODIFIER of a component below stays with the tab that is
     its only user — `.issues.corrected` and `.issues-clean` (the Upload tab's
     repairs and clean-file panels), `details.dx.notes`/`.checks-dx`/
     `.tr-person`/`.tc-period`, `.overlay-panel .fstrip:first-child`,
     `.seg.mode`, `.payroll .alert`, `.payroll .verdict.am`, `.dayrail .head`,
     `.sec > .head`. Each of those is a class only one tab renders, and each
     out-specifies or post-dates the rule here that it modifies.

   THE `[hidden]` RULE (AGENTS.md, and the standing rule this app has been
   caught by eight times): anything given `display` by a class needs its own
   `[hidden] { display: none }`, because the browser's own `[hidden]` is a
   low-specificity UA rule and loses to every one of them. `.overlay` and
   `.btn` are given `hidden` by the app today and both carry their override in
   the same block; `.seg` and `.selwrap` are new roots that give themselves
   `display`, and both carry one too, on the principle rather than on a
   renderer that needs it yet. The others that set `display` on their root
   (`.chip`, `.head`, `.alert`, `.kv`, `.issue`) are never given `hidden` by
   any renderer today and so carry none; adding one in task 5 would have been
   a change rather than a move. If a future renderer hides one, the override
   belongs here beside it, not in that tab's stylesheet.

   `.in` deliberately has NO `[hidden]` override, and that is not an oversight:
   it sets no `display` at all, because a field's layout belongs to the form
   arranging it. The rule only bites on a component that gives itself one.
   ========================================================================= */

/* =========================================================================
   THE CONTROL LIBRARY — task 6 of `tasks/todo.md`, Appendix D of
   `docs/audits/craft/00-design-system.md`, with task 8 (three segmented
   implementations become one) folded in.

   Four components — `.btn`, `.in`, `.seg` and `details.dx` — replacing the
   hand-styled controls Finding K1 counted. The fifth, `.switch`, arrived in
   task 9 with the one binary control that needs it, so that `role="switch"`
   and the thing wearing it landed together; it is below `.seg`, which is the
   control it is most often confused with.

   EVERY ONE OF THESE HAS ALL SIX STATES: rest, hover, active, `:focus-visible`,
   disabled and busy. That is the bar Appendix D sets — "a control shipped
   without all six is not done" — and it is the reason this section is longer
   than the one it replaced. Of the 27 controls K1 counted, exactly one had a
   designed disabled state and two had any disabled state at all.

   THREE CHANGES OF CHARACTER, and they matter more than the states.

   1. BUTTONS COME OFF UPPERCASE MONOSPACE. `.btn` used to set `var(--mono)`,
      `text-transform: uppercase` and `letter-spacing: 0.04em`, and it is the
      only button in the app — so "Sign in", "Sign out", "This week" and "Save
      the night" all rendered as SIGN IN, THIS WEEK. The design-system report
      calls that the single largest gap between this app and software people
      pay for. They are the interface sans at sentence case now, and mono is
      left for figures (Adam's decision 2, and task 11 finishes the job).

   2. DISABLED STOPS BEING `opacity: 0.35`. On a dark ground that fades the
      label toward the background and leaves the border where it was, so a
      disabled button read as a rendering glitch rather than as a state. A dark
      UI disables by dropping the ink to `--text-3` and the fill to
      `--surface-1`, at full opacity. Every control below does it that way.

   3. ONE FOCUS TREATMENT, and it is `box-shadow: var(--ring)` (or
      `--ring-inset`) on `:focus-visible` — never `outline`. A box-shadow
      follows `border-radius`, composites without reflow, and combines with the
      control's own shadow. Each control states `outline: none` on its own
      `:focus-visible` selector, which at (0,2,0) beats the global
      `:focus-visible { outline: 2px solid var(--acc) }` in `tokens.css` at
      (0,1,0). That global rule STAYS: it is the fallback for everything not
      yet converted, and there are still fourteen hand-rolled fields (task 7)
      and a dozen tab-local controls waiting for phase 4.

   TWO RULES THAT ARE NOT NEGOTIABLE HERE.

   Every transition names a PROPERTY LIST and one of `--t-tap` / `--t-hover` /
   `--t-layer` with `--ease`. Never a bare `transition: 0.14s`: the bare form
   animates every animatable property, `visibility` and `width` included, and a
   `.btn` that is the first focusable thing in a layer raised from
   `visibility: hidden` is then still hidden at the instant focus is moved into
   it — which is how the phone's account sheet took the keyboard nowhere.

   Nothing here shifts layout on a state change. No weight change on hover, no
   border-width change, and in particular the press is NOT
   `transform: translateY(1px)`, which inside a bordered group shifts one
   control a pixel out of line with its neighbours. Every press reads through
   the fill instead: it drops one step down the surface ladder from wherever
   the hover put it.
   ========================================================================= */

/* ------------------------------------------------------------------ .btn
   Appendix D.1. Sans 13px/500, `--surface-2` fill, `--edge-soft` hairline,
   `--r-ctl` (Adam's decision 4: controls are 6px), 32px tall.

   `min-height` rather than `height`, because the phone raises the same
   component to 44 and 46px for a thumb (`.ph-sheet .btn`, `.ph-todo .btn`) and
   a fixed height would have to be beaten rather than met. The line-height is
   stated so the 32px is the padding's doing and not the inherited 1.5. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  min-height: 32px;
  padding: var(--s-1) var(--s-4);
  background: var(--surface-2);
  border: 1px solid var(--edge-soft);
  border-radius: var(--r-ctl);
  color: var(--text);
  cursor: pointer;
  font-family: var(--sans);
  font-size: var(--fs-md);
  font-weight: var(--fw-mid);
  letter-spacing: 0;
  text-transform: none;
  line-height: 1.2;
  /* Five of these are `<a>`s (a download link, a "check this period" link),
     and without this they render underlined inside the button box. */
  text-decoration: none;
  transition:
    background-color var(--t-hover) var(--ease),
    border-color var(--t-hover) var(--ease),
    color var(--t-hover) var(--ease),
    box-shadow var(--t-hover) var(--ease);
}

/*
 * `#weekToday` is a real `<button class="btn sm" hidden>`, and `[hidden]` alone
 * is a low-specificity UA rule that loses to `.btn { display: inline-flex }`.
 * The standing rule this app has been caught by eight times.
 */
.btn[hidden] {
  display: none;
}

/* hover — one step up the ladder, and the hairline comes forward. */
.btn:hover:not([disabled]):not([aria-busy="true"]) {
  background: var(--surface-3);
  border-color: var(--edge);
}

/* active — back down to the resting fill, which is a step DARKER than the
   hover the pointer is already showing, so the press is visible without
   moving anything. `--t-tap`, because a press is faster than a fade. */
.btn:active:not([disabled]):not([aria-busy="true"]) {
  background: var(--surface-2);
  border-color: var(--edge-2);
  transition-duration: var(--t-tap);
}

.btn:focus-visible {
  outline: none;
  box-shadow: var(--ring);
}

/* disabled — ink and fill, never opacity. `cursor: default`, not
   `not-allowed`: the control is not refusing, it has nothing to do yet. */
.btn[disabled],
.btn[aria-disabled="true"] {
  background: var(--surface-1);
  border-color: var(--edge-soft);
  color: var(--text-3);
  cursor: default;
  opacity: 1;
  box-shadow: none;
}

/* busy — the label stays where it is (a button whose words change under the
   thumb is a button you have to re-read), a 12px spinner takes the leading
   icon slot, and the control stops accepting clicks. `aria-busy` is the
   contract; there is no class, so a renderer cannot set the look without
   saying it to a screen reader too. */
.btn[aria-busy="true"] {
  background: var(--surface-1);
  border-color: var(--edge-soft);
  color: var(--text-3);
  cursor: default;
  pointer-events: none;
}

.btn[aria-busy="true"] > svg {
  display: none;
}

.btn[aria-busy="true"]::before {
  content: "";
  flex: 0 0 auto;
  width: 12px;
  height: 12px;
  border-radius: var(--r-full);
  border: 2px solid currentColor;
  border-top-color: transparent;
  animation: btn-spin 0.7s linear infinite;
}

@keyframes btn-spin {
  to {
    transform: rotate(360deg);
  }
}

/*
 * Reduced motion does not mean no feedback — a still broken ring reads as a
 * spinner that has stopped, which is the opposite of what busy means. It steps
 * instead, which says "working" without smooth motion.
 */
@media (prefers-reduced-motion: reduce) {
  .btn[aria-busy="true"]::before {
    animation-duration: 1.6s;
    animation-timing-function: steps(8);
  }
}

/* .pri — the primary action, and now a real filled button. The `#fff` literal
   this used to carry is 1.67:1 on `--acc`, which is the whole reason the dark
   theme had to override it into a tinted outline; `--on-acc` is 11.33:1 on
   this palette.

   Its hover is `--acc-2` per Appendix D.1 and its press is the same fill with
   the ambient glow taken away — the light going out under the thumb. That is
   the fill reading the press, without a second green nobody chose. */
.btn.pri {
  background: var(--acc);
  border-color: var(--acc);
  color: var(--on-acc);
}

/* `:not(:focus-visible)` on both, because a `box-shadow` cannot be stacked
   across rules: without it the hover's glow — a more specific selector than
   the focus rule below — would replace the ring on a button that is both
   hovered and keyboard-focused, and the focus indicator would vanish under
   the pointer. The colour change still says "hovered" there. */
.btn.pri:hover:not([disabled]):not([aria-busy="true"]):not(:focus-visible) {
  background: var(--acc-2);
  border-color: var(--acc-2);
  box-shadow: var(--glow-soft);
}

.btn.pri:hover:not([disabled]):not([aria-busy="true"]) {
  background: var(--acc-2);
  border-color: var(--acc-2);
}

.btn.pri:active:not([disabled]):not([aria-busy="true"]):not(:focus-visible) {
  box-shadow: none;
}

.btn.pri:active:not([disabled]):not([aria-busy="true"]) {
  background: var(--acc-2);
  border-color: var(--acc-2);
  transition-duration: var(--t-tap);
}

/* The accent ring held off an accent fill by 2px of ground — see `--ring-gap`.
   Green on green is a bloom, not a ring. */
.btn.pri:focus-visible {
  box-shadow: var(--ring-gap);
}

.btn.pri[disabled],
.btn.pri[aria-disabled="true"],
.btn.pri[aria-busy="true"] {
  background: var(--acc-dim);
  border-color: transparent;
  color: var(--text-3);
}

/* .qui — a quiet action beside a louder one: no fill, no border, secondary
   ink, and it gains a surface only under the pointer. */
.btn.qui {
  background: transparent;
  border-color: transparent;
  color: var(--text-2);
}

.btn.qui:hover:not([disabled]):not([aria-busy="true"]) {
  background: var(--surface-2);
  border-color: transparent;
  color: var(--text);
}

.btn.qui:active:not([disabled]):not([aria-busy="true"]) {
  background: var(--surface-1);
}

.btn.qui[disabled],
.btn.qui[aria-disabled="true"],
.btn.qui[aria-busy="true"] {
  background: transparent;
  border-color: transparent;
}

/* .dgr — a destructive action, stated in the app's magenta but never shouted.
   Moved here verbatim from `todo.css`, where it was the only copy. */
.btn.dgr {
  color: var(--mg);
  border-color: var(--edge-soft);
}

.btn.dgr:hover:not([disabled]):not([aria-busy="true"]) {
  border-color: var(--mg);
  background: var(--mg-dim);
  color: var(--mg);
}

.btn.dgr:active:not([disabled]):not([aria-busy="true"]) {
  background: var(--surface-2);
  border-color: var(--mg);
}

.btn.dgr[disabled],
.btn.dgr[aria-disabled="true"],
.btn.dgr[aria-busy="true"] {
  color: var(--text-3);
  border-color: var(--edge-soft);
}

.btn.sm {
  min-height: 28px;
  padding: var(--s-1) var(--s-3);
  font-size: var(--fs-sm);
  gap: var(--s-1);
}

/* ------------------------------------------------------------------- .in
   Appendix D.2. One component for `input`, `textarea` and `select`, replacing
   the fourteen hand-rolled field rules Finding K4 counted — which between them
   used five different paddings for the same box. Task 7 moves those fourteen
   onto it; this task builds it.

   THE FOCUS IS THE RING AND NOTHING ELSE. Today the login password field gets
   a border-colour change AND a glow AND the global 2px outline: three
   indicators stacked on one 380px card, which reads as a fault rather than as
   focus. Here the border moves to the accent and the ring is drawn outside it —
   one mark, one meaning.

   No `display` and no `width`: `.in` is a field's SKIN and its layout belongs
   to whatever is arranging the form (a grid cell, a flex row, a 340px note
   box). That is also why it carries no `[hidden]` override — the rule only
   bites on a component that gives itself `display`. `.selwrap` below does, and
   carries one.

   EVERY RULE HERE IS BOUND TO ITS ELEMENT — `:is(input, textarea, select).in`,
   never a bare `.in` — AND THAT IS NOT STYLE, IT IS THE ONLY THING THAT MAKES
   THE NAME SAFE. `in` is already taken in this app, twice: it is the phone
   shell's *raised* state (`.ph-push.in`, `.ph-sheet.in`, `.ph-dim.in`, added
   by `openPush`/`openSheet`), and it is the class on the matrix section
   heading `<h3 class="in">` in `dashboard.js`. A bare `.in { border: 1px }`
   therefore puts a hairline around every pushed page and every phone sheet:
   measured, it took the night page and the person page from 390px of usable
   width to 388 and moved the account sheet's own bottom edge a pixel off the
   bottom of the screen. Nothing in the page said so — the pages still drew,
   just narrower. Appendix D.2 names the component `.in` and names the three
   elements it is for in the same breath; binding it to them is that sentence
   written as a selector. Do not "simplify" one of these back to a bare `.in`. */
:is(input, textarea, select).in {
  min-height: 32px;
  padding: var(--s-1) var(--s-3);
  background: var(--surface-2);
  border: 1px solid var(--edge-soft);
  border-radius: var(--r-ctl);
  color: var(--text);
  font-family: var(--sans);
  font-size: var(--fs-md);
  font-weight: var(--fw-body);
  line-height: 1.3;
  transition:
    background-color var(--t-hover) var(--ease),
    border-color var(--t-hover) var(--ease),
    color var(--t-hover) var(--ease),
    box-shadow var(--t-hover) var(--ease);
}

/* A figure field, and ONLY a figure field, is set in the tabular monospace —
   so a column of money lines up and a column of words does not pretend to. */
:is(input, textarea, select).in.fig {
  font-family: var(--mono);
  font-size: var(--fs-fig);
  font-weight: var(--fw-bold);
  letter-spacing: var(--ls-tight);
  /* No `tabular-nums`: it is the type roles' one declaration, and this face is
     monospace, where every digit is already the same width. */
}

:is(input, textarea, select).in::placeholder {
  color: var(--text-3);
}

/* `:not(:focus)` is load-bearing, and it is the same guard `.btn.pri`'s hover
   carries above for the same reason. Without it this selector is (0,4,1) and
   the focus rule below is (0,2,1), so a field with the pointer resting on it
   drew the HOVER border while it was focused: click into a box and the accent
   border never arrives, only the ring. Measured, on the fourteen fields task 7
   moved onto this component — and it is a regression the old hand-rolled
   rules did not have, because each of them was one class and focus was simply
   declared after hover. A field being used is not a field being pointed at. */
:is(input, textarea, select).in:hover:not([disabled]):not([readonly]):not(:focus) {
  border-color: var(--edge);
}

/* active: a field has no press, but it does have a caret — so the "active"
   state of a field is the one below, and it is the same for pointer and
   keyboard because a field that is focused is a field being used. */
:is(input, textarea, select).in:focus-visible,
:is(input, textarea, select).in:focus {
  outline: none;
  border-color: var(--acc);
  box-shadow: var(--ring);
}

:is(input, textarea, select).in[disabled],
:is(input, textarea, select).in[aria-disabled="true"] {
  background: var(--surface-1);
  border-color: var(--edge-soft);
  color: var(--text-3);
  cursor: default;
  opacity: 1;
}

/* invalid — the border says which field, and the message under it is the
   `.issue` component, so the refusal's wording is drawn the one way. */
:is(input, textarea, select).in[aria-invalid="true"] {
  border-color: var(--mg);
}

:is(input, textarea, select).in[aria-invalid="true"]:focus-visible,
:is(input, textarea, select).in[aria-invalid="true"]:focus {
  border-color: var(--mg);
}

/* busy — the round trip a submit path holds its fields for. `readonly` rather
   than `disabled` on purpose: the words stay selectable and the field keeps
   its place in the tab order, which is what "wait" should feel like. */
:is(input, textarea, select).in[aria-busy="true"],
:is(input, textarea, select).in[readonly] {
  color: var(--text-3);
  background: var(--surface-1);
  cursor: default;
}

textarea.in {
  min-height: 64px;
  resize: vertical;
}

/*
 * A `<select>`'s own arrow cannot be styled, so it is turned off and drawn on
 * the wrapper. It is a GLYPH and not a background image, because the Worker's
 * CSP (`src/http.ts`) has no `img-src`, falls back to `default-src 'self'` and
 * blocks a `data:` URI in CSS as surely as it blocks an inline style — with no
 * visible failure. `todo.css` worked this out for the app's one `<select>`;
 * this is that trick promoted, so the next `<select>` inherits it.
 */
.selwrap {
  position: relative;
  display: inline-flex;
}

.selwrap[hidden] {
  display: none;
}

.selwrap::after {
  content: "\25BE";
  position: absolute;
  right: var(--s-2);
  top: 50%;
  transform: translateY(-50%);
  pointer-events: none;
  font-size: var(--fs-micro);
  color: var(--acc);
}

select.in {
  appearance: none;
  padding-right: var(--s-5);
  cursor: pointer;
}

/* ------------------------------------------------------------------ .seg
   Appendix D.5, and task 8 folded in: ONE implementation replacing three
   (`app.css`'s `.tswitch`/`.topt`, now gone with the theme switch;
   `dashboard.css`'s `.seg`; `calendar.css`'s `.kinds`).

   Two or more NAMED ALTERNATIVES, exactly one on — which is what tells it
   apart from `.switch` (task 9), where there is one state that is on or off.

   The selected segment is a raised chip LIGHTER than its track, not darker.
   That is the macOS shape, and it is the opposite of what all three old
   implementations did: they drew the selected option in `--panel`, i.e. one
   step DOWN from the `--panel-2` track, so the thing that was chosen looked
   like the hole the others sat above. */
.seg {
  display: inline-flex;
  align-items: stretch;
  gap: var(--s-0);
  padding: var(--s-0);
  background: var(--surface-2);
  border: 1px solid var(--edge-soft);
  border-radius: var(--r-ctl);
  align-self: flex-start;
}

.seg[hidden] {
  display: none;
}

.seg button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 28px;
  padding: var(--s-1) var(--s-3);
  border: 0;
  /* The chip nests inside a 2px-padded track, so its radius is the track's
     less that padding — derived, not a fourth radius nobody chose. */
  border-radius: calc(var(--r-ctl) - var(--s-0));
  background: transparent;
  color: var(--text-2);
  cursor: pointer;
  font-family: var(--sans);
  font-size: var(--fs-sm);
  font-weight: var(--fw-mid);
  letter-spacing: 0;
  text-transform: none;
  line-height: 1.2;
  white-space: nowrap;
  transition:
    background-color var(--t-hover) var(--ease),
    color var(--t-hover) var(--ease),
    box-shadow var(--t-hover) var(--ease);
}

.seg button:hover:not(.on):not([disabled]) {
  background: var(--surface-3);
  color: var(--text);
}

/* The press drops back to the track's own tone, the same "one step down from
   the hover" every control here uses. */
.seg button:active:not([disabled]) {
  background: transparent;
  transition-duration: var(--t-tap);
}

.seg button.on {
  background: var(--surface-4);
  color: var(--text);
  box-shadow: var(--e-1);
}

.seg button.on:active:not([disabled]) {
  background: var(--surface-3);
  box-shadow: none;
}

/* Inset, because an outward ring on an option inside a 2px-padded track would
   be drawn half underneath the track's own border. */
.seg button:focus-visible {
  outline: none;
  box-shadow: var(--ring-inset);
}

.seg button.on:focus-visible {
  box-shadow: var(--e-1), var(--ring-inset);
}

.seg button[disabled],
.seg button[aria-disabled="true"] {
  color: var(--text-3);
  cursor: default;
  opacity: 1;
}

/* busy — the whole track, because a segmented control is one control: while
   an answer is on the wire no option may be pressed, including the one that
   is already on. */
.seg[aria-busy="true"] {
  pointer-events: none;
}

.seg[aria-busy="true"] button {
  color: var(--text-3);
}

/*
 * NEITHER DISABLED NOR BUSY MAY TAKE AWAY WHICH OPTION IS ON. That is the one
 * thing a segmented control says, and a first pass at these two states let
 * `background: transparent` out-order `.on` and flatten the track — photographed
 * side by side, the disabled control did not say whether it was showing dollars
 * or percent. The chip stays raised, one level down and without its shadow,
 * which reads as "chosen, and not yours to change just now".
 */
.seg button.on[disabled],
.seg button.on[aria-disabled="true"],
.seg[aria-busy="true"] button.on {
  background: var(--surface-3);
  color: var(--text-2);
  box-shadow: none;
}

/* --------------------------------------------------------------- .switch
   Appendix D.4, and task 9 of `tasks/todo.md`. THE REAL TOGGLE ADAM ASKED
   FOR — his brief said "toggle for buttons", and Finding K3 found no
   `role="switch"` anywhere in the codebase. Every binary state in the app was
   either a segmented pair of named alternatives (correct, and now `.seg`) or
   a button that looked identical on and off.

   USE IT ONLY WHERE A CONTROL CARRIES ONE BINARY STATE. Two or more named
   alternatives is `.seg` next door, and a tick box is a checkbox. K3
   inventoried nine binary controls; eight stay as they are and the ninth —
   the Enter tab's "Manage the list" — is this component's one consumer.

   THE MARKUP, and both halves of it are load-bearing:

     <button type="button" class="switch" role="switch" aria-checked="…">
       Manage the list
     </button>

   THE LABEL NEVER CHANGES WORDING TO INDICATE THE STATE. That is exactly what
   the control this replaces did — "Manage the list" became "Done managing" —
   and a label that moves is a label you have to re-read to find out what the
   button will do rather than what it is. The words are inside the button so
   that a pointer landing on them toggles it, and they are the accessible
   name; the state is `aria-checked` and the knob, and nothing else.

   THE KNOB NEVER MOVES OPTIMISTICALLY. Where there is a server behind the
   state, the caller sets `aria-busy="true"` and leaves `aria-checked` where it
   was until the answer lands: a knob that slides across and slides back is
   worse than one that pauses, because the first says something untrue and
   then takes it back. The one consumer today is a local view state with
   nothing on the wire, so its answer is instant and the knob is never busy —
   the state is built here for the first consumer that has a round trip.

   THE TRACK IS `::before` AND THE KNOB IS `::after`, so the component needs no
   second class name. `AGENTS.md`: a short class name in this file is global
   and reaches every element in the app that carries it, which is how a bare
   `.in` drew a border round every phone sheet. `.track` and `.knob` are one
   grep from the same accident; two pseudo-elements cannot collide with
   anything.

   THE TRAVEL IS DERIVED, NOT COPIED. Appendix D.4 says `translateX(14px)`,
   which is right for a 34px track measured to the padding box. This app is
   `box-sizing: border-box` everywhere, so a 34px track spends 2px on its
   border and 4px on its padding and the knob has 12px to cross. The appendix's
   drawn size is what matters and it is kept exactly; the number that has to
   agree with it is worked out rather than carried over. */
.switch {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--text);
  cursor: pointer;
  font-family: var(--sans);
  font-size: var(--fs-md);
  font-weight: var(--fw-mid);
  letter-spacing: 0;
  text-transform: none;
  line-height: 1.2;
  text-align: left;
  border-radius: var(--r-ctl);
}

/* The standing rule this app has been caught by eight times: `[hidden]` is a
   low-specificity UA rule and loses to `display: inline-flex` above. */
.switch[hidden] {
  display: none;
}

/* The track. 34 x 20, fully round (Adam's decision 4 names `--r-full` for
   dots and the switch), one step up the surface ladder so it reads as a well
   the knob sits in rather than as a pill lying on the panel. */
.switch::before {
  content: "";
  flex: 0 0 auto;
  width: 34px;
  height: 20px;
  border-radius: var(--r-full);
  background: var(--surface-3);
  border: 1px solid var(--edge-soft);
  transition:
    background-color var(--t-tap) var(--ease),
    border-color var(--t-hover) var(--ease),
    box-shadow var(--t-hover) var(--ease);
}

/* The knob. Absolutely positioned against the control rather than laid out
   inside the track, because it has to be able to move without the track
   resizing around it. `left: 3px` is the track's border plus its 2px of
   padding; `top: calc(50% - 8px)` centres 16px of knob on a 20px track that
   is itself centred on the label's line box. */
.switch::after {
  content: "";
  position: absolute;
  left: 3px;
  top: calc(50% - 8px);
  width: 16px;
  height: 16px;
  border-radius: var(--r-full);
  background: var(--text-2);
  transform: translateX(0);
  transition:
    transform var(--t-tap) var(--ease),
    width var(--t-tap) var(--ease),
    background-color var(--t-tap) var(--ease),
    box-shadow var(--t-hover) var(--ease);
}

/* on — the track takes the accent and the knob the ink that goes on it
   (11.5:1), plus the ambient glow the appendix asks for. 34 - 2 border -
   4 padding - 16 knob = 12. */
.switch[aria-checked="true"]::before {
  background: var(--acc);
  border-color: var(--acc);
  box-shadow: var(--glow-soft);
}

.switch[aria-checked="true"]::after {
  background: var(--on-acc);
  transform: translateX(12px);
}

/* hover — one level lighter off, and on the accent track the same answer
   `.btn.pri` already gives: `--acc-2` with the glow coming up rather than a
   second green nobody chose. Appendix D.4 says "brighter" here and D.1 said
   the same of the primary button; task 6 settled it as `--acc-2` for that
   control, and one product does not get two answers to one question. */
.switch:hover:not([disabled]):not([aria-busy="true"])::before {
  background: var(--surface-4);
  border-color: var(--edge);
}

.switch[aria-checked="true"]:hover:not([disabled]):not([aria-busy="true"])::before {
  background: var(--acc-2);
  border-color: var(--acc-2);
  box-shadow: var(--glow);
}

/* active — the macOS squash: the knob widens by 2px and nothing else moves.
   Width only, never height, and when the switch is on it grows towards the
   centre (10px of travel instead of 12) so it cannot overhang the track. */
.switch:active:not([disabled]):not([aria-busy="true"])::after {
  width: 18px;
}

.switch[aria-checked="true"]:active:not([disabled]):not([aria-busy="true"])::after {
  transform: translateX(10px);
}

/* focus — the ring on the track, because the track is the control's shape.
   An `on` switch has an accent FILL, so it takes the ring held off by 2px of
   ground for the same reason `.btn.pri` does: green on green is a bloom. */
.switch:focus-visible {
  outline: none;
}

.switch:focus-visible::before {
  box-shadow: var(--ring);
}

.switch[aria-checked="true"]:focus-visible::before {
  box-shadow: var(--ring-gap);
}

/* disabled — ink and fill, never opacity, like every other control here. The
   knob stays where it is: a disabled switch still has to say which way it is
   set, exactly as a disabled `.seg` still has to say which option is on. */
.switch[disabled],
.switch[aria-disabled="true"] {
  color: var(--text-3);
  cursor: default;
  opacity: 1;
}

.switch[disabled]::before,
.switch[aria-disabled="true"]::before {
  background: var(--surface-2);
  border-color: var(--edge-soft);
  box-shadow: none;
}

.switch[disabled]::after,
.switch[aria-disabled="true"]::after {
  background: var(--text-3);
}

/* busy — the knob takes a 2px accent ring and the track does NOT move. This
   is the whole rule stated in CSS: while the answer is on the wire the
   control shows that it is working and says nothing about the outcome. */
.switch[aria-busy="true"] {
  color: var(--text-2);
  cursor: default;
  pointer-events: none;
}

.switch[aria-busy="true"]::after {
  box-shadow: 0 0 0 2px var(--acc);
}

/*
 * Reduced motion: the knob jumps rather than slides, and every colour change
 * is left alone — the state is still said, it is just said instantly. This is
 * the phone's own rule ("reduced motion is not reduced feedback") applied to
 * the one control whose position IS its state.
 */
@media (prefers-reduced-motion: reduce) {
  .switch::after {
    transition-property: width, background-color, box-shadow;
  }
}

/* ------------------------------------------------------------ details.dx
   Appendix D.3. Thirty `<details>` across seven files inherit this at once —
   `dashboard.js` (4), `entry.js` (3), `labour.js` (2), `payroll.js` (3),
   `settings.js` (1), `upload-timereport.js` (6) and `upload.js` (11).

   The summary was mono-uppercase 10.5px at `--text-3`, which made "Roster",
   "Petty cash" and "Show the raw sheet" the least legible words on the busiest
   screen in the app. It is the interface sans at 13px/500 now.

   A disclosure is never disabled — if there is nothing behind it, it is not
   rendered — so the six states here are rest, hover, open, focus, busy and
   that deliberate absence. */
details.dx {
  border-top: 1px solid var(--edge-soft);
}

details.dx > summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
  font-family: var(--sans);
  font-size: var(--fs-md);
  font-weight: var(--fw-mid);
  letter-spacing: 0;
  text-transform: none;
  color: var(--text-2);
  transition:
    background-color var(--t-hover) var(--ease),
    color var(--t-hover) var(--ease);
}

details.dx > summary::-webkit-details-marker {
  display: none;
}

details.dx > summary:hover {
  background: var(--surface-2);
  color: var(--text);
}

details.dx > summary:active {
  background: var(--surface-1);
  transition-duration: var(--t-tap);
}

/* Inset: a summary is the full width of the panel it opens, so an outward ring
   would be drawn outside that panel's own border. */
details.dx > summary:focus-visible {
  outline: none;
  box-shadow: var(--ring-inset);
}

/*
 * THE CHEVRON. It replaces the `.tri` character — ▸, ▶, and in three places
 * (`payroll.js` twice, `settings.js` once) an EMPTY span that drew nothing at
 * all, so a third of the app's disclosures had no affordance. Drawn with two
 * borders on a rotated box rather than as a glyph or an image: a glyph varies
 * by platform font and a `data:` URI is blocked outright by the Worker's CSP.
 * `font-size: 0` is what stops whatever character the markup carries being
 * drawn beside it; the markup is left alone, because seven files render it.
 */
details.dx > summary .tri {
  position: relative;
  flex: 0 0 auto;
  width: 12px;
  height: 12px;
  font-size: 0;
  color: var(--acc);
  transition: transform var(--t-hover) var(--ease);
}

details.dx > summary .tri::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 5px;
  height: 5px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: translate(-70%, -50%) rotate(-45deg);
}

/* open — the chevron turns to point at what it opened, and the heading takes
   full ink because the section under it is now the thing being read. */
details.dx[open] > summary {
  border-bottom: 1px solid var(--edge-soft);
  color: var(--text);
}

details.dx[open] > summary .tri {
  transform: rotate(90deg);
}

/* busy — the summary stays live and the body shows whatever it is waiting on;
   the count on the right goes quiet so it cannot be read as final. */
details.dx[aria-busy="true"] > summary .r {
  color: var(--text-3);
}

details.dx > summary .r {
  margin-left: auto;
  color: var(--text-3);
  font-size: var(--fs-xs);
  font-weight: var(--fw-body);
  text-align: right;
}

/* A disclosure's body is the panel's own gutter continued, so it names the
   panel padding rather than repeating the step. */
.dxb {
  padding: var(--pad);
}

/* =========================================================================
   THE TYPE ROLES — task 11 of `tasks/todo.md`, Findings T1, T2, T3, T4 and N1
   of `docs/audits/craft/00-design-system.md`, Appendix B.2.

   `tokens.css` holds the PRIMITIVES — `--fs-*`, `--fw-*`, `--ls-*` — and says
   in its own comment that the composite ROLES belong here. This is that block.
   A role bundles a size WITH a face, a transform, a weight, tracking and (for a
   figure) `tabular-nums`, which is why it is a class and not a custom
   property: a custom property cannot carry five declarations.

   NINE ROLES, AND THAT IS THE WHOLE VOCABULARY.

     .ty-sec     a section heading — the panel's own name
     .lab        the quiet micro-label — a column head, a sub-label, a stamp
     .ty-xs      a caption, or the sub-line under a row
     .ty-sm      secondary body, a dense table cell
     .ty-md      body — the default, and what <body> itself is set to
     .ty-lg      a panel title, the toolbar's h1
     .ty-fig     any money / hours / percent / count figure
     .ty-fig-lg  a tile's figure
     .ty-hero    the one hero figure a screen is allowed

   WHAT THIS REPLACED. 25 distinct `font-size` values and 7 distinct
   `font-weight` values — 12px, 12.5px and 13px were three sizes doing one job,
   and 600, 640, 550 and 660 four weights the eye cannot separate. And, the
   strongest single number in the audit, 24 distinct signatures of what is
   conceptually ONE component: the uppercase micro-label. 9.5px at 0.16em here,
   10px at 0.16em there, 10.5px at 0.12em, 9px at 0.1em, 8.5px at 0.14em. Seven
   of the 24 differed from a neighbour by 0.01em of tracking or half a pixel of
   size — differences nobody chose and nobody can see, but which together meant
   no two section headings in this app were the same object. They are now two:
   `.ty-sec` and `.lab`.

   MONOSPACE IS FOR FIGURES (Adam's decision 2, 2026-09-20). It is allowed on
   exactly five of the nine roles — the three figures, and the two micro-labels,
   where the face is this app's own voice for a stamped label. NOWHERE ELSE: not
   on a button, not on a `<summary>`, not on a panel title, not on the sidebar
   brand line, not on the period stepper's week label, not on the account
   initial, not on a filename, not on a tab bar. Before this task
   `var(--mono)` was used 99 times against 30 figures: it was the costume
   rather than the character.

   `tabular-nums` IS DECLARED ONCE, HERE, on the three figure roles. It used to
   be written out on 30 selectors — and 28 of those were already monospace,
   where every digit is the same width by construction and the declaration does
   nothing at all. The two that were NOT monospace (the corrections diff's
   `.was` and `.now`) are money, so they take the mono face instead and stop
   being the exception. A figure that ever has to be set in the sans takes
   `.ty-fig` and gets the property with it.

   A ROLE SETS NO `display`, so none of them needs an `[hidden]` override. The
   standing trap this app has hit ten times is about a component given
   `display` by a class; a type role deliberately is not one.

   THE NAMES. `ty-` rather than `t-`: `.t` is already a class here (`table.t` is
   the Upload tab's data table, `.savebar .t` its status line), and AGENTS.md
   carries the sharp edge about a short name in the SHARED layer being global —
   `.in` drew a hairline round every phone sheet before it was bound to its
   element. `ty-` collides with nothing in any stylesheet, any renderer or
   either HTML page.

   WHERE A SELECTOR CANNOT TAKE THE CLASS, IT NAMES THE SAME TOKENS. A tab's own
   renderer emits markup this file does not own, so a rule in `dashboard.css` or
   `todo.css` spells the role out in the tokens rather than reaching for the
   class. That is still one object — the values live in one place, and changing
   a role changes every consumer. The shared components this file DOES own are
   grouped into the role's own selector below, which is the shape phase 4
   extends as it touches each surface.
   ========================================================================= */

/* A section heading: the name of the panel, or of the block under it. */
.ty-sec,
.head h2,
.verdict .word {
  font-family: var(--mono);
  font-size: var(--fs-micro);
  font-weight: var(--fw-bold);
  letter-spacing: var(--ls-micro);
  text-transform: uppercase;
  color: var(--text-2);
}

/* A SENTENCE INSIDE A DISCLOSURE, and the reason it is here rather than in a
   tab's own file. Eight of these on Settings, four on Payroll's people panel,
   two on Labour - including the statutory-holiday caption, which `AGENTS.md`
   protects because a wage half again the usual with nothing beside it reads as
   a typo. Every one of them was `class="lab dx-note"`: a 90-to-180-character
   explanation set as a 10px uppercase monospace micro-label, one of them 1132px
   on a single line.

   Three tasks met it from three different files in the same run. Settings gave
   it a body definition and had to scope it to its own tab; Payroll and the
   time-report panel dropped `lab` from their markup; Labour still carried the
   pair. So the same class was drawn three different ways depending on which tab
   rendered it. One definition, here, is what stops that - and it is why a
   sentence is not a label, which the type pass could not fix because the defect
   was in the usage rather than in the scale. */
.dx-note {
  max-width: 68ch;
  padding: var(--s-2) 0;
  font-family: var(--sans);
  font-size: var(--fs-md);
  font-weight: var(--fw-body);
  letter-spacing: 0;
  text-transform: none;
  line-height: 1.5;
  color: var(--text-2);
}

/* The quiet micro-label: a column head, a sub-label, a stamp. This is what
   `.lab` has always been, now at one size, one weight and one tracking. */
.lab {
  font-family: var(--mono);
  font-size: var(--fs-micro);
  font-weight: var(--fw-mid);
  letter-spacing: var(--ls-micro);
  text-transform: uppercase;
  color: var(--text-3);
}

/* A caption, or the sub-line under a row. */
.ty-xs {
  font-family: var(--sans);
  font-size: var(--fs-xs);
  font-weight: var(--fw-body);
  letter-spacing: 0;
  text-transform: none;
}

/* Secondary body: a dense table cell, a note beside a figure. */
.ty-sm {
  font-family: var(--sans);
  font-size: var(--fs-sm);
  font-weight: var(--fw-body);
  letter-spacing: 0;
  text-transform: none;
}

/* Body. `<body>` itself is set to this size in `tokens.css`; the class is for
   an element that has to come back to body after something else set it. */
.ty-md {
  font-family: var(--sans);
  font-size: var(--fs-md);
  font-weight: var(--fw-body);
  letter-spacing: 0;
  text-transform: none;
}

/* A panel title. Finding T4: at 15px a heading needs about -0.01em or it reads
   loose — a heading is not merely bigger text. */
.ty-lg,
.issues h3 {
  font-family: var(--sans);
  font-size: var(--fs-lg);
  font-weight: var(--fw-bold);
  letter-spacing: var(--ls-tight);
  text-transform: none;
}

/* Any money, hours, percent or count figure. */
.ty-fig,
.kv .v {
  font-family: var(--mono);
  font-size: var(--fs-fig);
  font-weight: var(--fw-bold);
  letter-spacing: var(--ls-tight);
}

/* A tile's figure. */
.ty-fig-lg {
  font-family: var(--mono);
  font-size: var(--fs-fig-lg);
  font-weight: var(--fw-bold);
  letter-spacing: var(--ls-hero);
}

/* The one hero figure a screen is allowed. */
.ty-hero,
.verdict .big {
  font-family: var(--mono);
  font-size: var(--fs-hero);
  font-weight: var(--fw-bold);
  letter-spacing: var(--ls-hero);
}

/* The one `tabular-nums` declaration in the app, for all three figure roles. */
.ty-fig,
.ty-fig-lg,
.ty-hero,
.kv .v,
.verdict .big {
  font-variant-numeric: tabular-nums;
}

/* =========================================================================
   THE PANEL — task 10 of `tasks/todo.md`, Findings D1 and X2 of
   `docs/audits/craft/00-design-system.md`.

   `.p` is the one container the whole app is built out of, on both shells.
   Until this task it set a background and a border and NOTHING ELSE — no
   padding, no radius — and two things followed from that.

   The first is the app's 98 distinct padding values. A panel that does not
   pad itself makes every one of its ~40 consumers invent an inset, so the
   Settings heading sat 2px from the panel edge while the body under it sat
   at 18px, and no two tabs agreed. The panel now owns its own inset, once,
   from the spacing scale.

   The second is the decoration. Four surface levels spanned 1.16:1 of
   contrast, which on any real monitor is one colour, so the panels did not
   sit on the page — and somebody compensated with a graph-paper texture on
   `<body>` and 9px cut corner brackets at two corners of every `.p`. Both
   are banned by Adam's decision 2. They are deleted in this same commit
   rather than a later one, because taking them off before the ladder
   underneath them was real would have left every panel with nothing
   dividing it from the page at all.

   ELEVATION IS CARRIED BY SURFACE LEVEL FIRST AND SHADOW SECOND. A panel
   rises by moving up the ladder — `--surface-1` here, `--surface-4` for the
   overlay and the phone sheet, which are the only two things in the app
   that genuinely float over content. `.p` therefore has NO box-shadow
   (`--e-0` is `none`), and only `--e-2` and `--e-3` are ever drawn. Do not
   stamp a shadow on every block: a shadow on everything is a shadow on
   nothing, and it flattens the hierarchy rather than making one.

   TWO MODIFIERS, AND NO MORE. `.tight` for a dense panel — one row of
   content, a file strip, a status line — which is one step tighter on the
   VERTICAL only, so every panel in the app keeps one left edge. `.flush`
   for a panel whose children own its edge: a full-width title bar with a
   rule under it, a table, a list of rows with dividers, the Enter tab's
   seven-tile week strip. A flush panel clips to its own radius, because its
   first and last children are square and would otherwise square off the
   corners the panel just gained.
   ========================================================================= */
.p {
  position: relative;
  padding: var(--pad);
  background: var(--surface-1);
  border: 1px solid var(--edge-soft);
  border-radius: var(--r-panel);
}

/* A dense panel. Vertical only: the gutter stays the panel gutter so a tight
   panel and an ordinary one above it still start their text on one line. */
.p.tight {
  padding: var(--s-3) var(--pad);
}

/* The children own the edge. `overflow: hidden` is not decoration here — a
   square row reaching the panel's corner is what would make a 10px radius
   read as a rendering fault. */
.p.flush {
  padding: 0;
  overflow: hidden;
}

/* The brand mark. It was a `polygon()` clip — a cut corner, decision 2 —
   which is now the control radius: it is a 28px tile, not a panel. Its
   resting outer glow went with the avatar's (decision 2 keeps the glow for
   focus and hover); the inset wash is a fill, and stays. */
.mark {
  width: 28px;
  height: 28px;
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  color: var(--acc);
  border: 1px solid var(--acc);
  border-radius: var(--r-ctl);
  box-shadow: inset 0 0 12px var(--acc-dim);
}

/* ===================================================== from upload.css */

/* ----------------------------------------------------------------- chips */
/* The chip is the quiet micro-label in a box. It is the one place the two
   label roles do not account for a signature outright: colour is the chip's own
   semantic axis (`.ok`, `.am`, `.mg`, `.cy`, `.mu` below), so the base tone
   stays `--text-2` rather than the label's `--text-3`. Face, size, weight and
   tracking are the role's. */
.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--s-1);
  font-family: var(--mono);
  font-size: var(--fs-micro);
  font-weight: var(--fw-mid);
  letter-spacing: var(--ls-micro);
  text-transform: uppercase;
  padding: var(--s-1) var(--s-2);
  border: 1px solid var(--edge-soft);
  color: var(--text-2);
  white-space: nowrap;
}

.chip .dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
}

.chip.ok {
  color: var(--acc);
  border-color: var(--acc);
  background: var(--acc-dim);
}

.chip.am {
  color: var(--am);
  border-color: var(--am);
  background: var(--am-dim);
}

.chip.mg {
  color: var(--mg);
  border-color: var(--mg);
  background: var(--mg-dim);
}

.chip.cy {
  color: var(--cy);
  border-color: var(--cy);
  background: var(--cy-dim);
}

.chip.mu {
  color: var(--text-3);
}

/* --------------------------------------------------- issues (needs attention) */
/* Always `class="p issues …"`, so the panel's own fill and hairline are
   `.p`'s; the tone variants below layer a wash over that same fill. */
/* Roomier than an ordinary `.p`, and roomier across than down — the shape it
   already had at 18/20, now on the scale. It is the one panel on a screen that
   is asking for a decision, so the extra gutter is the point. */
.issues {
  overflow: hidden;
  padding: var(--s-4) var(--s-5);
}

.issues.am {
  background: linear-gradient(115deg, var(--am-dim), transparent 58%), var(--surface-1);
}

.issues.mg {
  background: linear-gradient(115deg, var(--mg-dim), transparent 58%), var(--surface-1);
}

.issues .rail {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 2px;
  background: var(--am);
}

.issues.mg .rail {
  background: var(--mg);
}

/*
 * What the app corrected. Its own tone, not amber: nothing here needs a
 * decision — the night is already right — and colouring it as if it did would
 * teach the owner to skip the one panel that says the app moved his figures.
 */
.issues.corrected {
  background: linear-gradient(115deg, var(--cy-dim), transparent 58%), var(--surface-1);
}

.issues.corrected .rail {
  background: var(--cy);
}

.issues.corrected .lab {
  color: var(--cy);
}

/*
 * A corrected line carries no severity class — it is not an issue — so it would
 * otherwise take `.issue .dot`'s size with no colour and read as a blank column.
 */
.issues.corrected .dot {
  background: var(--cy);
}

.issues.corrected .ia {
  display: flex;
  align-items: center;
  gap: var(--s-1);
  white-space: nowrap;
}

/* The workbook's own figure and the app's, side by side. Both are money, so
   both take the figure face — which is also what took the last two
   `tabular-nums` declarations outside a monospace rule off this stylesheet. */
.issues.corrected .was {
  color: var(--text-2);
  font-family: var(--mono);
  text-decoration: line-through;
}

.issues.corrected .arrow {
  color: var(--text-2);
}

.issues.corrected .now {
  font-family: var(--mono);
  font-weight: var(--fw-bold);
}

.issues .lab {
  color: var(--am);
}

.issues.mg .lab {
  color: var(--mg);
}

/* Face, size, weight and tracking come from `.ty-lg`, which names it. */
.issues h3 {
  margin-top: var(--s-1);
}

.issues p {
  font-size: var(--fs-md);
  color: var(--text-2);
  margin-top: var(--s-1);
  max-width: 72ch;
}

.issue {
  display: grid;
  grid-template-columns: var(--s-2) minmax(0, 1fr) auto;
  gap: var(--s-3);
  align-items: start;
  padding: var(--s-2) 0;
  border-top: 1px solid var(--edge-soft);
  font-size: var(--fs-md);
}

.issue.resolved {
  opacity: 0.55;
}

.issue .ia {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  flex-wrap: wrap;
  justify-content: flex-end;
}

.issue:first-of-type {
  margin-top: var(--s-3);
}

.issue .dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  margin-top: var(--s-1);
}

.issue.error .dot {
  background: var(--mg);
  box-shadow: 0 0 8px var(--mg);
}

.issue.warn .dot {
  background: var(--am);
}

.issue.info .dot {
  background: var(--text-3);
}

.issue .m {
  color: var(--text);
}

.issue .m small {
  display: block;
  color: var(--text-3);
  font-size: var(--fs-xs);
}

.head {
  display: flex;
  align-items: center;
  gap: var(--s-2);
  padding: 0 var(--s-0);
}

.head .tick {
  width: 10px;
  height: 2px;
  background: var(--acc);
}

/* `.head h2` is set by the `.ty-sec` rule above, which names it. */

.head .r {
  margin-left: auto;
  font-size: var(--fs-xs);
  color: var(--text-3);
}

.head.mb {
  margin-bottom: 8px;
}

/* The day view's section heading carries its button as a direct child, with no
   `.r` spacer text before it (the week view's heads nest theirs inside `.r`,
   which keeps its own `margin-left: auto`). */
.head > .btn {
  margin-left: auto;
}

/* -------------------------------------------------------------- overlay */
.overlay {
  position: fixed;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--s-5);
  z-index: 50;
}

/*
 * Belt and braces: `[hidden]` alone is only a low-specificity UA-stylesheet
 * rule, so it silently loses to `.overlay { display: flex }` above unless
 * this is stated explicitly. Without it a "closed" overlay still renders
 * (invisibly, since the backdrop is transparent here) and intercepts every
 * click on the page underneath it.
 */
.overlay[hidden] {
  display: none;
}

.overlay-backdrop {
  position: absolute;
  inset: 0;
  background: var(--scrim);
}

/*
 * THE ONE THING IN THE DESKTOP APP THAT FLOATS (Finding D3, task 10). It
 * carried two colour literals — `rgba(0,0,0,0.6)` on the backdrop above and
 * `0 20px 60px rgba(0,0,0,0.45)` here — and sat on `--panel`, the same level
 * as every panel underneath it, so the only thing saying it was above the
 * page was the dimming. It is now the top of the ladder (`--surface-4`), the
 * elevation scale's top step, and the panel radius. `--e-3` carries a 1px
 * white inset ring of its own, which is why the border here is `--edge`
 * rather than the default hairline: the two read as one edge.
 *
 * ONE CAUTION, AND IT IS A DESIGN RULE RATHER THAN A NOTE. Measured in
 * Chrome against these tokens: `--mg` as a WORD directly on `--surface-4` is
 * 3.78:1, and a `.chip.mg` — red on `--mg-dim` over this fill — is 3.36:1.
 * Both fail AA; the same marks are 5.60 and 4.91 on `--surface-1`. So a
 * refusal that sits on the overlay's OWN fill is a border or a dot, never a
 * word. Nothing red lands there today: everything the overlay draws is
 * inside a nested `.p` (still `--surface-1`), inside `.issues` (its own
 * fill) or inside a `.btn.dgr` (its own `--surface-2`, 5.09:1 wherever it
 * stands). A NESTED PANEL STAYING AT LEVEL 1 IS DELIBERATE: the overlay is a
 * window whose chrome floats, and its content is ordinary panels — which is
 * also what keeps those figures safe.
 */
.overlay-panel {
  position: relative;
  background: var(--surface-4);
  border: 1px solid var(--edge);
  border-radius: var(--r-panel);
  box-shadow: var(--e-3);
  width: 100%;
  max-width: 780px;
  max-height: min(88vh, 900px);
  overflow: auto;
}

/* A control, so the control radius and the control's own resting fill — one
   step below the panel it sits on, which is what reads as a well rather than
   as a second floating thing. */
.overlay-close {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 2;
  width: 28px;
  height: 28px;
  display: grid;
  place-items: center;
  border: 1px solid var(--edge-soft);
  border-radius: var(--r-ctl);
  background: var(--surface-2);
  color: var(--text-2);
  cursor: pointer;
}

.overlay-close:hover {
  color: var(--text);
  border-color: var(--edge);
  background: var(--surface-3);
}

.overlay-close svg {
  width: 14px;
  height: 14px;
}

/* ================================================== from dashboard.css */

/* ------------------------------------------------------------- banner */
.alert {
  display: flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
  border: 1px solid var(--am);
  background: var(--am-dim);
}

/* A circled "i" or "!". A glyph, not a figure, so it is not monospace. */
.alert .ex {
  font-size: var(--fs-md);
  font-weight: var(--fw-bold);
  color: var(--am);
  line-height: 1;
  width: 21px;
  height: 21px;
  border-radius: 50%;
  border: 1px solid var(--am);
  display: grid;
  place-items: center;
  flex: 0 0 auto;
}

.alert b {
  font-size: var(--fs-md);
  font-weight: var(--fw-bold);
}

.alert p {
  font-size: var(--fs-sm);
  color: var(--text-3);
  margin-top: 2px;
}

.alert .btn {
  margin-left: auto;
  flex: 0 0 auto;
}

.alert.cy {
  border-color: var(--cy);
  background: var(--cy-dim);
}

.alert.cy .ex {
  color: var(--cy);
  border-color: var(--cy);
}

/* ------------------------------------------------------------- verdict */
/*
 * The one verdict a screen is allowed. It is always drawn as `class="p
 * verdict"`, so task 10 took the hand-made panel shape off it — the
 * background, the hairline and the `position: relative` its own left rail
 * needs are `.p`'s, and what is left here is the one thing that is the
 * verdict's alone: more air than an ordinary panel, from the scale. The
 * `overflow: hidden` stays, and now does a second job: it clips the rail to
 * the panel's radius.
 */
.verdict {
  padding: var(--s-5);
  overflow: hidden;
}

.verdict .rail {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 2px;
  background: var(--acc);
}

.verdict.bad .rail {
  background: var(--mg);
}

.verdict.thin .rail {
  background: var(--cy);
}

/* Face, size, weight, tracking and `tabular-nums` come from `.ty-hero`, which
   names this selector. Only the colour and the line box are the verdict's.

   THE HALO FOLLOWS THE INK — task 27, defect D-1, and it lands on every
   surface that draws a verdict at once. `--glow` is a *green* shadow, and the
   two tone modifiers below override only the colour: so a losing week drew a
   magenta number wearing a green halo, and a thin one a cyan number wearing a
   green halo — the largest object on those screens, in two colours at once,
   one of them saying the opposite of the other. `currentColor` is the whole
   fix, and the mix reproduces `--glow`'s own 12px at 30% exactly, so the
   accent case is pixel-identical to what it was. */
.verdict .big {
  line-height: 1;
  color: var(--acc);
  text-shadow: 0 0 12px color-mix(in srgb, currentColor 30%, transparent);
}

.verdict.bad .big {
  color: var(--mg);
}

.verdict.thin .big {
  color: var(--cy);
}

/* KEPT / LOST, beside the hero. Set by `.ty-sec`, which names it: it was 11px
   at 0.18em in `--text-3`, which is what made the Dashboard's own verdict the
   least legible word on the page. It is a heading for the number beside it. */
.verdict .word {
  margin-left: 12px;
}

.verdict .say {
  font-size: var(--fs-lg);
  margin-top: 13px;
  line-height: 1.55;
  max-width: 64ch;
  color: var(--text-2);
}

.verdict .say b {
  color: var(--text);
  font-weight: var(--fw-bold);
}

.verdict .note {
  margin-top: var(--s-3);
  font-size: var(--fs-md);
  color: var(--text-3);
  display: flex;
  align-items: center;
  gap: var(--s-2);
  flex-wrap: wrap;
}

.verdict .note b {
  color: var(--text-2);
  font-weight: var(--fw-bold);
}

.verdict .headline {
  margin-top: 8px;
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
}

.verdict .note.strong {
  color: var(--text-2);
}

/* -------------------------------------------------------------- kv rows */
.kv {
  display: flex;
  flex-direction: column;
}

.kv .row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-4);
  padding: var(--s-2) 0;
  border-top: 1px solid var(--edge-soft);
}

.kv .row:first-child {
  border-top: 0;
}

.kv .k {
  font-size: var(--fs-md);
  color: var(--text-2);
}

.kv .k small {
  display: block;
  font-size: var(--fs-micro);
  color: var(--text-3);
}

/* Face, size, weight, tracking and `tabular-nums` come from `.ty-fig`, which
   names this selector. */
.kv .v {
  white-space: nowrap;
}

.kv .row.tot {
  border-top: 1px solid var(--edge);
  margin-top: var(--s-0);
  padding-top: var(--s-3);
}

.kv .row.tot .k,
.kv .row.tot .v {
  color: var(--text);
  font-weight: var(--fw-bold);
}

.kv .row.z .k,
.kv .row.z .v {
  color: var(--text-3);
}

.kv .foot {
  margin-top: 8px;
}
