/* styles/wkh-ui.css — the Why Knott Hire design system.
   Loaded by <link> from index.html. See docs/superpowers/specs/2026-07-30-ui-refresh-foundation-design.md

   ONE spacing scale serves both surfaces. Admin density comes from which step a component
   reaches for and from tighter type — NOT from a second, denser scale. Two scales would be
   two systems again, which is the problem this replaces. */

/* @tokens-start
   Literal values are permitted ONLY between these markers.
   tools/ui-tokens-test.js fails on any hex or off-scale px outside them. */
:root{
  --wkh-s1:4px; --wkh-s2:8px; --wkh-s3:12px; --wkh-s4:16px; --wkh-s5:24px; --wkh-s6:32px;
  --wkh-pill:999px;
  --wkh-dur:.15s;
}

/* Customer — light. A customer arrives once or twice, on their own phone, usually to pay a
   balance or upload a licence; dark chrome is the wrong bet on that surface.
   :root is included in this selector so the light/customer palette is the DEFAULT scope — a
   component rendered before either .wkh-admin or .wkh-cust is applied still resolves every
   token instead of hitting invalid-at-computed-value-time on every property. Customer is the
   public-facing surface, so it is the right fallback. tools/ui-tokens-test.js locates the
   spacing scale via the FIRST `:root\s*{` match, which is the standalone :root block above this
   one — keep that block first in the file. */
:root, .wkh-cust{
  --wkh-chrome:#3A4048; --wkh-chrome-2:#2B3037;
  --wkh-chrome-ink:#F2F1EC; --wkh-chrome-ink-2:#A8AEB6;
  --wkh-accent:#FCB900; --wkh-on-accent:#22262B;
  /* Accent as TEXT on a light ground only. #FCB900 on white is ~1.9:1 and fails AA;
     #8A6600 is ~5.3:1 and passes. Never use --wkh-accent for text on --wkh-card here. */
  --wkh-accent-ink:#8A6600;
  /* …and the mirror of it, for accent-as-text on the DARK chrome (sidenav current item,
     booking-detail ref label). --wkh-accent-ink is the WRONG token there — #8A6600 on
     #3A4048 is ~2:1. The shipped gold already clears 6:1 on chrome, so this default IS
     --wkh-accent; it exists so that an operator accent dark enough to vanish against the
     chrome can be lifted instead (lib/theme.js accentFamily). Never use --wkh-accent for
     text on --wkh-chrome / --wkh-chrome-2. */
  --wkh-accent-chrome:#FCB900;
  --wkh-panel:#F6F5F0; --wkh-card:#FFFFFF;
  --wkh-ink:#22262B; --wkh-ink-2:#696F76; --wkh-line:#E4E2DA;
  /* Darkened from the original #16A34A/#D97706/#DC2626 — those read fine to the eye but fail
     4.5:1 at the 10px/bold sizes every one of these is actually used at (chips, kpi details,
     field errors). See the report for the full computed table. */
  --wkh-ok:#166534; --wkh-warn:#92400E; --wkh-bad:#B91C1C;
  --wkh-ok-bg:#E8F5EC; --wkh-warn-bg:#FDF3E4; --wkh-bad-bg:#FCEAEA;
  /* Customer geometry (One UI phase 4, spec §2.5): soft — 16px cards, pill 48px buttons,
     heading step 17. Admin overrides all of these in its density scope below. --wkh-btn-r /
     --wkh-btn-h are consumed by the legacy Btn component's inline styles (app.js:2287), which
     is how one shared component renders pill on customer surfaces and 8px in admin. */
  --wkh-r-sm:8px; --wkh-r:16px;
  --wkh-btn-r:999px; --wkh-btn-h:48px;
  --wkh-t-lbl:10px; --wkh-t-body:13.5px; --wkh-t-h:17px; --wkh-t-num:22px;
  /* The two steps the four-step scale never had — why every screen hand-rolled fontSize:18.
     display = the page greeting; title = card/section titles. One UI spec §2.2. */
  --wkh-t-display:28px; --wkh-t-title:22px;
  --wkh-w-med:600; --wkh-w-bold:700; --wkh-w-heavy:800;
  --wkh-shadow-1:0 1px 2px rgba(28,32,38,.05); --wkh-shadow-2:0 4px 10px -4px rgba(28,32,38,.08);
  /* --wkh-accent as an outline is 1.74:1 on white — invisible. --wkh-accent-ink is the
     foreground-safe token this scope already carries for exactly this reason. */
  --wkh-focus:#8A6600;
}

/* Admin — DENSITY scope only. The dark operator palette that lived here 31 Jul–25 Aug was
   deliberately withheld from the console wrapper and had ZERO consumers; deleted per
   docs/superpowers/specs/2026-08-25-one-ui-build-design.md §2.1 (one light palette everywhere —
   the palette cliff between migrated and unmigrated tabs is what stalled the 2026-07 migration).
   Colour tokens inherit from :root above. Do not "restore" the dark palette.
   THIS BLOCK STAYS LAST: `:root` and `.wkh-admin` tie on specificity (0,1,0), so if a consumer
   puts the scope class on <html> itself, source order alone decides — declared last, the admin
   density wins that tie. */
.wkh-admin{
  --wkh-r-sm:6px; --wkh-r:12px;
  --wkh-btn-r:8px; --wkh-btn-h:auto;
  --wkh-t-lbl:10px; --wkh-t-body:12.5px; --wkh-t-h:15px; --wkh-t-num:20px;
  --wkh-t-display:22px; --wkh-t-title:20px;
}
/* @tokens-end */

/* ── Components. Every value below is a token; literals fail tools/ui-tokens-test.js. ── */
.wkh-card{
  background:var(--wkh-card); border:1px solid var(--wkh-line);
  border-radius:var(--wkh-r); padding:var(--wkh-s4); box-shadow:var(--wkh-shadow-1);
  color:var(--wkh-ink);
}
.wkh-sheet{
  background:var(--wkh-card); border-radius:var(--wkh-r);
  padding:var(--wkh-s5); box-shadow:var(--wkh-shadow-2); color:var(--wkh-ink);
}
.wkh-row{
  display:flex; align-items:center; gap:var(--wkh-s3);
  padding:var(--wkh-s3) 0; border-bottom:1px solid var(--wkh-line);
  font-size:var(--wkh-t-body); color:var(--wkh-ink);
}
.wkh-row:last-child{ border-bottom:0; }

.wkh-btn{
  font-family:inherit; font-size:var(--wkh-t-body); font-weight:var(--wkh-w-bold); cursor:pointer;
  border:1px solid transparent; border-radius:var(--wkh-r-sm);
  padding:var(--wkh-s2) var(--wkh-s4); transition:opacity var(--wkh-dur) ease;
}
.wkh-btn:disabled{ opacity:.55; cursor:not-allowed; }
.wkh-btn--primary{ background:var(--wkh-accent); color:var(--wkh-on-accent); }
/* secondary = the outlined "do this next" action (Yard's Fix-blocker button). It existed as
   a legacy Btn variant only; UI.Btn({ variant: "secondary" }) rendered a bare browser button
   until 20 Sep 2026. Ink outline so it reads as a button beside --primary without competing. */
.wkh-btn--secondary{ background:var(--wkh-card); color:var(--wkh-ink); border-color:var(--wkh-ink); }
.wkh-btn--ghost{ background:transparent; color:var(--wkh-ink); border-color:var(--wkh-line); }
.wkh-btn--danger{ background:var(--wkh-bad); color:var(--wkh-card); }

.wkh-chip{
  /* inline-flex + align-items:center, not inline-block: a chip carrying a UI.Icon (a 16px
     replaced element, .wkh-i below) next to 10px text has no shared baseline to align on —
     inline-block left it sitting on the text baseline, hanging high and inflating the chip's
     own line-height enough to stand taller than plain-text neighbours wrapping in the same row.
     A flex row centers both against each other regardless of their individual box heights. */
  display:inline-flex; align-items:center; border-radius:var(--wkh-pill);
  padding:var(--wkh-s1) var(--wkh-s3); font-size:var(--wkh-t-lbl);
  font-weight:var(--wkh-w-bold); white-space:nowrap; background:var(--wkh-panel); color:var(--wkh-ink-2);
}
.wkh-chip--ok{ background:var(--wkh-ok-bg); color:var(--wkh-ok); }
/* Money is ALWAYS tabular so figures align in columns (One UI spec §2.6). */
.wkh-money{ font-variant-numeric:tabular-nums; font-weight:var(--wkh-w-bold); color:var(--wkh-ink); }
.wkh-money--bad{ color:var(--wkh-bad); }
.wkh-money--ok{ color:var(--wkh-ok); }

/* Table — dense data inside its own overflow container; the page body never scrolls sideways. */
.wkh-scrollx{ overflow-x:auto; max-width:100%; }
.wkh-table{ width:100%; border-collapse:collapse; font-size:var(--wkh-t-body); color:var(--wkh-ink); }
.wkh-table th{
  position:sticky; top:0; background:var(--wkh-card); text-align:left;
  font-size:var(--wkh-t-lbl); font-weight:var(--wkh-w-bold); letter-spacing:.07em;
  text-transform:uppercase; color:var(--wkh-ink-2);
  padding:var(--wkh-s2) var(--wkh-s3); border-bottom:1px solid var(--wkh-line);
}
.wkh-table td{ padding:var(--wkh-s3); border-bottom:1px solid var(--wkh-line); }
.wkh-table tr:last-child td{ border-bottom:0; }
.wkh-table__num{ text-align:right; font-variant-numeric:tabular-nums; }

/* Modal — parent-controlled dialog. Payment, approval and document-review flows share this
   instead of each inventing their own overlay. */
.wkh-modal__overlay{
  position:fixed; inset:0; background:rgba(0,0,0,.4); z-index:200;
  display:flex; align-items:center; justify-content:center; padding:var(--wkh-s4);
}
.wkh-modal{
  background:var(--wkh-card); color:var(--wkh-ink); border-radius:var(--wkh-r);
  box-shadow:var(--wkh-shadow-2); width:100%; max-width:440px; max-height:90vh;
  display:flex; flex-direction:column;
}
.wkh-modal__head{ display:flex; align-items:center; gap:var(--wkh-s3); padding:var(--wkh-s4); border-bottom:1px solid var(--wkh-line); }
.wkh-modal__title{ flex:1; font-size:var(--wkh-t-h); font-weight:var(--wkh-w-bold); }
.wkh-modal__close{ background:none; border:0; cursor:pointer; color:var(--wkh-ink-2); padding:var(--wkh-s2); }
.wkh-modal__body{ padding:var(--wkh-s4); overflow-y:auto; }
.wkh-modal__foot{ display:flex; gap:var(--wkh-s2); justify-content:flex-end; padding:var(--wkh-s4); border-top:1px solid var(--wkh-line); }

/* Toast — the alert() replacement. Imperative (UI.toast), stacks bottom-centre. */
/* bottom clears the app tab bar (56px) plus the home indicator in the native shell —
   nothing may sit under the tab bar (brief §8). On the plain website both terms are 0/absent
   and the toast sits at its normal offset. */
.wkh-toast-host{ position:fixed; left:0; right:0; bottom:calc(var(--wkh-s5) + env(safe-area-inset-bottom)); display:flex; flex-direction:column; align-items:center; gap:var(--wkh-s2); z-index:300; pointer-events:none; padding:0 var(--wkh-s4); }
.wkh-toast{
  background:var(--wkh-chrome); color:var(--wkh-chrome-ink); border-radius:var(--wkh-r);
  padding:var(--wkh-s3) var(--wkh-s4); font-size:var(--wkh-t-body); font-weight:var(--wkh-w-med);
  box-shadow:var(--wkh-shadow-2); transition:opacity var(--wkh-dur) ease;
}
.wkh-toast--bad{ background:var(--wkh-bad); }
.wkh-toast--out{ opacity:0; }
@media (prefers-reduced-motion:reduce){ .wkh-toast{ transition:none; } }

/* Empty state — a new tenant's day one is made of these. */
.wkh-empty{
  display:flex; flex-direction:column; align-items:center; gap:var(--wkh-s2);
  text-align:center; padding:var(--wkh-s6) var(--wkh-s4);
  border:2px dashed var(--wkh-line); border-radius:var(--wkh-r); color:var(--wkh-ink-2);
}
.wkh-empty__title{ font-size:var(--wkh-t-h); font-weight:var(--wkh-w-bold); color:var(--wkh-ink); }
.wkh-empty__hint{ font-size:var(--wkh-t-body); max-width:32ch; }

/* Status timeline — journey, payment ladder, change-request history. */
.wkh-timeline{ display:flex; align-items:center; }
.wkh-timeline__node{
  width:var(--wkh-s5); height:var(--wkh-s5); border-radius:var(--wkh-pill); flex:0 0 auto;
  display:flex; align-items:center; justify-content:center;
  font-size:var(--wkh-t-lbl); font-weight:var(--wkh-w-heavy);
  border:2px solid var(--wkh-line); color:var(--wkh-ink-2); background:var(--wkh-card);
}
.wkh-timeline__node--done{ background:var(--wkh-ok); border-color:var(--wkh-ok); color:var(--wkh-card); }
.wkh-timeline__node--now{ background:var(--wkh-accent); border-color:var(--wkh-accent); color:var(--wkh-on-accent); }
.wkh-timeline__bar{ flex:1; height:var(--wkh-s1); background:var(--wkh-line); }
.wkh-timeline__bar--done{ background:var(--wkh-ok); }

/* Upload / capture tile — licence, proof-of-address, condition photos, damage evidence. */
.wkh-uptile{
  display:flex; flex-direction:column; align-items:center; justify-content:center; gap:var(--wkh-s1);
  min-height:calc(var(--wkh-s6) * 2 + var(--wkh-s3));
  border:2px dashed var(--wkh-line); border-radius:var(--wkh-r);
  background:none; cursor:pointer; color:var(--wkh-ink-2);
  font-family:inherit; font-size:var(--wkh-t-lbl); font-weight:var(--wkh-w-bold);
}
.wkh-uptile--done{ border-style:solid; border-color:var(--wkh-ok-bg); background:var(--wkh-ok-bg); color:var(--wkh-ok); }
.wkh-chip--warn{ background:var(--wkh-warn-bg); color:var(--wkh-warn); }
.wkh-chip--bad{ background:var(--wkh-bad-bg); color:var(--wkh-bad); }
/* .wkh-i's default size (--wkh-s4, 16px) is sized for standalone chrome (nav, tab headers) —
   next to a chip's own 10px text it reads oversized and forces the chip noticeably taller than
   an icon-less neighbour wrapping in the same row. Down to --wkh-t-lbl (10px, the chip's own
   font-size token) so an icon-bearing chip sits close to the same height as a plain-text one. */
.wkh-chip .wkh-i{ width:var(--wkh-t-lbl); height:var(--wkh-t-lbl); }

.wkh-pill{
  font-family:inherit; cursor:pointer; background:var(--wkh-card);
  border:1px solid var(--wkh-line); color:var(--wkh-ink-2);
  border-radius:var(--wkh-pill); padding:var(--wkh-s2) var(--wkh-s3); font-size:var(--wkh-t-body);
}
.wkh-pill[aria-pressed="true"]{
  background:var(--wkh-chrome); border-color:var(--wkh-chrome); color:var(--wkh-accent-chrome);
}

.wkh-kpi{
  display:flex; flex-direction:column; gap:var(--wkh-s1);
  background:var(--wkh-card); border:1px solid var(--wkh-line);
  border-radius:var(--wkh-r); padding:var(--wkh-s3); box-shadow:var(--wkh-shadow-1);
  min-width:0;
}
.wkh-kpi__label{
  font-size:var(--wkh-t-lbl); text-transform:uppercase; letter-spacing:.1em;
  font-weight:var(--wkh-w-bold); color:var(--wkh-ink-2);
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.wkh-kpi__value{
  font-size:var(--wkh-t-num); font-weight:var(--wkh-w-heavy); color:var(--wkh-ink);
  font-variant-numeric:tabular-nums;
}
.wkh-kpi__detail{ font-size:var(--wkh-t-lbl); font-weight:var(--wkh-w-med); color:var(--wkh-ink-2); }
.wkh-kpi__detail--ok{ color:var(--wkh-ok); }
.wkh-kpi__detail--warn{ color:var(--wkh-warn); }
.wkh-kpi__detail--bad{ color:var(--wkh-bad); }

.wkh-field{ display:flex; flex-direction:column; gap:var(--wkh-s1); }
.wkh-field__label{ font-size:var(--wkh-t-lbl); font-weight:var(--wkh-w-bold); color:var(--wkh-ink-2); }
.wkh-field__hint{ font-size:var(--wkh-t-lbl); color:var(--wkh-ink-2); }
.wkh-field__error{ font-size:var(--wkh-t-lbl); font-weight:var(--wkh-w-bold); color:var(--wkh-bad); }
.wkh-field--error input, .wkh-field--error textarea, .wkh-field--error select{
  border-color:var(--wkh-bad);
}

.wkh-tabhead{ display:flex; flex-direction:column; align-items:stretch; gap:var(--wkh-s3); }
.wkh-tabhead__title{ margin:0; font-size:var(--wkh-t-num); font-weight:var(--wkh-w-heavy); color:var(--wkh-ink); }
.wkh-tabhead__sub{ font-size:var(--wkh-t-body); color:var(--wkh-ink-2); }
.wkh-tabhead__row{ display:flex; align-items:center; gap:var(--wkh-s3); flex-wrap:wrap; }
.wkh-tabhead__main{ display:flex; align-items:center; gap:var(--wkh-s2); min-width:0; }
/* Deliberately the raw accent, not --wkh-accent-ink: this class IS consumed (lib/ui.js
   TabHeader, every admin tab via app.js tabIcon), so swapping tokens here is a real pixel
   change (gold -> dark amber on every tab-header icon), not the no-op it was believed to be
   when tried in 445b5aa. Reverted 2026-09-02; a contrast fix here needs its own explicit
   design decision, not a drive-by token rename. */
.wkh-tabhead__icon{ color:var(--wkh-accent); flex:0 0 auto; }
.wkh-tabhead__brief{
  /* .wkh-tabhead's own gap (above) now provides the space above this element, so this
     margin stays 0 rather than doubling up on it. */
  margin:0; font-size:var(--wkh-t-body); color:var(--wkh-ink-2);
  /* max-width and line-height are deliberate single-use raw values, not tokens — a token
     with exactly one caller is indirection without benefit (the owner's ruling on
     single-use font-weights in the previous sub-project applies the same way here).
     tools/ui-tokens-test.js does not scan `ch` units or unitless numbers, so their
     absence from the guard is a known, accepted gap — not an oversight. */
  max-width:70ch; line-height:1.5;
}
.wkh-tabhead__cta{ margin-left:auto; }

.wkh-i{ width:var(--wkh-s4); height:var(--wkh-s4); flex:0 0 auto; }

:where(.wkh-btn, .wkh-pill):focus-visible{
  outline:2px solid var(--wkh-focus); outline-offset:2px;
}
@media (prefers-reduced-motion:reduce){
  .wkh-btn{ transition:none; }
}

/* ── Admin navigation. Hub bar + sub-nav when the user can open 2+ hubs; flat strip otherwise.
   See lib/admin-nav.js for why: the default staff set is three tabs all inside one hub, where a
   hub bar would be pure overhead. ── */
/* ── Admin shell: grouped side rail + content pane (One UI spec §2.4, Chris's call 25 Aug —
   echoing the pre-Jul-2026 rail). ≥1200 the full 216px rail; 700–1199 a 64px icon strip
   ON TOUCH DEVICES ONLY (pointer:coarse) — a mouse-driven desktop window of any width ≥700
   keeps the full rail with its labels (Chris, 8 Sep 2026: "on the desktop version I want
   the names next to the tabs"); <700 an overlay drawer opened by the fixed menu button. Slate chrome tokens throughout;
   the content pane stays on the light panel. navMode() still decides furniture: flat mode
   renders the items with NO group labels, so a cleaner with one hub never sees hub chrome. ── */
.wkh-shell{ display:flex; align-items:stretch; }
.wkh-shell__main{ flex:1; min-width:0; }
/* The content pane needs a gutter, or it butts straight against the rail. The rail is a
   solid slate block, so with no padding every tab heading, brief and card edge in the
   console sits flush against it — found 28 Aug 2026 while screenshotting the admin for
   the marketing site. Scoped to >=700 deliberately: below that the rail is an overlay
   drawer with nothing adjacent to it, and 24px a side would narrow a 375px screen for
   no reason. The max-width:699 rule further down sets only padding-bottom, so the two
   never fight. */
@media (min-width:700px){
  .wkh-shell__main{ padding-inline:var(--wkh-s5); }
}
.wkh-sidenav{
  background:var(--wkh-chrome); color:var(--wkh-chrome-ink-2);
  display:flex; flex-direction:column; padding:var(--wkh-s4) 0 var(--wkh-s3);
  flex:0 0 216px; position:sticky; top:0; align-self:flex-start;
  /* border-box, or the padding is added ON TOP of the 100vh and the rail overhangs the
     viewport by 28px — the last item then sits below the fold with no way to reach it. */
  box-sizing:border-box; height:100vh; overflow-y:auto; z-index:90;
}
.wkh-sidenav__brand{
  display:flex; align-items:center; gap:var(--wkh-s2); padding:0 var(--wkh-s4) var(--wkh-s3);
  color:var(--wkh-chrome-ink); font-weight:var(--wkh-w-heavy); font-size:var(--wkh-t-h);
}
.wkh-sidenav__group{
  font-size:var(--wkh-t-lbl); font-weight:var(--wkh-w-bold); letter-spacing:.09em;
  text-transform:uppercase; color:var(--wkh-chrome-ink-2); padding:var(--wkh-s4) var(--wkh-s4) var(--wkh-s1);
}
.wkh-sidenav__item{
  /* width:auto, NOT calc(100% - s4): as a column flex child it already stretches to the rail
     minus its own margins, so the two are identical here — but the percentage poisons the
     phone drawer's max-content sizing (a % width is treated as auto while the container's
     intrinsic width is being measured, and engines disagree on whether the margins are added
     back), which is what decides the drawer's width below 700px. */
  display:flex; align-items:center; gap:var(--wkh-s2); width:auto;
  margin:0 var(--wkh-s2); padding:var(--wkh-s2) var(--wkh-s3); border:0; border-radius:var(--wkh-r-sm);
  background:none; cursor:pointer; text-align:left; font-family:inherit;
  font-size:var(--wkh-t-body); font-weight:var(--wkh-w-med); color:var(--wkh-chrome-ink-2);
}
.wkh-sidenav__item:hover{ color:var(--wkh-chrome-ink); }
.wkh-sidenav__item[aria-current]{ background:var(--wkh-chrome-2); color:var(--wkh-accent-chrome); }
.wkh-sidenav__item:focus-visible{ outline:2px solid var(--wkh-focus); outline-offset:-2px; }
.wkh-sidenav__label{ flex:1; }
.wkh-sidenav__key{
  font-size:var(--wkh-t-lbl); border:1px solid var(--wkh-chrome-2); border-radius:var(--wkh-r-sm);
  padding:0 var(--wkh-s1); white-space:nowrap;
}
.wkh-sidenav__menubtn{ display:none; }
.wkh-sidenav__scrim{ display:none; }
@media (min-width:700px) and (max-width:1199px) and (pointer:coarse){
  /* Tablet: icon strip. Labels and group headings hide; the icon keeps the item legible via
     its title attribute, and the badge stays (a count with no label still reads).
     pointer:coarse gates this to TOUCH tablets. Before 8 Sep 2026 it was width-only, so a
     desktop browser that was not full-screen (anything under 1200px — a laptop with the
     window shared, a PWA window, devtools open) also lost every label, and a mouse user has
     no finger-sized reason to give the content pane those 152px back. The width tiers are
     untouched (tools/ui-tokens-test.js pins them); this only adds the input-type gate. */
  .wkh-sidenav{ flex-basis:64px; }
  .wkh-sidenav .wkh-sidenav__label, .wkh-sidenav .wkh-sidenav__group,
  .wkh-sidenav .wkh-sidenav__brandname, .wkh-sidenav .wkh-sidenav__key{ display:none; }
  .wkh-sidenav__item{ justify-content:center; }
  .wkh-sidenav__brand{ justify-content:center; padding:0 0 var(--wkh-s3); }
}
@media (max-width:699px){
  .wkh-shell{ display:block; }
  .wkh-sidenav{
    position:fixed; top:0; left:0; z-index:150;
    /* `bottom:0; height:auto` was over-constrained, and CSS resolves that by DROPPING
       `bottom`: the used height came from the content, so the drawer laid out 1065px tall
       inside an 812px viewport (measured 2026-08-29 at 375x812). scrollHeight then equalled
       clientHeight, the base rule's overflow-y:auto had nothing to scroll, and every item
       from Comms down was simply unreachable on a phone. A DEFINITE height is what makes
       the overflow scrollable. dvh tracks Safari's collapsing toolbars so the drawer never
       runs under the address bar; the vh line above it is the fallback. */
    height:100vh; height:100dvh;
    /* Only as wide as the longest label. 78% was a 292px slab on a 375px screen, most of it
       empty slate. max-content measures the widest row — the brand name, or the longest tab
       label ("Manager's specials") — and the cap stops a long tenant-specific label (Noun-driven, so
       it varies per site) from swallowing the screen. No min-width: the brand row is always
       rendered and is the widest fixed row, so it already floors a short staff-only menu —
       and a literal floor here would be an off-scale px (tools/ui-tokens-test.js). */
    width:max-content; max-width:80vw;
    /* The last item has to clear the home indicator once the list is scrolled to the end. */
    padding-bottom:calc(var(--wkh-s3) + env(safe-area-inset-bottom));
    transform:translateX(-102%); transition:transform var(--wkh-dur) ease;
  }
  .wkh-sidenav--open{ transform:none; box-shadow:var(--wkh-shadow-2); }
  .wkh-sidenav__scrim{ display:block; position:fixed; inset:0; background:rgba(0,0,0,.4); z-index:145; }
  .wkh-sidenav__menubtn{
    display:inline-flex; align-items:center; justify-content:center;
    /* Native shell: the home indicator eats the bottom edge (brief §8). */
    position:fixed; bottom:calc(var(--wkh-s4) + env(safe-area-inset-bottom)); left:var(--wkh-s4); z-index:140;
    width:44px; height:44px; border:0; border-radius:var(--wkh-pill); cursor:pointer;
    background:var(--wkh-chrome); color:var(--wkh-chrome-ink); box-shadow:var(--wkh-shadow-2);
  }
  /* …and the content has to end ABOVE it. The button is fixed, so without this the last
     rows of any list scroll underneath it — measured on the demo at 375, where it sat on
     top of a filter chip on the Bookings tab (2026-08-27). 44px of button, plus its own
     bottom offset, plus the same gap again so the last row is not flush against it. */
  .wkh-shell__main{
    padding-bottom:calc(44px + (var(--wkh-s4) * 2) + env(safe-area-inset-bottom));
  }
}
@media (prefers-reduced-motion:reduce){ .wkh-sidenav{ transition:none; } }
/* Kept: the rail items reuse this count badge. */
.wkh-hub__badge{
  background:var(--wkh-bad); color:var(--wkh-card); border-radius:var(--wkh-pill);
  font-size:var(--wkh-t-lbl); font-weight:var(--wkh-w-heavy); padding:0 var(--wkh-s2);
  min-width:var(--wkh-s4); text-align:center;
}

/* ── Customer portal: "Things to do". The first thing a signed-in customer sees on My Booking.
   Rows are derived by lib/portal-todo.js — this file only decides how the three urgencies look.
   Urgency is carried by [data-urgency] rather than a modifier class so the row's appearance can
   never disagree with the data that produced it. ── */
.wkh-todo{ display:flex; flex-direction:column; gap:var(--wkh-s3); }
.wkh-todo__head{ display:flex; align-items:center; gap:var(--wkh-s2); }
.wkh-todo__title{
  margin:0; font-size:var(--wkh-t-h); font-weight:var(--wkh-w-heavy); color:var(--wkh-ink);
}
.wkh-todo__count{
  margin-left:auto; font-size:var(--wkh-t-lbl); font-weight:var(--wkh-w-bold);
  color:var(--wkh-ink-2); white-space:nowrap;
}
.wkh-todo__list{ display:flex; flex-direction:column; gap:var(--wkh-s2); }
/* A whole row is the target, not a small link inside it — this is read one-handed on a phone. */
.wkh-todo__item{
  display:flex; align-items:flex-start; gap:var(--wkh-s3); width:100%; text-align:left;
  background:var(--wkh-panel); border:1px solid var(--wkh-line); border-left-width:2px;
  border-radius:var(--wkh-r-sm); padding:var(--wkh-s3); font-family:inherit; cursor:pointer;
  color:var(--wkh-ink); transition:border-color var(--wkh-dur) ease;
}
.wkh-todo__item[disabled]{ cursor:default; opacity:.7; }
.wkh-todo__item[data-urgency="now"]{ border-left-color:var(--wkh-bad); }
.wkh-todo__item[data-urgency="soon"]{ border-left-color:var(--wkh-warn); }
.wkh-todo__item[data-urgency="waiting"]{ border-left-color:var(--wkh-line); }
.wkh-todo__icon{ margin-top:var(--wkh-s1); color:var(--wkh-ink-2); }
.wkh-todo__item[data-urgency="now"] .wkh-todo__icon{ color:var(--wkh-bad); }
.wkh-todo__item[data-urgency="soon"] .wkh-todo__icon{ color:var(--wkh-warn); }
.wkh-todo__body{ display:flex; flex-direction:column; gap:var(--wkh-s1); min-width:0; flex:1; }
.wkh-todo__label{
  font-size:var(--wkh-t-body); font-weight:var(--wkh-w-bold); color:var(--wkh-ink);
}
.wkh-todo__detail{ font-size:var(--wkh-t-lbl); color:var(--wkh-ink-2); line-height:1.5; }
.wkh-todo__go{ align-self:center; color:var(--wkh-ink-2); }
/* All-clear. A box that silently vanishes leaves people wondering what they missed. */
.wkh-todo__clear{
  display:flex; align-items:center; gap:var(--wkh-s3);
  background:var(--wkh-ok-bg); border:1px solid var(--wkh-line); border-radius:var(--wkh-r-sm);
  padding:var(--wkh-s3); color:var(--wkh-ok);
  font-size:var(--wkh-t-body); font-weight:var(--wkh-w-bold);
}
.wkh-todo__clear .wkh-i{ color:var(--wkh-ok); }

/* The Hire Journey, collapsed. It still earns its place for a first-time hirer — "everything from
   booking to collection, nothing to remember" — but it must not outrank the actions above it. */
.wkh-journey__toggle{
  display:flex; align-items:center; gap:var(--wkh-s2); width:100%; text-align:left;
  background:transparent; border:0; padding:var(--wkh-s2) 0; font-family:inherit; cursor:pointer;
  font-size:var(--wkh-t-body); font-weight:var(--wkh-w-bold); color:var(--wkh-ink-2);
}
.wkh-journey__step{ margin-left:auto; font-weight:var(--wkh-w-med); }
.wkh-journey__chev{ transition:transform var(--wkh-dur) ease; }
.wkh-journey__toggle[aria-expanded="true"] .wkh-journey__chev{ transform:rotate(90deg); }

:where(.wkh-todo__item, .wkh-journey__toggle):focus-visible{
  outline:2px solid var(--wkh-focus); outline-offset:2px;
}
@media (prefers-reduced-motion:reduce){
  .wkh-todo__item, .wkh-journey__chev{ transition:none; }
}

/* ── Responsive. THE ONLY breakpoints in the codebase: phone <700, tablet 700-1199, desktop >=1200.
   Media queries for page layout; container queries where a component must respond to its own
   column rather than the viewport (a booking row in a 300px sidebar needs the phone layout even
   on a desktop). Give any element .wkh-cq to make it a query container. ── */
.wkh-cq{ container-type:inline-size; }

.wkh-stack{ display:flex; flex-direction:column; gap:var(--wkh-s4); min-width:0; }
.wkh-grid{ display:grid; gap:var(--wkh-s3); grid-template-columns:1fr; min-width:0; }
.wkh-scroll-x{ overflow-x:auto; -webkit-overflow-scrolling:touch; }

.wkh-only-phone{ display:none; }
.wkh-only-desk{ display:none; }

@media (max-width:699px){
  .wkh-only-phone{ display:revert; }
  .wkh-row{ flex-wrap:wrap; }
  /* The ⌘K key-cap hides on phone (no keyboard to advertise); the Search rail item itself
     stays, so the palette is still reachable by touch. */
  .wkh-sidenav__key{ display:none; }
}
@media (min-width:700px){
  .wkh-grid{ grid-template-columns:repeat(2, minmax(0, 1fr)); }
}
@media (min-width:1200px){
  .wkh-grid{ grid-template-columns:repeat(3, minmax(0, 1fr)); }
  .wkh-only-desk{ display:revert; }
}

/* Container-query equivalents, for components inside a narrow column. */
@container (max-width:699px){
  .wkh-grid{ grid-template-columns:1fr; }
  .wkh-row{ flex-wrap:wrap; }
}
@container (min-width:700px) and (max-width:1199px){
  .wkh-grid{ grid-template-columns:repeat(2, minmax(0, 1fr)); }
}
@container (min-width:1200px){
  .wkh-grid{ grid-template-columns:repeat(3, minmax(0, 1fr)); }
}

/* OUT pack — six-step shell over the live staff handover (18 Sep 2026). */
.wkh-outpack{ display:flex; flex-direction:column; min-height:100dvh; }
.wkh-outpack__rail{ position:sticky; top:0; z-index:20; background:var(--wkh-card); border-bottom:1px solid var(--wkh-line); padding:var(--wkh-s2) 0; }
.wkh-outpack__steps{ display:flex; gap:var(--wkh-s1); overflow-x:auto; -webkit-overflow-scrolling:touch; padding:0 0 var(--wkh-s1); }
.wkh-outpack__step{ flex:1 0 auto; min-width:0; border:none; background:none; padding:var(--wkh-s1); cursor:pointer; font-family:inherit; text-align:center; }
.wkh-outpack__step:disabled{ cursor:default; opacity:.45; }
.wkh-outpack__n{ display:inline-flex; width:var(--wkh-s5); height:var(--wkh-s5); border-radius:var(--wkh-pill); align-items:center; justify-content:center; font-size:var(--wkh-t-lbl); font-weight:var(--wkh-w-heavy); background:var(--wkh-panel); color:var(--wkh-ink-2); }
.wkh-outpack__step.is-done .wkh-outpack__n{ background:var(--wkh-ok-bg); color:var(--wkh-ok); }
.wkh-outpack__step.is-on .wkh-outpack__n{ background:var(--wkh-ink); color:var(--wkh-accent-chrome); }
.wkh-outpack__lab{ display:block; font-size:var(--wkh-t-lbl); font-weight:var(--wkh-w-bold); color:var(--wkh-ink-2); margin-top:var(--wkh-s1); white-space:nowrap; }
.wkh-outpack__step.is-on .wkh-outpack__lab{ color:var(--wkh-ink); }
.wkh-outpack__body{ flex:1; padding:var(--wkh-s3) 0 var(--wkh-s6); }
.wkh-outpack__foot{ position:sticky; bottom:0; z-index:20; background:var(--wkh-card); border-top:1px solid var(--wkh-line); padding:var(--wkh-s2) 0 calc(var(--wkh-s2) + env(safe-area-inset-bottom, 0)); display:flex; gap:var(--wkh-s2); align-items:stretch; }
.wkh-outpack__foot .wkh-outpack__why{ flex:1; font-size:var(--wkh-t-body); color:var(--wkh-ink-2); line-height:1.4; display:flex; align-items:center; }
.wkh-outpack__chips{ display:flex; flex-wrap:wrap; gap:var(--wkh-s2); }
.wkh-outpack__chip{ border:1px solid var(--wkh-line); background:var(--wkh-card); border-radius:var(--wkh-pill); padding:var(--wkh-s2) var(--wkh-s3); font-size:var(--wkh-t-body); font-weight:var(--wkh-w-bold); font-family:inherit; cursor:pointer; color:var(--wkh-ink); }
.wkh-outpack__chip.is-on{ border-color:var(--wkh-ink); background:var(--wkh-ink); color:var(--wkh-chrome-ink); }
.wkh-outpack__fail{ border-color:var(--wkh-bad) !important; background:var(--wkh-bad-bg) !important; }

/* Staff Sales tab (van sales stage 1) */
.sale-board{display:grid;grid-template-columns:repeat(6,minmax(11rem,1fr));gap:var(--wkh-s3);overflow-x:auto}
.sale-col h3{font-size:var(--wkh-t-lbl);margin:var(--wkh-s2) 0}
.sale-card{display:flex;flex-direction:column;gap:var(--wkh-s1);text-align:left;width:100%;margin-bottom:var(--wkh-s2);cursor:pointer}
.sale-card img{width:100%;aspect-ratio:4/3;object-fit:cover;border-radius:var(--wkh-r-sm)}
.sale-card .warn,.sale-van .warn{color:var(--wkh-warn)}
.sale-card .muted,.sale-van .muted{color:var(--wkh-ink-2);font-size:var(--wkh-t-lbl)}
.sale-bar{display:flex;gap:var(--wkh-s3);align-items:center;flex-wrap:wrap;margin:var(--wkh-s2) 0}
.sale-sec{padding:var(--wkh-s4);margin:var(--wkh-s4) 0}
.sale-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(14rem,1fr));gap:var(--wkh-s2) var(--wkh-s3)}
.sale-f{display:flex;flex-direction:column;gap:var(--wkh-s1);font-size:var(--wkh-t-lbl);margin:var(--wkh-s1) 0}
.sale-photos{display:flex;gap:var(--wkh-s2);flex-wrap:wrap;margin-top:var(--wkh-s2)}
.sale-photo img{width:8rem;aspect-ratio:4/3;object-fit:cover;border-radius:var(--wkh-r-sm);display:block}
.sale-act{padding:var(--wkh-s2) 0;border-top:1px solid var(--wkh-line)}
.sale-line{display:flex;justify-content:space-between;gap:var(--wkh-s3);padding:var(--wkh-s1) 0}
.sale-line.strong{font-weight:var(--wkh-w-bold);border-top:1px solid var(--wkh-line);margin-top:var(--wkh-s1);padding-top:var(--wkh-s2)}
.sale-staff{background:var(--wkh-panel);border-radius:var(--wkh-r-sm);padding:var(--wkh-s2) var(--wkh-s3);margin:var(--wkh-s2) 0}
@media (max-width:700px){.sale-board{grid-template-columns:1fr}}
