/* ============================================================
   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; }
.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 ────────────────────────────── */
.hero { padding: var(--s7) 0 var(--s8); }
.hero-grid { display: grid; gap: var(--s6); grid-template-columns: 1fr; align-items: center; }
@media (min-width: 940px) { .hero-grid { grid-template-columns: 1.05fr .95fr; gap: 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);
}

/* ⚠ auto-fit, NOT `1fr 1fr`. This renders every active category, and the
   catalog went from two to eight — as a two-column grid that is FOUR rows of
   tiles sitting directly above the Search button, which lands it below the fold
   on every phone. That is the same failure the `#results` fragment exists to
   prevent, re-created above the fold instead of below it.

   auto-fit with a 104px floor gives three across at 390px and four on a wider
   phone, so eight categories cost two or three rows rather than four, and the
   grid keeps working when a ninth category is added without anyone touching
   this file. */
.cat-picker {
  display: grid; gap: var(--s2);
  grid-template-columns: repeat(auto-fit, minmax(104px, 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; }
.form-check { width: 20px; height: 20px; border-radius: 6px; accent-color: var(--primary-fill); }
/* --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.

   ⚠ 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 > .social-auth { order: 40; }
.reg-form > [data-reg="provider"] { order: 50; }
.reg-form > [data-reg="premises"] { order: 60; }
.reg-form > [data-reg="details"] { order: 70; }
/* Gaps of ten so this one value can slot between two of them. */
.reg-form:has(input[name="user_type"][value="provider"]:checked) > .social-auth { order: 55; }

/* 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; }
/* 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 still owes an answer to "where do you work?" before the rest. */
.reg-form:has(input[name="user_type"][value="provider"]:checked):not(:has(input[name="service_location"]:checked)) [data-reg="details"] { display: none; }

/* ── 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; }

/* ── 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); }
.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 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; }
}

@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; }
}
