/* 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;
  --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;
  --wkh-r-sm:8px; --wkh-r:12px;
  --wkh-t-lbl:10px; --wkh-t-body:13.5px; --wkh-t-h:15px; --wkh-t-num: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 — dark operator chrome. Verified contrast (WCAG 2.x, real arithmetic — see
   .superpowers/sdd/2026-07-30-ui-refresh-foundation/final-fixes-report.md): accent 11.33:1,
   ok 8.22:1, warn 8.66:1, bad 5.58:1, focus ring 9.44:1 (all clear AA; focus clears the 3:1
   non-text minimum by a wide margin).
   THIS BLOCK MUST STAY LAST of the three. `:root` and `.wkh-admin` have equal specificity
   (0,1,0), so if a consumer ever puts the scope class on <html> itself — a common theming
   pattern — both rules match the same element and source order alone decides the winner.
   Declared last, admin wins that tie. Declared first, every admin component would silently
   render in the customer palette, with no test able to catch it. */
.wkh-admin{
  --wkh-chrome:#191D24; --wkh-chrome-2:#101317;
  --wkh-chrome-ink:#DDE3EA; --wkh-chrome-ink-2:#79828E;
  --wkh-accent:#FFC61A; --wkh-on-accent:#14181D;
  /* --wkh-accent IS safe as text here (9.44:1 on --wkh-card) — unlike the light scope, where
     accent-as-text fails AA and --wkh-accent-ink exists instead. Defined here too so a shared
     component that reaches for --wkh-accent-ink resolves on both scopes, not just customer. */
  --wkh-accent-ink:#FFC61A;
  --wkh-panel:#1A1F26; --wkh-card:#212832;
  --wkh-ink:#E7ECF2; --wkh-ink-2:#8D97A3; --wkh-line:#2E3742;
  --wkh-ok:#3DD68C; --wkh-warn:#FFB020; --wkh-bad:#FF5A5A;
  --wkh-ok-bg:#12291F; --wkh-warn-bg:#2A2113; --wkh-bad-bg:#2B1618;
  --wkh-r-sm:6px; --wkh-r:8px;
  --wkh-t-lbl:10px; --wkh-t-body:12.5px; --wkh-t-h:15px; --wkh-t-num:20px;
  --wkh-w-med:600; --wkh-w-bold:700; --wkh-w-heavy:800;
  --wkh-shadow-1:none; --wkh-shadow-2:none;
  /* Focus ring MUST be a foreground-safe token, never --wkh-accent used blind — see .wkh-cust. */
  --wkh-focus:#FFC61A;
}
/* @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); }
.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); }
.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);
}

.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; }
.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. ── */
/* width:100% is a safety net, not decoration — .wkh-nav is meant to be the full-width top bar in
   AdminDashboard's column layout (app.js, the "wkh-admin" wrapper), but a flex/grid child sizes to
   max-content by default. Without this, a future container that regresses back to a row (as this
   one briefly did) collapses the sibling content pane to 0 rather than just losing the intended
   full-bleed look — this rule makes that failure mode a visual nit instead of a blank screen. */
/* Sticky so the nav stays reachable while a long list (e.g. Bookings, hundreds of rows) scrolls
   underneath it — restores the behaviour the old AdminRail had (position:sticky; top:12) before the
   rail-to-top-bar refactor. z-index is one below #wkh-header's 100 (app.js) so the two never fight
   for a layer: the header is position:relative and scrolls away normally above this bar, it is not
   itself sticky/fixed, so this is a safety margin rather than an observed conflict. */
.wkh-nav{ background:var(--wkh-chrome); border-bottom:1px solid var(--wkh-line); width:100%; position:sticky; top:0; z-index:90; }
.wkh-nav__hubs, .wkh-nav__flat{
  display:flex; gap:var(--wkh-s1); padding:0 var(--wkh-s4);
  overflow-x:auto; -webkit-overflow-scrolling:touch;
}
.wkh-hub{
  position:relative; background:transparent; border:0; border-bottom:2px solid transparent;
  color:var(--wkh-chrome-ink-2); padding:var(--wkh-s3) var(--wkh-s4);
  font-size:var(--wkh-t-body); font-weight:var(--wkh-w-bold); white-space:nowrap;
  display:inline-flex; align-items:center; gap:var(--wkh-s2); font-family:inherit; cursor:pointer;
}
.wkh-hub[aria-current]{ color:var(--wkh-chrome-ink); border-bottom-color:var(--wkh-accent); }
.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;
}
.wkh-nav__sub{
  display:flex; gap:var(--wkh-s2); flex-wrap:wrap;
  padding:var(--wkh-s3) var(--wkh-s4); background:var(--wkh-panel); border-top:1px solid var(--wkh-line);
}
.wkh-subl{
  background:transparent; border:1px solid transparent; color:var(--wkh-ink-2);
  border-radius:var(--wkh-pill); padding:var(--wkh-s1) var(--wkh-s3);
  font-size:var(--wkh-t-body); font-weight:var(--wkh-w-med); font-family:inherit; cursor:pointer;
  display:inline-flex; align-items:center; gap:var(--wkh-s2); white-space:nowrap;
}
.wkh-subl[aria-current]{
  background:var(--wkh-accent); color:var(--wkh-on-accent); border-color:var(--wkh-accent);
  font-weight:var(--wkh-w-bold);
}
/* margin-left:auto only right-aligns a flex child if its parent doesn't itself scroll — but
   .wkh-nav__hubs and .wkh-nav__flat are both overflow-x:auto (needed so a wide hub set or a long
   flat tab list doesn't break the layout). With a plain margin-left:auto, a hub set wide enough to
   overflow pushes this button to the END of the scrollable content instead of the visible edge —
   found by measuring scrollWidth vs clientWidth at 375px with all 6 hubs, not by inspection.
   position:sticky pins it to the actual viewport edge at any scroll offset, including the
   never-scrolled initial position. The opaque background is required, not decorative — sticky
   keeps the element in flow, so whatever hub/tab sits at that flex position scrolls underneath it;
   without a solid background matching the bar, that label would show through. */
.wkh-nav__kbd{
  margin-left:auto; align-self:center; display:inline-flex; align-items:center; gap:var(--wkh-s2);
  position:sticky; right:var(--wkh-s2);
  /* Border uses --wkh-chrome-2 (the dark bar's own darker shade), not --wkh-line — in the light
     scope now in force --wkh-line is pale (for borders on light cards) while --wkh-chrome stays
     dark, so pairing them here drew a bright box around this button on the dark nav bar. */
  background:var(--wkh-chrome); border:1px solid var(--wkh-chrome-2); border-radius:var(--wkh-r-sm);
  color:var(--wkh-chrome-ink-2); padding:var(--wkh-s1) var(--wkh-s3);
  font-size:var(--wkh-t-lbl); font-family:inherit; cursor:pointer; white-space:nowrap;
}
:where(.wkh-hub, .wkh-subl, .wkh-nav__kbd):focus-visible{
  outline:2px solid var(--wkh-focus); outline-offset:-2px;
}

/* ── 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; }
  /* .wkh-nav__sub wraps by default, which is fine at tablet/desktop width where at most two rows
     ever result. On phone the widest hub (Operations, 8 tabs) wraps to 4 rows — about 195px of
     chrome sitting above every admin screen before any content is visible. Scrolling it
     horizontally instead, matching .wkh-nav__hubs, keeps it to one ~42px row. This is a deliberate
     choice, not the default carried over: a phone screen is short enough that four rows of nav
     crowds out content on the very screens most likely to be checked one-handed mid-task. */
  .wkh-nav__sub{ flex-wrap:nowrap; overflow-x:auto; -webkit-overflow-scrolling:touch; }
  /* The ⌘K affordance is hidden on phone, for two reasons that compound.
     It is `position:sticky` with an opaque background so it stays at the visible right edge
     while the hub strip scrolls underneath — which on a 375px screen means it permanently sits
     ON TOP of whichever hubs are under that edge. Measured with all six hubs: it hid 47% of
     "Money" and 32% of "Comms", and `document.elementFromPoint` at the right-hand end of
     "Money" returned the ⌘K button — so a tap that looks like a hub opens the command palette
     instead. Widening the bar or unsticking it only moves which hub gets covered.
     And it advertises a KEYBOARD shortcut on a device with no keyboard, so nothing is lost:
     AdminPalette is still reachable by ⌘K / Ctrl-K wherever a keyboard exists. */
  .wkh-nav__kbd{ 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)); }
}
