/* ============================================================
   Snabbservice — main.css
   Single stylesheet. No framework, no build step. Sections are
   numbered; search for "── N." to jump.

   Visual language: white dominates, pink attracts, green
   reassures — roughly 90/5/5. Soft hairline borders, small
   diffuse shadows, generous whitespace, rounded corners.
   Values and rationale: docs/technical/design-system.md

   Mobile-first throughout. That is not cosmetic: providers tap
   On the way / Arrived / Started / Completed from a phone while
   standing outside a customer's door, so those controls are sized
   for thumbs first and widened for desktop after.
   ============================================================ */

/* ── 1. THEME (set before body renders via <head> JS) ─────── */
/*
   ⚠ CONTRAST — do not "simplify" these back to the brief's two pinks.
   #f43f7e with white text is 3.59:1 and FAILS WCAG AA at button sizes
   (16px/600 is NOT "large text" — that needs >=18.66px bold or >=24px).
   So #f43f7e is the DECORATIVE pink (borders, dots, underlines, icon
   strokes) and never carries small text, while --primary-fill #e11d68
   (4.59:1) is the button/badge background. Same split for green.
   Full table: docs/technical/design-system.md §3.3

   Suffixes are roles, not lightness, because lightness inverts between
   themes and roles don't:
     (none)      decorative — borders, dots, icon strokes
     -fill       a background carrying WHITE text
     -ink        coloured TEXT on a light surface
     -soft       a tinted background
     -soft-ink   text sitting on -soft
*/
:root { color-scheme: light dark; }

[data-theme="light"] {
  /* surfaces — white is the page AND the card */
  --bg:               #ffffff;
  --surface:          #ffffff;
  --bg2:              #fdf7f9;   /* the one tinted band / hover wash */
  --bg3:              #fdeef4;   /* deeper wash, avatar placeholder */

  /* text */
  --heading:          #1f2937;
  --text:             #4b5563;
  --text-m:           #6b7280;
  --text-d:           #9ca3af;

  /* lines */
  --border:           #f1dce5;
  --border-s:         #f7ebf0;
  --border-str:       #e7c9d6;

  /* pink */
  --primary:          #f43f7e;
  --primary-fill:     #e11d68;
  --primary-fill-h:   #c51a5c;
  --primary-on:       #ffffff;
  --primary-ink:      #c51a5c;
  --primary-soft:     #fde7ef;
  --primary-soft-ink: #9d174d;

  /* status */
  --green:            #22c55e;
  --green-fill:       #166534;
  --green-ink:        #15803d;
  --green-soft:       #dcfce7;
  --red:              #dc2626;
  --red-soft:         #fee2e2;
  --warn:             #b45309;
  --warn-line:        #f59e0b;
  --warn-soft:        #fef3c7;
  --blue:             #2563eb;
  --blue-soft:        #dbeafe;
  --star:             #f59e0b;

  --focus:            #9d174d;
  --scrim:            rgba(31,41,55,.38);
  --shadow:           0 2px 10px rgba(31,41,55,.05);
}

[data-theme="dark"] {
  /* warm plum-charcoal, never neutral grey — the same brand at night */
  --bg:               #171114;
  --surface:          #201820;
  --bg2:              #241b23;
  --bg3:              #2e2430;

  --heading:          #fce7f3;
  --text:             #ddd0d7;
  --text-m:           #b09fa9;
  --text-d:           #8a7883;

  --border:           #3a2c36;
  --border-s:         #2b212a;
  --border-str:       #4a3843;

  --primary:          #fb7185;
  --primary-fill:     #e11d68;
  --primary-fill-h:   #f43f7e;
  --primary-on:       #ffffff;
  --primary-ink:      #fda4c0;
  --primary-soft:     #3b1e2b;
  --primary-soft-ink: #fbcfe8;

  --green:            #4ade80;
  --green-fill:       #15803d;
  --green-ink:        #86efac;
  --green-soft:       #17281d;
  --red:              #f87171;
  --red-soft:         #2d1a1a;
  --warn:             #fcd34d;
  --warn-line:        #f59e0b;
  --warn-soft:        #2b2114;
  --blue:             #60a5fa;
  --blue-soft:        #172138;
  --star:             #fbbf24;

  --focus:            #fbcfe8;
  --scrim:            rgba(0,0,0,.62);
  --shadow:           0 2px 10px rgba(0,0,0,.40);
}

/* ── 2. SCALES (theme-independent) ────────────────────────── */
:root {
  --font-h:    'Poppins', 'Segoe UI', system-ui, sans-serif;
  --font-body: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
  /* Genuine tabular data only — booking references, timeline timestamps,
     cookie names. System stack, so no webfont download. Never used for labels. */
  --font-mono: ui-monospace, SFMono-Regular, 'Cascadia Mono', Menlo, Consolas, monospace;

  --s1: 4px;  --s2: 8px;  --s3: 12px; --s4: 16px;
  --s5: 24px; --s6: 32px; --s7: 48px; --s8: 64px;

  --r-sm:   8px;
  --r:     12px;   /* buttons, inputs */
  --r-lg:  16px;   /* cards */
  --r-xl:  24px;   /* hero, panels */
  --r-full: 999px;

  --sh-sm:   0 1px 2px rgba(31,41,55,.04);
  --sh:      var(--shadow);
  --sh-md:   0 8px 24px rgba(31,41,55,.06);
  --sh-lg:   0 16px 40px rgba(31,41,55,.08);
  --sh-pink: 0 6px 18px rgba(225,29,104,.22);
  --stack:   var(--s4);   /* the gap between two rows of a form */

  --tr-fast: 120ms cubic-bezier(.4,0,.2,1);
  --tr:      180ms cubic-bezier(.4,0,.2,1);
  --tr-slow: 280ms cubic-bezier(.4,0,.2,1);

  --z-nav: 40; --z-scrim: 45; --z-drawer: 46; --z-flash: 60; --z-skip: 100;

  --wrap:        1180px;
  --wrap-narrow:  760px;

  --ctl-h:    48px;   /* minimum touch target */
  --ctl-h-sm: 40px;
}

/* ── 2b. PHONE DENSITY ────────────────────────────────────── */
/* The spacing scale, re-cut for a screen where vertical space is the scarce
   resource. ONE change that reaches every section, card, stack and gap on the
   site, because they all measure themselves in these tokens.

   Only the four LARGE steps move. s1–s4 are the tight gaps — the space between
   a label and its input, between two chips — and shrinking those makes things
   look broken rather than dense. It is the 48s and 64s that were spending a
   third of a phone screen on nothing.

     --s5  24 → 20     --s7  48 → 32
     --s6  32 → 24     --s8  64 → 40

   Measured on the provider home page: the first card below the hero starts
   474px down, and 426px after — 48px earlier at every section boundary, on
   every page, for four lines of CSS.

   ⚠ TOUCH TARGETS ARE NOT ON THIS SCALE. --ctl-h stays 48px and --ctl-h-sm
   stays 40px, deliberately, so nothing here can shrink a tap target. Density
   comes out of the space BETWEEN controls, never out of the controls. That is
   the rule this whole block rests on — if a future edit reaches for --s* to
   size a button, it is on the wrong scale.

   ⚠ --s* is also used for HORIZONTAL padding in places (.wrap is
   `0 var(--s5)`). That is a bonus, not a side effect: a narrower gutter is more
   line length on the screen that needs it most. */
@media (max-width: 559px) {
  :root {
    --s5: 20px; --s6: 24px; --s7: 32px; --s8: 40px;
    --stack: 10px;   /* form rows sit closer than the 16px desktop gap */
  }
}

/* ── 3. RESET ─────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.6;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  -webkit-font-smoothing: antialiased;
}
img { max-width: 100%; height: auto; display: block; }
a { color: inherit; text-decoration: none; }
button { font: inherit; cursor: pointer; }
main { flex: 1 0 auto; }
hr { border: 0; border-top: 1px solid var(--border-s); margin: var(--s5) 0; }
strong, b { color: var(--heading); font-weight: 600; }

/* ⚠ MUST stay. The browser's built-in [hidden] rule is `display: none` at
   UA-stylesheet strength, so ANY class rule that sets `display` beats it —
   which silently defeats the `hidden` attribute. That is exactly what happened
   to .nav-dropdown and .nav-drawer (both `display: flex`): they were never
   actually hidden, so the account menu looked permanently stuck open and the
   mobile drawer rendered on top of page content on desktop.
   Keep this rule ABOVE any component that toggles visibility via `hidden`.
   This now also covers every [data-panel] driven by initTabs(). */
[hidden] { display: none !important; }

/* Anything a redirect or an in-page link jumps to.

   The navbar is position:sticky, so a bare anchor jump parks the target
   underneath it and you land looking at the row ABOVE the one you wanted. 88px
   clears it with a little air; it is the same figure § 34 uses for #cal, and
   apps/seo/tests.py fails the build if a new target invents its own number. */
.anchor-target { scroll-margin-top: 88px; }

.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: var(--z-skip);
  background: var(--primary-fill); color: var(--primary-on);
  padding: var(--s3) var(--s4); border-radius: var(--r); font-weight: 600;
}
.skip-link:focus { left: var(--s2); top: var(--s2); }

/* Visible keyboard focus everywhere — never remove without a replacement.
   --focus is chosen to stay legible on white, on --primary-soft, AND on a
   solid pink button. */
:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }

/* The brief commits to WCAG AA, so this is required, not a nicety. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ── 4. DECORATION ────────────────────────────────────────── */
/* Replaces the old dotted `.grid-bg` texture, which was the single most
   brutalist thing on the page. Pure CSS — no assets, nothing for the prod
   static manifest to resolve. */
.deco { position: relative; overflow: hidden; isolation: isolate; }
.deco-blob {
  position: absolute; z-index: -1; pointer-events: none;
  border-radius: 50%; filter: blur(70px); opacity: .55;
}
.deco-blob-a { width: 460px; height: 460px; top: -180px; right: -120px; background: var(--primary-soft); }
.deco-blob-b { width: 380px; height: 380px; bottom: -220px; left: -140px; background: var(--green-soft); opacity: .38; }
.deco-wash { background: linear-gradient(180deg, var(--primary-soft) 0%, var(--bg) 62%); }

/* ── 5. TYPOGRAPHY ────────────────────────────────────────── */
h1, h2, h3, h4 {
  font-family: var(--font-h); font-weight: 700; line-height: 1.2;
  color: var(--heading); margin: 0 0 .5em; letter-spacing: -.01em;
}
.t-display { font-size: clamp(2rem, 5vw, 2.625rem); letter-spacing: -.025em; line-height: 1.12; }
.t-section  { font-size: clamp(1.5rem, 3.2vw, 2rem); }
.t-h3       { font-size: 1.5rem; }
.t-card     { font-size: 1.125rem; font-family: var(--font-h); font-weight: 600; }
/* Eyebrow labels — replaces the old uppercase mono treatment. The brief
   forbids all-uppercase text, so nothing here sets text-transform. */
.t-eyebrow  { font-size: .8125rem; font-weight: 600; color: var(--primary-ink); letter-spacing: .01em; }
/* Genuine monospace for genuine tabular data only. */
.t-mono     { font-family: var(--font-mono); font-size: .875rem; font-variant-numeric: tabular-nums; }
.t-num      { font-variant-numeric: tabular-nums; }
.t-muted    { color: var(--text-m); }
.t-dim      { color: var(--text-m); font-size: .875rem; }
.t-lead     { font-size: 1.0625rem; color: var(--text-m); max-width: 62ch; line-height: 1.65; }
.text-pink  { color: var(--primary-ink); }

/* ── 6. LAYOUT ────────────────────────────────────────────── */
/* Reading-width containers. These exist so page templates stop reaching for
   style="max-width:…", which beats every stylesheet rule including media
   queries — see conventions.md §7. */
.wrap { width: 100%; max-width: var(--wrap); margin: 0 auto; padding: 0 var(--s5); }
.wrap-mid { width: 100%; max-width: 900px; margin: 0 auto; padding: 0 var(--s5); }
.wrap-narrow { width: 100%; max-width: var(--wrap-narrow); margin: 0 auto; padding: 0 var(--s5); }
.wrap-tight { width: 100%; max-width: 620px; margin: 0 auto; padding: 0 var(--s5); }
.section { padding: var(--s8) 0; }
.section-tight { padding: var(--s6) 0; }
.band { background: var(--bg2); }
.stack > * + * { margin-top: var(--stack); }
.row { display: flex; gap: var(--s3); flex-wrap: wrap; align-items: center; }
.row-center { justify-content: center; }
/* For a row where one item hangs something below it (a reason disclosure under
   a Reject button): without this the short item stretches to match the tall
   one. */
.row-top { align-items: flex-start; }
.row-between { display: flex; gap: var(--s3); flex-wrap: wrap; align-items: center; justify-content: space-between; }
/* A divided list of rows inside a card — saved addresses, settings lists. */
.row-divided { display: flex; gap: var(--s3); flex-wrap: wrap; align-items: center;
               justify-content: space-between; padding: var(--s3) 0;
               border-bottom: 1px solid var(--border-s); }
.row-divided:last-child { border-bottom: 0; }
.grid { display: grid; gap: var(--s5); grid-template-columns: 1fr; }
@media (min-width: 700px)  { .grid-2 { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 700px)  { .grid-3 { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1000px) { .grid-3 { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 900px)  { .grid-4 { grid-template-columns: repeat(4, 1fr); } }

/* Three across from the smallest screen, for short cards that are a NAME and
   one line of meta. A separate class from .grid on purpose: .grid is one column
   on a phone and that is right for the cards that carry real content — a
   provider card, a booking. This is for tiles.

   Anything marked .tile-more is advisory and goes below 560px, where a third of
   the width is ~100px and a category chip is wider than the card it sits in. */
.tile-grid {
  display: grid; gap: var(--s2);
  grid-template-columns: repeat(3, 1fr);
}
@media (min-width: 560px) {
  .tile-grid { grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: var(--s4); }
}
.tile { display: block; }
.tile-name { margin: 0; text-wrap: balance; }
@media (max-width: 559px) {
  .tile { padding: var(--s3) var(--s2); text-align: center; }
  .tile-more { display: none !important; }
  .tile-name { font-size: .8125rem; line-height: 1.25; }
  .tile-meta { font-size: .6875rem; margin-top: 2px; }
}
@media (min-width: 560px) { .tile-name { margin-top: var(--s2); } }

/* Sidebar + content. Serves the search filter rail and the booking summary. */
.split { display: grid; gap: var(--s5); grid-template-columns: 1fr; }
@media (min-width: 900px) { .split { grid-template-columns: 300px 1fr; align-items: start; } }
@media (min-width: 900px) { .split-end { grid-template-columns: 1fr 320px; } }

/* ── 7. UTILITIES ─────────────────────────────────────────── */
/* Deliberately tiny. Anything the set doesn't cover wants a real component,
   not another utility — see docs/technical/design-system.md §5. */
.center { text-align: center; }
.mt-0 { margin-top: 0; } .mb-0 { margin-bottom: 0; }
.mt-8 { margin-top: var(--s2); } .mt-16 { margin-top: var(--s4); }
.mt-24 { margin-top: var(--s5); } .mt-32 { margin-top: var(--s6); }
.mb-16 { margin-bottom: var(--s4); }
.full { width: 100%; }
.hide { display: none !important; }
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}
/* Add alongside .sr-only when the hidden thing is FOCUSABLE — it comes back on
   screen the moment keyboard focus lands inside it, so the control is never a
   trap. This is why the avatar row is .sr-only and not display:none, which
   would drop it out of the tab order and the accessibility tree entirely. */
.sr-only.sr-focusable:focus-within {
  position: static; width: auto; height: auto; margin: 0;
  overflow: visible; clip: auto; white-space: normal;
}

/* ── 8. ICONS ─────────────────────────────────────────────── */
/* Inline SVG from templates/includes/icon.html. Stroke-only outline set;
   colour comes from currentColor so an icon inherits its context — including
   inside a pink button and in dark mode. That is why emoji were replaced:
   they are full-colour glyphs that ignore currentColor. */
.ic {
  width: 20px; height: 20px; flex: none;
  fill: none; stroke: currentColor;
  stroke-width: 1.75; stroke-linecap: round; stroke-linejoin: round;
  vertical-align: -.2em;
}
.ic-sm { width: 16px; height: 16px; }
.ic-lg { width: 26px; height: 26px; }
.ic-xl { width: 32px; height: 32px; stroke-width: 1.5; }
.ic-fill { fill: currentColor; stroke: none; }
.ic-badge {
  width: 44px; height: 44px; border-radius: 50%; flex: none;
  display: grid; place-items: center;
  background: var(--primary-soft); color: var(--primary-ink);
}

/* ── 9. NAVBAR ────────────────────────────────────────────── */
.nav {
  position: sticky; top: 0; z-index: var(--z-nav);
  background: var(--surface); border-bottom: 1px solid var(--border);
  transition: box-shadow var(--tr);
}
.nav.is-scrolled { box-shadow: var(--sh); }
.nav-inner {
  max-width: var(--wrap); margin: 0 auto; padding: var(--s3) var(--s5);
  display: flex; align-items: center; gap: var(--s4);
}
.nav-logo { display: flex; align-items: center; gap: var(--s2); font-family: var(--font-h); font-weight: 700; }
.nav-logo-mark {
  width: 36px; height: 36px; display: grid; place-items: center; flex: none;
  background: var(--primary-fill); color: var(--primary-on);
  border-radius: var(--r);
}
.nav-logo-mark .ic { width: 20px; height: 20px; }
.nav-logo-text { font-size: 1.0625rem; letter-spacing: -.02em; color: var(--heading); }
.nav-links { display: flex; gap: var(--s5); margin-left: var(--s3); }
.nav-links a { font-size: .9375rem; font-weight: 500; color: var(--text-m); transition: color var(--tr-fast); }
.nav-links a:hover { color: var(--primary-ink); }
.nav-actions { margin-left: auto; display: flex; align-items: center; gap: var(--s2); }
.nav-icon-btn {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r);
  width: 44px; height: 44px; display: grid; place-items: center; color: var(--text-m);
  transition: background var(--tr-fast), color var(--tr-fast), border-color var(--tr-fast);
}
.nav-icon-btn:hover { background: var(--primary-soft); color: var(--primary-ink); border-color: var(--primary); }
.nav-bell {
  position: relative; width: 44px; height: 44px; border-radius: var(--r);
  display: grid; place-items: center; color: var(--text-m);
  transition: background var(--tr-fast), color var(--tr-fast);
}
.nav-bell:hover { background: var(--primary-soft); color: var(--primary-ink); }
.nav-badge {
  position: absolute; top: 5px; right: 5px;
  background: var(--primary-fill); color: var(--primary-on);
  font-size: .6875rem; font-weight: 700; min-width: 18px; height: 18px;
  border-radius: var(--r-full); display: grid; place-items: center; padding: 0 5px;
  border: 2px solid var(--surface);
}
/* The theme toggle renders BOTH glyphs and CSS picks one, driven by the
   attribute the <head> guard already stamped — so the correct icon is right
   before main.js loads. Safe to set `display` here: neither span is toggled
   via the `hidden` attribute. */
[data-theme="light"] .theme-ic-sun  { display: none; }
[data-theme="dark"]  .theme-ic-moon { display: none; }

.nav-account { position: relative; }
.nav-dropdown {
  position: absolute; right: 0; top: calc(100% + var(--s2)); min-width: 210px;
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-lg);
  padding: var(--s2); display: flex; flex-direction: column; box-shadow: var(--sh-md);
  opacity: 0; transform: translateY(-4px); transition: opacity var(--tr), transform var(--tr);
}
.nav-dropdown.is-open { opacity: 1; transform: none; }
.nav-dropdown a { padding: 10px var(--s3); border-radius: var(--r-sm); font-size: .9375rem; color: var(--text); }
.nav-dropdown a:hover { background: var(--primary-soft); color: var(--primary-ink); }

/* ⚠ A VISIBILITY UTILITY MAY ONLY EVER HIDE. Never give one the visible state.
   This is the same incident as the `[hidden]` rule in § 3, between two classes
   instead of a class and an attribute, and it shipped:

     .nav-desktop { display: none; }              ← here, line ~383
     .btn-f, .btn-g { display: inline-flex; }     ← § 10, declared LATER

   Equal specificity, later wins, so the Log in and Sign up buttons rendered on
   every phone. The navbar then overflowed the viewport and the whole document
   scrolled sideways and rocked on any touch — on every page of the site.
   `.nav-links.nav-desktop` escaped only because .nav-links happens to be
   declared earlier, which is luck and not a design.

   The queries below are inverted so these classes never set a visible display:
   they only remove. Off the other side of the breakpoint the element's own
   display applies naturally and there is no cascade race left to lose.
   `!important` because a hiding utility must outrank the component it is
   hiding — that is the whole job. Guarded by apps/seo/tests.py. */
@media (max-width: 899px) { .nav-desktop     { display: none !important; } }
@media (min-width: 900px) { .nav-mobile-only { display: none !important; } }

/* Mobile drawer.
   ⚠ .nav-scrim and .nav-drawer are toggled by the `hidden` attribute. Their
   .is-open class animates opacity/transform ONLY — it must never set
   `display`, or [hidden] stops working. See §3 and conventions.md §7. */
.nav-scrim {
  position: fixed; inset: 0; background: var(--scrim); z-index: var(--z-scrim);
  opacity: 0; transition: opacity var(--tr-slow);
}
.nav-scrim.is-open { opacity: 1; }
.nav-drawer {
  position: fixed; top: 0; left: 0; bottom: 0; width: min(82vw, 320px); z-index: var(--z-drawer);
  background: var(--surface); border-right: 1px solid var(--border);
  padding: var(--s5) var(--s4); display: flex; flex-direction: column; gap: var(--s1);
  overflow-y: auto; box-shadow: var(--sh-lg);
  transform: translateX(-100%); transition: transform var(--tr-slow);
}
.nav-drawer.is-open { transform: none; }
.nav-drawer a { padding: var(--s3) var(--s3); border-radius: var(--r-sm); font-weight: 500; color: var(--text); }
.nav-drawer a:hover { background: var(--primary-soft); color: var(--primary-ink); }
.drawer-rule { width: 100%; }

/* ── 10. BUTTONS ──────────────────────────────────────────── */
.btn-f, .btn-g, .btn-social {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--s2);
  min-height: var(--ctl-h); padding: 12px var(--s5);
  border-radius: var(--r); border: 1px solid transparent;
  font-family: var(--font-body); font-weight: 600; font-size: .9375rem;
  text-align: center; line-height: 1.2;
  transition: background var(--tr), border-color var(--tr), color var(--tr),
              box-shadow var(--tr), transform var(--tr-fast);
}
.btn-f {
  background: var(--primary-fill); color: var(--primary-on); box-shadow: var(--sh-sm);
}
.btn-f:hover { background: var(--primary-fill-h); box-shadow: var(--sh-pink); transform: translateY(-1px); }
.btn-f:active { transform: none; box-shadow: var(--sh-sm); }
.btn-g {
  background: var(--surface); color: var(--primary-ink); border-color: var(--primary);
}
.btn-g:hover { background: var(--primary-soft); border-color: var(--primary-fill); transform: translateY(-1px); }
.btn-g:active { transform: none; }
.btn-sm { min-height: var(--ctl-h-sm); padding: 8px var(--s4); font-size: .875rem; }
.btn-lg { min-height: 56px; padding: var(--s4) var(--s6); font-size: 1.0625rem; }
.btn-full { width: 100%; }
.btn-pill { border-radius: var(--r-full); }
/* Danger is an OUTLINE button per the brief. .btn-danger-solid stays for the
   handful of places a filled destructive action is the honest signal. */
.btn-danger { background: var(--surface); color: var(--red); border-color: var(--red); }
.btn-danger:hover { background: var(--red-soft); transform: translateY(-1px); }
.btn-danger-solid { background: var(--red); color: #fff; border-color: var(--red); }
.btn-ok { background: var(--green-fill); color: #fff; border-color: var(--green-fill); }
.btn-ok:hover { filter: brightness(1.12); transform: translateY(-1px); }
.btn-f[disabled], .btn-g[disabled], .btn-ok[disabled], .btn-danger[disabled] {
  opacity: .45; cursor: not-allowed; transform: none; box-shadow: none;
}
.btn-icon {
  display: inline-grid; place-items: center; width: 44px; height: 44px; flex: none;
  border-radius: var(--r); border: 1px solid var(--border);
  background: var(--surface); color: var(--text-m);
  transition: background var(--tr-fast), color var(--tr-fast), border-color var(--tr-fast);
}
.btn-icon:hover { background: var(--primary-soft); color: var(--primary-ink); border-color: var(--primary); }

/* Switch-shaped SUBMIT button, for a one-row POST toggle inside a table.
   Deliberately not a checkbox: there is no form to save, so the click itself
   must post. State lives in aria-checked and the CSS styles on that attribute
   — there is no is-on class to fall out of sync with the announced state.
   Track and knob match .sched-slider exactly so the two switches on the
   provider's own pages read as one control. */
.switch-btn {
  position: relative; display: inline-flex; align-items: center;
  padding: 0; border: 0; background: none; cursor: pointer;
}
.switch-btn::before {
  content: ""; flex: none; width: 44px; height: 26px; border-radius: var(--r-full);
  background: var(--bg3); border: 1px solid var(--border);
  transition: background var(--tr), border-color var(--tr);
}
.switch-btn::after {
  content: ""; position: absolute; left: 3px; top: 50%;
  width: 20px; height: 20px; border-radius: 50%;
  background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.2);
  transform: translateY(-50%);
  transition: transform var(--tr);
}
/* Green only in the on state — this is an availability signal, which is what
   green is reserved for. */
.switch-btn[aria-checked="true"]::before { background: var(--green); border-color: var(--green); }
.switch-btn[aria-checked="true"]::after { transform: translateY(-50%) translateX(18px); }
.switch-btn:hover::before { border-color: var(--text-d); }
.switch-btn:focus-visible { outline: none; }
.switch-btn:focus-visible::before { outline: 3px solid var(--focus); outline-offset: 2px; }
.switch-btn[disabled] { opacity: .45; cursor: not-allowed; }

/* Provider job-action buttons: big thumb targets, stacked on phones. */
.job-actions { display: grid; gap: var(--s3); grid-template-columns: 1fr; }
.job-actions .btn-f, .job-actions .btn-g, .job-actions .btn-ok, .job-actions .btn-danger {
  min-height: 56px; font-size: 1.0625rem;
}
@media (min-width: 640px) { .job-actions { grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); } }

/* ── 11. CHIPS / BADGES ───────────────────────────────────── */
.chip {
  display: inline-flex; align-items: center; gap: 6px; padding: 5px var(--s3);
  border-radius: var(--r-full); font-size: .8125rem; font-weight: 600;
  border: 1px solid var(--border); background: var(--bg2); color: var(--text-m);
  line-height: 1.4;
}
.chip .ic { width: 14px; height: 14px; }
.chip-green  { background: var(--green-soft);   border-color: var(--green);   color: var(--green-ink); }
.chip-orange { background: var(--warn-soft);    border-color: var(--warn-line); color: var(--warn); }
.chip-red    { background: var(--red-soft);     border-color: var(--red);     color: var(--red); }
.chip-blue   { background: var(--blue-soft);    border-color: var(--blue);    color: var(--blue); }
.chip-pink   { background: var(--primary-soft); border-color: var(--primary); color: var(--primary-soft-ink); }
.chip-verified { background: var(--green-fill); border-color: var(--green-fill); color: #fff; }
.chip-dot::before {
  content: ""; width: 7px; height: 7px; border-radius: 50%; flex: none;
  background: currentColor;
}

/* ── 12. CARDS ────────────────────────────────────────────── */
.card {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-lg);
  padding: var(--s5); box-shadow: var(--sh);
}
.card-flat { background: var(--bg2); box-shadow: none; }
.card-tint { background: var(--primary-soft); border-color: var(--primary); }
.card-warn { background: var(--warn-soft); border-color: var(--warn-line); }
.card-pad-sm { padding: var(--s4); }
.card-link {
  display: block;
  transition: transform var(--tr-slow), box-shadow var(--tr-slow), border-color var(--tr-slow);
}
.card-link:hover { transform: translateY(-2px); box-shadow: var(--sh-md); border-color: var(--primary); }

.stat-card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-lg); padding: var(--s4) var(--s5); box-shadow: var(--sh);
}
.stat-card .n { font-family: var(--font-h); font-weight: 700; font-size: 1.75rem; color: var(--heading); font-variant-numeric: tabular-nums; }
.stat-card .l { font-size: .8125rem; color: var(--text-m); }

/* Call-to-action band: copy on the left, button on the right, stacking on
   phones. Replaces an inline flex:1 / min-width pair on the home page. */
.cta-copy { flex: 1; min-width: 260px; }

/* ── 13. PROVIDER CARD ────────────────────────────────────── */
/* Bigger than a list row on purpose: people book people, so the face is the
   first thing that has to land. */
.pcard { display: flex; gap: var(--s3); align-items: flex-start; }
.pcard-media { flex: none; width: 96px; }
.pcard-avatar {
  width: 96px; height: 96px; border-radius: 50%; object-fit: cover; flex: none;
  border: 3px solid var(--surface); box-shadow: 0 0 0 1px var(--border);
  background: var(--primary-soft); color: var(--primary-ink);
  display: grid; place-items: center;
  font-family: var(--font-h); font-weight: 600; font-size: 2rem;
}
.pcard-avatar-lg { width: 120px; height: 120px; font-size: 2.5rem; }
.pcard-body { flex: 1; min-width: 0; }

/* ⚠ The name, the price and the heart share ONE row, in the flow.
   They used to compete for the same corner — .pcard-save was absolutely
   positioned top/right and the price block was rendered into the same space,
   so a 40px circle sat on top of "25 kr" and only the word "from" escaped.
   Two things wanting one corner is not a nudge problem. */
.pcard-head { display: flex; align-items: flex-start; gap: var(--s2); }
.pcard-name { flex: 1; min-width: 0; }
.pcard-name h3 { text-wrap: balance; }
.pcard-price {
  font-family: var(--font-h); font-weight: 700; font-size: 1.0625rem;
  color: var(--heading); white-space: nowrap; font-variant-numeric: tabular-nums;
  flex: none; line-height: 1.3;
}
.pcard-price small { display: block; font-family: var(--font-body); font-weight: 400; font-size: .6875rem; color: var(--text-m); }

.pcard-meta { display: flex; flex-wrap: wrap; gap: var(--s1) var(--s3); font-size: .8125rem; color: var(--text-m); align-items: center; }
.pcard-meta .ic { width: 15px; height: 15px; color: var(--primary); }

/* ⚠ A 4px gap and a tighter chip, so a row FILLS before it wraps. With .row's
   --s3 gap and full-size chips, five languages in a narrow column came out one
   per row — five rows to say four words. */
.pcard-tags { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; }
.chip-sm { padding: 1px 8px; font-size: .6875rem; }
/* ⚠ One line, always. "Next: Tue 11 Aug, 04:04" wrapped after the comma, so
   the card stated a date and then a time as if they were separate facts. */
.pcard-next { font-size: .75rem; color: var(--text-m); white-space: nowrap; }

/* Portfolio thumbnails under the avatar, filling the column the face leaves
   empty. Two across at 96px; a third row appears only if there is work to show.

   DECORATION, not a control: the card is already one big link to the profile,
   and a second tap target inside it is how people open the wrong thing. The
   point is only "this person has photographed their work", which is the single
   most common reason someone opens a profile at all. */
.pcard-thumbs {
  display: grid; grid-template-columns: repeat(2, 1fr); gap: 3px;
  margin-top: 6px; pointer-events: none;
}
.pcard-thumbs img {
  width: 100%; aspect-ratio: 1; object-fit: cover;
  border-radius: 6px; border: 1px solid var(--border-s); background: var(--bg2);
}

.pcard-wrap { position: relative; }
.pcard-save {
  flex: none; background: var(--surface); border: 1px solid var(--border);
  border-radius: 50%; width: 36px; height: 36px;
  display: grid; place-items: center; color: var(--text-d);
  transition: color var(--tr-fast), border-color var(--tr-fast), transform var(--tr-fast);
}
.pcard-save:hover { color: var(--primary-fill); border-color: var(--primary); }
.pcard-save.is-on { color: var(--primary-fill); border-color: var(--primary); }
.pcard-save:active { transform: scale(1.15); }

/* ⚠ The narrow column WAS the bug behind both the wrapping languages and the
   split date. At 360px a 96px avatar plus a third price column left the body
   ~135px, which is one chip wide — so the fix is width, not white-space.
   64px here buys the body ~65px back, enough for three language chips or a
   whole date on one line. */
@media (max-width: 559px) {
  .pcard-media { width: 64px; }
  .pcard-avatar { width: 64px; height: 64px; font-size: 1.5rem; border-width: 2px; }
  .pcard-thumbs { gap: 2px; }
  .pcard-price { font-size: .9375rem; }
  .pcard-save { width: 32px; height: 32px; }
  .pcard-save .ic { width: 16px; height: 16px; }
}

/* ── 13b. PROFILE IDENTITY CARD ───────────────────────────── */
/* The pencil is a <label for="id_avatar">, not a button. The input it drives
   sits inside the profile <form> lower down the page, visually hidden, because
   a form up here cannot wrap the two-column grid without nesting the password
   form inside it. A <label> finds its control by id anywhere in the DOCUMENT —
   same form owner not required — which is what keeps "pick a photo, press Save
   profile" working with JavaScript off. Same round-badge-on-a-corner geometry
   as .pcard-save above, but PINK: orange means warning and green is reserved
   for verified/confirmed, so neither can be borrowed for an edit affordance. */
.idcard {
  display: flex; flex-direction: column; align-items: center;
  gap: var(--s2); text-align: center;
}
.avatar-edit-wrap { position: relative; display: inline-grid; }
.avatar-edit {
  position: absolute; right: var(--s1); bottom: var(--s1);
  width: 40px; height: 40px; border-radius: 50%;
  display: grid; place-items: center; cursor: pointer;
  background: var(--primary-fill); color: var(--primary-on);
  border: 3px solid var(--surface);
  transition: background var(--tr-fast), transform var(--tr-fast);
}
.avatar-edit:hover { background: var(--primary-fill-h); }
.avatar-edit:active { transform: scale(1.1); }

/* Removal stays pending until Save, so the photo greys out rather than
   vanishing — deleting it on click would promise something the page has not
   actually done yet. */
.avatar-edit-wrap.is-clearing .pcard-avatar { opacity: .4; filter: grayscale(1); }
.avatar-remove {
  cursor: pointer; font-size: .875rem; font-weight: 600;
  color: var(--text-m); transition: color var(--tr-fast);
}
.avatar-remove:hover { color: var(--red); }

/* ── 14. HERO / LANDING SEARCH ────────────────────────────── */
/* ⚠ Measured 2026-08-16: this was `var(--s7) 0 var(--s8)` — 48px above and
   64px below — and the 64px stacked with the next section's own padding into a
   96px void between the search card and the trust strip. The hero is the first
   screen; its job is to end where its content ends. */
.hero { padding: var(--s6) 0 var(--s5); }

/* ⚠ `align-items: center`, which this used to carry, is what made the right
   column FLOAT. The columns are 686px and 462px, so centring the shorter one
   left ~112px of dead canvas above AND below it — read as "a large empty area
   reserved for images". Stretch instead, and let .hero-side (§ 39) distribute
   that height across the photographs it actually contains.

   ⚠ Row gap and column gap are set separately on purpose. The 48px column gap
   is the horizontal gutter between the search card and the collage and is
   worth keeping; it was only ever the ROW gap — the one that applies once this
   collapses to a single column on a phone — that needed to come down. */
.hero-grid {
  display: grid; gap: var(--s5); grid-template-columns: 1fr; align-items: stretch;
}
@media (min-width: 940px) {
  .hero-grid { grid-template-columns: 1.05fr .95fr; gap: var(--s5) var(--s7); }
}
.hero-title { max-width: 15ch; }
.hero-media {
  border-radius: var(--r-xl); min-height: 300px; box-shadow: var(--sh-md);
  /* Photography is a SLOT, not a dependency — main.css must contain no url()
     pointing at a static file or the prod manifest build fails. To add a photo:
     drop it in static/img/ then set --hero-image here. design-system.md §7. */
  background: var(--hero-image, radial-gradient(120% 100% at 30% 20%, #ffd9e6 0%, #ffe9f0 45%, #fff5f8 100%));
  background-size: cover; background-position: center;
}
@media (min-width: 940px) { .hero-media { min-height: 420px; } }

.search-card {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-xl);
  padding: var(--s5); box-shadow: var(--sh-md);
}
.search-grid { display: grid; gap: var(--s4); grid-template-columns: 1fr; }
@media (min-width: 620px) { .search-grid { grid-template-columns: 1fr 1fr; } }

.trust-row { display: flex; flex-wrap: wrap; gap: var(--s3) var(--s5); margin-top: var(--s5); }
.trust-item { display: flex; align-items: center; gap: var(--s2); font-size: .875rem; color: var(--text-m); font-weight: 500; }
.trust-item::before {
  content: ""; width: 8px; height: 8px; border-radius: 50%; flex: none; background: var(--primary);
}

/* ⚠ An EXPLICIT column count, NOT `auto-fit`. auto-fit fills whatever width it
   is handed, and the hero is a single column below 940px — so between 900 and
   939px the eight categories laid out SEVEN across with "Events & Personal"
   orphaned alone on a second row. A tile grid whose column count changes with
   every viewport, and which can strand one tile, reads as broken.

   2 / 4 both divide eight exactly, so there is never an orphan at any width,
   and 4-across is the shape the landing page is designed around.

   ⚠ A NINTH category would strand one tile on row 3 above 560px. The fix then
   is a fifth column here — not a return to auto-fit, which is what put a lone
   tile on its own row in the first place. */
.cat-picker {
  display: grid; gap: var(--s2);
  grid-template-columns: repeat(2, 1fr);
}
@media (min-width: 560px) { .cat-picker { grid-template-columns: repeat(4, 1fr); } }
/* The one door out of postal-code-first, for work that has no address. Quiet
   on purpose: it is the minority path, and the hero's job is still the
   postal-code box above it. */
.hero-alt {
  display: block; margin-top: var(--s3); text-align: center;
  font-size: .8125rem; color: var(--text-m);
}
.hero-alt:hover { color: var(--primary-ink); }
.cat-card {
  border: 1px solid var(--border); border-radius: var(--r); padding: var(--s3) var(--s2);
  text-align: center; cursor: pointer; background: var(--surface);
  transition: border-color var(--tr), background var(--tr), box-shadow var(--tr);
}
.cat-card:hover { border-color: var(--primary); background: var(--primary-soft); }
.cat-card.on { border-color: var(--primary-fill); background: var(--primary-soft); box-shadow: 0 0 0 3px var(--primary-soft); }
.cat-card .cat-icon { display: block; color: var(--primary-ink); margin: 0 auto var(--s1); }
.cat-card .cat-icon .ic { width: 22px; height: 22px; }
/* Two short lines rather than one clipped one — "Beauty & Wellness" and
   "Business & Online" cannot fit a 104px column on one line, and truncating a
   category to "Business &…" makes the choice unreadable. */
.cat-card .cat-name {
  font-weight: 600; font-size: .8125rem; color: var(--heading);
  line-height: 1.25; text-wrap: balance;
}

/* ── 15. CATEGORY PANELS ──────────────────────────────────── */
/* Both panels are white. Green is reserved for verified / confirmed /
   available / success and is never a decorative surface — design-system.md §2.2. */
/* ⚠ auto-fit, for the same reason .cat-picker above uses it: this renders EVERY
   active category and there are now eight. As `1fr` / `1fr 1fr` that was eight
   full-height panels — roughly eight phone screens of the landing page, for a
   section whose job is "here is the shape of the catalog".

   The floor is 250px so it pairs up at ~530px and reaches four across on a
   desktop. Gap drops to --s4: with eight panels the gaps alone were costing
   more than a whole panel. */
.cat-panels { display: grid; }
.cat-panel {
  display: flex; flex-direction: column; gap: var(--s3);
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-xl); padding: var(--s5);
  transition: transform var(--tr-slow), box-shadow var(--tr-slow), border-color var(--tr-slow);
}
.cat-panel:hover { transform: translateY(-2px); box-shadow: var(--sh-md); border-color: var(--primary); }
.cat-panel-head { display: flex; align-items: center; gap: var(--s3); }
.cat-panel-subs { display: flex; flex-wrap: wrap; gap: var(--s2); }
.cat-panel-count { font-size: .8125rem; color: var(--text-m); }

/* ── The phone form: three tiles across, not eight full cards ──
   The arithmetic, so nobody re-measures it: at 360px the wrap spends 2×20 and
   the grid 2×8 in gaps, leaving 304/3 ≈ 101px a tile. An icon badge and a
   two-line name fit that; a description and four service chips do not, which
   is why they are the things that go.

   The whole tile is the anchor, so "Explore" was chrome and is gone entirely —
   it said nothing the tap did not. */
.cat-panels { grid-template-columns: repeat(3, 1fr); gap: var(--s2); }
@media (max-width: 559px) {
  .cat-panel { padding: var(--s3) var(--s2); gap: var(--s1); text-align: center; align-items: center; }
  .cat-panel-head { flex-direction: column; gap: var(--s1); }
  .cat-panel-head .t-h3 { font-size: .8125rem; line-height: 1.2; text-wrap: balance; }
  .cat-panel-count { font-size: .6875rem; }
  .cat-panel .ic-badge { width: 36px; height: 36px; }
  .cat-panel .ic-badge .ic { width: 20px; height: 20px; }
  /* Advisory only. The category page carries both, always. */
  .cat-panel-more { display: none !important; }
  .cat-panel:hover { transform: none; }
}
@media (min-width: 560px) {
  .cat-panels { grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: var(--s4); }
}

/* ── 16. STEPS (how it works) ─────────────────────────────── */
/* Two across on a phone, auto-fit above.
   ⚠ Was one column below 640px, which spent FIVE phone screens explaining a
   process nobody is stuck on — the landing page's job is the search box at the
   top, and everything below it is reassurance. Two across halves that without
   losing a word of it, because a step is an icon, a three-word title and one
   short sentence.

   Not three across: unlike the tiles above, a step carries a real sentence, and
   at ~100px "The provider accepts, declines or offers another time" becomes a
   column of single words. */
.steps { display: grid; gap: var(--s4); grid-template-columns: repeat(2, 1fr); }
@media (min-width: 640px) { .steps { grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: var(--s5); } }
/* Kept as a modifier: the provider branch asked for exactly two, and now says
   so explicitly rather than relying on what .steps happens to do. */
.steps-2up { grid-template-columns: repeat(2, 1fr); gap: var(--s4); }
.step { text-align: center; }
.step-num {
  width: var(--ctl-h); height: var(--ctl-h); border-radius: 50%; margin: 0 auto var(--s3);
  display: grid; place-items: center;
  background: var(--primary-soft); color: var(--primary-ink);
}
.step-n { font-family: var(--font-h); font-weight: 600; font-size: .8125rem; color: var(--primary-ink); }
/* ⚠ --ctl-h is a TAP TARGET token and must not shrink on the density scale, so
   the phone size is set here on the decoration itself. Nothing in .step is
   tappable — the whole thing is prose. */
@media (max-width: 559px) {
  .step-num { width: 38px; height: 38px; margin-bottom: var(--s2); }
  .step-num .ic { width: 18px; height: 18px; }
  .step .t-card { font-size: .9375rem; }
  .step p { font-size: .8125rem; }
}

/* ── 17. FORMS ────────────────────────────────────────────── */
/* 4px, not 6. A label belongs TO the input under it — the tighter that pair
   sits, the more obviously it is one thing, and the row gap below does the
   separating. */
.form-label { display: block; font-weight: 600; font-size: .875rem; color: var(--heading); margin-bottom: 4px; }
.form-input, .form-select, .form-textarea {
  width: 100%; min-height: var(--ctl-h); padding: 12px var(--s4);
  font: inherit; font-size: 1rem;
  color: var(--text); background: var(--surface);
  border: 1px solid var(--border-str); border-radius: var(--r);
  transition: border-color var(--tr), box-shadow var(--tr);
}
.form-input::placeholder, .form-textarea::placeholder { color: var(--text-d); }
.form-input:focus, .form-select:focus, .form-textarea:focus {
  border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-soft); outline: none;
}
/* Disabled controls. Today that is only the email field on /accounts/profile/:
   shown because it IS the sign-in name, disabled because email and username
   have to move together or the account can never log in again (rule #2). The
   browser default here is a hardcoded grey that ignores the theme tokens and
   turns to mud on the dark surface, so state it explicitly. */
.form-input:disabled, .form-select:disabled, .form-textarea:disabled {
  background: var(--bg2); color: var(--text-m); border-color: var(--border);
  cursor: not-allowed; opacity: 1;
  -webkit-text-fill-color: var(--text-m);   /* iOS Safari overrides `color` here */
}
/* data: URI — exempt from the prod static manifest, unlike a url() to a file. */
.form-select {
  appearance: none; padding-right: var(--s8);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239ca3af' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right var(--s4) center; background-size: 18px;
}
.form-textarea { resize: vertical; min-height: 110px; }
/* `flex: none` because this box lives inside flex rows (.check-row,
   .form-inline) and a 20px square is happily squashed to 6px by a long label
   next to it — which reads as a rendering fault, not as a checkbox. */
.form-check { flex: none; width: 20px; height: 20px; border-radius: 6px; accent-color: var(--primary-fill); }
/* One standalone checkbox and the sentence belonging to it — includes/field_check.html.
   ⚠ It is NOT .form-inline. That one centres, which is right for "Remember me"
   and wrong here: these labels are agreements, they wrap to two or three lines,
   and a centred box floats beside the middle of a paragraph attached to nothing.
   flex-start puts it on the first line, where a checkbox belongs. */
.check-row { display: flex; align-items: flex-start; gap: var(--s3); }
.check-row label { margin: 0; font-size: .9375rem; line-height: 1.45; cursor: pointer; }
/* 2px, not 0: the box is 20px and the line it sits against is ~21px, so the two
   optical centres are off by exactly this much. */
.check-row .form-check { margin-top: 2px; }
/* --stack, not --s4 directly: it is 16px on a desktop and 10px on a phone, set
   once in § 2b. Every form on the site reads this one number, so the density of
   a five-field form is a single value rather than five templates' worth of
   utility classes. */
.form-row { margin-bottom: var(--stack); }
.form-error { color: var(--red); font-size: .875rem; margin-top: 5px; }
.form-help { color: var(--text-m); font-size: .8125rem; margin-top: 5px; }
.form-inline { display: flex; align-items: center; gap: var(--s2); }
.errorlist { list-style: none; padding: 0; margin: 6px 0 0; color: var(--red); font-size: .875rem; }
@media (min-width: 640px) { .form-2col { display: grid; grid-template-columns: 1fr 1fr; gap: 0 var(--s4); } }

/* CheckboxSelectMultiple wrapper.
   Django 4.2 renders this widget as nested <div>s (NOT a <ul>), and it applies
   widget.attrs['class'] to the OUTER wrapper rather than to each input. So the
   wrapper class must describe a container, never a control — passing
   `form-check` here collapses the whole list into an 18x18 box and every option
   overflows on top of the next field. See apps/accounts/forms.py. */
/* Short-label checkbox/radio lists — languages, payment methods, the panel's
   broadcast audience.

   ⚠ The floor was 150px, which needs 316px for two columns plus the gap. A
   card on a 360px phone has ~224px inside it, so every one of these lists
   collapsed to a SINGLE column: eleven languages and five payment methods each
   ate a full screen of vertical space one item at a time.

   104px is (224 − 16) ÷ 2 — two columns at 360px, and auto-fill grows it to
   three or four on a wider screen with no media query. The floor is a
   constraint on the CONTENT: these labels are one or two words, and anything
   longer belongs in a different control (see .addon-list in § 37). */
.check-grid {
  display: grid; gap: var(--s2) var(--s3);
  grid-template-columns: repeat(auto-fill, minmax(104px, 1fr));
  margin-top: var(--s1);
}
.check-grid > div { margin: 0; min-width: 0; }
.check-grid label {
  display: flex; align-items: center; gap: var(--s2);
  font-size: .9375rem; cursor: pointer; line-height: 1.3;
}
.check-grid input[type="checkbox"],
.check-grid input[type="radio"] {
  flex: none; width: 20px; height: 20px; margin: 0;
  accent-color: var(--primary-fill);
}

/* ── 17b. PROVIDER SETUP WIZARD ───────────────────────────── */
/* The three setup pages (profile → services → hours). Carved out rather than
   numbered fresh so the section list above stays stable, the same way 13b and
   25b were. Everything here is composition — no new colours, no new radii. */

/* Progress bar. Steps behind the current one are links; steps ahead are inert
   <span>s, because skipping to hours before pricing anything produces a
   provider who has finished setup and still cannot be found. */
.wiz {
  display: flex; flex-wrap: wrap; justify-content: center; align-items: center;
  gap: var(--s2); list-style: none; padding: 0; margin: 0 0 var(--s6);
}
.wiz-step { display: flex; align-items: center; gap: var(--s2); }
.wiz-step + .wiz-step::before {
  content: ""; flex: none; width: 28px; height: 1px; background: var(--border);
}
.wiz-link { display: flex; align-items: center; gap: var(--s2); }
a.wiz-link:hover .wiz-label { text-decoration: underline; }
.wiz-dot {
  width: 26px; height: 26px; border-radius: 50%; flex: none;
  display: grid; place-items: center;
  font-family: var(--font-h); font-weight: 600; font-size: .8125rem;
  background: var(--bg3); color: var(--text-d); border: 1px solid var(--border);
}
.wiz-dot .ic { width: 14px; height: 14px; }
.wiz-step.is-now .wiz-dot, .wiz-step.is-done .wiz-dot {
  background: var(--primary-fill); color: var(--primary-on); border-color: transparent;
}
.wiz-label { font-size: .8125rem; font-weight: 600; color: var(--text-d); }
.wiz-step.is-now .wiz-label, .wiz-step.is-done .wiz-label { color: var(--primary-ink); }
/* Three labels do not fit on a phone. The dots and the connectors still carry
   "step 2 of 3", and the page's own eyebrow says it in words underneath. */
@media (max-width: 559px) {
  .wiz-step:not(.is-now) .wiz-label { display: none; }
  .wiz-step + .wiz-step::before { width: 20px; }
}

/* Page heading with the tip box alongside it. */
.page-head {
  display: flex; flex-wrap: wrap; gap: var(--s4);
  align-items: flex-start; justify-content: space-between;
}
.page-head > div:first-child { flex: 1 1 320px; min-width: 0; }
/* The width belongs to this slot, not to .tip — the same component also runs
   full-width inside a form step. */
.page-head .tip { flex: 0 1 280px; }
.tip {
  display: flex; gap: var(--s3); align-items: flex-start;
  background: var(--primary-soft); color: var(--primary-soft-ink);
  border-radius: var(--r-lg); padding: var(--s3) var(--s4); font-size: .8125rem;
}
.tip strong { font-family: var(--font-h); display: block; color: inherit; }
.tip-icon {
  width: 28px; height: 28px; border-radius: 50%; flex: none;
  display: grid; place-items: center;
  background: var(--primary-fill); color: var(--primary-on);
}
.tip-icon .ic { width: 15px; height: 15px; }

/* Card header: round pink icon, title + subtitle, optional control on the far
   right. Used by every card on the services page so they read as one set. */
.card-head {
  display: flex; flex-wrap: wrap; gap: var(--s3);
  align-items: flex-start; margin-bottom: var(--s4);
}
.card-head-icon {
  width: 36px; height: 36px; border-radius: 50%; flex: none;
  display: grid; place-items: center;
  background: var(--primary-soft); color: var(--primary-ink);
}
.card-head-text { flex: 1 1 200px; min-width: 0; }
/* Both carry their own margins from the type scale and the UA <p> default;
   inside the header the gap is the flex gap, not theirs. */
.card-head-text .t-card { margin: 0 0 2px; }
.card-head-text .t-dim { margin: 0; }
.card-head-aside { margin-left: auto; flex: none; }

/* Postal codes go in one at a time — Enter submits, because this input is the
   only control in its form. */
.area-add { display: flex; flex-wrap: wrap; gap: var(--s3) var(--s5); align-items: flex-start; }
.area-add-form { flex: 1 1 260px; }
.area-add-row { display: flex; gap: var(--s2); }
.area-add-row .form-input { flex: 1 1 auto; min-width: 0; }
.area-add-row .btn-g { flex: none; }
.area-add-help { margin: var(--s2) 0 0; }
/* Decoration only — first thing to go when the row runs out of width, and gone
   entirely on a phone where every pixel belongs to the input. */
.area-deco { flex: none; margin-left: auto; }
@media (max-width: 767px) { .area-deco { display: none; } }
kbd {
  font-family: var(--font-mono); font-size: .75rem; padding: 1px 6px;
  border: 1px solid var(--border-str); border-bottom-width: 2px;
  border-radius: var(--r-sm); background: var(--bg2); color: var(--text);
}

/* One <form> per removable chip. They are siblings, never nested — nesting
   forms is invalid HTML and the browser drops the inner one silently. */
.chip-form { display: inline-flex; }
.chip-x {
  display: inline-grid; place-items: center; flex: none;
  width: 18px; height: 18px; margin: -2px -4px -2px 0; padding: 0;
  border: 0; background: none; border-radius: 50%;
  color: var(--text-d); cursor: pointer;
  transition: background var(--tr-fast), color var(--tr-fast);
}
.chip-x:hover { background: var(--red-soft); color: var(--red); }
.chip-x:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }

/* Unit welded to the right edge of a number input — "SEK / hour", "hours",
   "min". No image and no pseudo-element content: the unit is real markup in
   includes/field_unit.html, so it survives with CSS disabled, differs per
   field, and can be translated. */
.field-unit { display: flex; align-items: stretch; }
.field-unit .form-input {
  border-radius: var(--r) 0 0 var(--r); border-right: 0; min-width: 0; flex: 1 1 auto;
}
.field-unit-tag {
  display: grid; place-items: center; flex: none;
  padding: 0 var(--s3); white-space: nowrap;
  font-size: .875rem; color: var(--text-m); background: var(--surface);
  border: 1px solid var(--border-str); border-left-color: var(--border);
  border-radius: 0 var(--r) var(--r) 0;
  transition: border-color var(--tr);
}
.field-unit:has(.form-input:focus) .field-unit-tag { border-color: var(--primary); }

/* ── 17c. THE SERVICE FORM ────────────────────────────────── */
/* "Add a service", deliberately FLAT — it used to be a numbered 1-2-3-4
   walkthrough, and on a phone the four section headers cost more vertical
   space than the fields they introduced. Three decisions do not need signposts.

   No JavaScript anywhere in it: the pricing toggle and duration chips are real
   radios and the optional block is a real <details>, so every branch works with
   scripting off.

   The show/hide rules are written :not(:has(…)) rather than :has(…) on purpose.
   A browser without :has() drops the whole rule, and this direction makes that
   failure show every field instead of hiding one the provider cannot reach. */

.form-step-opt { font-weight: 400; color: var(--text-m); }
.form-step-deco-row { display: flex; gap: var(--s4); align-items: flex-start; }
.form-step-grow { flex: 1 1 auto; min-width: 0; }
.step-deco {
  width: 44px; height: 44px; border-radius: var(--r); flex: none;
  display: grid; place-items: center;
  background: var(--primary-soft); color: var(--primary-ink);
}
@media (max-width: 559px) { .step-deco { display: none; } }

/* Two required fields side by side from 380px, not the 640px .form-2col uses.
   On a phone this pair is the difference between one screen and two, and both
   labels are short enough to survive a ~170px column.

   ⚠ FOR TEXT INPUTS, NOT SELECTS. A column here is ~135–165px, and
   `.form-select` reserves --s8 (64px) on its right for the chevron — sized for
   a full-width field. That leaves about 65px of text, which truncates anything
   longer than a word: "Prefer not to say" on the setup page is why the
   years/gender pair there is still on the 640px .form-2col. Pair selects only
   where the longest option is short, or trim the chevron padding first, the way
   .sched-time does. */
@media (min-width: 380px) {
  .form-2col-tight {
    display: grid; grid-template-columns: 1fr 1fr; gap: 0 var(--s3);
  }
}

/* Pricing type as one segmented row. It replaced two 80px tiles: the choice
   only picks which of two field sets you fill, which does not need an
   illustrated card each. It stays on the surface rather than moving into "More
   options" below, because hiding it would make the visible fields change for a
   reason the provider cannot see. */
.seg {
  display: grid; grid-template-columns: 1fr 1fr; gap: 0;
  border: 1px solid var(--border); border-radius: var(--r); padding: 3px; margin: 0;
  background: var(--bg2);
}
.seg-opt {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  padding: 9px var(--s3); cursor: pointer; white-space: nowrap;
  font-size: .875rem; font-weight: 600; color: var(--text-m);
  border-radius: calc(var(--r) - 4px);
  transition: background var(--tr-fast), color var(--tr-fast), box-shadow var(--tr-fast);
}
/* Same reason as .opt-chip: the radio covers the half rather than being
   display:none, which would drop it out of the tab order entirely. */
.seg-opt input {
  position: absolute; inset: 0; width: 100%; height: 100%;
  margin: 0; opacity: 0; cursor: pointer; appearance: none;
}
.seg-opt:hover { color: var(--primary-ink); }
.seg-opt:has(input:checked) {
  background: var(--surface); color: var(--primary-ink); box-shadow: var(--sh-sm);
}
.seg-opt:has(input:focus-visible) { outline: 3px solid var(--focus); outline-offset: 2px; }

.pay-panel { margin-top: var(--s4); }
.pay:not(:has(input[name="pricing_type"][value="hourly"]:checked)) .pay-panel[data-pay="hourly"],
.pay:not(:has(input[name="pricing_type"][value="flat"]:checked))   .pay-panel[data-pay="flat"] {
  display: none;
}

/* Duration chips. .sched-preset geometry, plus the checked state it lacks. */
.opt-chips {
  display: flex; flex-wrap: wrap; gap: var(--s2);
  border: 0; padding: 0; margin: 0;
}
.opt-chip {
  position: relative;
  display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
  padding: 8px var(--s4); white-space: nowrap;
  font-size: .875rem; font-weight: 600; color: var(--text-m);
  background: var(--surface);
  border: 1px solid var(--border); border-radius: var(--r-full);
  transition: border-color var(--tr-fast), color var(--tr-fast), background var(--tr-fast);
}
.opt-chip:hover { border-color: var(--primary); color: var(--primary-ink); background: var(--primary-soft); }
/* The radio covers the whole chip rather than being display:none — a hidden
   input is not focusable, and the chip would drop out of the tab order. */
.opt-chip input {
  position: absolute; inset: 0; width: 100%; height: 100%;
  margin: 0; opacity: 0; cursor: pointer; appearance: none;
}
.opt-chip:has(input:checked) {
  border-color: var(--primary-fill); color: var(--primary-ink); background: var(--primary-soft);
}
.opt-chip:has(input:focus-visible) { outline: 3px solid var(--focus); outline-offset: 2px; }
.opt-chip .oc-on { display: none; }
.opt-chip:has(input:checked) .oc-on { display: block; }
.opt-chip:has(input:checked) .oc-off { display: none; }

.dur-custom { margin-top: var(--s4); }
.dur:not(:has(input[name="duration_choice"][value="custom"]:checked)) .dur-custom { display: none; }
.dur-help { margin: var(--s3) 0 0; }
/* Hours and minutes, side by side even on a phone — they are one answer, and
   stacking them reads as two unrelated questions. Equal columns so neither
   looks like the important one. */
.dur-hm { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s3); }

/* <details> accordion, used by "More options" and by the areas editor. The
   marker reset is the same pair summary.btn-g uses; the chevron flip is new —
   nothing else in the sheet rotates on [open]. */
.disclose > summary { cursor: pointer; list-style: none; }
.disclose > summary::-webkit-details-marker { display: none; }
.disclose-head { display: flex; gap: var(--s3); align-items: center; }
.disclose-text { display: flex; flex-direction: column; min-width: 0; flex: 1 1 auto; }
.disclose-text strong { font-family: var(--font-h); font-size: .9375rem; }
.disclose-chev { margin-left: auto; flex: none; color: var(--text-m); transition: transform var(--tr); }
.disclose[open] > summary .disclose-chev { transform: rotate(180deg); }
.disclose > summary:focus-visible { outline: 3px solid var(--focus); outline-offset: 4px; border-radius: var(--r-sm); }
.disclose-body { margin-top: var(--s4); }

/* Read-back rows: what a field currently says, before you decide to change it.
   Account info is mostly VISITED to look — which email is on the account, what
   number is stored — and five open inputs answer that in the most expensive
   way available, while inviting an accidental edit to something nobody came
   for. */
.kv { margin: var(--s4) 0 0; display: grid; gap: var(--s1); }
.kv > div {
  display: flex; gap: var(--s3); align-items: baseline;
  padding: 7px 0; border-bottom: 1px solid var(--border-s);
}
.kv > div:last-child { border-bottom: 0; }
.kv dt { flex: none; width: 120px; font-size: .8125rem; color: var(--text-m); }
.kv dd { margin: 0; min-width: 0; font-size: .9375rem; color: var(--heading); word-break: break-word; }
@media (max-width: 400px) { .kv dt { width: 96px; } }
/* Once the form is open the inputs say the same thing, and saying it twice
   makes the card longer than the version it replaced. */
.disclose[open] .kv { display: none; }
/* The "More options" block is a bordered row of its own, so it reads as a
   closed drawer rather than a heading with nothing under it. */
.disclose-block {
  border: 1px solid var(--border); border-radius: var(--r);
  padding: var(--s3) var(--s4); margin-top: var(--s4);
}

/* The service chooser: a searchable list grouped by category, replacing a flat
   <select> of the whole catalog. Only the FILTERING is scripted — with
   JavaScript off this is the full grouped list, which still selects and
   submits. See templates/includes/service_picker.html. */
.pick { border: 1px solid var(--border-str); border-radius: var(--r); background: var(--surface); }
.pick[open] { box-shadow: var(--sh); }
.pick > summary { cursor: pointer; list-style: none; }
.pick > summary::-webkit-details-marker { display: none; }
.pick > summary:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; border-radius: var(--r); }
.pick-btn {
  display: flex; align-items: center; gap: var(--s2);
  min-height: var(--ctl-h); padding: 12px var(--s4);
  font-size: 1rem; color: var(--text-d);
}
.pick-btn .ic { margin-left: auto; flex: none; color: var(--text-d); transition: transform var(--tr); }
.pick[open] .pick-btn .ic { transform: rotate(180deg); }
.pick-label.chosen { color: var(--heading); font-weight: 600; }
.pick-panel { border-top: 1px solid var(--border-s); padding: var(--s3); }
.pick-search { margin-bottom: var(--s2); }
/* Bounded so the panel cannot swallow the page on a phone; the list scrolls
   inside itself rather than pushing the form footer off screen. */
.pick-list { max-height: 260px; overflow-y: auto; }
.pick-group-h {
  display: flex; align-items: center; gap: 6px; margin: var(--s3) 0 var(--s1);
  font-size: .6875rem; font-weight: 700; letter-spacing: .07em; text-transform: uppercase;
  color: var(--primary-ink);
}
.pick-group:first-child .pick-group-h { margin-top: 0; }
/* Indented under its heading, the way an <optgroup> indents its options — the
   indent is what makes the grouping readable at a glance. */
.pick-opt {
  position: relative; display: flex; align-items: center;
  padding: 10px var(--s3) 10px var(--s5);
  border-radius: var(--r-sm); font-size: .9375rem; color: var(--text); cursor: pointer;
  transition: background var(--tr-fast), color var(--tr-fast);
}
.pick-opt:hover { background: var(--primary-soft); color: var(--primary-ink); }
/* Covers the row rather than display:none — a hidden input is not focusable,
   and the option would drop out of the tab order entirely. */
.pick-opt input {
  position: absolute; inset: 0; width: 100%; height: 100%;
  margin: 0; opacity: 0; cursor: pointer; appearance: none;
}
.pick-opt:has(input:checked) { background: var(--primary-soft); color: var(--primary-ink); font-weight: 600; }
.pick-opt:has(input:focus-visible) { outline: 3px solid var(--focus); outline-offset: -2px; }
/* initServicePicker() toggles this and nothing else — it never rewrites a row,
   so a selected radio survives every keystroke. */
.pick-off { display: none; }
.pick-none { margin: var(--s3) var(--s3) var(--s2); font-size: .875rem; color: var(--text-m); }

/* The add-a-service drawer. It used to be a permanently-open card above the
   list; almost every visit to that page is to pause a service or fix a price,
   so the ~400px of form now only exists once someone asks for it.

   Closed it reads as an invitation (dashed pink outline); open it becomes a
   solid panel with the summary as its header. Both states are the same
   <details> — no JavaScript, no second page. */
.add-drawer {
  margin-top: var(--s4);
  border: 1px dashed var(--primary); border-radius: var(--r);
  /* initAddDrawer() scrolls this to the top of the screen when it opens — on a
     phone the summary sits low, so the form used to open entirely below the
     fold and the tap read as doing nothing. Same 88px sticky-nav offset as
     .anchor-target; scroll-margin applies to scrollIntoView too, not only to
     fragment jumps. */
  scroll-margin-top: 88px;
}
.add-drawer[open] { border-style: solid; border-color: var(--border); }
/* No border of its own when the summary IS the button — .btn-f draws it. */
.add-drawer:has(> summary.btn-f) { border: 0; }
.add-drawer > summary { cursor: pointer; list-style: none; }
.add-drawer > summary::-webkit-details-marker { display: none; }
.add-drawer > summary:focus-visible { outline: 3px solid var(--focus); outline-offset: 3px; border-radius: var(--r); }
.add-drawer-btn {
  display: flex; align-items: center; gap: var(--s2);
  padding: var(--s3) var(--s4);
  font-size: .9375rem; font-weight: 600; color: var(--primary-ink);
  transition: background var(--tr-fast);
}
.add-drawer-btn:hover { background: var(--primary-soft); border-radius: var(--r); }
.add-drawer[open] > .add-drawer-btn {
  border-bottom: 1px solid var(--border-s); border-radius: var(--r) var(--r) 0 0;
}
.add-drawer[open] > summary .disclose-chev { transform: rotate(180deg); }
.add-drawer-body { padding: var(--s4); }
/* The empty state already ends in a button, so the drawer needs no gap of its
   own above it. */
.empty + .add-drawer { margin-top: 0; }

/* Services the provider already offers. Extends .list-row (§27) with an icon,
   a one-line meta and the two controls — it replaced a table whose 520px
   min-width side-scrolled on every phone. Delete is deliberately not among
   them; it lives on the edit page. */
.svc-item { align-items: center; gap: var(--s3); }
.svc-text { display: flex; flex-direction: column; min-width: 0; }
.svc-text strong { font-family: var(--font-h); font-size: .9375rem; }
.svc-meta { font-size: .8125rem; color: var(--text-m); }

/* Decorative two-tone illustrations (templates/includes/illustration.html).
   Unlike .ic these do NOT inherit currentColor — they carry their own palette,
   which is why they live in a separate partial. Both themes are handled by the
   tokens, so there is no dark-mode block for them. */
.illus { width: 72px; height: auto; flex: none; }
.illus-lg { width: 104px; }
.illus-fill  { fill: var(--primary-soft); }
.illus-line  { stroke: var(--primary); stroke-width: 1.5; stroke-linejoin: round; stroke-linecap: round; }
.illus-solid { fill: var(--primary-fill); }
.illus-dot   { fill: var(--surface); }
/* Lifts the pin off the map behind it — the surface colour, so it reads as a
   gap in both themes rather than a white line drawn on a dark card. */
.illus-lift  { stroke: var(--surface); stroke-width: 2.5; stroke-linejoin: round; }

/* ── 17d. PHONE DENSITY ───────────────────────────────────── */
/* The provider setup pages ran to about five screens on a phone. These are the
   cuts that are purely about width, not about what the page offers — nothing
   here removes a control, only advisory text and padding. */
@media (max-width: 767px) {
  /* Both are advice, not actions: the tip repeats what the lead already says,
     and the note describes a step the provider is about to reach anyway. */
  .page-head .tip, .form-foot .note-pink { display: none; }

  .section { padding: var(--s6) 0; }
  .card { padding: var(--s4); }
  /* Back and Next side by side rather than stacked — two 56px buttons plus a
     gap is most of a phone's remaining screen. */
  .wiz-foot > * { flex: 1 1 0; min-width: 0; }
  .wiz-foot .btn-lg { padding: var(--s4) var(--s3); font-size: .9375rem; }
}

/* Form footer: primary action left, a note or a secondary action after it. */
.form-foot {
  display: flex; flex-wrap: wrap; gap: var(--s3); align-items: center;
  margin-top: var(--s5); padding-top: var(--s4);
  border-top: 1px solid var(--border-s);
}
/* The note is a <p>, so its UA margins have to go or it sits off-centre next
   to the button. Scoped here rather than on .note-pink, which two other pages
   rely on for its bottom margin. */
.form-foot .note-pink { margin: 0 0 0 auto; flex: 0 1 auto; }

/* Edit + delete pair inside a table row. */
.row-actions { display: flex; gap: var(--s2); align-items: center; }
.btn-icon-sm { width: var(--ctl-h-sm); height: var(--ctl-h-sm); }
.svc-name { display: inline-flex; align-items: center; gap: var(--s2); }
.svc-icon {
  width: 28px; height: 28px; border-radius: var(--r-sm); flex: none;
  display: grid; place-items: center;
  background: var(--primary-soft); color: var(--primary-ink);
}

/* Back / Next. Wraps to two full-width buttons on a phone, with Next first in
   the visual order so the thumb lands on the way forward. */
.wiz-foot {
  display: flex; flex-wrap: wrap-reverse; gap: var(--s3);
  justify-content: space-between; margin-top: var(--s5);
}
.wiz-foot > * { flex: 1 1 220px; }
.back-link {
  display: inline-flex; align-items: center; gap: var(--s2);
  font-size: .875rem; font-weight: 600; color: var(--text-m);
}
.back-link:hover { color: var(--primary-ink); }

/* ── 17e. SLOT PICKER (.slotpick-*) ───────────────────────── */
/* Tap-to-book date and time. Carved out rather than renumbering, the same way
   13b, 17b and 25b were.

   The whole interaction is CSS. Real radios named `date` and `time` carry the
   state; :has() reveals the matching panel. No script runs to change days, so
   there is no frame where the page is wrong, and it works with JavaScript off.

   Composition only — every colour, radius and shadow is an existing token. */

/* The collapsed state. Everything below the summary row lives in
   .slotpick-body, which is closed until the checkbox is checked — and the
   checkbox is unchecked in the markup, so "collapsed" needs no script at all.
   Expanded, this card was ~620px of a phone screen. */
.slotpick-body { display: none; }
.slotpick:has(.slotpick-open:checked) .slotpick-body { display: block; }

.slotpick-summary {
  display: flex; align-items: center; gap: var(--s3);
  min-height: 60px; padding: var(--s3) var(--s4);
  border: 1.5px solid var(--primary); border-radius: var(--r);
  background: var(--surface); cursor: pointer;
  transition: border-color var(--tr-fast), background var(--tr-fast);
}
.slotpick-summary:hover { background: var(--bg2); }
.slotpick-summary > .ic { flex: none; color: var(--primary-ink); }
.slotpick-summary-text { flex: 1; min-width: 0; }
.slotpick-summary-key {
  display: block; font-size: .6875rem; font-weight: 700; letter-spacing: .07em;
  text-transform: uppercase; color: var(--text-d);
}
.slotpick-summary-value {
  display: block; font-family: var(--font-h); font-size: .9375rem; font-weight: 700;
  color: var(--heading);
}
/* A time error inside a closed card is an error nobody reads. The template
   also checks the box, so this only has to say so. */
.slotpick-summary[data-invalid] { border-color: var(--red); }

/* ⚠ Scoped `> .slotpick-chev`, not bare. icon.html renders class="ic <cls>", so
   both icons in this row match .slotpick-summary > .ic above — and that
   selector outranks a bare .slotpick-chev whatever the source order, which
   would leave the chevron pink. Matching its specificity is the fix. */
.slotpick-summary > .slotpick-chev {
  flex: none; color: var(--text-m); transition: transform var(--tr);
}
.slotpick:has(.slotpick-open:checked) .slotpick-chev { transform: rotate(180deg); }

/* Two wordings, one true at a time. Neither names a time — only
   initSlotPicker() knows which one was picked, and it replaces this whole span
   with the specific string. Without it the row stays vague rather than stale. */
.slotpick-sum-set { display: none; }
.slotpick:has(input[name="time"]:checked) .slotpick-sum-none { display: none; }
.slotpick:has(input[name="time"]:checked) .slotpick-sum-set { display: inline; }

.slotpick-done { display: flex; width: 100%; margin-top: var(--s5); }

/* How long. A native range input, so it posts and validates with no script;
   only the readout beside it needs JavaScript, and the server renders that
   value for the first paint. */
.slotpick-length { margin-bottom: var(--s5); }
.lbl-inline {
  display: block; font-size: .72rem; font-weight: 700; letter-spacing: .08em;
  text-transform: uppercase; color: var(--text-d); margin-bottom: var(--s2);
}
.slotpick-length-read {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--s3); margin-bottom: var(--s1);
}
.slotpick-length-read output {
  font-family: var(--font-h); font-size: 1.125rem; font-weight: 700;
  color: var(--heading);
}
/* The number the customer is actually deciding about. Pink ink rather than a
   filled badge — it changes on every drag, and a moving block of colour reads
   as an alert. */
.slotpick-length-price {
  font-family: var(--font-h); font-size: 1.125rem; font-weight: 700;
  color: var(--primary-ink); font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.slotpick-range {
  width: 100%; margin: 0; accent-color: var(--primary-fill);
  height: 36px;                       /* a thumb-sized target, not the 4px track */
}
.slotpick-range:focus-visible { outline: 2px solid var(--focus); outline-offset: 4px; }
.slotpick-ticks {
  display: flex; justify-content: space-between;
  font-size: .75rem; color: var(--text-d); font-variant-numeric: tabular-nums;
}
/* Why the slider stops where it does. Composes .note-pink (§ 29) and adds a
   border, because this one appears mid-interaction rather than sitting on the
   page from the start — it has to be seen the moment the slider refuses to go
   further. The first version was small grey body text and people read straight
   past it, which was the whole problem it existed to solve.

   Toggled with `hidden`, never :empty — it always has an icon child. */
.slotpick-cap {
  margin: var(--s3) 0 0; border: 1px solid var(--primary); line-height: 1.45;
}
.slotpick-cap strong { font-weight: 700; }

.slotpick-days, .slotpick-rest {
  border: 0; padding: 0; margin: 0 0 var(--s4);
  display: flex; flex-wrap: wrap; gap: var(--s2);
}
.slotpick-rest { margin-top: calc(var(--s4) * -1); }

/* Every chip — days, times, More — is one shape. */
.slotpick-chip {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 2px; min-height: 56px; padding: var(--s2) var(--s4);
  border: 1.5px solid var(--border-str); border-radius: var(--r-full);
  background: var(--surface); color: var(--text);
  font-size: .9375rem; font-weight: 600; line-height: 1.2;
  cursor: pointer; text-align: center; white-space: nowrap;
  transition: background var(--tr-fast), border-color var(--tr-fast),
              color var(--tr-fast);
}
.slotpick-chip:hover { border-color: var(--primary); color: var(--primary-ink); }

/* Checked state. Styled from the input rather than a class so the radio stays
   the single source of truth — nothing has to be kept in sync. */
.slotpick-daywrap input:checked + .slotpick-chip,
.slotpick-time input:checked + .slotpick-chip {
  background: var(--primary-fill); border-color: var(--primary-fill);
  color: var(--primary-on); box-shadow: var(--sh-pink);
}
/* The focus ring has to come from the hidden input, which is what actually
   receives focus — without this the whole picker is invisible to a keyboard. */
.slotpick-daywrap input:focus-visible + .slotpick-chip,
.slotpick-time input:focus-visible + .slotpick-chip,
.slotpick-more-check:focus-visible + .slotpick-more,
.slotpick-open:focus-visible + .slotpick-summary {
  outline: 2px solid var(--focus); outline-offset: 2px;
}

.slotpick-daywrap { display: flex; flex: 0 0 auto; }
.slotpick-day-name { font-family: var(--font-h); }
.slotpick-day-date { font-size: .75rem; font-weight: 500; color: var(--text-m); }
.slotpick-daywrap input:checked + .slotpick-chip .slotpick-day-date,
.slotpick-daywrap input:checked + .slotpick-chip .slotpick-day-note {
  color: var(--primary-on); opacity: .85;
}
.slotpick-day-note { font-size: .6875rem; font-weight: 600; color: var(--text-d); }

/* Two states, two looks, because they are two different facts. `is-closed` is a
   day the provider does not work — dimmed, dashed, and its radio is disabled.
   `is-empty` is a day they DO work with nothing left on it: muted but fully
   tappable, because tapping it is the only way to reach the empty state and the
   "Next available" way out of it. */
.slotpick-chip.is-closed {
  opacity: .45; cursor: not-allowed; border-style: dashed;
  background: var(--bg2);
}
.slotpick-chip.is-closed:hover { border-color: var(--border-str); color: var(--text); }
.slotpick-chip.is-empty { opacity: .7; background: var(--bg2); }

/* "More" disclosure. The rest of the days are always in the page; this only
   reveals them, which is why opening it costs no request. */
.slotpick-rest, .slotpick-more-shut { display: none; }
.slotpick:has(.slotpick-more-check:checked) .slotpick-rest { display: flex; }
.slotpick:has(.slotpick-more-check:checked) .slotpick-more-open { display: none; }
.slotpick:has(.slotpick-more-check:checked) .slotpick-more-shut { display: inline; }
.slotpick-more { border-style: dashed; color: var(--primary-ink); }

.slotpick-heading {
  font-family: var(--font-h); font-size: .875rem; font-weight: 600;
  letter-spacing: .02em; color: var(--heading); margin: 0 0 var(--s3);
}

/* One panel per day; the checked radio reveals the matching one.

   ⚠ Keyed on POSITION, not on the date. The obvious selector —
   .slotpick:has(#day-2026-08-14:checked) .slotpick-panel[data-day="2026-08-14"]
   — has a date in it, so it could only be generated into an inline <style>,
   and rule #8 says one stylesheet. Matching the Nth day chip to the Nth panel
   says the same thing with static CSS.

   ⚠ The count below is SLOT_DAYS in apps/booking/views.py: four chips, then ten
   behind "More". Change one and change the other.

   ⚠ TWO pickers ride on these rules — includes/slot_picker.html (booking) and
   includes/kitchen_slot_picker.html (a food order's handover time). So the
   count is really THREE things that must agree: this block, booking's
   SLOT_DAYS, and kitchen.windows.PICKER_DAYS. Disagree and the wrong day's
   times appear under a chip, silently and only for the picker that drifted.

   ⚠ A browser without :has() drops all of this and every day's times render at
   once, each under its own legend — more scrolling, still bookable. Written in
   this direction on purpose: failing open beats hiding the control the
   customer came for. */
.slotpick-panel { border: 0; padding: 0; margin: 0; display: none; }

.slotpick:has(.slotpick-days > div:nth-of-type(1)  input:checked) .slotpick-panels > fieldset:nth-of-type(1),
.slotpick:has(.slotpick-days > div:nth-of-type(2)  input:checked) .slotpick-panels > fieldset:nth-of-type(2),
.slotpick:has(.slotpick-days > div:nth-of-type(3)  input:checked) .slotpick-panels > fieldset:nth-of-type(3),
.slotpick:has(.slotpick-days > div:nth-of-type(4)  input:checked) .slotpick-panels > fieldset:nth-of-type(4),
.slotpick:has(.slotpick-rest > div:nth-of-type(1)  input:checked) .slotpick-panels > fieldset:nth-of-type(5),
.slotpick:has(.slotpick-rest > div:nth-of-type(2)  input:checked) .slotpick-panels > fieldset:nth-of-type(6),
.slotpick:has(.slotpick-rest > div:nth-of-type(3)  input:checked) .slotpick-panels > fieldset:nth-of-type(7),
.slotpick:has(.slotpick-rest > div:nth-of-type(4)  input:checked) .slotpick-panels > fieldset:nth-of-type(8),
.slotpick:has(.slotpick-rest > div:nth-of-type(5)  input:checked) .slotpick-panels > fieldset:nth-of-type(9),
.slotpick:has(.slotpick-rest > div:nth-of-type(6)  input:checked) .slotpick-panels > fieldset:nth-of-type(10),
.slotpick:has(.slotpick-rest > div:nth-of-type(7)  input:checked) .slotpick-panels > fieldset:nth-of-type(11),
.slotpick:has(.slotpick-rest > div:nth-of-type(8)  input:checked) .slotpick-panels > fieldset:nth-of-type(12),
.slotpick:has(.slotpick-rest > div:nth-of-type(9)  input:checked) .slotpick-panels > fieldset:nth-of-type(13),
.slotpick:has(.slotpick-rest > div:nth-of-type(10) input:checked) .slotpick-panels > fieldset:nth-of-type(14) {
  display: block;
}
.slotpick-times {
  display: grid; gap: var(--s2);
  grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
}
.slotpick-time { display: flex; }
.slotpick-time > .slotpick-chip { flex: 1; }

/* Open, but nothing left on it. Both ways out are labels pointing at another
   day's radio, so they work with no script. */
.slotpick-empty {
  display: flex; flex-direction: column; align-items: center; gap: var(--s3);
  padding: var(--s6) var(--s4); text-align: center; color: var(--text-m);
  background: var(--bg2); border-radius: var(--r);
}
/* Same shape as .slotpick-empty, but for a day the customer outgrew rather than
   one that was already full — script reveals it, so it cannot be the same
   element as the server-rendered one. */
.slotpick-outgrown {
  display: flex; flex-direction: column; align-items: center; gap: var(--s3);
  padding: var(--s6) var(--s4); text-align: center; color: var(--text-m);
  background: var(--bg2); border-radius: var(--r);
}
.slotpick-empty-face { font-size: 2rem; line-height: 1; }
.slotpick-empty-nav { display: flex; flex-wrap: wrap; gap: var(--s2); justify-content: center; }

@media (prefers-reduced-motion: reduce) {
  .slotpick-chip, .slotpick-summary, .slotpick-chev { transition: none; }
}

/* ── 18. AUTH CARD ────────────────────────────────────────── */
.auth-wrap { max-width: 500px; margin: var(--s7) auto; padding: 0 var(--s5); }
.auth-card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-xl); padding: var(--s6); box-shadow: var(--sh-md);
}
/* On a phone the two outer paddings were spending 112px of a 399px screen on
   whitespace, which is what forced the role tiles to stack. Tightening them
   gives each tile ~135px of text instead of ~105px — enough for two of them
   side by side. Applies to every auth page, which all gain the room. */
@media (max-width: 559px) {
  .auth-wrap { padding: 0 var(--s4); margin-top: var(--s5); }
  .auth-card { padding: var(--s5) var(--s4); }
}
.auth-alt { text-align: center; font-size: .9375rem; color: var(--text-m); margin-top: var(--s5); }

/* ── 19. ROLE PICKER + STAGED REGISTRATION ────────────────── */
/* Each card wraps a REAL radio. It used to be a clickable <div> writing to a
   hidden input in JavaScript, which made registration impossible with scripting
   off — the only control here that was not progressively enhanced. */
/* TWO ACROSS AT EVERY WIDTH. It used to go single-column below 560px, which put
   "How do you want to use Snabbservice?" and "Where do you work?" on separate
   screens — two binary questions, four tiles, and you had to scroll to see the
   second half of the first one.

   A pair of options is a comparison, and a comparison you have to scroll
   between is not one. Both are short: an icon and two lines. */
.role-picker {
  display: grid; gap: var(--s3); grid-template-columns: 1fr 1fr;
  border: 0; padding: 0; margin: 0 0 var(--s5);
}
/* The safety net, not the default. Below ~340px "I provide services" cannot
   break into a column this narrow without looking broken, so it stacks — the
   same failing-wide-not-clipped choice the schedule row makes. */
@media (max-width: 339px) { .role-picker { grid-template-columns: 1fr; } }

/* Four options, not two — "Where do you work?" since the marketplace grew a
   third and fourth mode. Stays 2-up until there is genuinely room for four.

   ⚠⚠ THIS ASKS ABOUT THE CONTAINER, NOT THE WINDOW, AND THAT IS THE WHOLE BUG.
   It used to read `@media (min-width: 700px)`, on the assumption that a 700px
   window meant a 700px row. It never did: these tiles live inside .auth-card,
   and .auth-wrap is capped at 500px at EVERY viewport. Take off 24px of wrap
   padding each side and 32px of card padding each side and the grid gets 388px
   — so on every laptop the rule fired and produced four ~88px columns, with
   "At customer's place" wrapping to three lines and "Online / virtual" broken
   across two. Phones sat BELOW the breakpoint, stayed 2x2, and looked correct,
   which is why it read as a desktop-only bug.

   A container query asks the question that actually decides the layout. At the
   current card width it resolves to 2x2 (188px a tile). Widen .auth-wrap later
   and four-up appears on its own, with no breakpoint to re-guess.

   ⚠ Polarity, as with the :has() rules in §23b: the 4-up rule is the ADDITION,
   so a browser without container queries drops it and keeps the 2x2 base —
   which is the legible state. Never write this the other way round.

   ⚠ Scoped to .role-picker-4, NOT applied to .role-card globally. The tiles are
   tighter here because there are twice as many of them; the user_type picker
   above keeps the roomier default, because it is the first thing anyone sees on
   the site and it is a straight two-way choice. Shrinking that one was not
   asked for.

   Density comes out of the padding and the icon — never out of a tap target.
   The whole card is the target and it stays well over 44px either way. */
.reg-step[data-reg="provider"] { container-type: inline-size; }
@container (min-width: 560px) { .role-picker-4 { grid-template-columns: repeat(4, 1fr); } }
.role-picker-4 .role-card { padding: var(--s4) var(--s3); }
.role-picker-4 .role-icon { width: 36px; height: 36px; margin-bottom: var(--s1); }
.role-picker-4 .role-icon .ic { width: 18px; height: 18px; }
.role-picker-4 .role-name { font-size: .875rem; }
.role-picker-4 .role-desc { font-size: .6875rem; margin-top: 1px; }
@media (max-width: 559px) {
  .role-picker-4 .role-card { padding: var(--s3) var(--s2); }
  .role-picker-4 .role-icon { width: 32px; height: 32px; }
  .role-picker-4 .role-name { font-size: .8125rem; }
}
.role-card {
  position: relative; display: block;
  border: 1px solid var(--border); border-radius: var(--r-lg); padding: var(--s5) var(--s4);
  cursor: pointer; text-align: center; background: var(--surface);
  transition: border-color var(--tr), background var(--tr), box-shadow var(--tr);
}
@media (max-width: 559px) {
  .role-card { padding: var(--s4) var(--s3); }
}
.role-card:hover { border-color: var(--primary); background: var(--primary-soft); }
/* The radio covers the whole card rather than display:none — a hidden input is
   not focusable, and the card would drop out of the tab order entirely. */
.role-card input {
  position: absolute; inset: 0; width: 100%; height: 100%;
  margin: 0; opacity: 0; cursor: pointer; appearance: none;
}
.role-card:has(input:checked) {
  border-color: var(--primary-fill); background: var(--primary-soft);
  box-shadow: 0 0 0 3px var(--primary-soft);
}
.role-card:has(input:focus-visible) { outline: 3px solid var(--focus); outline-offset: 2px; }
.role-card .role-icon {
  width: 44px; height: 44px; border-radius: 50%; margin: 0 auto var(--s2);
  display: grid; place-items: center;
  background: var(--primary-soft); color: var(--primary-ink);
}
.role-card:has(input:checked) .role-icon { background: var(--surface); }
/* display:block so the name and the description are separate lines. They were
   inline spans, so in a narrow column "I provide services Get booked by
   customers" ran together as one paragraph and the label stopped reading as a
   label. balance evens the two lines a title now wraps to. */
.role-card .role-name {
  display: block; font-family: var(--font-h); font-weight: 600;
  font-size: 1rem; color: var(--heading); text-wrap: balance;
}
.role-card .role-desc {
  display: block; margin-top: 2px; font-size: .8125rem;
  color: var(--text-m); line-height: 1.35; text-wrap: balance;
}
@media (max-width: 559px) {
  .role-card .role-icon { width: 38px; height: 38px; }
  .role-card .role-name { font-size: .9375rem; }
  .role-card .role-desc { font-size: .75rem; }
}

/* The escape hatch on wizard step 4, stated right after the reason to stay
   rather than under the fold. Pink so it is not mistaken for fine print — it
   answers "do I have to do this now", which is the question actually being
   asked at that moment. */
.reg-skip {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s3);
  justify-content: space-between;
  margin: var(--s4) 0 0; padding: var(--s3) var(--s4);
  background: var(--primary-soft); color: var(--primary-soft-ink);
  border-radius: var(--r); font-size: .875rem; line-height: 1.45;
}
.reg-skip > span { flex: 1; min-width: 190px; }

/* ── 19b. WHERE GOOGLE SITS, PER ROLE ─────────────────────── */
/* Google needs one thing: the role. So it belongs directly after whatever
   question is the last one it makes redundant — which differs by role.

     customer  role → Google → your details
     provider  role → where do you work → Google → premises → your details

   A provider who chose Google is asked their working mode again on the social
   signup page, so putting Google above that question would mean answering it
   for nothing.

   Rendered ONCE and reordered, never rendered twice: two copies means two
   elements sharing id="google-signup-btn", and the script would bind to
   whichever came first.

   ⚠ `order` moves the box, not the tab sequence — a keyboard user still
   reaches Google before "Where do you work?". Accepted deliberately: the
   alternative is moving the node with JavaScript, which puts the whole layout
   behind a script, and a browser with no :has() simply gets the default order,
   which is the old (correct-for-customers) one. */
.reg-form { display: flex; flex-direction: column; }
.reg-form > .reg-step { order: 30; }                 /* the role picker */
.reg-form > [data-reg="provider"] { order: 50; }   /* what do you offer? */
/* ⚠ REQUIRED, not decoration. This block is also a .reg-step, so without its
   own order it inherits 30 from the rule above and renders "Where do you work?"
   ABOVE "What do you want to offer?" — which reads as being asked to describe a
   business before saying what the business is. 58 keeps it after the capability
   picker (50) and the Google hand-off (55) and before premises (60). */
.reg-form > [data-reg="location"] { order: 58; }
.reg-form > [data-reg="premises"] { order: 60; }
/* ⚠ CONSENT AND GOOGLE SIT AFTER THE LAST OPTION BOX AND BEFORE "Your details",
   for every role, always (owner's call).

   Two earlier arrangements were both wrong. At 35/40 they landed in the MIDDLE
   of the questions, so everything below read as an optional extra after the form
   had apparently ended. Moved to 75/80 they went below "Your details" — and the
   Create account button lives INSIDE that block, so the tick box and the Google
   button ended up under the submit, which is nobody's reading order.

   Google needs the tile answers above it (role and capabilities are parked for
   the hand-off) and nothing from "Your details", so this is the one position
   where both paths are complete at the point they are offered. Consent stays
   DIRECTLY above the button: it is the last thing read before either path.

   ⚠ 62/65, not 71/72 — [data-reg="details"] is 70 and carries the submit. */
.reg-form > .reg-terms { order: 62; }
.reg-form > .social-auth { order: 65; }
.reg-form > [data-reg="details"] { order: 70; }
/* .social-auth already carries margin-top, so only the inner .form-row's own
   bottom margin is cancelled — otherwise the tick box and the button sit two
   stacked gaps apart and read as belonging to different questions. */
.reg-terms > .form-row { margin-bottom: 0; }

/* Staged reveal. Written :not(:has(…)) on purpose: a browser without :has()
   drops the rule and shows every stage, which is a longer form — not a form
   with a required field the visitor can never reach. */
.reg-step + .reg-step { border-top: 1px solid var(--border-s); padding-top: var(--s5); }
/* Nothing past step 1 exists until a role is chosen. */
.reg-form:not(:has(input[name="user_type"]:checked)) [data-reg] { display: none; }
/* The provider questions are for providers. */
.reg-form:not(:has(input[name="user_type"][value="provider"]:checked)) [data-reg="provider"] { display: none; }
/* ⚠ "Where do you work?" is a question about BOOKINGS, so it only applies to a
   provider who offers services. A cook selling only food has no appointments —
   asking them where they work would be a required question with no meaning, and
   their kitchen has its own collection/delivery field.
   Written :not(:has(…)) like its siblings: a browser without :has() drops the
   rule and shows the question, which is a longer form rather than a form with a
   field nobody can reach. */
.reg-form:not(:has(input[name="capabilities"][value="services"]:checked)) [data-reg="location"] { display: none; }
/* Premises: providers who chose "they come to me", and nobody else. */
.reg-form:not(:has(input[name="service_location"][value="at_provider"]:checked)) [data-reg="premises"] { display: none; }
/* ⚠ A provider owes an answer to "what do you offer?" before the rest — and, if
   they offer services, to "where do you work?" as well. Two rules rather than
   one compound selector, because a cook who ticked only Kitchen Corner must
   reach the details step without ever answering service_location. */
.reg-form:has(input[name="user_type"][value="provider"]:checked):not(:has(input[name="capabilities"]:checked)) [data-reg="details"] { display: none; }
.reg-form:has(input[name="capabilities"][value="services"]:checked):not(:has(input[name="service_location"]:checked)) [data-reg="details"] { display: none; }

/* ── 19c. THE WAY OUT, WHICH NOTHING MAY HIDE ─────────────── */
/* ⚠ order: 90 is REQUIRED, not decoration. .reg-form is a flex column and
   every other child above carries an explicit order; a new child defaults to
   0, which would render the submit button above the first question.

   The social signup page's consent row and submit button used to live inside
   [data-reg="details"] — which the rule directly above hides for a provider
   with no service_location. That is an ordinary session state (nothing ever
   cleared signup_service_location), so the page could render with no reachable
   way to finish. Whatever else stages, the exit does not. */
.reg-form > .reg-actions { order: 90; }
.reg-consent-note {
  display: flex; align-items: flex-start; gap: var(--s2);
  margin: 0; font-size: .875rem; color: var(--text-m);
}
.reg-consent-note .ic { flex: none; margin-top: 2px; color: var(--green-ink); }

/* ── 19d. CONFIRM, DON'T RE-ASK (.reg-confirm) ────────────── */
/* A question already answered, collapsed to one line with a way back in.

   The <details> `open` is decided by the SERVER — CSS cannot open or close one,
   and the server is the only party that knows whether an answer survived the
   trip to Google. Collapsed therefore MEANS "you already told us this", which
   is what keeps rule #3 true: with no answer the block renders open with
   nothing checked, so a collapsed line cannot be produced by a default.

   Same three-line idiom as .disclose (§ 13c) and .pick (§ 14b). */
.reg-confirm > summary { cursor: pointer; list-style: none; }
.reg-confirm > summary::-webkit-details-marker { display: none; }
.reg-confirm > summary:focus-visible { outline: 3px solid var(--focus); outline-offset: 4px; border-radius: var(--r-sm); }
/* ⚠ The flex goes on a span INSIDE the summary, never on the <summary> itself:
   `display: flex` there drops the disclosure triangle in some engines and has
   broken toggling outright in older Safari. .disclose-head does the same. */
.reg-confirm-head { display: flex; align-items: baseline; gap: var(--s2); flex-wrap: wrap; }
.reg-confirm-q { font-weight: 600; font-size: .875rem; color: var(--heading); }
.reg-confirm-a {
  display: inline-flex; align-items: center; padding: 2px 10px;
  border-radius: var(--r-full); background: var(--green-soft); color: var(--green-ink);
  font-size: .8125rem; font-weight: 600;
}
.reg-confirm-change { margin-left: auto; font-size: .8125rem; color: var(--primary-ink); }
.reg-confirm > summary:hover .reg-confirm-change { text-decoration: underline; }
/* Once it is open the picker says the same thing, and saying it twice makes the
   open state taller than the line it replaced. Mirrors `.disclose[open] .kv`. */
.reg-confirm[open] .reg-confirm-a,
.reg-confirm[open] .reg-confirm-change { display: none; }
.reg-confirm[open] > summary { margin-bottom: var(--s3); }

/* ── 20. SOCIAL LOGIN ─────────────────────────────────────── */
/* ── 19c. THE SHAKE (.is-shaking) ─────────────────────────── */
/* Answering "you have not filled this in" by MOVING the thing, because a
   sentence somewhere else on the page is a sentence nobody reads.

   Two callers: pressing Continue with Google before choosing a role, and
   submitting any registration step with something missing. Before this the
   Google button did nothing at all when no role was picked — nudgeRolePicker()
   only scrolled, and on a short page the picker was already on screen, so the
   tap was silently ignored.

   ⚠ It must still SAY something when motion is off. § 3's global
   reduced-motion rule collapses every animation to .01ms, so without the
   fallback below this feature would be silently disabled for exactly the
   people most likely to need clear feedback. The outline is declared after it
   and is not motion. */
@keyframes shake-x {
  0%, 100% { transform: translateX(0); }
  16%      { transform: translateX(-7px); }
  33%      { transform: translateX(6px); }
  50%      { transform: translateX(-4px); }
  66%      { transform: translateX(3px); }
  83%      { transform: translateX(-2px); }
}
.is-shaking { animation: shake-x 420ms cubic-bezier(.36,.07,.19,.97); }
/* The colour is the durable half — it survives after the movement stops, so
   the field is still identifiable when someone looks back at the page. */
.is-shaking > .form-input, .is-shaking > .form-select, .is-shaking > .form-textarea,
.is-shaking .form-input, .is-shaking .form-select, .is-shaking .form-textarea {
  border-color: var(--red);
}
.is-shaking .role-card, .is-shaking.role-picker .role-card { border-color: var(--red); }
@media (prefers-reduced-motion: reduce) {
  .is-shaking {
    animation: none;
    outline: 2px solid var(--red); outline-offset: 4px; border-radius: var(--r);
  }
}

.social-auth { margin-top: var(--s5); }
.social-divider { display: flex; align-items: center; gap: var(--s3); color: var(--text-d); font-size: .8125rem; margin-bottom: var(--s4); }
/* Below the button instead of above it — used where Google comes FIRST and the
   manual form is the alternative, which is the register page. The margin flips
   with it, or the rule hugs the button it just separated from. */
.social-divider-after { margin: var(--s4) 0 0; }
.social-divider::before, .social-divider::after { content: ""; flex: 1; height: 1px; background: var(--border); }
.btn-social { width: 100%; background: var(--surface); color: var(--heading); border-color: var(--border-str); }
.btn-social:hover { background: var(--bg2); border-color: var(--text-d); }
/* The Google mark is a brand asset carrying its own four colours — it must NOT
   inherit `currentColor` like every other icon on the site, and must not be
   resized off Google's 18px. See includes/google_mark.html. */
.social-icon { width: 18px; height: 18px; flex: 0 0 18px; }

/* ── 21. TABS & PILLS ─────────────────────────────────────── */
/* ⚠ Never give [data-panel] a `display` rule. Panels toggle via the `hidden`
   attribute (initTabs), and any class rule setting `display` beats the UA
   [hidden] rule — see §3 and conventions.md §7. With JS off every panel
   renders, which is the intended degraded state. */
.tabs { display: flex; gap: var(--s5); border-bottom: 1px solid var(--border); overflow-x: auto; }
.tab {
  background: none; border: 0; border-bottom: 2px solid transparent; margin-bottom: -1px;
  padding: var(--s3) 0; font-weight: 600; font-size: .9375rem; color: var(--text-m);
  white-space: nowrap; transition: color var(--tr), border-color var(--tr);
}
.tab:hover { color: var(--primary-ink); }
.tab[aria-selected="true"] { color: var(--primary-ink); border-bottom-color: var(--primary-fill); }

.pills { display: inline-flex; gap: var(--s1); padding: var(--s1); background: var(--bg2); border-radius: var(--r-full); }
.pill {
  background: none; border: 0; border-radius: var(--r-full);
  padding: var(--s2) var(--s4); font-weight: 600; font-size: .875rem; color: var(--text-m);
  white-space: nowrap; transition: background var(--tr), color var(--tr), box-shadow var(--tr);
}
.pill:hover { color: var(--primary-ink); }
.pill[aria-selected="true"] { background: var(--surface); color: var(--primary-ink); box-shadow: var(--sh-sm); }

/* ── 22. BREADCRUMBS ──────────────────────────────────────── */
.crumbs { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; font-size: .875rem; color: var(--text-d); margin-bottom: var(--s4); }
.crumbs a { color: var(--text-m); }
.crumbs a:hover { color: var(--primary-ink); }
.crumbs .ic { width: 14px; height: 14px; }

/* ── 23. FILTER SIDEBAR ───────────────────────────────────── */
.filters-head { display: flex; align-items: center; gap: var(--s2); margin-bottom: var(--s4); }

/* Category+Service and Date+Time, two to a row from 380px. Five stacked rows
   made the panel a full phone screen, which is the reason a submit felt like
   it had done nothing. */
@media (min-width: 380px) {
  .filter-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 0 var(--s3); }

  /* ⚠ THIS is what stops Date and Time overlapping, and it is why the bug only
     appeared on SOME phones.

     A grid item defaults to `min-width: auto`, so a 1fr track can never be
     narrower than its content's min-content width. A native <input type="date">
     reports the width of its own shadow-DOM editor, and that figure is set by
     the browser and the locale, not by us — Samsung Internet and several
     Android WebViews are far wider than Chrome for the same field. On any
     device whose date widget was wider than half the panel, both tracks blew
     out past the card and the second box ran over the first. Nothing else on
     the page renders a native date control, which is why nothing else moved.

     `min-width: 0` makes 1fr mean 1fr again: the columns are half the panel on
     every device, so neither box can reach its neighbour whatever the browser
     wants the widget to be. */
  .filter-pair > * { min-width: 0; }
}
/* ⚠ The chevron, trimmed — the same fix .sched-time documents. .form-select
   reserves --s8 (64px) on the right for a full-width field; in a ~165px column
   that leaves ~100px of text and "All services" only just survives. Pulling the
   arrow in buys back 30px. */
.filter-pair .form-select { padding-right: var(--s6); background-position: right 10px center; }
.filter-pair .form-input { padding-left: var(--s3); }
/* Native date/time, sized to the half-column they now cannot escape.
   `appearance: none` is for iOS Safari, which sizes these to their content and
   ignores width:100% — the same overlap arriving from the other end. It does
   not remove the picker: ::-webkit-calendar-picker-indicator is a separate
   pseudo-element and still draws, which is why the right padding is trimmed to
   --s2 rather than left at --s4 — the browser draws its own button in there.

   The arithmetic at the 380px breakpoint, so nobody has to re-measure it:
   380 − 40 (wrap) − 40 (card) = 300 inner; minus the 12px gap, halved = 144px
   a column; minus 12 + 8 padding and 2 border = 122px of field. "2026-08-10"
   is ~88px at 1rem and the picker glyph ~18px, so it fits with room to spare. */
.filter-pair input[type="date"], .filter-pair input[type="time"] {
  appearance: none; -webkit-appearance: none;
  width: 100%; padding-right: var(--s2);
}

/* ⚠ The picker's panel must ESCAPE its column. Left in flow it would be a
   search box plus a scrolling list inside ~165px, which is unusable. Anchored
   to the right edge and widened across both columns, it behaves like the
   dropdown it looks like. Scoped to .filter-pair so the provider's services
   page keeps its in-flow panel. */
.filter-pair .pick { position: relative; }
.filter-pair .pick[open] { box-shadow: none; }
.filter-pair .pick-panel {
  position: absolute; z-index: 20; top: calc(100% + 4px); right: 0;
  width: calc(200% + var(--s3));
  background: var(--surface); border: 1px solid var(--border-str);
  border-radius: var(--r); box-shadow: var(--sh-lg);
}
.pick-any { font-weight: 600; }

/* ── 23b. THE SEARCH PANEL, UNFOLDING (.search-steps) ─────── */
/* One question at a time: service → category → where → how → when. Same
   mechanism as the registration form (§ 19) — real radios and :has(), no
   JavaScript — but the rules live HERE rather than there, because mixing them
   means one edit breaks two unrelated pages.

   ⚠ THE :has() POLARITY IS CHOSEN PER RULE, and it is not a matter of taste.
   Every rule below declares `display: none`, so a browser that does not
   understand the selector DROPS the rule and the block stays visible. That is
   what "fail open" means here:

     · a REVEAL ("not answered yet, so hide the next question") must be
       :not(:has(…)) — dropped, everything shows, which is merely a longer form.
     · a HIDE ("this question cannot apply, so remove it") must be a bare
       :has(…) — dropped, the box shows and is simply ignored by the server.

   Applying one habit to both would hide the postal box in every browser
   without :has(), for everyone, permanently.

   ⚠ Scoped to :not(.steps-open) — the server sets that class as soon as any
   question has an answer. A search arriving from the home hero has a postal
   code and no service, and CSS cannot read a value. */

/* Nothing past step 1 until a real service is chosen.
   ⚠ `:not([value=""])` matters: the picker renders a leading "Anything" radio
   with an empty value that is CHECKED on a virgin page, so a bare
   `input[name="service"]:checked` matches immediately and the staging never
   happens at all. */
.search-steps:not(.steps-open):not(:has(input[name="service"]:checked:not([value=""]))) [data-step] {
  display: none;
}
/* And the date/time pair waits for a delivery mode. */
.search-steps:not(.steps-open):not(:has(input[name="service_mode"]:checked:not([value=""]))) [data-step="when"] {
  display: none;
}
/* HIDES — bare :has(), for the reason above. A service that can only happen
   online has no geography to ask about, and neither does an explicit Online
   search. PostalSearchForm.clean() enforces the same rule server-side through
   the same geography_applies(); this is presentation. */
.search-steps:has(input[name="service"][data-virtual-only]:checked) [data-step="where"] {
  display: none;
}
.search-steps:has(input[name="service_mode"][value="online"]:checked) [data-step="where"] {
  display: none;
}

/* Category and Where side by side.
   ⚠ flex, not .filter-pair's `1fr 1fr` grid: Where DISAPPEARS for a virtual
   search, and in a two-column grid that strands the chip in half the width
   with an empty column beside it. `flex: 1 1 130px` lets the survivor grow and
   still wraps to two rows on a very narrow screen. */
.filter-row { display: flex; flex-wrap: wrap; gap: var(--s3); }
.filter-row > .form-row { flex: 1 1 130px; min-width: 0; margin-bottom: 0; }

/* The derived category: an output, so it is a chip and not a control.
   min-height matches the input beside it, or the two sit on different
   baselines and the row reads as broken. */
.derived-chip {
  display: inline-flex; align-items: center; gap: var(--s2);
  min-height: var(--ctl-h-sm); padding: 6px var(--s3); border-radius: var(--r);
  background: var(--bg2); border: 1px solid var(--border);
  font-size: .8125rem; font-weight: 600; color: var(--heading);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.derived-chip .ic { flex: none; }
.derived-chip.is-empty { color: var(--text-d); font-weight: 400; font-style: italic; }

/* Search takes the room; Clear is a quiet secondary beside it. Two full-width
   buttons stacked read as two equal choices, which they are not. */
.filter-actions { display: flex; gap: var(--s2); align-items: stretch; }
.filter-actions .btn-f { flex: 1; justify-content: center; }
.filter-actions .btn-g { flex: none; white-space: nowrap; }

/* Delivery mode: real radios drawn as chips, the icon-picker mechanism. */
.mode-row { display: flex; flex-wrap: wrap; gap: 5px; }
.mode-opt { display: block; cursor: pointer; }
.mode-opt input { position: absolute; opacity: 0; width: 1px; height: 1px; }
/* Tight enough that Any / My place / Their place / Online fit ONE row at 360px:
   4 chips of ~62px plus three 5px gaps is ~263px against ~270px of panel. */
.mode-chip {
  display: block; padding: 6px 10px; border-radius: var(--r-full);
  border: 1px solid var(--border-str); background: var(--surface);
  font-size: .75rem; color: var(--text-m); white-space: nowrap;
  transition: border-color var(--tr), background var(--tr), color var(--tr);
}
.mode-opt:hover .mode-chip { border-color: var(--primary); color: var(--primary-ink); }
.mode-opt input:checked + .mode-chip {
  border-color: var(--primary-fill); background: var(--primary-soft);
  color: var(--primary-ink); font-weight: 600;
}
.mode-opt input:focus-visible + .mode-chip { outline: 2px solid var(--focus); outline-offset: 2px; }

/* "online" beside a service or category name. ONE class, three callers: the
   search picker (where it is why the postal box is about to disappear), the
   landing-page category cards, and the staff catalog table.

   Blue, not green — green is reserved for verified / confirmed / available and
   must not start meaning "deliverable online" as well. */
.tag-online {
  display: inline-block; vertical-align: middle;
  margin-left: var(--s1); padding: 0 6px; border-radius: var(--r-full);
  background: var(--blue-soft); color: var(--blue);
  font-size: .6875rem; font-weight: 600; line-height: 1.6;
}
/* On a category card the tag is its own line under the name, not trailing it —
   a 104px tile has no room beside "Business & Online". */
.cat-card .tag-online { display: block; width: fit-content; margin: 3px auto 0; }

/* The search form's action ends in #results, so a submit lands here instead of
   back at the top of the filter panel. ⚠ scroll-margin-top is what keeps the
   count clear of the sticky navbar — without it the browser scrolls the anchor
   to y=0, which puts it underneath the bar. */
#results { scroll-margin-top: 88px; }   /* the one navbar offset — see seo/tests.py */
#results:focus { outline: none; }        /* focused only to move the caret */
.results-count { margin: 0 0 var(--s4); color: var(--text-m); font-size: .9375rem; }
@media (min-width: 900px) { .filters { position: sticky; top: 88px; } }

/* ── 24. SUMMARY SIDEBAR ──────────────────────────────────── */
@media (min-width: 900px) { .summary { position: sticky; top: 88px; } }
.summary-row {
  display: flex; justify-content: space-between; gap: var(--s3);
  padding: var(--s3) 0; border-bottom: 1px solid var(--border-s); font-size: .9375rem;
}
.summary-row:last-of-type { border-bottom: 0; }
.summary-row dt { color: var(--text-m); margin: 0; }
.summary-row dd { margin: 0; color: var(--heading); font-weight: 500; text-align: right; }
.summary-total {
  display: flex; justify-content: space-between; gap: var(--s3); align-items: baseline;
  padding-top: var(--s3); margin-top: var(--s2); border-top: 1px solid var(--border);
  font-family: var(--font-h); font-weight: 700; color: var(--primary-ink);
}

/* ── 25. BOOKING STATUS / TIMELINE / PANELS ───────────────── */
.status-banner {
  border: 1px solid var(--border); border-radius: var(--r-lg); padding: var(--s4) var(--s5);
  display: flex; align-items: center; gap: var(--s4); flex-wrap: wrap; background: var(--bg2);
}
.status-banner.is-live { border-color: var(--green); background: var(--green-soft); color: var(--green-ink); }
.status-banner.is-warn { border-color: var(--warn-line); background: var(--warn-soft); color: var(--warn); }
.status-banner.is-dead { border-color: var(--border); background: var(--bg2); color: var(--text-m); }
/* Something is waiting for you — not a warning, because nothing is wrong.
   Pink, which is how this site asks for attention (§ 29); amber here would put
   "you have requests" and "you are invisible in search" in the same voice. */
.status-banner.is-info {
  border-color: var(--primary); background: var(--primary-soft);
  color: var(--primary-soft-ink);
}
.timeline { list-style: none; padding: 0; margin: 0; border-left: 1px solid var(--border); }
.timeline li { position: relative; padding: 0 0 var(--s5) var(--s5); }
.timeline li::before {
  content: ""; position: absolute; left: -6px; top: 6px; width: 11px; height: 11px;
  border-radius: 50%; background: var(--primary); border: 2px solid var(--surface);
  box-shadow: 0 0 0 1px var(--primary);
}
.timeline li.is-done::before { background: var(--green); box-shadow: 0 0 0 1px var(--green); }
.timeline li:last-child { padding-bottom: 0; }
.timeline .tl-when { font-family: var(--font-mono); font-size: .8125rem; color: var(--text-d); }

/* Contact panel — only rendered once a booking is confirmed. */
.contact-box {
  border: 1px solid var(--green); border-radius: var(--r-lg); padding: var(--s4) var(--s5);
  background: var(--green-soft);
}
.contact-phone { font-family: var(--font-h); font-weight: 700; font-size: 1.375rem; color: var(--green-ink); }

/* Payment disclaimer — the platform handles no money. */
.disclaimer {
  border-left: 3px solid var(--warn-line); background: var(--warn-soft); color: var(--warn);
  padding: var(--s3) var(--s4); border-radius: 0 var(--r) var(--r) 0; font-size: .9375rem;
}
/* Booking photo thumbnails. `summary` gets an explicit display because a
   <details> marker box otherwise ignores the button padding. */
.photo-figure { margin: 0; }
.photo-thumb { border-radius: var(--r); border: 1px solid var(--border); }
summary.btn-g { display: inline-flex; list-style: none; }
summary.btn-g::-webkit-details-marker { display: none; }

/* Reassurance panel — "you'll see contact details once confirmed". */
.note-pink {
  background: var(--primary-soft); color: var(--primary-soft-ink);
  border-radius: var(--r); padding: var(--s3) var(--s4); font-size: .875rem;
  display: flex; gap: var(--s2); align-items: flex-start;
}
/* Same shape in blue, for the privacy line under the provider setup steps.
   Blue rather than pink because it is not a nudge about this page — it is a
   standing fact about the platform, and pink is how we ask for attention. */
.note-blue {
  background: var(--blue-soft); color: var(--blue);
  border-radius: var(--r); padding: var(--s3) var(--s4); font-size: .875rem;
  display: flex; gap: var(--s2); align-items: flex-start;
}
/* And in green, for something that has already gone right. Green is reserved
   strictly for verified/confirmed/success (design-system.md §3.3), which is
   exactly what "this was agreed, it cost nobody anything" is. */
.note-green {
  background: var(--green-soft); color: var(--green-ink);
  border-radius: var(--r); padding: var(--s3) var(--s4); font-size: .875rem;
  display: flex; gap: var(--s2); align-items: flex-start;
}
/* Optically centre the icon on the FIRST line of the note, which is what the
   eye expects — these panels are flex, so vertical-align on .ic does nothing.
   A note is .875rem on the body's 1.6 line-height, so the line box is 22.4px:
   a 20px icon wants (22.4−20)/2 ≈ 1px and a 16px one wants ≈ 3px. One margin
   cannot serve both, which is why the 16px case was sitting a little high. */
.note-pink .ic, .note-blue .ic, .note-green .ic { flex: none; margin-top: 1px; }
.note-pink .ic-sm, .note-blue .ic-sm, .note-green .ic-sm { margin-top: 3px; }
/* The reset pins `strong` to --heading, which is a grey that ignores the tint
   these panels sit on. Inside a note the emphasis is part of the note. */
.note-pink strong, .note-blue strong { color: inherit; }

/* ── 25b. WEEKLY SCHEDULE GRID (.sched-*) ─────────────────── */
/* Compact by design: only ranges that exist are rendered — no blank spare
   rows — because seven empty rows dominated the screen. "+ Add time" clones a
   hidden prototype instead. Mobile-first; a day becomes one aligned row at
   640px and up.

   ⚠ apps/accounts/tests/test_availability.py PINS THIS MARKUP: `sched-copy`
   must appear exactly 7 times (once per day, so copying works whichever day is
   filled in first), data-fill="weekdays|all|weekend" must survive, and the two
   time controls must be <select>s. Restyle freely; change the DOM only with
   that module open beside you.

   The per-day <details> overflow menu that used to hold "Copy to all days" is
   gone: its popup opened off the side of a phone screen, so the one action it
   held was unreachable on the device this page is mostly used from. */
.sched-card { padding: 0; overflow: visible; }

.sched-head {
  display: flex; flex-wrap: wrap; gap: var(--s3);
  align-items: flex-start; justify-content: space-between;
  padding: var(--s5); border-bottom: 1px solid var(--border-s);
}
.sched-presets { display: flex; flex-wrap: wrap; gap: var(--s2); }
.sched-preset {
  background: var(--surface); color: var(--text-m);
  border: 1px solid var(--border); border-radius: var(--r-full);
  padding: 8px var(--s4); font-size: .875rem; font-weight: 600; white-space: nowrap;
  transition: border-color var(--tr-fast), color var(--tr-fast), background var(--tr-fast);
}
.sched-preset:hover { border-color: var(--primary); color: var(--primary-ink); background: var(--primary-soft); }
.sched-preset.is-danger:hover { border-color: var(--red); color: var(--red); background: var(--red-soft); }

/* toggle · name · ranges · actions — one line on a desktop.
   The actions are their OWN cell, not part of the ranges cell, so the phone
   layout below can drop them to a full-width second line. See § the media
   query at the end of this section. */
.sched-day {
  display: grid; gap: var(--s1) var(--s3); align-items: center;
  grid-template-columns: auto auto 1fr auto;
  padding: var(--s4) var(--s5); border-bottom: 1px solid var(--border-s);
}
.sched-actions { display: flex; flex-wrap: wrap; gap: var(--s2); align-items: center; }
.sched-day:last-of-type { border-bottom: 0; }
.sched-day.has-error { background: var(--red-soft); }
/* ⚠ nowrap and a real min-width, both load-bearing. The day name sits in an
   `auto` grid column, and an auto column WILL shrink below its content when the
   row runs out of room — so "Mon" was rendered under the start-time dropdown
   and read as "Mor". Text is the one thing in this row that must not give: the
   selects shrink instead (they carry flex-basis on phones). */
.sched-name {
  font-family: var(--font-h); font-weight: 600; font-size: .875rem;
  color: var(--heading); min-width: 40px; white-space: nowrap;
}

/* Toggle switch — a restyled checkbox, so it still submits normally.
   Checked is GREEN: "available" is one of the four sanctioned green uses. */
.sched-switch { position: relative; display: inline-flex; flex: none; cursor: pointer; }
.sched-check { position: absolute; opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer; }
.sched-slider {
  width: 44px; height: 26px; border-radius: var(--r-full);
  background: var(--bg3); border: 1px solid var(--border);
  transition: background var(--tr), border-color var(--tr); position: relative; flex: none;
}
.sched-slider::after {
  content: ""; position: absolute; top: 2px; left: 2px;
  width: 20px; height: 20px; border-radius: 50%;
  background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.2);
  transition: transform var(--tr);
}
.sched-check:checked + .sched-slider { background: var(--green); border-color: var(--green); }
.sched-check:checked + .sched-slider::after { transform: translateX(18px); }
.sched-check:focus-visible + .sched-slider { outline: 3px solid var(--focus); outline-offset: 2px; }

.sched-windows { display: flex; flex-wrap: wrap; gap: var(--s2); align-items: center; }
.sched-window { display: flex; align-items: center; gap: var(--s2); }
/* A <select>, not an <input type="time">: Android's clock dial produced 04:04
   and 16:32 here. .form-select's own padding-right is --s8 (64px) for a
   full-width field — far too much for a control holding five characters, so
   the chevron is pulled in and the padding cut to match. */
.sched-time {
  width: 100px; min-height: var(--ctl-h-sm);
  padding: 8px 28px 8px var(--s3);
  font-size: .875rem; font-variant-numeric: tabular-nums;
  background-position: right 8px center; background-size: 16px;
}
.sched-dash { color: var(--text-d); }

.sched-remove {
  background: none; border: 0; color: var(--text-d);
  line-height: 1; padding: 6px; border-radius: var(--r-sm);
  transition: color var(--tr-fast), background var(--tr-fast);
}
.sched-remove:hover { color: var(--red); background: var(--red-soft); }

.sched-add {
  background: var(--surface); color: var(--text-m);
  border: 1px solid var(--border); border-radius: var(--r-full);
  padding: 8px var(--s3); font-size: .8125rem; font-weight: 600; white-space: nowrap;
  transition: border-color var(--tr-fast), color var(--tr-fast), background var(--tr-fast);
}
.sched-add:hover { border-color: var(--primary); color: var(--primary-ink); background: var(--primary-soft); }

/* "Copy to all days" — a plain button beside "+ Add time".
   It used to sit inside a per-day <details> overflow menu whose popup opened
   off the side of a phone screen: half a word of it was reachable, behind a
   chevron that said nothing about what it opened. A row of two named buttons
   costs less height than the chevron did and needs no explaining. */
.sched-copy {
  background: var(--surface); color: var(--text-m);
  border: 1px solid var(--border); border-radius: var(--r-full);
  padding: 8px var(--s3); font-size: .8125rem; font-weight: 600; white-space: nowrap;
  transition: border-color var(--tr-fast), color var(--tr-fast), background var(--tr-fast);
}
.sched-copy:hover { border-color: var(--primary); color: var(--primary-ink); background: var(--primary-soft); }
.sched-copy[disabled] { opacity: .45; cursor: not-allowed; }

.sched-error { grid-column: 1 / -1; margin: 2px 0 0; }

/* A switched-off day collapses to just the toggle and "+ Add time". Its inputs
   are also `disabled` by initSchedule(), so nothing is submitted. */
.sched-day.is-off .sched-window { display: none; }
.sched-day.is-off .sched-name { color: var(--text-d); }

.sched-tip {
  display: flex; gap: var(--s3); align-items: flex-start;
  margin: var(--s4) var(--s5); padding: var(--s4);
  background: var(--primary-soft); color: var(--primary-soft-ink); border-radius: var(--r);
}
.sched-tip-icon {
  flex: none; width: 28px; height: 28px; border-radius: 50%;
  background: var(--primary-fill); color: var(--primary-on); display: grid; place-items: center;
}
.sched-tip-icon .ic { width: 16px; height: 16px; }

.sched-foot { display: flex; justify-content: flex-end; padding: 0 var(--s5) var(--s5); }

@media (max-width: 560px) {
  /* TWO lines, not three:
       line 1  [toggle] Mon  [09:00]–[17:00] ×
       line 2  + Add time   Copy to all days

     The day keeps its own row and the actions span underneath it. This is what
     .sched-actions being a separate grid child buys — inside .sched-windows it
     could only ever sit in the third column, which pushed it to a line of its
     own and made the day three rows tall. */
  .sched-day { grid-template-columns: auto auto 1fr; row-gap: var(--s2); column-gap: var(--s2); }
  .sched-actions { grid-column: 1 / -1; }

  /* The times share whatever the first line has left, and they are the ONLY
     thing that gives.

     ⚠ .sched-name gets NO min-width:0 here. It had one, which let the auto
     column collapse under its own text and painted the start-time select on
     top of "Mon". Shrink the controls, never the label.

     ⚠ min-width on .sched-time is a FLOOR, not a hint. Without it flex-shrink
     happily takes the select below the width of "11:00" and the digits end up
     under the chevron — the same unreadable result, arrived at politely. */
  .sched-window { flex: 1 1 auto; min-width: 0; gap: 6px; }
  .sched-time {
    flex: 1 1 66px; width: auto; min-width: 66px; max-width: 130px;
    padding: 8px 24px 8px var(--s2);
    background-position: right 6px center; background-size: 14px;
  }
  .sched-head { flex-direction: column; }
}

/* Narrow phones: the toggle, the day name and both times cannot share one line
   without squeezing a time until it is unreadable.

   A real breakpoint, not flex-wrap — .sched-window is a single non-wrapping
   flex row, so its parts CANNOT reflow; left to itself the grid column would
   instead be forced past the card's width and bring back the horizontal page
   scroll that § 9 exists to prevent. So the times take their own line here and
   the day reads as three rows: name, times, actions. One row taller beats a
   time you cannot read, and beats a page that rocks sideways.

   383px is arithmetic, not taste. The row needs 44 (toggle) + 8 + 40 (name) +
   8 + 186 (two 66px selects, the dash, the × and their gaps) = 286px, and a
   viewport gives it `width − 48 (.wrap-narrow) − 48 (.sched-day)`. That crosses
   286 at 384px. So 390/393/412 — iPhone 12 and up, Pixel, most Samsungs — get
   two lines; 360 and 375 get three. Re-measure before moving this number. */
@media (max-width: 383px) {
  .sched-day { grid-template-columns: auto 1fr; }
  .sched-windows { grid-column: 1 / -1; }
}

/* ── 25c. RELIABILITY (.rel-*) ────────────────────────────── */
/* The second axis. Stars say whether the work was good; this says whether the
   booking happened at all, and the two are never merged — every card carries
   the star rating as a quiet footnote so they read as separate questions.

   The meter is the ONLY place in the design where green, amber and red grade
   into each other. Everywhere else green is reserved strictly for
   verified/confirmed/available (design-system.md §3.3). It earns the exception
   because a score is the one thing here that genuinely IS a scale — and the
   number beside it carries the same information for anyone who cannot see the
   colour. */

.rel-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--s3); }
.rel-title { font-family: var(--font-h); font-size: .95rem; font-weight: 600; color: var(--heading); }
.rel-score {
  font-family: var(--font-h); font-weight: 700; color: var(--heading);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.rel-score b { font-size: 1.6rem; }
.rel-score span { font-size: .9rem; color: var(--text-m); font-weight: 600; }

.rel-meter {
  height: 8px; border-radius: var(--r-full); background: var(--bg3);
  overflow: hidden; margin: var(--s3) 0 var(--s2);
}
.rel-meter i { display: block; height: 100%; border-radius: var(--r-full); background: var(--green-fill); }
.rel-meter i.is-mid { background: var(--warn-line); }
.rel-meter i.is-low { background: var(--red); }

.rel-band { display: flex; justify-content: space-between; font-size: .8rem; color: var(--text-m); }
.rel-band b { color: var(--heading); }

.rel-lines {
  display: flex; flex-direction: column; gap: var(--s2);
  margin-top: var(--s4); padding-top: var(--s4); border-top: 1px solid var(--border-s);
}
.rel-line { display: flex; align-items: baseline; gap: var(--s2); font-size: .9rem; }
.rel-line .rel-mark { flex: none; width: 1em; font-weight: 700; }
.rel-line.is-good .rel-mark { color: var(--green-ink); }
.rel-line.is-bad .rel-mark { color: var(--text-d); }
.rel-line.is-bad .rel-what { color: var(--heading); font-weight: 600; }
.rel-what { flex: 1; }
.rel-cost { color: var(--text-m); font-size: .82rem; font-variant-numeric: tabular-nums; }

/* The star rating, deliberately small and last. */
.rel-foot {
  display: flex; align-items: center; gap: var(--s2);
  margin-top: var(--s4); padding-top: var(--s3); border-top: 1px solid var(--border-s);
  font-size: .85rem; color: var(--text-m);
}
.rel-foot .ic { color: var(--star); }

/* No score yet. Never styled like a low score — "new" and "poor" must not look
   alike, which is why this branch has no meter and no number at all. */
.rel-new {
  display: flex; flex-direction: column; align-items: center; gap: var(--s2);
  padding: var(--s5) var(--s3); text-align: center; font-size: .9rem; color: var(--text-m);
}

/* Compact, for beside Accept and Decline on a pending request.
   ⚠ The label and the /100 are load-bearing, not decoration. A bare number
   next to a customer's name reads as nothing at all — the provider is about to
   accept or decline on it and has to know what it measures. */
.rel-inline {
  padding: var(--s3) var(--s4); border: 1px solid var(--border);
  border-radius: var(--r); background: var(--bg2);
}
.rel-inline-label {
  margin: 0 0 var(--s2); font-size: .6875rem; font-weight: 700;
  letter-spacing: .08em; text-transform: uppercase; color: var(--text-d);
}
.rel-inline-body { display: flex; align-items: center; gap: var(--s3); }
.rel-inline-score {
  font-family: var(--font-h); font-size: 1.5rem; font-weight: 700;
  color: var(--heading); font-variant-numeric: tabular-nums; white-space: nowrap;
}
.rel-inline-max { font-size: .8125rem; font-weight: 600; color: var(--text-m); }
.rel-inline-score.is-none { color: var(--text-d); font-size: 1.25rem; }
.rel-inline-text { flex: 1; min-width: 0; font-size: .85rem; }
.rel-inline-text b { display: block; color: var(--heading); }
.rel-inline-note {
  margin: var(--s2) 0 0; font-size: .75rem; color: var(--text-m);
}

/* ── 26. STARS / RATINGS ──────────────────────────────────── */
/* --star is 2.15:1 on white, which would fail SC 1.4.11 as a lone graphic.
   It is acceptable only because every rating is ALSO text — where a star row
   is glyph-only, the template adds an .sr-only numeric. */
.stars { display: inline-flex; align-items: center; gap: 2px; color: var(--star); }
.stars .ic { width: 16px; height: 16px; }
.rating-value { font-weight: 600; color: var(--heading); font-variant-numeric: tabular-nums; }
.star-input { display: flex; flex-direction: row-reverse; justify-content: flex-end; gap: var(--s1); }
.star-input label { cursor: pointer; color: var(--border-str); transition: color var(--tr-fast); }
.star-input label .ic { width: 32px; height: 32px; }
.star-input input { display: none; }
.star-input input:checked ~ label,
.star-input label:hover, .star-input label:hover ~ label { color: var(--star); }

/* ── 27. TABLES / LIST ROWS ───────────────────────────────── */
.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
table.data { width: 100%; border-collapse: collapse; font-size: .9375rem; min-width: 520px; }
table.data th, table.data td { padding: var(--s3) var(--s4); text-align: left; border-bottom: 1px solid var(--border-s); }
table.data th { font-size: .8125rem; font-weight: 600; color: var(--text-m); }
table.data tbody tr:hover { background: var(--bg2); }
table.data td.num { text-align: right; font-variant-numeric: tabular-nums; }

/* Softer alternative to a dense table — used for services & pricing. */
.list-rows { display: flex; flex-direction: column; }
.list-row {
  display: flex; justify-content: space-between; align-items: baseline; gap: var(--s4);
  padding: var(--s3) 0; border-bottom: 1px solid var(--border-s);
}
.list-row:last-child { border-bottom: 0; }
.list-row-price { font-weight: 600; color: var(--heading); white-space: nowrap; font-variant-numeric: tabular-nums; }

/* A price row that also carries a Book button (provider profile → Services).
   The price and the button stack on a phone, where side by side would push the
   service name down to two lines, and sit on one line from 480px up. `baseline`
   is wrong once a button is in the row — it hangs the button off the price's
   text baseline — so this overrides it to `center`. */
.list-row.svc-book { align-items: center; }
.svc-book-end {
  display: flex; flex-direction: column; align-items: flex-end; gap: var(--s2);
  flex: none;
}
@media (min-width: 480px) {
  .svc-book-end { flex-direction: row; align-items: center; gap: var(--s3); }
}

/* ── 28. EMPTY STATES ─────────────────────────────────────── */
.empty { text-align: center; padding: var(--s8) var(--s5); color: var(--text-m); }
.empty-icon {
  width: 72px; height: 72px; border-radius: 50%; margin: 0 auto var(--s4);
  display: grid; place-items: center;
  background: var(--primary-soft); color: var(--primary-ink);
}
.empty-icon .ic { width: 32px; height: 32px; }

/* ── 29. FLASH MESSAGES ───────────────────────────────────── */
.flash-wrap { position: fixed; top: 80px; right: var(--s4); left: var(--s4); z-index: var(--z-flash); display: flex; flex-direction: column; gap: var(--s2); pointer-events: none; }
@media (min-width: 620px) { .flash-wrap { left: auto; width: 380px; } }
.flash {
  pointer-events: auto; display: flex; gap: var(--s3); align-items: flex-start;
  background: var(--surface); border: 1px solid var(--border); border-left: 4px solid var(--text-d);
  border-radius: var(--r); padding: var(--s3) var(--s4); box-shadow: var(--sh-md); font-size: .9375rem;
  animation: flash-in var(--tr-slow) cubic-bezier(.4,0,.2,1);
}
@keyframes flash-in { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: none; } }
.flash-success { border-left-color: var(--green); }
.flash-error   { border-left-color: var(--red); }
.flash-warning { border-left-color: var(--warn-line); }
.flash-info    { border-left-color: var(--blue); }
.flash-close { background: none; border: 0; line-height: 1; color: var(--text-d); margin-left: auto; padding: 2px; border-radius: var(--r-sm); }
.flash-close:hover { color: var(--heading); }

/* ── 30. FOOTER ───────────────────────────────────────────── */
.foot { border-top: 1px solid var(--border); background: var(--bg2); margin-top: var(--s8); }
/* THREE columns on a phone, not one.
   Stacked, the three link groups cost ~330px of scrolling to show nine short
   words — the longest is "My services". Side by side they cost ~120px, and the
   brand line keeps the full width above them because it is a sentence. */
.foot-inner {
  max-width: var(--wrap); margin: 0 auto; padding: var(--s6) var(--s5);
  display: grid; gap: var(--s5) var(--s3);
  grid-template-columns: repeat(3, 1fr);
}
.foot-brand { grid-column: 1 / -1; }
@media (min-width: 700px) {
  .foot-inner { grid-template-columns: 2fr 1fr 1fr 1fr; gap: var(--s6); }
  .foot-brand { grid-column: auto; }
}
.foot-col { display: flex; flex-direction: column; }
.foot-col h4 {
  font-size: .8125rem; font-family: var(--font-h); font-weight: 600;
  letter-spacing: .02em; color: var(--heading); margin-bottom: var(--s2);
}
/* ⚠ The vertical rhythm is PADDING, not gap. Same pitch either way, but
   padding is part of the link, so the tap target is ~35px instead of ~21px.
   Density comes out of the space between controls, never out of the control. */
.foot-col a {
  font-size: .875rem; line-height: 1.35; color: var(--text-m);
  padding: 7px 0; transition: color var(--tr-fast);
}
.foot-col a:hover { color: var(--primary-ink); }
.foot-blurb { font-size: .875rem; color: var(--text-m); max-width: 36ch; }
@media (min-width: 700px) {
  .foot-col h4, .foot-col a, .foot-blurb { font-size: .9375rem; }
  .foot-col a { padding: 3px 0; }
}
.foot-bar {
  border-top: 1px solid var(--border); padding: var(--s4) var(--s5);
  max-width: var(--wrap); margin: 0 auto;
  display: flex; justify-content: space-between; align-items: center; gap: var(--s3);
  font-size: .875rem; color: var(--text-m); flex-wrap: wrap;
}
.lang-select {
  background: var(--surface); color: var(--text); border: 1px solid var(--border);
  border-radius: var(--r); padding: 8px var(--s3); font: inherit; font-size: .875rem;
}

/* ── 31. STAFF PANEL (.adm-*) ─────────────────────────────── */
/* The console shell. It inherits the site palette rather than having its own —
   .adm-stat is REUSED by accounts/provider_dashboard.html, a customer-facing
   page, so this section can never become panel-private.

   panel/base.html hardcodes data-theme="light": there is no toggle here, and
   without it a staff member on a dark OS would get dark tokens on a console
   never designed against them. */
.adm-shell { display: grid; grid-template-columns: 1fr; min-height: 100vh; }
@media (min-width: 900px) { .adm-shell { grid-template-columns: 232px 1fr; } }

/* ── sidebar ── */
.adm-side {
  background: var(--bg2); border-right: 1px solid var(--border);
  padding: var(--s4) var(--s3); overflow-y: auto;
}
.adm-brand {
  display: flex; align-items: center; gap: var(--s2);
  padding: 0 var(--s2) var(--s3);
  font-family: var(--font-h); font-weight: 700; font-size: 1rem; color: var(--heading);
}
.adm-brand:hover { color: var(--primary-ink); }
/* The group label is the whole point of the redesign: ten equal links said
   nothing about which were work waiting and which were reference. */
.adm-grp {
  margin: var(--s4) 0 var(--s1); padding: 0 10px;
  font-size: .625rem; font-weight: 700; letter-spacing: .12em;
  text-transform: uppercase; color: var(--text-d);
}
.adm-side a {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s2);
  padding: 7px 10px; border-radius: var(--r-sm);
  font-size: .875rem; color: var(--text-m);
}
.adm-side a:hover { background: var(--bg3); color: var(--primary-ink); }
.adm-side a.on { background: var(--primary-soft); color: var(--primary-ink); font-weight: 600; }
.adm-side hr { border: 0; border-top: 1px solid var(--border); margin: var(--s4) var(--s1); }
.adm-badge {
  background: var(--primary-fill); color: var(--primary-on);
  border-radius: var(--r-full); padding: 0 7px;
  font-size: .6875rem; font-weight: 700; min-width: 20px; text-align: center;
  font-variant-numeric: tabular-nums;
}
/* Portfolio is a QUEUE, not an alarm — amber, so a moderation backlog does not
   read with the same urgency as an open dispute. */
.adm-badge.warn { background: var(--warn-line); color: #3f2d06; }
/* Errors are the one thing here that is not somebody waiting on staff — it is
   the software failing for a customer right now. Red is reserved for it, and
   nothing else in the sidebar may use this. */
.adm-badge.danger { background: var(--red); color: #fff; }

/* A traceback is much wider than the panel column. It scrolls INSIDE its own
   box — without this the whole page scrolls sideways on every screen it is read
   on, which is the one place that rule is always worth enforcing. */
.adm-trace { overflow-x: auto; }
.adm-trace pre {
  margin: 0; font-family: var(--font-mono); font-size: .75rem; line-height: 1.65;
  white-space: pre; color: var(--text);
}

/* ── top bar ── */
.adm-main { display: flex; flex-direction: column; min-width: 0; }
.adm-top {
  display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap;
  padding: var(--s3) var(--s5);
  background: var(--surface); border-bottom: 1px solid var(--border);
  position: sticky; top: 0; z-index: var(--z-nav);
}
.adm-top-spacer { flex: 1 1 auto; }
.adm-crumbs { display: flex; align-items: center; gap: 6px; font-size: .8125rem; color: var(--text-d); }
.adm-crumbs a { color: var(--text-m); }
.adm-crumbs a:hover { color: var(--primary-ink); }
.adm-crumbs b { color: var(--heading); font-weight: 600; }
.adm-search { position: relative; flex: 0 1 300px; min-width: 180px; }
.adm-search .ic {
  position: absolute; left: 11px; top: 50%; transform: translateY(-50%);
  color: var(--text-d); pointer-events: none;
}
/* Not --ctl-h: the top bar is a desktop console strip, and a 48px control here
   pushes the page content down on every screen. The 40px --ctl-h-sm is still a
   real target, and this row is never the primary action on a phone. */
.adm-search .form-input { padding: 8px var(--s3) 8px 34px; min-height: var(--ctl-h-sm); font-size: .875rem; }
.adm-who { display: flex; align-items: center; gap: var(--s2); font-size: .8125rem; color: var(--text-m); }
.adm-av {
  width: 28px; height: 28px; flex: 0 0 28px; border-radius: 50%;
  background: var(--bg3); color: var(--primary-ink);
  display: grid; place-items: center;
  font-family: var(--font-h); font-weight: 700; font-size: .75rem;
}
.adm-content { padding: var(--s5); flex: 1; min-width: 0; }

/* ── mobile drawer ── */
/* Off-screen, not `hidden`: it stays keyboard-focusable, and absolute keeps it
   out of the grid so it never becomes a phantom track. */
.adm-drawer-t { position: absolute; opacity: 0; width: 1px; height: 1px; }
.adm-burger, .adm-scrim { display: none; }
@media (max-width: 899px) {
  .adm-burger {
    display: grid; place-items: center;
    width: var(--ctl-h-sm); height: var(--ctl-h-sm); flex: 0 0 auto;
    border-radius: var(--r-sm); color: var(--text-m); cursor: pointer;
  }
  .adm-burger:hover { background: var(--bg2); color: var(--primary-ink); }
  /* ⚠ Opened by TRANSFORM, never by `display`. A component rule declared later
     can win a `display` race against a utility — that is the incident in § 9 —
     and transform has no such competitor. It also animates. */
  .adm-side {
    position: fixed; top: 0; bottom: 0; left: 0; width: 250px;
    z-index: var(--z-drawer); transform: translateX(-100%);
    transition: transform var(--tr-slow); box-shadow: var(--sh-lg);
  }
  .adm-drawer-t:checked ~ .adm-side { transform: none; }
  .adm-drawer-t:checked ~ .adm-scrim {
    display: block; position: fixed; inset: 0;
    background: var(--scrim); z-index: var(--z-scrim);
  }
  .adm-drawer-t:focus-visible ~ .adm-side { outline: 2px solid var(--primary); }
  .adm-content { padding: var(--s4); }
  .adm-top { padding: var(--s3) var(--s4); }
  .adm-who-name { display: none; }
}

/* ── page furniture ── */
.adm-head { display: flex; align-items: flex-start; gap: var(--s3); flex-wrap: wrap; }
.adm-head-spacer { flex: 1 1 auto; }
.adm-sub { font-size: .875rem; color: var(--text-m); margin: var(--s1) 0 var(--s5); max-width: 76ch; }
.adm-stat { background: var(--surface); border: 1px solid var(--border); border-radius: var(--r-lg); padding: var(--s4) var(--s5); box-shadow: var(--sh); }
.adm-stat .n { font-family: var(--font-h); font-weight: 700; font-size: 1.75rem; color: var(--heading); font-variant-numeric: tabular-nums; }
.adm-stat .l { font-size: .8125rem; color: var(--text-m); }
/* A compact 4-up strip. --ctl-h-sm padding, not --s5: these are read, not tapped. */
.adm-stats { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--s2); margin-bottom: var(--s4); }
@media (min-width: 700px) { .adm-stats { grid-template-columns: repeat(4, 1fr); } }
.adm-stats .adm-stat { padding: 10px var(--s4); box-shadow: none; }
.adm-stats .adm-stat .n { font-size: 1.3125rem; line-height: 1.2; }
.adm-stats .adm-stat .l { font-size: .6875rem; }

/* The filter strip that sits above a list. */
.adm-tools {
  display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap;
  padding: 10px var(--s3); margin-bottom: var(--s4);
  background: var(--bg2); border: 1px solid var(--border); border-radius: var(--r);
}
.adm-tools-spacer { flex: 1 1 auto; }
.adm-tools .form-input, .adm-tools .form-select { min-height: var(--ctl-h-sm); padding: 7px var(--s3); font-size: .875rem; }
.adm-tools .form-select { padding-right: var(--s7); background-position: right 10px center; }

/* Segmented filter built from real LINKS, not a scripted toggle: each is a GET
   carrying the current category and query, so a filtered view is a URL staff
   can keep and paste into a ticket. */
.seg-links { display: flex; padding: 3px; gap: 2px; border-radius: 9px; background: var(--surface); border: 1px solid var(--border); }
.seg-links a { padding: 4px 11px; border-radius: 7px; font-size: .8125rem; color: var(--text-m); }
.seg-links a:hover { color: var(--primary-ink); }
.seg-links a.on { background: var(--primary-soft); color: var(--primary-ink); font-weight: 600; }

.row-end { display: flex; gap: var(--s1); justify-content: flex-end; }

/* ── 31b. CATALOG MASTER–DETAIL (.cat-*) ──────────────────── */
/* One column on a phone — the category list, then the services below it. Two
   from 900px, where the list can stay put while the table scrolls. */
.cat-split { display: grid; grid-template-columns: 1fr; gap: var(--s4); }
@media (min-width: 900px) { .cat-split { grid-template-columns: 264px 1fr; align-items: start; } }

.cat-list {
  background: var(--bg2); border: 1px solid var(--border);
  border-radius: var(--r-lg); padding: var(--s2);
}
/* Sticky so the STRUCTURE of the catalog never scrolls away — the single
   reason this layout was chosen over stacked tables. Same 88px the rest of the
   site uses to clear a sticky bar; here it clears the panel's own top bar. */
@media (min-width: 900px) { .cat-list { position: sticky; top: 88px; } }

.cat-row { display: flex; align-items: center; gap: var(--s1); border-radius: var(--r); }
.cat-row.on { background: var(--surface); box-shadow: var(--sh-sm); }
.cat-link {
  display: flex; align-items: center; gap: var(--s2);
  flex: 1; min-width: 0; padding: 9px var(--s2); border-radius: var(--r); color: var(--text);
}
.cat-row:not(.on) .cat-link:hover { background: var(--bg3); }
.cat-text { flex: 1; min-width: 0; }
.cat-text b { display: block; font-size: .875rem; font-weight: 600; color: var(--heading); }
.cat-text span { font-size: .75rem; color: var(--text-m); }
.cat-mark {
  width: 30px; height: 30px; flex: 0 0 30px; border-radius: var(--r-sm);
  display: grid; place-items: center;
  background: var(--bg3); color: var(--primary-ink);
}
.cat-mark-lg { width: 36px; height: 36px; flex-basis: 36px; vertical-align: -9px; margin-right: var(--s1); }

/* Reorder arrows. Two separate forms, and the DIRECTION IS IN THE URL — a
   <button name="direction"> posts nothing here, because initSubmitGuard
   disables it before the browser serialises the form. */
.cat-move { display: flex; flex-direction: column; gap: 1px; }
.cat-arrow {
  display: block; width: 20px; height: 15px; padding: 0; line-height: 1;
  border: 0; border-radius: 4px; cursor: pointer;
  background: transparent; color: var(--text-d); font-size: .5rem;
}
.cat-arrow:hover:not(:disabled) { background: var(--primary-soft); color: var(--primary-ink); }
.cat-arrow:disabled { opacity: .3; cursor: default; }
.cat-add {
  display: block; margin-top: var(--s2); padding: 9px;
  border: 1px dashed var(--border-str); border-radius: var(--r);
  text-align: center; font-size: .8125rem; color: var(--primary-ink);
}
.cat-add:hover { background: var(--surface); border-color: var(--primary); }

.cat-detail { min-width: 0; }
.cat-meta { font-size: .8125rem; color: var(--text-m); margin-top: var(--s1); }
.cat-meta code { font-family: var(--font-mono); font-size: .75rem; color: var(--text-d); }
.svc-name { color: var(--heading); font-weight: 600; }
.svc-slug { font-family: var(--font-mono); font-size: .6875rem; color: var(--text-d); }
/* Delivery modes under the service name. The two physical ones are quiet grey
   text — they are the norm — so the blue "online" tag is what the eye picks out
   scanning a column of 14 services, which is exactly the question being asked. */
.svc-modes { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; margin-top: 2px; }
.svc-mode {
  padding: 0 6px; border-radius: var(--r-full);
  background: var(--bg2); color: var(--text-m);
  font-size: .6875rem; line-height: 1.6; white-space: nowrap;
}
.svc-modes .tag-online { margin-left: 0; }

/* ── 31c. ICON PICKER (.icon-*) ───────────────────────────── */
/* A grid of the real drawings, because picking a picture by reading the word
   for it is the exact thing a picker exists to avoid.

   The radio is visually hidden but NOT display:none — it has to stay focusable,
   and the whole tile is its label. Checked state is drawn with `:checked +`,
   so this needs no JavaScript at all. */
.icon-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
  gap: var(--s2); margin-top: var(--s1);
}
.icon-opt { display: block; cursor: pointer; }
/* Selected on the ELEMENT, not a class: the input is Django's own
   `{{ choice.tag }}`, which carries no class of ours to hook. */
.icon-opt input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.icon-tile {
  display: flex; flex-direction: column; align-items: center; gap: 5px;
  padding: 10px 6px; border-radius: var(--r);
  border: 1px solid var(--border); background: var(--surface); color: var(--text-m);
  transition: border-color var(--tr), background var(--tr), color var(--tr);
}
.icon-tile .ic { width: 22px; height: 22px; }
.icon-tile-name {
  font-size: .625rem; color: var(--text-d); text-align: center; line-height: 1.25;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%;
}
.icon-opt:hover .icon-tile { border-color: var(--primary); color: var(--primary-ink); }
.icon-opt input:checked + .icon-tile {
  border-color: var(--primary); background: var(--primary-soft); color: var(--primary-ink);
  box-shadow: 0 0 0 2px var(--primary-soft);
}
.icon-opt input:checked + .icon-tile .icon-tile-name { color: var(--primary-soft-ink); }
/* Focus lands on the hidden input, so the ring has to be drawn on the tile —
   otherwise keyboard users tab through an invisible control. */
.icon-opt input:focus-visible + .icon-tile { outline: 2px solid var(--focus); outline-offset: 2px; }

/* ── 31d. ERASURE (.erase-*) ──────────────────────────────── */
/* Its own visual language, and deliberately not a card. Every other panel
   action is reversible; this one is not, and the screen should not look like
   the screens that are. */
.erase-panel {
  max-width: 720px; border: 1px solid var(--red); border-radius: var(--r-lg);
  overflow: hidden; background: var(--surface);
}
.erase-head {
  display: flex; align-items: center; gap: var(--s2);
  padding: 11px var(--s4); background: var(--red-soft);
  border-bottom: 1px solid var(--red); color: var(--red);
}
.erase-head b { font-family: var(--font-h); font-size: .875rem; }
.erase-body { padding: var(--s4); }
.erase-body > p { font-size: .9375rem; margin: 0 0 var(--s4); }

.erase-list { border: 1px solid var(--border); border-radius: var(--r); overflow: hidden; margin-bottom: var(--s4); }
.erase-list-h {
  margin: 0; padding: 8px var(--s3); background: var(--bg2);
  border-bottom: 1px solid var(--border);
  font-size: .6875rem; font-weight: 700; letter-spacing: .08em;
  text-transform: uppercase; color: var(--text-m);
}
.erase-row {
  display: flex; align-items: center; gap: var(--s3);
  padding: 7px var(--s3); border-bottom: 1px solid var(--border-s); font-size: .875rem;
}
.erase-row:last-child { border-bottom: 0; }
.erase-row .n {
  font-family: var(--font-mono); font-size: .8125rem; font-weight: 600;
  color: var(--heading); min-width: 36px; text-align: right;
  font-variant-numeric: tabular-nums; flex: 0 0 auto;
}
.erase-row .w { flex: 1; min-width: 0; }
.erase-row .fate { font-size: .75rem; flex: 0 0 auto; }
.erase-row .fate.gone { color: var(--red); }
.erase-row .fate.kept { color: var(--green-ink); }

.erase-confirm {
  background: var(--warn-soft); border: 1px solid var(--warn-line);
  border-radius: var(--r); padding: var(--s3);
}
.erase-confirm .form-label { color: var(--warn); margin-bottom: var(--s2); }
.erase-confirm code {
  font-family: var(--font-mono); font-size: .8125rem;
  background: rgba(255,255,255,.7); padding: 1px 6px; border-radius: 5px;
}
.erase-confirm .form-input { background: var(--surface); }

/* ── 32. PRINT (receipt) ──────────────────────────────────── */
/* ── 33. MESSAGING (.msg-*, .conv-*) ──────────────────────── */
/* Bubble alignment is flexbox align-self, never floats or margins — which is
   why bubbles size to their content and wrap on a phone with no media query.
   Note there is deliberately NO `.msg-typing[hidden]` rule here: the global
   `[hidden] { display: none !important }` in §3 already wins over the
   `display: flex` below. Removing that global rule breaks this. */

/* Inbox rows */
.conv-row-unread { border-color: var(--primary); }
.conv-context { display: flex; align-items: center; gap: var(--s2); margin-top: var(--s2); flex-wrap: wrap; }
.conv-new {
  background: var(--primary-fill); color: var(--primary-on);
  font-size: .6875rem; font-weight: 600; letter-spacing: .02em;
  padding: 2px var(--s2); border-radius: var(--r-full);
}
.conv-preview {
  margin-top: var(--s2);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* Thread header */
.conv-header { display: flex; align-items: center; gap: var(--s3); margin: var(--s4) 0 var(--s5); }
.conv-avatar {
  width: 48px; height: 48px; flex: 0 0 48px;
  border-radius: 50%; border: 1px solid var(--border);
  background: var(--bg2); color: var(--primary-ink);
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-h); font-weight: 700; font-size: 1.125rem;
}
.conv-header-text { min-width: 0; display: flex; flex-direction: column; gap: var(--s1); align-items: flex-start; }
.conv-name { font-family: var(--font-h); font-weight: 600; font-size: 1.0625rem; color: var(--heading); }

/* Thread */
.msg-thread { display: flex; flex-direction: column; gap: var(--s3); padding: var(--s2) 0; }
.msg { max-width: 72%; padding: var(--s3) var(--s4); border-radius: var(--r-lg); font-size: .9375rem; line-height: 1.5; }
/* pre-wrap keeps the sender's own line breaks (a TextField preserves \n, HTML
   would collapse them); break-word stops a 200-char URL blowing out the page. */
.msg-body { margin: 0; white-space: pre-wrap; word-break: break-word; }
.msg-mine {
  align-self: flex-end;
  background: var(--primary-fill); color: var(--primary-on);
  border-bottom-right-radius: var(--r-sm);
}
.msg-theirs {
  align-self: flex-start;
  background: var(--bg2); border: 1px solid var(--border-s); color: var(--text);
  border-bottom-left-radius: var(--r-sm);
}
.msg-meta { font-size: .6875rem; letter-spacing: .04em; margin-top: var(--s1); opacity: .75; }

/* The booking request card that opens every thread. A card, not a bubble, so
   nobody reads it as something the other party typed. */
.msg-system {
  align-self: stretch;
  background: var(--bg2); border: 1px dashed var(--border-str);
  border-radius: var(--r-lg); padding: var(--s3) var(--s4);
  font-size: .875rem; color: var(--text-m);
}
.msg-system-label {
  font-family: var(--font-h); font-weight: 600; font-size: .6875rem;
  letter-spacing: .08em; text-transform: uppercase;
  color: var(--text-d); margin-bottom: var(--s2);
}

/* Read receipt — appended into the meta line of the LAST own bubble only, so
   it is always correctly positioned and stays quiet. Swedish users expect this
   less than WhatsApp users; it is a footnote, not a pair of blue ticks. */
.msg-receipt { opacity: .9; }

.msg-empty { text-align: center; margin: var(--s6) 0; }

/* Typing indicator */
.msg-typing { display: flex; align-items: center; gap: var(--s2); margin-top: var(--s3); font-size: .75rem; color: var(--text-m); }
.msg-typing-dots { display: inline-flex; gap: 3px; }
.msg-typing-dots span {
  width: 6px; height: 6px; border-radius: 50%; background: var(--text-d);
  animation: msgTypingBounce 1s infinite ease-in-out;
}
.msg-typing-dots span:nth-child(2) { animation-delay: .15s; }
.msg-typing-dots span:nth-child(3) { animation-delay: .30s; }
@keyframes msgTypingBounce {
  0%, 60%, 100% { transform: translateY(0); opacity: .4; }
  30%           { transform: translateY(-4px); opacity: 1; }
}

/* Quick replies */
.msg-quick { margin-top: var(--s4); }
.msg-quick-label { font-size: .75rem; color: var(--text-d); }
.msg-quick-chips { display: flex; flex-wrap: wrap; gap: var(--s2); margin-top: var(--s2); }
.msg-quick-chip {
  background: var(--surface); border: 1px solid var(--border-str);
  color: var(--text); border-radius: var(--r-full);
  padding: var(--s2) var(--s3); font-size: .8125rem; cursor: pointer;
  transition: background var(--tr-fast), border-color var(--tr-fast);
}
.msg-quick-chip:hover { background: var(--bg2); border-color: var(--primary); }

/* Compose */
.msg-form { margin-top: var(--s4); display: flex; flex-direction: column; gap: var(--s3); }
.msg-form-actions { display: flex; justify-content: flex-end; }

/* Entry point from the booking page. A full-width row, not a button: it is a
   way in, not a competitor to Accept/Decline. */
.msg-cta {
  display: flex; align-items: center; gap: var(--s3);
  padding: var(--s3) var(--s4);
  background: var(--surface); border: 1px solid var(--border-str);
  border-radius: var(--r-lg); color: var(--text); text-decoration: none;
  transition: border-color var(--tr-fast), background var(--tr-fast);
}
.msg-cta:hover { border-color: var(--primary); background: var(--bg2); }
.msg-cta-icon { color: var(--primary-ink); display: flex; flex: 0 0 auto; }
.msg-cta-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
.msg-cta-text .t-dim { font-size: .8125rem; }

@media (max-width: 640px) {
  .msg { max-width: 86%; }
}

/* ── 34. WEEK CALENDAR (.cal-*) ───────────────────────────── */
/* Blocks are positioned by inline top/height percentages computed in
   apps/accounts/calendar.py — a VALUE, not a rule, so no selector here ever
   contains a date and the whole calendar stays in this one stylesheet. */

.cal {
  border: 1px solid var(--border); border-radius: var(--r-lg);
  background: var(--surface); overflow: hidden;
  /* Every view/week link carries #cal so changing the view returns you to the
     calendar instead of the top of the dashboard. The nav is position:sticky,
     so without this offset the anchor jump parks the calendar's own toolbar
     underneath it — 88px matches the other sticky offsets in this file. */
  scroll-margin-top: 88px;
}
.cal-bar { display: flex; align-items: center; gap: var(--s2); padding: var(--s3) var(--s4);
  border-bottom: 1px solid var(--border-s); flex-wrap: wrap; }
.cal-views { display: inline-flex; background: var(--bg2); border: 1px solid var(--border);
  border-radius: var(--r-full); padding: 3px; }
.cal-view { padding: 5px 13px; border-radius: var(--r-full); font-size: .8125rem;
  font-weight: 600; color: var(--text-m); text-decoration: none; }
.cal-view.is-on { background: var(--surface); color: var(--primary-ink); box-shadow: var(--sh-sm); }
.cal-when { display: flex; align-items: center; gap: var(--s1); margin-left: auto; }
.cal-step { width: 30px; height: 30px; border-radius: var(--r-sm); border: 1px solid var(--border);
  color: var(--text-m); text-decoration: none; display: grid; place-items: center; }
.cal-step:hover { border-color: var(--primary); color: var(--primary-ink); }
.cal-range { font-family: var(--font-h); font-weight: 600; font-size: .875rem;
  color: var(--heading); padding: 0 var(--s2); white-space: nowrap; }
.cal-today { font-size: .8125rem; font-weight: 600; color: var(--primary-ink); text-decoration: none; }

/* Seven columns are wider than a phone, so the GRID scrolls — never the page. */
.cal-scroll { overflow-x: auto; scroll-snap-type: x proximity; }
.cal-grid { min-width: 620px; }
.cal-grid.is-day { min-width: 0; }
.cal-head, .cal-body { display: grid; grid-template-columns: 46px repeat(var(--cal-days), minmax(78px, 1fr)); }
.cal-grid.is-day .cal-head, .cal-grid.is-day .cal-body { grid-template-columns: 46px 1fr; }
.cal-head { border-bottom: 1px solid var(--border-s); position: sticky; top: 0;
  background: var(--surface); z-index: 2; }
.cal-hcell { padding: var(--s2) 0; text-align: center; scroll-snap-align: start; }
.cal-dow { display: block; font-size: .625rem; font-weight: 700; letter-spacing: .07em;
  text-transform: uppercase; color: var(--text-d); }
.cal-date { display: block; font-family: var(--font-h); font-weight: 700;
  font-size: .9375rem; color: var(--heading); }
.cal-hcell.is-today { background: var(--green-soft); }
.cal-hcell.is-today .cal-dow, .cal-hcell.is-today .cal-date { color: var(--green-ink); }
.cal-hcell.is-off { background: var(--bg2); }

.cal-body { position: relative; }
.cal-hours { display: grid; }
.cal-hours span { height: 44px; font-size: .625rem; color: var(--text-d);
  text-align: right; padding-right: 6px; transform: translateY(-6px);
  font-variant-numeric: tabular-nums; }
.cal-col { position: relative; border-left: 1px solid var(--border-s); scroll-snap-align: start; }
.cal-col::before { content: ""; position: absolute; inset: 0;
  background: repeating-linear-gradient(to bottom, transparent 0 43px, var(--border-s) 43px 44px); }
.cal-col.is-today { background: rgba(34, 197, 94, .035); }
.cal-col.is-off::before { background: repeating-linear-gradient(135deg, var(--bg2) 0 7px, transparent 7px 14px); }
.cal-off { position: absolute; top: 50%; left: 0; right: 0; transform: translateY(-50%);
  text-align: center; font-size: .625rem; color: var(--text-d); pointer-events: none; }

.cal-blk { position: absolute; border-radius: var(--r-sm); padding: 4px 6px;
  overflow: hidden; text-decoration: none; transition: transform var(--tr-fast), box-shadow var(--tr-fast); }
.cal-blk:hover { transform: translateY(-1px); box-shadow: var(--sh-md); z-index: 3; }
.cal-blk:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; z-index: 3; }
.cal-blk-t { display: block; font-size: .6875rem; font-weight: 700; font-variant-numeric: tabular-nums; }
.cal-blk-s { display: block; font-size: .6875rem; font-weight: 600; line-height: 1.25; }
.cal-blk-c { display: block; font-size: .625rem; opacity: .85; }
.cal-blk-live { display: inline-block; margin-top: 2px; background: rgba(255,255,255,.22);
  color: #fff; font-size: .5625rem; font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; padding: 1px 6px; border-radius: var(--r-full); }

/* ⚠ Weight carries certainty. A confirmed job is SOLID; a request that nobody
   has accepted is lighter AND dashed, so "not booked yet" is said by the border
   style before any colour is read — which means it survives colour blindness,
   greyscale and a phone in bright sun. Do not make these two equal weight. */
.cal-blk.is-conf { background: var(--green-soft); color: var(--green-ink);
  border: 1px solid transparent; border-left: 3px solid var(--green-ink); }
.cal-blk.is-pend { background: var(--bg3); color: var(--primary-soft-ink);
  border: 1px dashed var(--primary); }
.cal-blk.is-live { background: var(--green-fill); color: #fff; border: 1px solid var(--green-fill); }
.cal-blk.is-done { background: var(--bg2); color: var(--text-d); border: 1px solid var(--border-s); }

/* The now line. Red is unambiguous here: terminal bookings (declined, expired,
   cancelled) are never drawn in this grid, so no block is ever red. */
.cal-now { position: absolute; left: 46px; right: 0; height: 0; z-index: 4; pointer-events: none;
  border-top: 2px solid var(--red); }
.cal-now-dot { position: absolute; left: -4px; top: -5px; width: 8px; height: 8px;
  border-radius: 50%; background: var(--red); }
.cal-now-time { position: absolute; right: calc(100% + 6px); top: -9px; font-size: .625rem;
  font-weight: 700; color: var(--red); background: var(--surface); padding: 0 2px;
  font-variant-numeric: tabular-nums; }

.cal-foot { display: flex; gap: var(--s4); flex-wrap: wrap; padding: var(--s3) var(--s4);
  border-top: 1px solid var(--border-s); font-size: .6875rem; color: var(--text-m); }
.cal-key i { width: 10px; height: 10px; border-radius: 3px; display: inline-block;
  margin-right: 5px; vertical-align: -1px; }
.k-conf { background: var(--green-soft); border-left: 2px solid var(--green-ink); }
.k-pend { background: var(--bg3); border: 1px dashed var(--primary); }
.k-live { background: var(--green-fill); }
.k-off { background: repeating-linear-gradient(135deg, var(--border) 0 3px, transparent 3px 6px); }

/* Action sheet. Toggled by the `hidden` attribute only — the global
   [hidden]{display:none!important} in §3 is what closes it. */
.cal-sheet { position: fixed; inset: 0; z-index: var(--z-flash);
  background: var(--scrim); display: flex; align-items: flex-end; justify-content: center; }
.cal-sheet-inner { position: relative; width: min(440px, 100%); background: var(--surface);
  border: 1px solid var(--border); border-bottom: 0; border-radius: var(--r-lg) var(--r-lg) 0 0;
  padding: var(--s4) var(--s4) var(--s6); box-shadow: var(--sh-lg);
  max-height: 88vh; overflow-y: auto; animation: calSheetUp var(--tr-slow) both; }
@keyframes calSheetUp { from { transform: translateY(100%); } to { transform: none; } }
.cal-sheet-grip { display: block; width: 36px; height: 4px; border-radius: 2px;
  background: var(--border-str); margin: 0 auto var(--s3); }
.cal-sheet-x { position: absolute; top: var(--s4); right: var(--s4); width: 28px; height: 28px;
  border-radius: 50%; border: 1px solid var(--border); background: var(--surface);
  color: var(--text-m); cursor: pointer; }
.cal-sheet-chip { display: inline-block; font-size: .6875rem; font-weight: 700;
  padding: 3px 10px; border-radius: var(--r-full); }
.cal-sheet-chip.is-conf, .cal-sheet-chip.is-done { background: var(--green-soft); color: var(--green-ink); }
.cal-sheet-chip.is-pend { background: var(--primary-soft); color: var(--primary-soft-ink); }
.cal-sheet-chip.is-live { background: var(--green-fill); color: #fff; }
.cal-sheet-title { font-family: var(--font-h); font-size: 1.0625rem; color: var(--heading); margin: var(--s2) 0 0; }
.cal-sheet-when, .cal-sheet-who { font-size: .875rem; color: var(--text-m); margin: 2px 0 0; }
.cal-sheet-acts { display: flex; flex-direction: column; gap: var(--s2); margin-top: var(--s4); }
.cal-sheet-acts form { display: contents; }

/* ── 35. REQUEST CARD (.req-*) ────────────────────────────── */
/* "Header band": who is asking, then what for. The band carries the same soft
   pink that means "awaiting your answer" everywhere else, so the card states
   its own status before a word is read. */
/* ⚠ ONE ROW, never a wrapping grid. Requests are a queue; a grid pushes the
   oldest one below the fold and makes the count in the heading the only way to
   know it exists. Horizontal scroll with snap, on phone and desktop alike. */
.req-list {
  display: flex; gap: var(--s3);
  overflow-x: auto; overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory; scroll-behavior: smooth;
  /* Room for the scrollbar and for the card's own shadow, so neither is clipped. */
  padding: 2px 2px var(--s2);
  scrollbar-width: thin;
}
.req-list > * {
  /* One expression for both: on a 390px phone this is ~320px, so the next card
     peeks and the row is visibly scrollable; on a wide screen it settles at
     340px and several fit. */
  flex: 0 0 min(82%, 340px);
  scroll-snap-align: start;
}
.req-list:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

.req-rail-nav { display: flex; align-items: center; gap: var(--s2); }
.req-step {
  width: 30px; height: 30px; border-radius: var(--r-sm); cursor: pointer;
  border: 1px solid var(--border); background: var(--surface); color: var(--text-m);
  display: none; place-items: center; font-size: 15px; line-height: 1;
}
.req-step:hover { border-color: var(--primary); color: var(--primary-ink); }
.req-step:disabled { opacity: .35; cursor: default; border-color: var(--border); color: var(--text-d); }
/* Shown only when the row actually overflows AND the pointer is one that cannot
   swipe — a touch user scrolls with a finger and does not need them. */
@media (pointer: fine) {
  .req-rail.is-scrollable .req-step { display: grid; }
  .req-rail.is-scrollable .req-rail-hint { display: none; }
}
.req-count { display: inline-flex; align-items: center; justify-content: center;
  min-width: 22px; height: 22px; padding: 0 6px; border-radius: var(--r-full);
  background: var(--primary-fill); color: var(--primary-on);
  font-family: var(--font-body); font-size: .75rem; font-weight: 700; vertical-align: middle; }

.req { border: 1px dashed var(--primary); border-radius: var(--r-lg);
  background: var(--surface); overflow: hidden; }
.req-band { display: flex; align-items: center; gap: var(--s3);
  padding: var(--s3) var(--s4); background: var(--primary-soft); }
.req-av { width: 56px; height: 56px; flex: 0 0 56px; border-radius: 50%;
  object-fit: cover; border: 2px solid var(--surface); box-shadow: 0 0 0 1px var(--border); }
/* The monogram is the design, not a placeholder: registration never asks for a
   photo, so this is the common case and must look deliberate. */
.req-av-mono { display: grid; place-items: center; background: var(--bg3);
  color: var(--primary-ink); font-family: var(--font-h); font-weight: 700; font-size: 1.375rem; }
.req-who { flex: 1; min-width: 0; }
.req-name { display: block; font-weight: 600; font-size: .9375rem; color: var(--primary-soft-ink); }
.req-prof { display: block; font-size: .75rem; color: var(--text-m); }
.req-prof b { color: var(--green-ink); }
.req-prof.is-none b { color: var(--text-d); }
.req-new { background: var(--primary-fill); color: var(--primary-on); font-size: .5625rem;
  font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  padding: 2px 7px; border-radius: var(--r-full); align-self: flex-start; }

.req-body { padding: var(--s4); }
.req-row { display: flex; justify-content: space-between; align-items: flex-end; gap: var(--s3); }
.req-when { display: block; font-size: .75rem; font-weight: 700; color: var(--primary-ink); }
.req-time { display: block; font-family: var(--font-h); font-weight: 700; font-size: 1.125rem;
  color: var(--heading); font-variant-numeric: tabular-nums; }
.req-svc { display: block; font-size: .875rem; }
.req-price { text-align: right; white-space: nowrap; }
.req-price b { display: block; font-family: var(--font-h); font-size: 1.0625rem; color: var(--heading); }
.req-price span { font-size: .6875rem; color: var(--text-d); }
.req-note { margin: var(--s3) 0 0; font-size: .875rem; color: var(--text-m); }
.req-deadline { margin: var(--s2) 0 0; font-size: .75rem; color: var(--warn); }
/* The dishes on a food-order request card. A card in a rail is ~340px and a
   long order would push its own Accept button off the bottom, so the list is
   capped and scrolls inside itself — the actions stay where the cook expects
   them however much food was ordered. */
.req-lines {
  list-style: none; margin: var(--s3) 0 0; padding: 0;
  display: flex; flex-direction: column; gap: 2px;
  font-size: .875rem; color: var(--heading);
  max-height: 4.5rem; overflow-y: auto;
}
.req-acts { display: flex; align-items: center; gap: var(--s2); margin-top: var(--s3); }
.req-acts form { flex: 1; }
.req-acts form button { width: 100%; }
.req-decline { border-color: var(--primary); color: var(--primary-ink); }
.req-open { font-size: .8125rem; font-weight: 600; color: var(--text-m); text-decoration: none; }
.req-open:hover { color: var(--primary-ink); }

/* week stats */
.wk-stats { display: grid; grid-template-columns: repeat(4, 1fr);
  border: 1px solid var(--border); border-radius: var(--r-lg); overflow: hidden; background: var(--surface); }
.wk-stat { padding: var(--s4) var(--s2); text-align: center; border-right: 1px solid var(--border-s); }
.wk-stat:last-child { border-right: 0; }
.wk-stat b { display: block; font-family: var(--font-h); font-size: 1.375rem;
  font-weight: 700; line-height: 1.1; color: var(--heading); }
.wk-stat span { font-size: .75rem; color: var(--text-m); }
.wk-stat.is-green b { color: var(--green-ink); }
.wk-stat.is-pink b { color: var(--primary-ink); }

.quick-link b { display: block; font-family: var(--font-h); color: var(--heading); margin-top: var(--s2); }
.quick-link .ic { color: var(--primary-ink); }

/* ⚠ THREE ACROSS ON A PHONE, same trade as .wk-stat above. Stacked, these three
   shortcuts cost ~340px immediately below the week calendar — the thing the page
   is opened for — to carry three short labels. Side by side they cost ~90px.

   The one-line description goes with them. It is advisory: "Update your working
   hours" restates "Set availability", and under 768px advisory content is the
   first thing to cut, never the action.

   Down to 320px: (320 − 40 wrap − 24 gaps) ÷ 3 = 85px a column, ~73px inside the
   padding. "Services & prices" wraps to two lines at 13px and still fits.

   :has() rather than a new class, so this is CSS-only — a browser without it
   simply keeps today's stacked layout, which is the safe direction. */
@media (max-width: 560px) {
  .grid:has(.quick-link) { grid-template-columns: repeat(3, 1fr); gap: var(--s3); }
  .quick-link { padding: var(--s3) 6px; text-align: center; }
  .quick-link b { font-size: .8125rem; line-height: 1.25; margin-top: 6px; }
  .quick-link .t-dim { display: none; }
}

/* ⚠ FOUR ACROSS ON A PHONE TOO. This wrapped to 2×2 below 560px, which cost a
   second row of ~76px directly above the week calendar — the thing the page is
   opened for. Four numbers this short do not need a grid; they need to be
   readable, and shrinking the type is the cheaper trade than spending a
   quarter of the screen on it.

   Down to 320px: (320 − 48 wrap) ÷ 4 = 68px a column, 60px inside the padding.
   "Awaiting" is ~42px at 11px and "40 h" ~36px at 18px, so both fit and the
   long label simply wraps to two lines. */
@media (max-width: 560px) {
  .wk-stat { padding: var(--s3) 4px; }
  .wk-stat b { font-size: 1.125rem; }
  .wk-stat span { display: block; font-size: .6875rem; line-height: 1.2; }
}

/* ── 36. PROVIDER PORTFOLIO (.pf-*, .segwrap) ─────────────── */
/* The My-profile page is two segments — About and Portfolio — because each is
   a separate job and a phone screen only has room for one of them at a time.
   The switch is the same .seg / .seg-opt control the pricing type uses; only
   the panel swapping below is new. */

/* ⚠ Written as :has(the OTHER one checked) in BOTH directions on purpose. A
   browser without :has() drops both rules and shows both panels — verbose but
   entirely usable. The opposite spelling would hide a panel that nothing could
   then reveal. */
.segwrap:has(#pf-work:checked) .pane-about { display: none; }
.segwrap:has(#pf-about:checked) .pane-work { display: none; }
.seg-pane { margin-top: var(--s4); }
.seg-count {
  margin-left: 6px; padding: 1px 7px; border-radius: 999px;
  background: var(--primary-soft); color: var(--primary-soft-ink);
  font-size: .6875rem; font-weight: 700; font-variant-numeric: tabular-nums;
}

/* The drop panel is a <label> for the file input, so the tap target is the
   size of a photo rather than the size of a button. */
.pf-drop {
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  padding: var(--s5) var(--s3); margin-bottom: var(--s3);
  border: 1.5px dashed var(--border-str); border-radius: var(--r-lg);
  background: var(--bg2); color: var(--text-m); cursor: pointer; text-align: center;
  transition: border-color var(--tr-fast), background var(--tr-fast);
}
.pf-drop:hover { border-color: var(--primary); background: var(--bg3); }
.pf-drop .ic { color: var(--primary-ink); }
.pf-drop-title { font-weight: 600; color: var(--primary-ink); }
.pf-drop-hint { font-size: .75rem; color: var(--text-d); }
/* Filled in by initPortfolioUpload once a file is chosen — without it the
   picker closes and nothing on screen says which file is about to be sent. */
.pf-drop-file:not(:empty) {
  margin-top: var(--s2); padding: 3px 10px; border-radius: 999px;
  background: var(--surface); border: 1px solid var(--border);
  font-size: .75rem; color: var(--heading); max-width: 100%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pf-upload:focus-within .pf-drop { border-color: var(--primary); }

.pf-grid {
  display: grid; gap: var(--s2);
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
}
.pf-tile {
  position: relative; margin: 0; border-radius: var(--r);
  overflow: hidden; border: 1px solid var(--border); background: var(--bg2);
  /* No stored dimensions, so the box is reserved here — otherwise every image
     that loads shoves the grid downwards. */
  aspect-ratio: 1 / 1;
}
.pf-thumb { width: 100%; height: 100%; object-fit: cover; display: block; }
.pf-tile-busy { display: grid; place-items: center; }

.pf-state {
  position: absolute; inset: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: var(--s2);
  padding: var(--s2); text-align: center;
  font-size: .75rem; color: var(--text-m); background: var(--bg2);
}
.pf-state .ic { color: var(--primary-ink); }
.pf-state-bad { background: var(--red-soft); color: var(--red); }
.pf-state-bad .ic { color: var(--red); }

.pf-badge {
  position: absolute; left: 6px; bottom: 6px;
  display: inline-flex; align-items: center; gap: 4px;
  padding: 3px 8px; border-radius: var(--r-sm);
  background: rgba(31, 41, 55, .72); color: #fff;
  font-size: .6875rem; font-weight: 600;
}
.pf-badge-bad { background: var(--red); }

.pf-kill-form { position: absolute; right: 6px; top: 6px; margin: 0; }
.pf-kill {
  width: 26px; height: 26px; display: grid; place-items: center;
  border: 0; border-radius: 50%; cursor: pointer;
  background: rgba(255, 255, 255, .92); color: var(--text);
  transition: background var(--tr-fast), color var(--tr-fast);
}
.pf-kill:hover { background: var(--red); color: #fff; }
.pf-kill:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

.pf-cap {
  position: absolute; left: 0; right: 0; bottom: 0;
  padding: 16px 8px 6px; font-size: .75rem; color: #fff;
  background: linear-gradient(to top, rgba(31, 41, 55, .78), transparent);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.pf-cap-bad { background: var(--red); bottom: auto; top: 0; }

/* ── the public strip ──
   On the provider's public page the work sits under the header, scrolling
   sideways: one tile's height instead of a grid's, and visible without a tap.
   Same snap mechanics as .req-list. */
.pf-strip {
  display: flex; gap: var(--s2); margin-top: var(--s3);
  overflow-x: auto; overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory; padding-bottom: var(--s2);
  scrollbar-width: thin;
}
/* One expression for both ends: ~132px on a 390px phone, so the next tile
   peeks and the row is visibly scrollable, settling at 168px on a wide screen.
   104px was too small to judge anything by — these are photographs of someone's
   work, and the whole job of the strip is to make them worth opening. */
.pf-strip > * { flex: 0 0 clamp(132px, 34vw, 168px); scroll-snap-align: start; }
.pf-strip .pf-tile { aspect-ratio: 1 / 1; margin: 0; }
.pf-strip:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

/* ⚠ VIDEOS ONLY, and nothing at all on a photo.
   There was briefly a magnifier badge on every tile. It sat dead centre —
   which on a portrait photo is the subject's face — and hid the one thing the
   tile exists to show. A mark over an image has to earn its place by carrying
   information; "you may tap this" is not information, the whole tile is
   already the link. "This one moves" is. */
.pf-play {
  position: absolute; inset: 0; margin: auto;
  width: 34px; height: 34px; display: grid; place-items: center;
  border-radius: 50%; background: rgba(255, 255, 255, .92); color: var(--heading);
  transition: transform var(--tr-fast), background var(--tr-fast);
  pointer-events: none;                 /* the link around it takes the click */
}
.pf-open:hover .pf-play { background: #fff; transform: scale(1.08); }
/* Nudged right by a pixel: a triangle centred by its bounding box LOOKS
   off-centre, because its visual mass sits to the left of the geometry. Every
   play button ever drawn does this. */
.pf-play .ic { width: 15px; height: 15px; transform: translateX(1px); }

/* The affordance for a photo is the picture itself reacting — it obscures
   nothing at rest, and there is nothing to react to on a touch screen anyway. */
.pf-open .pf-thumb { transition: transform var(--tr), filter var(--tr-fast); }
@media (hover: hover) and (pointer: fine) {
  .pf-open:hover .pf-thumb { transform: scale(1.04); filter: brightness(1.04); }
}

/* Upload progress. Hidden until a transfer starts — an idle 0% bar reads as
   something already broken. */
.pf-progress { display: flex; align-items: center; gap: var(--s3); margin: var(--s3) 0; }
.pf-progress-track {
  flex: 1; height: 8px; border-radius: 999px; overflow: hidden; background: var(--primary-soft);
}
.pf-progress-track i {
  display: block; height: 100%; width: 0; border-radius: 999px;
  background: var(--primary-fill); transition: width var(--tr-fast) linear;
}
.pf-progress-text {
  font-size: .75rem; font-weight: 600; color: var(--text-m);
  font-variant-numeric: tabular-nums; min-width: 5ch; text-align: right;
}

/* The upload's one failure surface. Sits OUTSIDE .pf-progress on purpose —
   fail() hides the bar, and the message used to be written into a span nested
   inside it, so nothing was ever shown.

   ⚠ Sets NO `display`. It toggles via the `hidden` attribute, and any class rule
   setting display beats the UA's [hidden] rule (§ 3) — the element would then
   never hide and every provider would see a stale error above the button. */
.pf-upload-error {
  margin-top: var(--s3); padding: var(--s3);
  border: 1px solid var(--red); border-radius: var(--r-sm);
  background: var(--surface); font-weight: 500;
}

/* ── the lightbox ──
   Was a CSS :target overlay. That could not pause a video — display:none does
   not stop playback — so closing it left the audio running from an element
   nobody could see. Pausing needs a script, which is also what buys prev/next,
   swipe and Escape. See initPortfolioGallery(). */
.pf-lb {
  position: fixed; inset: 0;
  /* Above --z-flash (60). A "Photo added." toast floating over a full-screen
     photo would look like part of it. */
  z-index: 70;
  display: flex; align-items: center; justify-content: center;
  padding: clamp(12px, 4vw, 40px);
}
/* The tile links are real links to the file, so with JS off there is no
   overlay to hide — but [hidden] must win over this display:flex either way.
   A class rule beating the UA's [hidden] is how elements end up permanently
   visible; see the note at the top of the reset. */
.pf-lb[hidden] { display: none; }
.pf-lb-scrim { position: absolute; inset: 0; background: rgba(15, 10, 13, .88); }
/* Stops the page behind scrolling under the overlay, which on a phone reads as
   the overlay itself drifting. */
body.pf-lb-lock { overflow: hidden; }

.pf-lb-stage {
  position: relative; margin: 0;
  display: flex; flex-direction: column; align-items: center; gap: var(--s3);
  max-width: min(1100px, 100%); max-height: 100%;
}
.pf-lb-stage img, .pf-lb-stage video {
  display: block; max-width: 100%; max-height: 82vh;
  border-radius: var(--r-lg); background: #000;
  /* object-fit so a portrait phone photo is shown whole rather than cropped —
     the point of opening it is to see all of it. */
  object-fit: contain;
}
.pf-lb-meta {
  display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap;
  justify-content: center; color: #fff; font-size: .875rem; text-align: center;
}
.pf-lb-count { color: rgba(255, 255, 255, .62); font-variant-numeric: tabular-nums; }

.pf-lb-btn {
  position: absolute; z-index: 2; cursor: pointer;
  width: 44px; height: 44px;               /* a thumb target, not a mouse one */
  display: grid; place-items: center;
  border: 0; border-radius: 50%;
  background: rgba(255, 255, 255, .14); color: #fff;
  transition: background var(--tr-fast);
}
.pf-lb-btn:hover { background: rgba(255, 255, 255, .28); }
.pf-lb-btn:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
.pf-lb-btn[hidden] { display: none; }
.pf-lb-close { top: clamp(8px, 2vw, 20px); right: clamp(8px, 2vw, 20px); }
.pf-lb-prev { left: clamp(4px, 1.5vw, 20px); top: 50%; transform: translateY(-50%); }
.pf-lb-next { right: clamp(4px, 1.5vw, 20px); top: 50%; transform: translateY(-50%); }
.pf-lb-prev .ic { transform: rotate(180deg); }      /* one chevron, both ways */

/* Below ~560px the arrows sit over the picture, so they move to the bottom
   where a thumb already is and the photo keeps its full width. */
@media (max-width: 560px) {
  .pf-lb-prev, .pf-lb-next { top: auto; bottom: 16px; transform: none; }
  .pf-lb-prev { left: 24px; }
  .pf-lb-next { right: 24px; }
}

/* The tile link. Covers the whole tile so the target is the picture, not a
   badge in the corner. */
.pf-open { display: block; text-decoration: none; border-radius: var(--r); }
.pf-open:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.pf-open-cover { position: absolute; inset: 0; z-index: 1; }
/* Above the cover link, or the tile swallows the click that removes it. */
.pf-kill-form { z-index: 2; }

@media (max-width: 560px) {
  .pf-grid { grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); }
}

/* ── 37. BOOKING ADD-ONS, ADDRESS + PAYMENT ──────────────── */
/* (.addon-*, .addr-*, .pay-chip) */

/* The add-on list. A row per service: name on the left, what it costs on the
   right — both kinds of cost, time and money.

   ⚠ Deliberately NOT .check-grid (§ 17). That grid is auto-fill minmax(150px)
   and is right for one-word labels; these rows are ~250px and wrapped inside
   it, breaking between a number and its unit while ~900px sat empty beside
   them. One full-width row each is both denser and readable. */
.addon-list { display: grid; gap: var(--s1); margin-top: var(--s1); }
.addon-row {
  display: flex; align-items: center; gap: var(--s3);
  padding: 7px var(--s3); border-radius: var(--r);
  cursor: pointer; transition: background var(--tr-fast);
}
.addon-row:hover { background: var(--bg2); }
.addon-row input[type="checkbox"] {
  flex: none; width: 20px; height: 20px; margin: 0;
  accent-color: var(--primary-fill);
}
/* min-width:0 so a long service name ellipses instead of shoving the cost
   off the right edge. The cost is the column that has to stay put. */
.addon-name {
  flex: 1; min-width: 0; font-size: .9375rem; line-height: 1.3;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.addon-cost {
  flex: none; font-size: .8125rem; font-weight: 600;
  color: var(--primary-ink); font-variant-numeric: tabular-nums;
  white-space: nowrap;                  /* never split a figure from its unit */
}
.addon-row:has(input:checked) { background: var(--primary-soft); }
.addon-row:has(input:checked) .addon-cost { color: var(--primary-soft-ink); }
.addon-row:has(input:focus-visible) { outline: 2px solid var(--focus); outline-offset: 2px; }

/* A line, not a panel. .note-pink is a bordered, filled box — right for the
   slider's cap explanation, which has to survive being read mid-drag, and far
   too much furniture for a one-sentence hint about the list above it. */
.addon-note {
  display: flex; align-items: flex-start; gap: 6px;
  margin: var(--s2) 0 0; padding: 0 var(--s3);
  font-size: .8125rem; line-height: 1.45; color: var(--primary-ink);
}
.addon-note > .ic { flex: none; margin-top: 2px; }

/* ── BOOKING ADDRESS + PAYMENT (.addr-*, .pay-chip) ───────── */
/* Two small things on the booking form, both about spending a phone screen on
   what the customer still has to decide rather than on what they decided once
   and we saved. */

/* The address, collapsed to one line with a pencil.

   ⚠ Written in the FAILING-OPEN direction, unlike § 17e above. A browser with
   no :has() drops both rules and shows the summary AND the fields — verbose,
   and completely usable. The opposite spelling would leave the address behind
   a disclosure nothing could open, on a required field. */
.addr:has(.addr-open:checked) .addr-summary { display: none; }
.addr:not(:has(.addr-open:checked)) .addr-body { display: none; }

.addr-summary {
  display: flex; align-items: center; gap: var(--s3);
  padding: var(--s3);
  border: 1px solid var(--border-str); border-radius: var(--r);
  background: var(--bg2); cursor: pointer;
  transition: border-color var(--tr), background var(--tr);
}
.addr-summary:hover { border-color: var(--primary); }
.addr-summary > .ic { flex: none; color: var(--primary-ink); }
.addr-text { flex: 1; min-width: 0; }          /* min-width:0 or ellipsis never fires */
.addr-line {
  display: block; font-weight: 500; color: var(--heading);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.addr-sub {
  display: block; font-size: .8125rem; color: var(--text-m);
  font-variant-numeric: tabular-nums;
}

/* A 34px circle is under the 44px thumb target, which is fine ONLY because the
   whole row is the label — the pencil is a sign, not the hit area. */
.addr-pencil {
  flex: none; width: 34px; height: 34px; border-radius: 50%;
  display: grid; place-items: center;
  background: var(--surface); border: 1px solid var(--border-str);
  color: var(--primary-ink);
  transition: background var(--tr), color var(--tr), border-color var(--tr);
}
.addr-summary:hover .addr-pencil {
  background: var(--primary-fill); border-color: var(--primary-fill); color: #fff;
}
.addr-open:focus-visible + .addr-summary {
  outline: 2px solid var(--focus); outline-offset: 2px;
}
.addr-done { display: flex; width: 100%; margin-top: var(--s4); }

/* Payment methods — one row of chips, from what the provider ticked at setup.
   Five methods wrap to two rows on a phone, which is why this is flex-wrap and
   not a grid: three-and-two reads better than a ragged fixed column count. */
.pay-chips { display: flex; flex-wrap: wrap; gap: var(--s2); }
.pay-chip {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 8px var(--s4); border-radius: var(--r-full);
  border: 1px solid var(--border-str); background: var(--surface);
  color: var(--text); font-size: .875rem; font-weight: 500;
  cursor: pointer; transition: background var(--tr), border-color var(--tr),
                               color var(--tr);
}
.pay-chip:hover { border-color: var(--primary); }
.pay-chip:has(input:checked) {
  background: var(--primary-fill); border-color: var(--primary-fill);
  color: var(--primary-on); font-weight: 600;
}
/* The radio is the control and the chip is its label, so the ring has to be
   drawn here — the input itself is visually hidden. */
.pay-chip:has(input:focus-visible) { outline: 2px solid var(--focus); outline-offset: 2px; }
.pay-chip input {
  position: absolute; width: 1px; height: 1px;
  opacity: 0; pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .addr-summary, .addr-pencil, .pay-chip { transition: none; }
}

/* ── 38. PROFILE COMPLETENESS (.pc-*) ─────────────────────── */
/* The provider's own to-do list, with the bar that says how far along it is.

   ⚠ PINK, not green — and that is the whole reason this is not just a reuse of
   .rel-meter. The provider already reads a bar of exactly this shape for
   Professionalism, which is GREEN because green on this site means verified,
   confirmed, available, done. Two green bars on one dashboard would read as the
   same score measured twice. This one is a task list, so it takes the colour
   the site uses to ask for attention (§ 29).

   Geometry is deliberately identical to .rel-meter so they still look like
   siblings; only the fill differs. */
/* ⚠ TWO LINES AT REST. Expanded, the eight checklist rows pushed the week
   calendar — the thing this page is opened for — clean off the screen. The
   summary is the whole collapsed state: a bar, then one sentence. */
.pc-card { border-color: var(--primary); padding: var(--s4); }
.pc-summary {
  list-style: none; cursor: pointer; display: block;
}
.pc-summary::-webkit-details-marker { display: none; }
.pc-summary:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; border-radius: var(--r-sm); }

.pc-bar { display: flex; align-items: center; gap: var(--s3); }
.pc-score {
  flex: none;
  font-family: var(--font-h); font-weight: 700; font-size: 1.0625rem;
  color: var(--primary-ink); font-variant-numeric: tabular-nums;
}
.pc-meter {
  flex: 1; height: 8px; border-radius: var(--r-full);
  background: var(--bg3); overflow: hidden;
}
.pc-meter i {
  display: block; height: 100%; border-radius: var(--r-full);
  background: var(--primary-fill);
  transition: width var(--tr-slow);
}

/* Line two: what is left, and the affordance. */
.pc-line {
  display: flex; align-items: center; gap: 6px;
  margin-top: 6px; font-size: .875rem; color: var(--text-m);
}
.pc-chev { flex: none; transition: transform var(--tr-fast); }
.pc-card[open] .pc-chev { transform: rotate(180deg); }
.pc-body { margin-top: var(--s4); }
.pc-lede { margin: 0 0 var(--s3); font-size: .875rem; }

.pc-list { list-style: none; margin: var(--s3) 0 0; padding: 0; }
.pc-list li + li { margin-top: var(--s1); }
.pc-item {
  display: flex; align-items: flex-start; gap: var(--s3);
  padding: 10px var(--s3); border-radius: var(--r);
  transition: background var(--tr-fast);
}
.pc-item:hover { background: var(--bg2); }
.pc-item:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

/* An empty checkbox, drawn rather than an <input> — there is nothing to submit
   here, and a real checkbox would invite a click that saves nothing. */
.pc-box {
  flex: none; width: 18px; height: 18px; margin-top: 2px;
  border: 2px solid var(--border-str); border-radius: 5px;
}
.pc-item:hover .pc-box { border-color: var(--primary); }
.pc-text { flex: 1; min-width: 0; }
.pc-text strong { display: block; font-size: .9375rem; font-weight: 600; }
.pc-why { display: block; font-size: .8125rem; color: var(--text-m); line-height: 1.4; }
.pc-go { flex: none; align-self: center; color: var(--text-d); }
.pc-item:hover .pc-go { color: var(--primary-ink); }

/* The done count, not the done rows. Nine ticked lines bury the two that are
   not, which is the opposite of what a to-do list is for. */
.pc-done {
  display: flex; align-items: center; gap: 6px;
  margin: var(--s3) 0 0; padding-top: var(--s3);
  border-top: 1px solid var(--border-s);
  font-size: .8125rem; color: var(--green-ink);
}

/* On My profile the provider is already on the page that fixes most of this,
   so the card states the number and the next steps without re-explaining why. */
.pc-compact { border-color: var(--border); box-shadow: none; }
.pc-compact .pc-score { font-size: 1.0625rem; }

@media (prefers-reduced-motion: reduce) {
  .pc-meter i { transition: none; }
}

/* ── 39. CONFIRM + EXPLAIN (.confirm-*, .reason-note) ─────── */
/* The confirmation step for an action that ENDS a booking, carrying the
   optional "why", and the one place that reason is shown back.

   ⚠ THIS REPLACED A window.confirm AND A COLLAPSED BOX, on evidence.
   The first build put a quiet "Add a reason (optional)" disclosure under the
   button and kept data-confirm. On the real site it was never seen: you tap
   Cancel, the browser dialog answers, and it is over. The reason box was
   correct, rendered on all six surfaces, and completely unreachable by the
   path people actually take.

   So the reason moved INTO the pause that already existed. Two taps, which is
   what a browser confirm always cost, except now the second tap is on a panel
   that says what will happen and offers somewhere to explain.

   ⚠ Still not a demand. The field is optional, says so in its own label, and
   "Yes, cancel" never checks it.

   No script: hidden checkbox + two labels, the booking address card's
   mechanism (§ 37). It cannot get stuck open across a POST-redirect-GET, and
   the checkbox is .sr-only rather than display:none so it stays FOCUSABLE —
   see the :focus-visible rule below, which is what a keyboard user sees. */
.confirm { display: block; }
.confirm-trigger { cursor: pointer; }
/* The panel is shut until the checkbox is ticked. Written as a plain sibling
   selector, not :has(), so there is no support question at all. */
.confirm-body { display: none; }
.confirm-open:checked ~ .confirm-body { display: block; }
/* ⚠ The trigger goes away once the panel is open. Leaving a "Cancel booking"
   button sitting above an open "Cancel this booking?" panel reads as two
   different offers, and people press the wrong one. */
.confirm-open:checked ~ .confirm-trigger { display: none; }
/* The ring lands on the LABEL, because the checkbox itself is off-screen. */
.confirm-open:focus-visible ~ .confirm-trigger {
  outline: 2px solid var(--focus); outline-offset: 2px;
}

.confirm-body {
  margin-top: var(--s2);
  padding: var(--s4);
  border: 1px solid var(--primary);
  border-radius: var(--r);
  background: var(--bg2);
}
.confirm-q {
  margin: 0 0 var(--s1);
  font-family: var(--font-h); font-weight: 600; font-size: 1rem;
  color: var(--heading);
}
/* The consequence, which is the reason a confirm step exists at all. */
.confirm-detail { margin: 0 0 var(--s4); font-size: .8125rem; color: var(--text-m); }
.confirm-label {
  display: block; margin-bottom: var(--s1);
  font-size: .8125rem; font-weight: 600; color: var(--text);
}
/* Shorter than the general .form-textarea (110px): this is three lines of
   courtesy, and a tall box asks for an essay. */
.confirm-body .form-textarea { min-height: 68px; font-size: .875rem; }
.confirm-hint { margin: var(--s1) 0 0; font-size: .75rem; color: var(--text-d); }

.confirm-acts { display: flex; gap: var(--s2); margin-top: var(--s4); }
.confirm-acts > * { flex: 1; }
/* ⚠ "Keep it" is the heavier button. The destructive choice must never be the
   one the thumb lands on by default. */
.confirm-keep { display: flex; align-items: center; justify-content: center;
  cursor: pointer; text-align: center; }
.confirm-yes { justify-content: center; }

/* ── the request card, where declining is a queue action ── */
/* ⚠ Accept and Decline stay side by side, same size, same place. Only the
   opened panel is allowed to change the card's height, and only for the
   request being declined.

   flex-start, because the decline form grows downward once open and the
   default `center` would drag Accept down with it. The Details link gets the
   padding back so it still reads level with the button text. */
.req-acts { align-items: flex-start; }
.req-acts .req-open { align-self: flex-start; padding-top: 9px; }
.req-decline-form { display: flex; flex-direction: column; }
/* Open, the panel needs the whole card, not a third of it. */
.req-acts:has(.confirm-open:checked) { flex-wrap: wrap; }
.req-acts:has(.confirm-open:checked) .req-decline-form { flex: 1 0 100%; }
.req-acts .confirm-body { padding: var(--s3); }
.req-acts .confirm-q { font-size: .9375rem; }

/* The same problem one screen over: the cook's order queue and the order detail
   page lay their buttons out with .filter-actions, so an open panel would be
   squeezed into the slice of the row its flex item was allotted.

   ⚠ Both rules are ADDITIONS, never a `display: none`. A browser without :has()
   drops them and keeps a narrow panel that still opens, reads and submits —
   which is the direction this has to fail in. Scoped to rows that actually
   contain a panel, so the filter bars elsewhere are untouched. */
.filter-actions:has(.confirm-open) { flex-wrap: wrap; }
.filter-actions form:has(.confirm-open:checked) { flex: 1 0 100%; }
.filter-actions .confirm-body { padding: var(--s3); }

/* ── the reason, shown to the other party ── */
/* Under the status banner, above the "was this agreed?" question — that block
   asks this reader to judge the cancellation, so the explanation has to arrive
   first. Quoted and set off by a rule rather than boxed: it is somebody's
   sentence, not a system notice. */
.reason-note { border-left: 3px solid var(--border); padding: 2px 0 2px var(--s3); }
.reason-note p:last-child { font-size: 1rem; color: var(--text); }

/* ── 39. LANDING PAGE (.home-*, .shot, .promise-*, .mission-*, .pro-*) ─── */
/* The customer landing page. Everything here is scoped to classes this page
   owns — nothing in this section restyles a shared component, so the hero on
   /search/ and the provider home page are untouched by any of it. */

/* ⚠ NOT .hero-title, which is capped at 15ch for the provider headline and
   would break "Find trusted services" over three lines. The <br> in the
   template is what puts the pink half on its own line; this only has to not
   fight it. */
/* ── THE PAGE'S VERTICAL RHYTHM ───────────────────────────────
   ⚠ ONE class for every section on this page, replacing .section (64px) and
   .section-tight (32px). Measured 2026-08-16, the mixture of the two cost
   **544px of dead space** between sections on a 3232px page: two adjacent
   .sections put 64px + 64px = a 128px void between "Top-rated professionals"
   and "What customers say", and every boundary was a different size, so the
   page had no rhythm to read.

   --s5 is 24px on a desktop and 20px on a phone, so one boundary is 48px /
   40px and every boundary is the same. The panels carry their own internal
   padding, which is what keeps this breathable rather than cramped.

   ⚠ Scoped to this page rather than re-cutting .section itself: that class is
   on 28 templates and this brief was about the landing page. Making it global
   is a one-line change in § 6 if the same treatment is ever wanted site-wide. */
.home-sec { padding: var(--s5) 0; }

/* ── the three phone shortcuts, above the hero ── */
.quick-row { background: var(--surface); border-bottom: 1px solid var(--border-s); padding: var(--s3) 0; }
.quick-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--s2); }
.quick-tile {
  position: relative; display: flex; flex-direction: column; align-items: center;
  justify-content: center; gap: 6px; text-align: center;
  min-height: 74px; padding: var(--s3) 6px;
  border: 1px solid var(--border); border-radius: var(--r-lg);
  background: var(--surface); color: var(--heading);
  transition: border-color var(--tr), background var(--tr);
}
.quick-tile:active { background: var(--primary-soft); border-color: var(--primary); }
.quick-mark { display: inline-flex; color: var(--primary-ink); }
.quick-label { font-size: .75rem; font-weight: 600; line-height: 1.2; text-wrap: balance; }
/* Sits under the label rather than over the icon: a badge on top of a 24px mark
   covers the one thing that identifies the tile. */
.quick-new {
  display: inline-block; padding: 1px 7px; border-radius: var(--r-full);
  background: var(--primary-fill); color: var(--primary-on);
  font-size: .625rem; font-weight: 700; line-height: 1.6;
}

/* ── the brand line above the headline ──
   ⚠ `display: inline` + box-decoration-break: clone. The pink background then
   BREAKS WITH THE TEXT, so a phone gets two stacked pills rather than one pill
   with a ragged second line. An inline-flex chip cannot do that. */
.hero-badge-line { margin: 0 0 var(--s3); }
.hero-badge {
  display: inline; box-decoration-break: clone; -webkit-box-decoration-break: clone;
  padding: 5px 12px; border-radius: var(--r-full);
  background: var(--primary-soft); color: var(--primary-ink);
  font-size: .8125rem; line-height: 2.1;
}
.hero-badge strong { font-weight: 700; }
/* The icon must sit ON the text baseline or it drags the line box taller and
   the pill stops looking like one piece. */
.hero-badge .ic { vertical-align: -3px; margin-right: 4px; }

.home-title { max-width: 20ch; letter-spacing: -.03em; margin-bottom: var(--s2); }
/* ⚠ Was `margin-top: var(--s3)` on top of the h1's own .5em bottom margin —
   two margins between a headline and its own subtitle, which is what made them
   read as unrelated blocks rather than one unit. */
.hero-sub { margin: 0 0 var(--s4); max-width: 42ch; }

/* The numbered steps in the search card. A number is cheaper than a sentence
   for saying "there are two of these and this is the first". */
.step-mark { font-weight: 700; color: var(--heading); margin-right: 5px; }
.step-mark::after { content: "."; }

/* ── the pin inside the postal box ──
   Absolutely positioned, so the field keeps its own height and focus ring.
   The input reserves the room on the right rather than the mark overlapping
   it — a five-digit code plus a space is wide, and text sliding under an icon
   is the failure this padding exists to prevent. */
.input-wrap { position: relative; display: block; }
.input-wrap .form-input { padding-right: 42px; }
.input-mark {
  position: absolute; right: 13px; top: 50%; transform: translateY(-50%);
  display: inline-flex; color: var(--primary); pointer-events: none;
}

/* ── the right-hand column ──
   ⚠ `1fr auto`, and NO align-content. This used to be `align-content: center`,
   which packed the collage and the promise card into their natural height and
   centred the pair — leaving dead canvas top and bottom.

   Now the promise card takes the height it needs (`auto`) and the collage
   absorbs everything left over (`1fr`). The column has no reserved height and
   no fixed height: it is exactly as tall as the hero's real content, which is
   the search card beside it, and the photographs take up the slack instead of
   empty space doing it. */
.hero-side { display: grid; grid-template-rows: 1fr auto; gap: var(--s3); }

/* ⚠ PHOTOGRAPHY IS A SLOT, NOT A DEPENDENCY.
   main.css must contain no url() pointing at a file that is not in the repo:
   the production ManifestStaticFilesStorage build resolves every url() in this
   stylesheet and FAILS the deploy on a miss. So each tile ships as a gradient
   and becomes a photograph the moment --shot-img is set.

   The seven photographs are live as of 2026-08-16 (see the block at the end of
   this section). Names, exact pixel sizes, crop behaviour and casting direction
   are in docs/technical/landing-photography.md — deliberately in docs/ and NOT
   beside the images, because anything under static/ is collected and served
   publicly in production. */
.shot {
  --shot-img: none;
  --shot-wash: linear-gradient(140deg, var(--primary-soft) 0%, var(--bg3) 100%);
  /* ⚠ No min-height. Every .shot is either a grid track that is told its height
     (the collage) or stretched to its row (the mission and provider panels), so
     a floor here only ever fought the layout — it could force a tile taller
     than the space it was given and push the panel out. */
  display: block; border-radius: var(--r-lg);
  background-color: var(--bg3);
  background-image: var(--shot-img), var(--shot-wash);
  background-size: cover; background-position: center;
  box-shadow: var(--sh-sm);
}

/* Five tiles on a six-column grid: two wide across the top, three under them.
   Six columns is what lets a 3+3 row and a 2+2+2 row share one track list, so
   the tiles line up down both edges without a nested grid. */
/* ⚠ `1fr 1fr`, NOT the `190px 150px` this used to carry. Two hard-coded row
   heights are exactly the "fixed area reserved for images" that made the hero
   feel like an empty canvas: the collage was 352px whatever the hero needed,
   so any mismatch with the left column became dead space.

   As fractions the two rows simply divide whatever height .hero-side hands
   them — the collage now ENDS level with the search card instead of floating
   above a gap. Photographs are `cover`, so they crop to any height and there
   is nothing to keep in sync. */
.collage {
  display: grid; gap: var(--s3);
  grid-template-columns: repeat(6, 1fr);
  grid-template-rows: 1fr 1fr;
}
.ph-1 { grid-column: 1 / 4; }
.ph-2 { grid-column: 4 / 7; }
.ph-3 { grid-column: 1 / 3; }
.ph-4 { grid-column: 3 / 5; }
.ph-5 { grid-column: 5 / 7; }
/* Each tile a slightly different wash, or five identical rectangles read as a
   loading state rather than a composition. */
.ph-2 { --shot-wash: linear-gradient(140deg, var(--bg3) 0%, var(--primary-soft) 100%); }
.ph-3 { --shot-wash: linear-gradient(160deg, var(--primary-soft) 0%, var(--bg2) 100%); }
.ph-4 { --shot-wash: linear-gradient(200deg, var(--bg3) 0%, var(--bg2) 100%); }
.ph-5 { --shot-wash: linear-gradient(180deg, var(--bg2) 0%, var(--primary-soft) 100%); }

/* ── the promise card under the collage ──
   Outside .collage on purpose: the photographs go on a phone, this does not.
   It is the product's core claim and the reason someone trusts the form above
   it enough to type a postal code into it. */
.promise {
  display: flex; align-items: center; gap: var(--s3);
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-lg); padding: var(--s4);
  box-shadow: var(--sh-md);
}
.promise-text { font-size: .9375rem; color: var(--text); line-height: 1.5; }

/* The one icon treatment shared by the promise card, the trust bar, the
   mission points and the provider points — four surfaces, one mark, so they
   read as the same voice rather than four decorated lists. */
.promise-mark {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; border-radius: var(--r);
  background: var(--primary-soft); color: var(--primary-ink);
}

/* ── the four promises ── */
/* Vertical padding one step below horizontal — the four cells are a single row
   of short text, so --s5 top and bottom was padding a 111px band out to 159px. */
.promise-bar {
  display: grid; gap: var(--s4);
  grid-template-columns: repeat(4, 1fr);
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-xl); padding: var(--s4) var(--s5);
  box-shadow: var(--sh-sm);
}
.promise-cell { display: flex; align-items: flex-start; gap: var(--s3); }
.promise-label {
  display: block; font-family: var(--font-h); font-weight: 600;
  font-size: .9375rem; color: var(--heading);
}
.promise-note { margin: 2px 0 0; font-size: .8125rem; color: var(--text-m); line-height: 1.5; }

/* ── Kitchen Corner ──
   Intro left, a scrolling rail of dishes right. The intro is a fixed-ish column
   and the rail takes the rest: `minmax(0, 1fr)` on the second track, because a
   flex/overflow child in an `auto` track sizes to its CONTENT and the row would
   grow to the width of all four cards instead of scrolling. */
.kc {
  display: grid; gap: var(--s5);
  grid-template-columns: minmax(0, .78fr) minmax(0, 1.22fr);
  align-items: center;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r-xl); padding: var(--s5) var(--s6);
  box-shadow: var(--sh-sm);
}
.kc-intro .chip { margin-bottom: var(--s3); }
.kc-title { display: flex; align-items: center; gap: var(--s2); margin: 0; }
.kc-title-mark { display: inline-flex; color: var(--primary-ink); }
.kc-tag { margin: var(--s2) 0 0; font-family: var(--font-h); font-weight: 600; color: var(--heading); }
.kc-body { margin: var(--s2) 0 var(--s4); max-width: 42ch; }

.kc-rail { position: relative; min-width: 0; }
.kc-rail-head { display: flex; justify-content: flex-end; margin-bottom: var(--s2); }
.kc-all { font-size: .8125rem; font-weight: 600; color: var(--primary-ink); }
.kc-all:hover { text-decoration: underline; }

/* Same one-row-with-snap contract as .req-list (§ 35). */
.kc-list {
  display: flex; gap: var(--s3);
  overflow-x: auto; overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory; scroll-behavior: smooth;
  padding: 2px 2px var(--s2);
  scrollbar-width: thin;
}
.kc-list > * { flex: 0 0 min(74%, 190px); scroll-snap-align: start; }
.kc-list:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

.kc-card {
  display: flex; flex-direction: column;
  border: 1px solid var(--border); border-radius: var(--r-lg);
  background: var(--surface); overflow: hidden;
}
/* A dish photo is 4:3 and comes from the row, so there is no fixed height here
   either — the tile's width decides it. */
.kc-shot { aspect-ratio: 4 / 3; border-radius: 0; }
.kc-shot-b { --shot-wash: linear-gradient(140deg, var(--bg3) 0%, var(--primary-soft) 100%); }
.kc-shot-c { --shot-wash: linear-gradient(160deg, var(--primary-soft) 0%, var(--bg2) 100%); }
.kc-shot-d { --shot-wash: linear-gradient(200deg, var(--bg3) 0%, var(--bg2) 100%); }
.kc-card-body { display: flex; flex-direction: column; gap: 2px; padding: var(--s3); }
.kc-name { font-family: var(--font-h); font-size: .875rem; font-weight: 600; color: var(--heading); margin: 0; }
.kc-by, .kc-meta { margin: 0; font-size: .75rem; color: var(--text-m); }
.kc-foot { display: flex; align-items: center; justify-content: space-between; gap: var(--s2); margin-top: var(--s2); }
.kc-rating { display: inline-flex; align-items: center; gap: 3px; font-size: .75rem; font-weight: 600; color: var(--heading); }
.kc-rating .ic { color: var(--star); }
.kc-count { color: var(--text-m); font-weight: 400; }
/* ⚠ A SPAN styled as a button, not a button. Nothing is orderable yet, and a
   live-looking control that does nothing is worse than an obvious preview.
   No :hover and no cursor:pointer, deliberately — it must not invite a click. */
.kc-order {
  padding: 5px 12px; border-radius: var(--r-full);
  background: var(--primary-fill); color: var(--primary-on);
  font-size: .75rem; font-weight: 600; line-height: 1.4;
}

/* Side arrows, echoing the reference. Hidden until the row actually overflows
   AND the pointer cannot swipe — same rule as .req-step. */
.kc-step {
  position: absolute; top: 58%; z-index: 1;
  width: 30px; height: 30px; border-radius: 50%; cursor: pointer;
  border: 1px solid var(--border); background: var(--surface); color: var(--text-m);
  display: none; place-items: center; font-size: 15px; line-height: 1;
  box-shadow: var(--sh-sm);
}
.kc-step-prev { left: calc(var(--s4) * -1); }
.kc-step-next { right: calc(var(--s4) * -1); }
.kc-step:hover { border-color: var(--primary); color: var(--primary-ink); }
.kc-step:disabled { opacity: .3; cursor: default; }
@media (pointer: fine) {
  .kc-rail.is-scrollable .kc-step { display: grid; }
}

/* ── our mission ── */
/* ⚠ Vertical padding down to --s5, horizontal left at --s6. The brief was to
   reclaim vertical space and leave the horizontal rhythm alone, and a panel
   that keeps its side padding still reads as generous. */
.mission {
  display: grid; gap: var(--s4) var(--s5);
  grid-template-columns: 1.05fr .95fr 1.1fr;
  align-items: center;
  background: linear-gradient(135deg, var(--primary-soft) 0%, var(--bg2) 70%);
  border: 1px solid var(--border-s);
  border-radius: var(--r-xl); padding: var(--s5) var(--s6);
}
.mission-eyebrow { display: inline-flex; align-items: center; gap: 6px; }
.mission-title { margin: var(--s2) 0 0; }
.mission-body { margin: var(--s2) 0 var(--s4); max-width: 46ch; }
.mission-points { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s3); }
.mission-point { display: flex; align-items: flex-start; gap: var(--s3); }
.mission-point .promise-mark { background: var(--surface); }
/* ⚠ `align-self: stretch` instead of `min-height: 260px`. The panel's height is
   set by its tallest column — the copy — and the photograph now fills whatever
   that turns out to be. A fixed 260px could only ever be too short (leaving a
   gap beside the copy) or too tall (padding the panel out on its own). */
.ph-mission { align-self: stretch; border-radius: var(--r-lg); }
/* Spans every column: it is the section's signature, not a caption on the
   photograph above it. */
.mission-foot {
  grid-column: 1 / -1; display: flex; align-items: center; justify-content: center;
  gap: 6px; color: var(--primary-ink); font-size: .9375rem; font-weight: 500;
  text-align: center;
}

/* ── are you a professional? ── */
.pro-cta {
  display: grid; gap: var(--s4) var(--s6);
  grid-template-columns: .85fr 1.15fr;
  align-items: center;
  background: linear-gradient(120deg, var(--bg2) 0%, var(--primary-soft) 100%);
  border: 1px solid var(--border-s);
  border-radius: var(--r-xl); padding: var(--s5) var(--s6);
}
/* Stretches so the photo column ends level with the copy — same reasoning as
   .ph-mission above, and the badge is positioned against it. */
.pro-media { position: relative; align-self: stretch; display: flex; }
.ph-pro { flex: 1; border-radius: var(--r-lg); }
/* Overlaps the photograph rather than sitting under it — the badge is the
   claim the picture is evidence for, and separating them costs a whole row for
   no gain.

   ⚠ Anchored to the TOP edge, not the bottom. At `bottom` it landed level with
   the "Join as a professional" button and read as a third thing in that row
   rather than a label on the photograph. */
.pro-badge {
  position: absolute; right: calc(var(--s4) * -1); top: var(--s5);
  display: inline-flex; align-items: center; gap: var(--s2);
  max-width: 78%; padding: var(--s3) var(--s4);
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--r); box-shadow: var(--sh-md);
  font-size: .8125rem; font-weight: 600; color: var(--heading); line-height: 1.35;
}
.pro-badge .ic { color: var(--primary-ink); flex: none; }
.pro-points {
  list-style: none; margin: var(--s4) 0; padding: 0;
  display: grid; gap: var(--s3) var(--s4); grid-template-columns: repeat(3, 1fr);
}
.pro-point { display: flex; align-items: flex-start; gap: var(--s2); }
.pro-point .promise-mark { width: 36px; height: 36px; background: var(--surface); }
.pro-actions { display: flex; align-items: center; gap: var(--s5); flex-wrap: wrap; }
.pro-more { font-weight: 600; color: var(--primary-ink); }
.pro-more:hover { text-decoration: underline; }

/* ── PHONE ────────────────────────────────────────────────────
   What is left is one column, and two places where the advisory half of a row
   is dropped so the row itself survives. */
@media (max-width: 939px) {
  /* ⚠ 939px, tied to .hero-grid's own two-column breakpoint above — NOT the
     899px .nav-desktop utility, which is what this used to ride on. Those two
     numbers disagreeing left a 40px-wide band (900–939px) where the hero had
     already collapsed to one column but the collage had not gone, so five
     photographs landed stacked full-width between the search card and the
     promise. The collage is the desktop hero's second column; it exists only
     while there IS a second column.

     Declared in the hiding direction, so a browser that drops the rule shows
     the photographs rather than hiding something nothing can reveal. */
  .collage { display: none; }

  .mission, .pro-cta, .kc { grid-template-columns: 1fr; padding: var(--s5); }
  .pro-cta { gap: var(--s4); }
  /* Stacked, the intro is above the rail and the side arrows would sit over the
     first and last card. A touch user swipes, so they are no loss. */
  .kc { gap: var(--s4); }
  .kc-step { display: none !important; }
  .kc-rail-head { margin-top: calc(var(--s2) * -1); }
  /* The badge has no photograph to hang off once the column is full width. */
  .pro-badge { position: static; max-width: none; margin-top: var(--s3); }

  /* ⚠ In one column there is no taller sibling to stretch against, and these
     are EMPTY decorative spans with no intrinsic height — left alone they
     collapse to nothing and the photograph silently disappears.

     aspect-ratio, not the `min-height: 200px` this used to carry: the height is
     then derived from the width the column actually has, so it stays in
     proportion at 320px and at 900px instead of being one number chosen for one
     phone. Photographs are `cover`, so the crop follows. */
  .pro-media { display: block; align-self: auto; }
  .ph-mission, .ph-pro { aspect-ratio: 16 / 9; align-self: auto; }
}

@media (max-width: 767px) {
  /* ⚠ Advisory, and only the DESCRIPTION goes — the four labels stay, four
     across, because they are the promises themselves. Four labelled paragraphs
     stacked is most of a phone screen directly above the mission panel that is
     the reason to keep scrolling. Cut chrome, never capability. */
  .promise-bar {
    grid-template-columns: repeat(4, 1fr); gap: var(--s2);
    padding: var(--s4) var(--s3);
  }
  .promise-cell { flex-direction: column; align-items: center; text-align: center; gap: var(--s2); }
  /* ⚠ Scoped to .promise-bar. As a bare `.promise-note` this also stripped the
     descriptions off the provider points, which are stacked ONE PER ROW on a
     phone and have all the width they need — so it was deleting readable copy
     to solve a problem that only exists in the four-across strip. .promise-mark
     and .promise-label are shared by four surfaces; only this one is squeezed. */
  .promise-bar .promise-note { display: none; }
  .promise-bar .promise-label { font-size: .75rem; line-height: 1.25; text-wrap: balance; }
  .promise-bar .promise-mark { width: 36px; height: 36px; }
  .promise-bar .promise-mark .ic { width: 18px; height: 18px; }

  /* ⚠ Also advisory: the heading, the paragraph and the button carry the
     mission on their own, and /about/ carries it in full. Delete this one rule
     to bring the three points back on a phone. */
  .mission-points { display: none; }

  .pro-points { grid-template-columns: 1fr; gap: var(--s3); }
}

/* ── 40. KITCHEN CORNER (.kc-collect, .dish-*, .section-*) ─────────────── */
/* ── the collection address, revealed by the fulfilment choice ──
   ⚠ A bare `:has(…)` HIDE, not a `:not(:has(…))` reveal, and the polarity is
   chosen per rule rather than by habit. Every disclosure rule here declares
   `display: none`, so a browser that cannot parse `:has()` drops the rule and
   the block stays VISIBLE. That is the safe direction: a delivery-only cook sees
   two fields they can ignore, rather than a collection cook facing a required
   address that nothing on the page can ever reveal.

   Keyed on `value="delivery"` — the one fulfilment that needs no address —
   because "collection" and "both" are the two that do, and testing the single
   negative case is one selector instead of two.

   ⚠ The fields are NOT `required` in HTML. A CSS-hidden required control makes
   the browser refuse to submit with nothing on screen; the rule lives in
   KitchenSetupForm.clean(). */
.reg-form:has(input[name="fulfilment"][value="delivery"]:checked) [data-kc-collect],
form:has(input[name="fulfilment"][value="delivery"]:checked) [data-kc-collect] {
  display: none;
}
.kc-collect { display: grid; gap: var(--stack); }
@media (min-width: 560px) { .kc-collect { grid-template-columns: 2fr 1fr; } }

/* ── the delivery areas, revealed by the same choice from the other side ──
   ⚠ Keyed on `value="collection"` — the one fulfilment that needs no delivery
   list — for the same reason .kc-collect keys on "delivery": testing the single
   negative case is one selector instead of two, and the bare `:has()` HIDE means
   a browser that cannot parse it shows the block rather than hiding a required
   field with nothing on screen to explain the refused submit. */
form:has(input[name="fulfilment"][value="collection"]:checked) [data-kc-deliver] {
  display: none;
}


/* A row list, not a table: the same decision the provider services page made,
   for the same reason. A `min-width` table side-scrolls on a phone, and this is
   read standing up in a kitchen. */
.dish-list { display: flex; flex-direction: column; gap: var(--s2); }
.dish {
  display: flex; align-items: center; gap: var(--s3);
  padding: var(--s3); border: 1px solid var(--border);
  border-radius: var(--r-lg); background: var(--surface);
}
/* Sold out is stated by the chip in the row; the fade is a second, non-colour
   cue so it survives greyscale and bright sun — the same reasoning as the
   dashed border on an unconfirmed calendar block. */
.dish.is-out { background: var(--bg2); }
.dish.is-out .dish-shot { opacity: .55; }

.dish-shot {
  flex: none; width: 72px; height: 72px; border-radius: var(--r);
  object-fit: cover; background: var(--bg3);
}
.dish-shot-empty {
  display: grid; place-items: center; color: var(--primary-ink);
  background: var(--primary-soft);
}
.dish-shot-empty .ic { width: 28px; height: 28px; }
.dish-shot-lg { width: 160px; height: 160px; margin-bottom: var(--s3); }

.dish-body { flex: 1; min-width: 0; }
.dish-name {
  display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap;
  font-family: var(--font-h); font-size: 1rem; font-weight: 600;
  color: var(--heading); margin: 0;
}
.dish-meta { margin: 2px 0 0; font-size: .875rem; color: var(--heading); }
.dish-unit { color: var(--text-m); font-weight: 400; }
.dish-desc { margin-top: 2px; }
/* Column on a phone so two buttons never squeeze the name to one word — the
   "suspect the COLUMN, not the wrapping" lesson from the provider card. */
.dish-acts { display: flex; flex-direction: column; gap: var(--s2); flex: none; }
@media (min-width: 620px) { .dish-acts { flex-direction: row; align-items: center; } }

.section-list { list-style: none; margin: var(--s3) 0 0; padding: 0;
  display: flex; flex-direction: column; gap: var(--s2); }
.section-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s3); padding: var(--s2) var(--s3);
  border: 1px solid var(--border); border-radius: var(--r);
}

/* ── the customer's side: /kitchen/ and /kitchen/<slug>/ ──
   Deliberately reuses .card, .filters, .mode-row and .dish-* rather than
   growing a parallel set. Kitchen Corner is a separate marketplace SPACE, not a
   separate design language — a second look would read as a different site
   bolted on, and rule 8 leaves one stylesheet to keep in step. */
.kit-hero { display: flex; flex-direction: column; gap: var(--s2); align-items: flex-start; }
.kit-hero-title { display: flex; align-items: center; gap: var(--s2); margin: 0; }

.kit-card { display: flex; flex-direction: column; gap: var(--s3); }
.kit-head { display: flex; flex-direction: column; gap: 2px; }
.kit-tag { margin: 0; font-size: .875rem; }
.kit-chips { display: flex; flex-wrap: wrap; gap: var(--s2); }

/* A strip, not a grid: three dishes are a taste of the menu, and a grid here
   competed with the card's own heading for the eye. */
.kit-dishes { list-style: none; margin: 0; padding: 0; display: flex; gap: var(--s3); }
.kit-dish { display: flex; flex-direction: column; gap: 4px; min-width: 0; flex: 1 1 0; }
.kit-dish-shot { width: 100%; height: 72px; }
/* One line, clipped. A long dish name used to push the three thumbnails to
   different heights and the row lost its grid. */
.kit-dish-name {
  font-size: .75rem; color: var(--text-m);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.kit-foot {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s2); font-size: .875rem;
}
.kit-price { font-weight: 600; color: var(--heading); }
.kit-cta { align-self: flex-start; }

.kit-terms { padding: var(--s4); }
.kit-term-list { list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: var(--s2); font-size: .9375rem; }
.kit-term-list li { display: flex; align-items: center; gap: var(--s2); color: var(--heading); }
.kit-term-list .ic { flex: none; color: var(--primary-ink); }

/* A real photograph in the rail card. .kc-shot sets the 4/3 box; this only has
   to fill it, and object-fit keeps a portrait phone photo from letterboxing. */
.kc-shot-img { width: 100%; object-fit: cover; }
.kc-diet {
  display: inline-flex; align-items: center; gap: 3px;
  font-size: .75rem; font-weight: 600; color: var(--primary-ink);
}
/* An anchor where the placeholder had a dead span. Nothing orders yet, so it
   goes to the menu — see includes/dish_card.html. */
a.kc-order:hover { text-decoration: underline; }

/* ── 40b. ORDERING (.kit-basket*, .kit-qty*, .kit-queue*) ───────────────
   The basket, checkout, the customer's orders and the cook's queue. Still no
   parallel design language: .card, .chip, .mode-row and .timeline do the work,
   and these are only the layouts those components have no opinion about. */

/* The Add button beside a dish. `flex: none` so a long dish name shortens
   instead of squeezing the button to two characters — the same failure the
   provider card's .dish-acts was fixed for. */
.dish-add { flex: none; }

/* Sticky at the BOTTOM, not the top: on a phone this page is a long menu, and
   the reason for the bar is being able to check out from wherever you scrolled
   to. Above the safe-area inset so it clears an iPhone's home indicator. */
.kit-basket-bar {
  position: sticky; bottom: 0; z-index: 5;
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s3); margin-top: var(--s4);
  padding: var(--s3) var(--s4);
  padding-bottom: calc(var(--s3) + env(safe-area-inset-bottom));
  border: 1px solid var(--primary); border-radius: var(--r-lg);
  background: var(--primary-soft); color: var(--primary-soft-ink);
  font-weight: 600; box-shadow: var(--sh-md);
}

.kit-basket { list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: var(--s3); }
/* Column on a phone. The row holds a name, a number input, two buttons and a
   price; side by side at 360px the name collapses to one word. */
.kit-basket-row {
  display: flex; flex-direction: column; gap: var(--s2);
  padding-bottom: var(--s3); border-bottom: 1px solid var(--border);
}
.kit-basket-row:last-child { border-bottom: 0; padding-bottom: 0; }
.kit-basket-name { flex: 1; min-width: 0; }
.kit-basket-total { font-weight: 600; color: var(--heading); }
@media (min-width: 620px) {
  .kit-basket-row { flex-direction: row; align-items: center; gap: var(--s3); }
  .kit-basket-total { margin-left: auto; }
}
/* A read-only summary row — checkout and the receipt. No controls, so it stays
   on one line at every width. */
.kit-basket-row.is-static { flex-direction: row; align-items: center; }
.kit-basket-row.is-static .kit-basket-total { margin-left: auto; }

.kit-basket-foot {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s3); margin-top: var(--s3); padding-top: var(--s3);
  border-top: 1px solid var(--border); font-size: .9375rem;
}
.kit-basket-foot.is-total { font-size: 1.0625rem; color: var(--heading); }

.kit-qty { display: flex; align-items: center; gap: var(--s2); }
/* Wide enough for three digits and the browser's own spinner. Any narrower and
   Safari clips the arrows over the number. */
.kit-qty-input { width: 84px; text-align: center; }

/* The cook's cancellation policy, under the terms of ordering. Their own
   words, so it keeps the body face rather than joining the icon list above —
   a sentence set as a list item reads as another one-line fact, and this one
   is the only thing on that card the customer may owe money over. */
.kit-term-policy {
  margin-top: var(--s3); padding-top: var(--s3);
  border-top: 1px solid var(--border-s); line-height: 1.5;
}
.kit-term-policy strong { color: var(--heading); }

/* Delivery address, revealed by the delivery radio. Asking someone who just
   tapped "I'll collect" for their delivery address is exactly what this avoids.

   ⚠ WRITTEN INSIDE @supports, and that is the load-bearing part. The natural
   spelling — hide by default, reveal with :has() — fails CLOSED on a browser
   without :has(): the reveal rule is dropped, the hide is not, and the address
   fields become invisible and unfillable, so delivery stops working entirely.
   Inside the guard, such a browser gets neither rule and simply shows both
   fields with their labels, which is what the page did before this existed.
   Same principle as § 17e's panels: failing open beats hiding the control the
   customer came for.

   ⚠ Visibility only. The SERVER blanks these on a collection order — see
   CheckoutForm._clean_delivery(). A hidden field still posts. */
@supports selector(:has(*)) {
  .kit-handover .kit-deliver-only { display: none; }
  .kit-handover:has(input[name="fulfilment"][value="delivery"]:checked)
    .kit-deliver-only { display: block; }
}

/* The checkout time picker. Structure, states and every colour come from
   § 17e — this is the booking picker's markup reused wholesale, and these four
   rules are the only difference.

   Why any difference at all: a booking chip carries a range as long as
   "09:00–12:30" and a second line of text, so 132px columns and a 56px row are
   right for it. A handover chip carries "12:00–12:30" and nothing else, and a
   cook open 09:00–17:00 has sixteen of them. At the booking size that is six
   rows on a phone; at this one it is four, three across, which is the whole
   point of slicing the day up in the first place. */
.slotpick-food .slotpick-times {
  grid-template-columns: repeat(auto-fill, minmax(104px, 1fr));
}
.slotpick-food .slotpick-time > .slotpick-chip {
  min-height: 46px; padding: var(--s2); font-size: .875rem;
}
/* "2 left" on a nearly-full slot. § 17e recolours this on a checked DAY chip
   only; unfixed, the note stays dark grey on pink the moment a time is picked. */
.slotpick-time input:checked + .slotpick-chip .slotpick-day-note {
  color: var(--primary-on); opacity: .85;
}

/* The customer's own orders. A whole card that is one link — the target has to
   be big, because this is tapped with a thumb. */
.kit-order-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--s3); margin-bottom: var(--s3); text-decoration: none;
}
.kit-order-row:hover { border-color: var(--primary); }
.kit-order-right { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; }

/* The cook's queue card. */
.kit-queue { display: flex; flex-direction: column; gap: var(--s3);
  margin-bottom: var(--s3); }
.kit-queue-head { display: flex; align-items: flex-start; justify-content: space-between;
  gap: var(--s3); flex-wrap: wrap; }
.kit-queue-lines { list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: 2px;
  font-size: .9375rem; color: var(--heading); }
.kit-queue-note { margin: 0; font-size: .875rem; }
/* The customer's professionalism on a queue card. Deliberately the same colours
   as .req-prof on the dashboard card — one number meaning one thing, wherever a
   cook meets it. Green reads "kept their word"; a missing score is grey, never
   red, because "no score yet" is a new customer and not a bad one. */
.kit-queue-prof { margin: var(--s2) 0 0; font-size: .75rem; color: var(--text-m); }
.kit-queue-prof b { color: var(--green-ink); }
.kit-queue-prof.is-none b { color: var(--text-d); }

/* The header basket. A count, not a price — a price in the nav would be a
   fourth place that has to agree with the menu. */
.nav-basket { display: inline-flex; align-items: center; gap: var(--s2); }
.nav-badge {
  display: inline-grid; place-items: center; min-width: 20px; height: 20px;
  padding: 0 6px; border-radius: var(--r-full);
  background: var(--primary-fill); color: #fff;
  font-size: .75rem; font-weight: 700; line-height: 1;
}

/* ── THE PHOTOGRAPHS ──────────────────────────────────────────
   ⚠ LIVE since 2026-08-16 — all seven files are in static/img/. These url()s
   may only be uncommented while those files exist: production's
   ManifestStaticFilesStorage resolves every url() in this stylesheet at deploy
   time and FAILS the deploy on a missing one. Removing a photograph means
   commenting its line back out in the same commit, not just deleting the file.

   The gradient in --shot-wash stays as the second background layer underneath,
   so a photograph that 404s degrades to the wash rather than to a blank box. */
.ph-1       { --shot-img: url("../img/hero-1.ef2ee16413f9.jpg"); }
.ph-2       { --shot-img: url("../img/hero-2.6fd61b0b03fe.jpg"); }
.ph-3       { --shot-img: url("../img/hero-3.5a0b83062e18.jpg"); }
.ph-4       { --shot-img: url("../img/hero-4.b3eeab098279.jpg"); }
.ph-5       { --shot-img: url("../img/hero-5.1b1e6266bb3a.jpg"); }
.ph-mission { --shot-img: url("../img/mission.858507579e43.jpg"); }
.ph-pro     { --shot-img: url("../img/professional.bf962fa05e02.jpg"); }

@media print {
  .nav, .foot, .flash-wrap, .job-actions, .no-print,
  .deco-blob, .hero-media, .nav-icon-btn { display: none !important; }
  body { background: #fff; color: #000; }
  .card { box-shadow: none; border-color: #ddd; }
}
