/* ==========================================================================
   Taskovery, marketing site
   One stylesheet. Tokens mirror TrustLaneKit/Design/Theme.swift exactly.
   ========================================================================== */

/* --------------------------------------------------------------------------
   0. Figtree, self-hosted
   Served from our own origin, not Google's. The stylesheet request to
   fonts.googleapis.com used to happen on every page load, on every device,
   and it carried the visitor's IP and user agent to Google before they had
   agreed to anything. That is a disclosure to an overseas third party, and
   naming it in the privacy policy is a worse answer than not making it.

   One variable file per subset covers the whole 300-900 range, so the four
   weights the site uses (400/600/700/800) cost one download instead of four.

   latin-ext is not optional here: the Maori macrons (a e i o u with macron)
   live in U+0100-017F, outside the latin subset. No page uses one today, but
   Auckland suburb names and anything a person types will. unicode-range
   means it only downloads when a glyph in it is actually rendered.

   Not preloaded on purpose. Apple platforms resolve ui-rounded to SF Pro
   Rounded first (see `body`), so Figtree is never fetched there at all, and
   a preload would force the download on the devices that do not need it.
   -------------------------------------------------------------------------- */

@font-face {
  font-family: 'Figtree';
  font-style: normal;
  font-weight: 300 900;
  font-display: swap;
  src: url(../fonts/figtree-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
                 U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
                 U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'Figtree';
  font-style: normal;
  font-weight: 300 900;
  font-display: swap;
  src: url(../fonts/figtree-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
                 U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF,
                 U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* --------------------------------------------------------------------------
   1. Palette
   Both themes are declared as raw values first, then mapped onto semantic
   names. Doing it in two steps is what makes `.invert` possible: a band can
   pull the *other* theme's raw values without any duplicated component CSS.
   -------------------------------------------------------------------------- */

:root {
  /* Light */
  --l-canvas:       #F1F6FD;
  --l-surface:      #FFFFFF;
  --l-surface-alt:  #E4EDF9;
  --l-hairline:     #D3E0F0;
  --l-ink:          #0C1B33;
  --l-ink-2:        #4A5C77;
  --l-ink-3:        #8394AB;
  --l-accent:       #1B62E8;
  --l-accent-deep:  #1450C9;
  /* Accent text has to clear 4.5:1 on the *worst* light background it lands
     on, the accent wash over surface-alt, not plain white. Measured: #0E45AE
     is 6.11:1 on that wash and 8.46:1 on surface. Fills stay at the true accent. */
  --l-accent-text:  #0E45AE;
  --l-accent-wash:  rgba(27, 98, 232, 0.12);
  /* Chip fills that carry *text*. On light a translucent wash is fine. On dark
     a translucent wash lightens the plate under the label and drags accent-on-
     wash down to 3.6:1, so the dark value is an opaque tint mixed toward the
     canvas instead, same warmth, background stays dark, label clears AA. */
  --l-chip-accent:  rgba(27, 98, 232, 0.12);
  --l-chip-green:   rgba(27, 143, 90, 0.14);
  --l-verified:     #1B8F5A;
  --l-verified-text:#166A44;   /* derived: #1B8F5A is 4.10:1 on white, and the
                                  shield badge sits on a green wash, not white */
  --l-amber:        #C98A0E;
  --l-amber-text:   #8A5E06;   /* derived, same reason as verified-text: #C98A0E
                                  is 2.9:1 on the light surface, which is fine for
                                  an icon and not for a glyph read as text */
  --l-danger:       #C8362B;

  /* Badge metal, from `VerificationCheck.metal` in the app. Authored as explicit
     light/dark pairs rather than derived from the trade pastels: a face bright
     enough to look proud on a black canvas is too bright to hold a white glyph
     on cream, so the two themes genuinely need different metal, not the same
     metal at a different opacity. Every `face` keeps a white glyph above 4:1. */
  --l-bdg-id-rim:   #0A5636;  --l-bdg-id-face:  #15834F;  --l-bdg-id-lit:  #36B375;
  --l-bdg-biz-rim:  #114A70;  --l-bdg-biz-face: #1C6E9F;  --l-bdg-biz-lit: #3F97CE;
  --l-bdg-lic-rim:  #452A82;  --l-bdg-lic-face: #66429F;  --l-bdg-lic-lit: #9375D8;
  --l-bdg-addr-rim: #6E4310;  --l-bdg-addr-face:#8F5A0B;  --l-bdg-addr-lit: #C08329;
  --l-bdg-wash-amber:  rgba(201,138,14,.16);
  --l-bdg-wash-danger: rgba(200,54,43,.12);

  /* The blue reverses what the orange needed. White on #1B62E8 measures 5.2:1
     and dark ink only 3.2:1, so light gets a white label. On dark the lighter
     #5B93FF flips it back: white is 2.97:1 and dark ink is 6.4:1. So the two
     themes genuinely need opposite labels, and matching them would fail one.
     Accessibility beat cross-theme identity here, deliberately. */
  --l-btn-bg:       #1B62E8;
  --l-btn-bg-hover: #1450C9;
  --l-btn-ink:      #FFFFFF;
  --l-shadow:       0 1px 2px rgba(22,27,34,.05), 0 10px 28px -12px rgba(22,27,34,.18);
  --l-shadow-lift:  0 2px 4px rgba(22,27,34,.06), 0 18px 40px -16px rgba(22,27,34,.24);
  --l-tint:         14%;
  --l-sheen:        rgba(255,255,255,.55);

  /* Dark, the canvas is the logo's own backdrop */
  --d-canvas:       #0B1220;
  --d-surface:      #131C2E;
  --d-surface-alt:  #1D2940;
  --d-hairline:     #2B3A57;
  --d-ink:          #EAF1FB;
  --d-ink-2:        #A3B4CC;
  --d-ink-3:        #6B7C96;
  --d-accent:       #5B93FF;
  /* These four were the pre-rebrand orange, left behind when the accent went
     blue. On dark they resolve into --accent-deep, --accent-wash, --chip-accent
     and the button hover, so a blue button's pressed state came back orange and
     every accent wash ran warm on a cool palette. Now the blue counterparts, and
     the same values the apps use in Theme.swift so the two agree. */
  --d-accent-deep:  #3B79EE;
  --d-accent-text:  #7FADFF;
  --d-accent-wash:  rgba(91, 147, 255, 0.22);
  /* Opaque, not a wash: a translucent tint lightens the plate under its own
     label and drags accent-on-wash under AA. Mixed toward the canvas so it keeps
     the hue. --d-accent-text on this is 6.4:1. */
  --d-chip-accent:  #192948;
  --d-chip-green:   #1F3833;
  --d-verified:     #4FD18C;
  --d-verified-text:#4FD18C;
  --d-amber:        #F6C453;
  --d-amber-text:   #F6C453;
  --d-danger:       #FF7365;

  --d-bdg-id-rim:   #0C6A43;  --d-bdg-id-face:  #198B59;  --d-bdg-id-lit:  #46C98A;
  --d-bdg-biz-rim:  #14567F;  --d-bdg-biz-face: #2380B6;  --d-bdg-biz-lit: #53A9DD;
  --d-bdg-lic-rim:  #4E3193;  --d-bdg-lic-face: #7350B4;  --d-bdg-lic-lit: #A184E6;
  --d-bdg-addr-rim: #8A5412;  --d-bdg-addr-face:#A96C19;  --d-bdg-addr-lit: #E0A44A;
  /* Opaque on dark: a translucent wash over a card lightens the plate under the
     hollow badge's own outline and drags it back under 3:1. */
  --d-bdg-wash-amber:  #362A0C;
  --d-bdg-wash-danger: #3A1512;

  --d-btn-bg:       #5B93FF;
  --d-btn-bg-hover: #7FADFF;   /* lighter on dark, where light goes darker */
  --d-btn-ink:      #08101E;
  --d-shadow:       0 1px 2px rgba(0,0,0,.35), 0 10px 28px -12px rgba(0,0,0,.55);
  --d-shadow-lift:  0 2px 4px rgba(0,0,0,.4), 0 18px 40px -16px rgba(0,0,0,.6);
  --d-tint:         24%;
  /* Rule learned in the apps: a sheen tuned for a light canvas becomes a grey
     stripe on dark. 5%, not 35%. */
  --d-sheen:        rgba(255,255,255,.05);

  /* Per-trade pastels (light hue / dark hue), Theme.Pastel */
  --l-sky:#2A7FB8;      --d-sky:#6FB8EA;
  --l-mint:#0F8C75;     --d-mint:#57D3B4;
  --l-butter:#C2870D;   --d-butter:#F6C453;
  --l-lavender:#7452C4; --d-lavender:#B79FF2;
  --l-sage:#4A8B28;     --d-sage:#96D671;
  --l-coral:#CE4F36;    --d-coral:#FF8E77;
  --l-peach:#C66B1A;    --d-peach:#FFAB6C;
  --l-rose:#C44476;     --d-rose:#F691B4;
  --l-sand:#97753F;     --d-sand:#D9B78C;
  --l-aqua:#158B88;     --d-aqua:#5FD4D1;
  --l-lilac:#8D42B5;    --d-lilac:#CB8EEA;
  --l-clay:#AF5729;     --d-clay:#E3946E;

  /* Shape, the apps' radii */
  --r-sm: 12px;
  --r-md: 16px;
  --r-lg: 22px;
  --r-xl: 28px;

  /* Space, 4 / 8 / 12 / 16 / 24 / 32 */
  --s1: 4px;  --s2: 8px;  --s3: 12px;
  --s4: 16px; --s5: 24px; --s6: 32px;
  --s7: 48px; --s8: 64px; --s9: 96px;

  --measure: 68ch;
}

/* Map raw → semantic. Light is the default; dark overrides. */
html,
html[data-theme="dark"] .invert {
  --canvas: var(--l-canvas);           --surface: var(--l-surface);
  --surface-alt: var(--l-surface-alt); --hairline: var(--l-hairline);
  --ink: var(--l-ink);                 --ink-2: var(--l-ink-2);
  --ink-3: var(--l-ink-3);             --accent: var(--l-accent);
  --accent-deep: var(--l-accent-deep); --accent-text: var(--l-accent-text);
  --accent-wash: var(--l-accent-wash); --verified: var(--l-verified);
  --chip-accent: var(--l-chip-accent); --chip-green: var(--l-chip-green);
  --verified-text: var(--l-verified-text);
  --amber: var(--l-amber);             --danger: var(--l-danger);
  --amber-text: var(--l-amber-text);
  --bdg-id-rim: var(--l-bdg-id-rim);   --bdg-id-face: var(--l-bdg-id-face);
  --bdg-id-lit: var(--l-bdg-id-lit);
  --bdg-biz-rim: var(--l-bdg-biz-rim); --bdg-biz-face: var(--l-bdg-biz-face);
  --bdg-biz-lit: var(--l-bdg-biz-lit);
  --bdg-lic-rim: var(--l-bdg-lic-rim); --bdg-lic-face: var(--l-bdg-lic-face);
  --bdg-lic-lit: var(--l-bdg-lic-lit);
  --bdg-addr-rim: var(--l-bdg-addr-rim); --bdg-addr-face: var(--l-bdg-addr-face);
  --bdg-addr-lit: var(--l-bdg-addr-lit);
  --bdg-wash-amber: var(--l-bdg-wash-amber);
  --bdg-wash-danger: var(--l-bdg-wash-danger);
  --btn-bg: var(--l-btn-bg);           --btn-bg-hover: var(--l-btn-bg-hover);
  --btn-ink: var(--l-btn-ink);         --shadow: var(--l-shadow);
  --shadow-lift: var(--l-shadow-lift); --tint: var(--l-tint);
  --sheen: var(--l-sheen);
  --sky: var(--l-sky);           --mint: var(--l-mint);
  --butter: var(--l-butter);     --lavender: var(--l-lavender);
  --sage: var(--l-sage);         --coral: var(--l-coral);
  --peach: var(--l-peach);       --rose: var(--l-rose);
  --sand: var(--l-sand);         --aqua: var(--l-aqua);
  --lilac: var(--l-lilac);       --clay: var(--l-clay);
  color-scheme: light;
}

html[data-theme="dark"],
html:not([data-theme="dark"]) .invert {
  --canvas: var(--d-canvas);           --surface: var(--d-surface);
  --surface-alt: var(--d-surface-alt); --hairline: var(--d-hairline);
  --ink: var(--d-ink);                 --ink-2: var(--d-ink-2);
  --ink-3: var(--d-ink-3);             --accent: var(--d-accent);
  --accent-deep: var(--d-accent-deep); --accent-text: var(--d-accent-text);
  --accent-wash: var(--d-accent-wash); --verified: var(--d-verified);
  --chip-accent: var(--d-chip-accent); --chip-green: var(--d-chip-green);
  --verified-text: var(--d-verified-text);
  --amber: var(--d-amber);             --danger: var(--d-danger);
  --amber-text: var(--d-amber-text);
  --bdg-id-rim: var(--d-bdg-id-rim);   --bdg-id-face: var(--d-bdg-id-face);
  --bdg-id-lit: var(--d-bdg-id-lit);
  --bdg-biz-rim: var(--d-bdg-biz-rim); --bdg-biz-face: var(--d-bdg-biz-face);
  --bdg-biz-lit: var(--d-bdg-biz-lit);
  --bdg-lic-rim: var(--d-bdg-lic-rim); --bdg-lic-face: var(--d-bdg-lic-face);
  --bdg-lic-lit: var(--d-bdg-lic-lit);
  --bdg-addr-rim: var(--d-bdg-addr-rim); --bdg-addr-face: var(--d-bdg-addr-face);
  --bdg-addr-lit: var(--d-bdg-addr-lit);
  --bdg-wash-amber: var(--d-bdg-wash-amber);
  --bdg-wash-danger: var(--d-bdg-wash-danger);
  --btn-bg: var(--d-btn-bg);           --btn-bg-hover: var(--d-btn-bg-hover);
  --btn-ink: var(--d-btn-ink);         --shadow: var(--d-shadow);
  --shadow-lift: var(--d-shadow-lift); --tint: var(--d-tint);
  --sheen: var(--d-sheen);
  --sky: var(--d-sky);           --mint: var(--d-mint);
  --butter: var(--d-butter);     --lavender: var(--d-lavender);
  --sage: var(--d-sage);         --coral: var(--d-coral);
  --peach: var(--d-peach);       --rose: var(--d-rose);
  --sand: var(--d-sand);         --aqua: var(--d-aqua);
  --lilac: var(--d-lilac);       --clay: var(--d-clay);
  color-scheme: dark;
}

/* --------------------------------------------------------------------------
   2. Base
   -------------------------------------------------------------------------- */

*, *::before, *::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background: var(--canvas);
  color: var(--ink);
  /* SF Rounded on Apple platforms, literally the apps' typeface. Figtree is
     the fallback everywhere else; it never downloads where ui-rounded resolves. */
  font-family: ui-rounded, "SF Pro Rounded", "Figtree", "Plus Jakarta Sans",
               "Nunito Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  font-size: 17px;
  line-height: 1.62;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  transition: background-color .25s ease, color .25s ease;
}

img, svg { max-width: 100%; }
svg { display: block; }

/* Sprite icons. Stroke properties are inherited by the <use> content, so every
   icon picks up its colour from the surrounding text. */
.ic {
  width: 24px; height: 24px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* The mark itself is mitred and butt/square capped, like the app's shapes. */

h1, h2, h3, h4 {
  margin: 0 0 var(--s3);
  line-height: 1.14;
  letter-spacing: -0.02em;
  font-weight: 800;
  text-wrap: balance;
}

/* Trimmed from a 3.7rem ceiling. At that size a heading is a poster, and every
   page here is something somebody reads rather than looks at. */
h1 { font-size: clamp(1.9rem, 1.3rem + 2.2vw, 2.8rem); }
h2 { font-size: clamp(1.45rem, 1.15rem + 1.2vw, 1.95rem); }
h3 { font-size: clamp(1.08rem, 1rem + 0.35vw, 1.24rem); letter-spacing: -0.01em; }
h4 { font-size: 1.02rem; letter-spacing: 0; font-weight: 700; }

/* A heading that has to stay level-correct for screen readers but sits in a
   small card. Level and size are separate problems. */
.h-compact { font-size: 1.02rem; letter-spacing: 0; font-weight: 700; }

p { margin: 0 0 var(--s4); max-width: var(--measure); }
p:last-child { margin-bottom: 0; }

a { color: var(--accent-text); text-decoration-thickness: 1.5px; text-underline-offset: 3px; }
a:hover { color: var(--accent); }

strong { font-weight: 700; }

ul, ol { margin: 0 0 var(--s4); padding-left: 1.25em; max-width: var(--measure); }
li { margin-bottom: var(--s2); }

:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 3px;
  border-radius: 4px;
}

.skip {
  position: absolute; left: var(--s4); top: -100px;
  z-index: 200; padding: var(--s3) var(--s5);
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--hairline); border-radius: 999px;
  font-weight: 700; text-decoration: none;
}
.skip:focus { top: var(--s4); }

/* `hidden` is an attribute the UA styles as display:none, and any author
   `display` rule beats it. Several things here are shown and hidden by setting
   `.hidden`, so the attribute has to win. */
[hidden] { 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;
}

/* --------------------------------------------------------------------------
   3. Layout
   -------------------------------------------------------------------------- */

.wrap { width: 100%; max-width: 1140px; margin-inline: auto; padding-inline: var(--s5); }
.wrap--narrow { max-width: 800px; }

.section { padding-block: clamp(var(--s8), 7vw, var(--s9)); }
.section--tight { padding-block: clamp(var(--s7), 5vw, var(--s8)); }
.section--alt { background: var(--surface-alt); }
.section--surface { background: var(--surface); }

.invert { background: var(--canvas); color: var(--ink); }

.head { max-width: 780px; margin-bottom: var(--s7); }
.head--centre { margin-inline: auto; text-align: center; }
.head--centre p { margin-inline: auto; }
/* `text-wrap: balance` shrinks a heading's box to the balanced line width, so a
   short heading in a centred block sits hard left with its text centred inside a
   narrow box. Long headings fill the width and hid this. */
.head--centre h1,
.head--centre h2,
.head--centre h3 { margin-inline: auto; }

.eyebrow {
  display: inline-block;
  margin: 0 0 var(--s3);
  font-size: 0.8rem;
  font-weight: 800;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--accent-text);
}

.lede { font-size: clamp(1.06rem, 1rem + 0.35vw, 1.22rem); color: var(--ink-2); }
.muted { color: var(--ink-2); }
.small { font-size: 0.9rem; }

.grid { display: grid; gap: var(--s5); }
.grid--2 { grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); }
.grid--3 { grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
.grid--4 { grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); }

.split {
  display: grid;
  gap: clamp(var(--s6), 5vw, var(--s8));
  align-items: center;
  grid-template-columns: 1fr;
}
@media (min-width: 900px) {
  .split { grid-template-columns: 1fr 1fr; }
  .split--wide-text { grid-template-columns: 1.15fr 0.85fr; }
  .split--wide-media { grid-template-columns: 0.85fr 1.15fr; }
  .split--flip > :first-child { order: 2; }
}

.stack > * + * { margin-top: var(--s4); }
.rule { height: 1px; background: var(--hairline); border: 0; margin-block: var(--s7); }

/* --------------------------------------------------------------------------
   4. Buttons, fully rounded capsules. This is a signature; never a rounded rect.
   -------------------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s2);
  padding: 15px 28px;
  border-radius: 999px;
  border: 1.5px solid transparent;
  font: inherit;
  font-size: 1.02rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  text-decoration: none;
  cursor: pointer;
  transition: transform .15s ease-out, background-color .15s ease-out,
              border-color .15s ease-out, box-shadow .15s ease-out;
}
.btn:hover { transform: translateY(-2px); }
.btn:active { transform: translateY(0); }

.btn--primary {
  background: var(--btn-bg);
  color: var(--btn-ink);
  box-shadow: var(--shadow);
}
.btn--primary:hover { background: var(--btn-bg-hover); color: var(--btn-ink); box-shadow: var(--shadow-lift); }

.btn--ghost {
  background: transparent;
  color: var(--ink);
  border-color: var(--hairline);
}
.btn--ghost:hover { background: var(--surface); color: var(--ink); border-color: var(--ink-3); }

.btn--small { padding: 10px 20px; font-size: 0.94rem; }

/* Unavailable controls get their own colour, never the accent at low opacity 
   orange at 45% over the dark canvas is a muddy brown that reads as broken. */
.btn--pending {
  background: var(--surface-alt);
  color: var(--ink-2);
  border-color: var(--hairline);
  cursor: default;
}
.btn--pending:hover { transform: none; background: var(--surface-alt); }

.btn-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
  align-items: center;
  margin-top: var(--s5);
}

/* Deliberately not a flex container. Flex promotes every child to a flex item,
 * including the bare text nodes around an inline <strong>, so a line that wraps
 * renders its own words out of order. That bug was live on the emergency-111
 * note. The icon is inline instead, which keeps the sentence a sentence. */
.note-line {
  margin-top: var(--s4);
  font-size: 0.9rem;
  color: var(--ink-2);
}
.note-line svg {
  display: inline-block;   /* svg is display:block globally, which breaks the line */
  width: 17px;
  height: 17px;
  vertical-align: -3px;
  margin-right: var(--s2);
  color: var(--ink-3);
}
.note-line--center { text-align: center; }

/* --------------------------------------------------------------------------
   5. Cards, surface + hairline + soft shadow.
   The hairline is not optional: on dark the surface sits a few percent off the
   canvas, so the border is the only thing separating them.
   -------------------------------------------------------------------------- */

.card {
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--r-lg);
  padding: var(--s5);
  box-shadow: var(--shadow);
}
/* The two ways into Taskovery, on signup.html.
   Flex column with the button pushed down, so the two cards' CTAs sit on one line
   whatever length the copy above them runs to. Two buttons at different heights in
   a two-up grid is the thing that makes a page look unfinished. */
.join-card { display: flex; flex-direction: column; padding: var(--s6); }
.join-card h2 { font-size: 1.32rem; margin-bottom: var(--s2); }
.join-card .ticks { margin-bottom: 0; }
.join-go { margin-top: auto; padding-top: var(--s5); }
.join-go .btn { width: 100%; justify-content: center; }
@media (min-width: 640px) { .join-go .btn { width: auto; } }

.card--lift { transition: transform .15s ease-out, box-shadow .15s ease-out; }
.card--lift:hover { transform: translateY(-2px); box-shadow: var(--shadow-lift); }
.card--flat { box-shadow: none; background: var(--surface-alt); }
.card > h3, .card > h4 { margin-bottom: var(--s2); }
.card p { color: var(--ink-2); font-size: 0.98rem; }

.card-icon {
  display: grid;
  place-items: center;
  width: 46px; height: 46px;
  border-radius: var(--r-sm);
  margin-bottom: var(--s4);
  background: var(--chip-accent);
  color: var(--accent-text);
}
.card-icon svg { width: 24px; height: 24px; }
.card-icon--green { background: var(--chip-green); color: var(--verified-text); }

/* Numbered step */
.step { position: relative; }
.step-no {
  display: grid; place-items: center;
  width: 34px; height: 34px;
  border-radius: 999px;
  background: var(--chip-accent);
  color: var(--accent-text);
  font-size: 0.92rem; font-weight: 800;
  margin-bottom: var(--s4);
}

/* Feature list with ticks */
.ticks { list-style: none; padding: 0; }
.ticks li {
  display: flex; gap: var(--s3);
  align-items: flex-start;
  margin-bottom: var(--s3);
  color: var(--ink-2);
}
.ticks svg { width: 21px; height: 21px; flex: none; margin-top: 3px; color: var(--verified-text); }
.ticks strong { color: var(--ink); font-weight: 700; }


/* --------------------------------------------------------------------------
   6. Header
   -------------------------------------------------------------------------- */

.site-header {
  position: sticky; top: 0; z-index: 100;
  background: color-mix(in srgb, var(--canvas) 88%, transparent);
  backdrop-filter: saturate(1.4) blur(14px);
  -webkit-backdrop-filter: saturate(1.4) blur(14px);
  border-bottom: 1px solid transparent;
  transition: border-color .2s ease, background-color .25s ease;
}
.site-header.is-stuck { border-bottom-color: var(--hairline); }

.header-in {
  display: flex; align-items: center; gap: var(--s4);
  min-height: 68px;
}

/* The logo is the wordmark, "Trust" in ink, "Lane" in the accent, no symbol.
   Same mark as the apps' launch screen and app icon, and the same rule the
   portals use. System UI rather than the rounded face the rest of the site is
   set in; that contrast is the mark. */
.brand { margin-right: auto; padding-block: 6px; }

/* The mark sits inside the .brand anchor rather than beside it, so it inherits the
   header treatments that already target .brand. Sized in em so it tracks the
   wordmark at every breakpoint instead of needing its own media queries.
   Filled with the same token as the "Work" half, so mark and wordmark are one
   colour decision, not two. */
.brand-mark {
  /* inline-block, because the reset above sets every svg to display:block, which
     would drop the wordmark onto a second line. */
  display: inline-block;
  width: .92em; height: auto; fill: var(--accent-text);
  vertical-align: -.1em; margin-right: .4em;
}

.wordmark {
  display: inline-block;
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  font-weight: 800; font-size: 1.24rem; letter-spacing: -0.035em;
  color: var(--ink); text-decoration: none; white-space: nowrap;
}
/* The accented half. It is Task now, not Work: one coined word with its first
   syllable lifted, rather than two words joined. */
.wordmark b { font-weight: 800; color: var(--accent-text); }
/* The hero. Clamped rather than fixed so it never wraps at 320px, where a
   broken wordmark is worse than a small one. */
.wordmark--hero {
  font-size: clamp(1.8rem, 5.5vw, 2.6rem);
  margin-bottom: var(--s5);
}

.nav { display: flex; align-items: center; gap: var(--s1); }
.nav a {
  padding: 9px 14px; border-radius: 999px;
  color: var(--ink-2); text-decoration: none;
  font-size: 0.96rem; font-weight: 600;
  transition: background-color .15s ease, color .15s ease;
}
.nav a:hover { background: var(--surface-alt); color: var(--ink); }
.nav a[aria-current="page"] { color: var(--ink); background: var(--surface-alt); }
/* `.nav a` out-specifies `.btn--primary`, which quietly painted the header CTA
   label secondary-grey on orange. Restate it. */
.nav a.btn--primary, .nav a.btn--primary:hover { color: var(--btn-ink); background: var(--btn-bg); }
.nav a.btn--primary:hover { background: var(--btn-bg-hover); }
.nav a.btn--ghost { color: var(--ink); }

.header-tools { display: flex; align-items: center; gap: var(--s2); }

.icon-btn {
  display: grid; place-items: center;
  width: 42px; height: 42px;
  border-radius: 999px;
  background: transparent;
  border: 1.5px solid var(--hairline);
  color: var(--ink);
  cursor: pointer;
  transition: background-color .15s ease, border-color .15s ease;
}
.icon-btn:hover { background: var(--surface-alt); }
.icon-btn svg { width: 20px; height: 20px; }

.theme-toggle .icon-moon { display: none; }
html[data-theme="dark"] .theme-toggle .icon-sun { display: none; }
html[data-theme="dark"] .theme-toggle .icon-moon { display: block; }

.header-cta { flex: none; }

.menu-btn { display: none; }
.menu-btn .icon-close { display: none; }
.menu-btn[aria-expanded="true"] .icon-open { display: none; }
.menu-btn[aria-expanded="true"] .icon-close { display: block; }

/* Below this the nav collapses behind the hamburger, so the primary action
   lives outside it, burying "Open Taskovery" one tap deep on the smallest
   screens is exactly backwards. */
@media (max-width: 520px) {
  /* Shorten the label rather than drop the control, at 360px the header still
     has room for the theme toggle once "Taskovery" goes. */
  .header-cta-full { display: none; }
}

@media (max-width: 900px) {
  .menu-btn { display: grid; }
  .nav {
    display: none;
    position: absolute; left: var(--s4); right: var(--s4); top: calc(100% + 8px);
    flex-direction: column; align-items: stretch;
    padding: var(--s3);
    background: var(--surface);
    border: 1px solid var(--hairline);
    border-radius: var(--r-lg);
    box-shadow: var(--shadow-lift);
  }
  .nav.is-open { display: flex; }
  .nav a { padding: 13px 16px; border-radius: var(--r-sm); font-size: 1.02rem; }
  .header-in { position: relative; }
  .nav .btn { margin-top: var(--s2); }
}

/* --------------------------------------------------------------------------
   7. Hero
   -------------------------------------------------------------------------- */

.hero { padding-block: clamp(var(--s7), 6vw, var(--s9)) clamp(var(--s7), 6vw, var(--s8)); position: relative; overflow: hidden; }

/* The hero used to carry a soft orange radial bloom. It is gone: a coloured
   glow behind a headline is the single most recognisable generated-page
   signature, and now that a real photograph sits back there the page has depth
   from something true instead. Nothing replaces it. */
.hero > * { position: relative; }


.hero h1 { max-width: 15ch; }

/* The claim inside the claim. The hero scrim is solid --canvas across the whole
   text column (see .hero--photo::after), so the text sits on the page ground and
   accent-text, which was derived to clear AA on exactly that, is the right token
   rather than the brighter fill value. */
.hero h1 b { color: var(--accent-text); font-weight: inherit; }
.hero .lede { max-width: 54ch; font-size: clamp(1.1rem, 1rem + 0.5vw, 1.3rem); }

/* --------------------------------------------------------------------------
   The job that starts on the landing page. One question, then two more once
   there is an answer to place. Sized to sit under a display headline without
   competing with it.
   -------------------------------------------------------------------------- */

.quick-post {
  max-width: 560px;
  margin-top: var(--s6);
  padding: var(--s5);
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-lift);
}

.qp-label {
  display: block;
  font-size: 0.95rem;
  font-weight: 700;
  margin-bottom: var(--s3);
}

.qp-row { display: flex; gap: var(--s2); flex-wrap: wrap; }

.qp-input {
  flex: 1 1 260px;
  min-width: 0;
  padding: 13px 15px;
  font: inherit;
  font-size: 1rem;
  color: var(--ink);
  background: var(--canvas);
  border: 1px solid var(--hairline);
  border-radius: var(--r-sm);
}
.qp-input::placeholder { color: var(--ink-3); }
.qp-input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: transparent;
}
.qp-input[aria-invalid="true"] { border-color: var(--danger); }

.qp-go { flex: 0 0 auto; }

/* Step two. Hidden until there is something to place, shown for good after. */
.qp-more { margin-top: var(--s4); }
.qp-pair { display: grid; gap: var(--s3); }
@media (min-width: 480px) { .qp-pair { grid-template-columns: 1fr 1fr; } }

.qp-sub {
  display: block;
  font-size: 0.82rem;
  font-weight: 700;
  color: var(--ink-2);
}

.qp-select {
  display: block;
  width: 100%;
  margin-top: 6px;
  padding: 11px 13px;
  font: inherit;
  font-size: 0.95rem;
  color: var(--ink);
  background: var(--canvas);
  border: 1px solid var(--hairline);
  border-radius: var(--r-sm);
}
.qp-select:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* The suburb box suggests as you type rather than opening a list of 135 places.
   Same shell as .qp-select so the pair still reads as two matching controls. */
.qp-combo { position: relative; display: block; }
.qp-combo-input::placeholder { color: var(--ink-3); }
.qp-combo-list {
  position: absolute; z-index: 30; top: calc(100% + 4px); left: 0; right: 0;
  margin: 0; padding: 4px; list-style: none; max-height: 15rem; overflow-y: auto;
  font-weight: 400;
  background: var(--canvas); border: 1px solid var(--hairline);
  border-radius: var(--r-sm); box-shadow: var(--shadow-lift);
  overscroll-behavior: contain;
}
.qp-combo-opt {
  display: flex; align-items: baseline; justify-content: space-between; gap: var(--s3);
  padding: 9px 10px; border-radius: 6px; cursor: pointer;
  font-size: 0.92rem; color: var(--ink);
}
.qp-combo-opt.is-active { background: var(--chip-accent); }
.qp-combo-name b { font-weight: 800; color: var(--accent-text); }
.qp-combo-note { font-size: 0.76rem; color: var(--ink-3); white-space: nowrap; }
.qp-combo-empty { padding: 10px; font-size: 0.84rem; color: var(--ink-2); line-height: 1.45; }


.qp-reach {
  margin: var(--s3) 0 0;
  font-size: 0.88rem;
  color: var(--verified-text);
}


/* --------------------------------------------------------------------------
   Public tradie profiles at /tradie/. Generated by build_profiles.py, styled
   here so they are the same site rather than a lookalike.
   -------------------------------------------------------------------------- */

/* The demo banner. Loud on purpose: a page that looks like a real profile has
   to say it is not one before anybody reads a review on it. */
.demo-banner {
  background: var(--chip-amber);
  border-bottom: 1px solid var(--hairline);
  padding: var(--s3) var(--s5);
  font-size: 0.9rem;
  color: var(--ink);
  text-align: center;
}
.demo-banner b { font-weight: 800; }

/* The demo index. A face beside each name, because the point of the page is to
   show what a filled-in profile looks like and a wall of text does not. */
.demo-grid { display: grid; gap: var(--s3); margin-top: var(--s5); }
@media (min-width: 600px) { .demo-grid { grid-template-columns: 1fr 1fr; } }
@media (min-width: 960px) { .demo-grid { grid-template-columns: repeat(3, 1fr); } }
.demo-card { display: flex; align-items: center; gap: var(--s3); padding: var(--s3) var(--s4); }
.demo-card > span { display: grid; min-width: 0; }
.demo-card b { font-size: 1rem; }
.demo-card .muted { font-size: 0.82rem; }

/* Avatars on the public profiles. Same rule as the portal's, ported rather than
   linked so a marketing page never depends on portal.css. Initials on a stable
   pastel is the design, not a placeholder: avatar_url is null for almost
   everyone, and initials read better at 44px than a grey silhouette. */
.tl-avatar {
  position: relative; flex: none;
  width: var(--size, 44px); height: var(--size, 44px); border-radius: 999px;
  display: grid; place-items: center; overflow: hidden;
  background: color-mix(in srgb, var(--hue) var(--tint), var(--canvas));
  color: var(--hue); font-weight: 800; font-size: calc(var(--size, 44px) * .36);
  letter-spacing: -0.02em;
}
.tl-avatar img { width: 100%; height: 100%; border-radius: inherit; object-fit: cover; }
.tl-avatar--lg { box-shadow: var(--shadow); }

/* The name sits beside the face, not under it. */
/* ── the public partner profile head ──────────────────────────────────────────
 *
 * Name, badges and face first; the numbers a customer is weighing next; then one
 * button. The order is the order somebody actually decides in, which is why the
 * button sits under the evidence rather than above it.
 *
 * Public by construction: these pages are plain HTML built with the publishable key,
 * so nothing here needs an account to read. Only messaging does. */

.pro-head { display: flex; align-items: flex-start; gap: var(--s4); }
.pro-head-main { flex: 1 1 auto; min-width: 0; }
.pro-name-row { display: flex; align-items: center; flex-wrap: wrap; gap: var(--s2); }
.pro-name-row h1 { margin: 0; font-size: clamp(1.4rem, 4.4vw, 1.9rem); letter-spacing: -0.03em; }
.pro-sub { margin: var(--s2) 0 0; font-size: 0.86rem; color: var(--ink-2); }

/* Only a verified partner gets the tick, and only a real rating gets the chip. A new
   profile shows neither: a greyed-out badge reads as failed rather than not yet done. */
.pro-badge {
  display: inline-flex; align-items: center; gap: 4px; white-space: nowrap;
  padding: 3px 9px; border-radius: 999px; font-size: 0.72rem; font-weight: 800;
  background: var(--chip-green); color: var(--verified-text);
}
.pro-rating-chip {
  display: inline-flex; align-items: center; gap: 4px; white-space: nowrap;
  padding: 3px 9px; border-radius: 999px; font-size: 0.72rem; font-weight: 800;
  background: var(--amber-wash, rgba(201,138,14,.14)); color: var(--amber-text, #8A5D00);
}

/* Three counts, always three across even on a narrow phone. Wrapping one onto its own
   line would make it look like the important one. */
.pro-counts {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--s3);
  margin-top: var(--s5);
}
.pro-counts b { display: block; font-size: 1.5rem; font-weight: 800; letter-spacing: -0.03em; }
.pro-counts span { display: block; margin-top: 2px; font-size: 0.74rem; color: var(--ink-2); }

/* The rating, given the weight the reference gives money. Deliberately not earnings:
   what a partner has been paid is their private business and publishing it by default
   is not a decision a profile layout should make for them. */
.pro-headline { margin-top: var(--s5); }
.pro-headline b { font-size: clamp(2rem, 8vw, 2.6rem); font-weight: 800; letter-spacing: -0.04em; }
.pro-headline-star { margin-left: 6px; font-size: 1.3rem; color: var(--amber-text, #C98A0E); }
.pro-headline p { margin: var(--s2) 0 0; max-width: 44ch; }

.btn--dark { background: var(--ink); color: var(--surface); border-color: var(--ink); }
.btn--dark:hover { background: var(--ink-2); border-color: var(--ink-2); color: var(--surface); }
.btn--full { display: flex; width: 100%; }

/* Message stays the one obvious thing to do and takes the room; copying the link is the
   quieter second. They wrap rather than shrink, because "Message Kirsten's Plumbing and
   Gas" is a long label and squeezing it next to a second button is how a primary action
   ends up with its own name broken across two lines on a phone. */
.pro-actions { display: flex; flex-wrap: wrap; gap: var(--s3); }
.pro-actions > .btn { flex: 1 1 14rem; }
.pro-share .ic { width: 17px; height: 17px; }
/* Confirmation is the label swapping under the pointer, so the colour only has to agree
   with it rather than carry the message on its own. */
.pro-share.is-done { color: var(--verified-text); border-color: var(--verified); }
.pro-share.is-failed { color: var(--ink-2); }

.pro-reply { margin: var(--s3) 0 0; text-align: center; font-size: 0.8rem; color: var(--ink-2); }

/* Anchors, not tabs. Everything stays in the page and in the HTML, so a crawler reads
   the reviews and nobody with JS off loses half the profile. */
.pro-tabs { display: flex; gap: var(--s5); margin-top: var(--s6);
            border-bottom: 1px solid var(--hairline); }
.pro-tabs a {
  padding: var(--s3) 0; text-decoration: none; font-weight: 700; font-size: 0.92rem;
  color: var(--ink-2); border-bottom: 2px solid transparent; margin-bottom: -1px;
}
.pro-tabs a.is-on { color: var(--ink); border-bottom-color: var(--accent); }
.pro-tabs a:hover { color: var(--ink); }

/* Five reviews, then the rest. `details` so the whole list ships in the HTML. */
.reviews-more { margin-top: var(--s4); }
.reviews-more > summary {
  list-style: none; cursor: pointer; text-align: center;
  padding: var(--s4); border-radius: var(--r-md);
  background: var(--surface-2, var(--surface)); border: 1px solid var(--hairline);
  font-weight: 700; font-size: 0.9rem;
}
.reviews-more > summary::-webkit-details-marker { display: none; }
.reviews-more > summary:hover { border-color: var(--accent); }
.reviews-more[open] > summary { margin-bottom: var(--s4); }

.pro-id { display: flex; align-items: center; gap: var(--s4); }
.pro-id h1 { margin: 0; }
.pro-id .eyebrow { margin-bottom: var(--s1); }

.crumbs {
  max-width: var(--wrap, 1180px); margin-inline: auto;
  padding: var(--s4) var(--s5) 0; font-size: 0.85rem; color: var(--ink-2);
}
.crumbs a { color: var(--ink-2); display: inline-block; padding-block: 8px; }
.crumbs span { color: var(--ink); font-weight: 600; }

.pro-stats {
  display: grid; grid-template-columns: repeat(2, 1fr); gap: 1px;
  background: var(--hairline); border: 1px solid var(--hairline);
  border-radius: var(--r-lg); overflow: hidden; margin-top: var(--s5); max-width: 620px;
}
@media (min-width: 560px) { .pro-stats { grid-template-columns: repeat(4, 1fr); } }
.pro-stats > div { background: var(--surface); padding: var(--s4) var(--s3); text-align: center; }
.pro-stats b { display: block; font-size: 1.45rem; font-weight: 800; letter-spacing: -0.02em;
               font-variant-numeric: tabular-nums; }
.pro-stats span { display: block; margin-top: 2px; font-size: 0.76rem; color: var(--ink-2); }

/* Every check, done or not, with what it covers. The not-done rows are the point:
   a customer can see what was never provided, which a list of ticks cannot show. */
/* Checks as a compact row. A full table with a paragraph per row was giving four
   short facts the height of a whole section. */
/* The browse list. A row per tradie rather than a card grid: the useful part is
   the name, where they are and what has been checked, and those read faster
   stacked than boxed. */
.browse-grid { display: grid; gap: var(--s2); margin: var(--s3) 0 var(--s6); }
@media (min-width: 700px) { .browse-grid { grid-template-columns: 1fr 1fr; } }
.browse-card { display: flex; align-items: center; gap: var(--s3); padding: var(--s3) var(--s4); }
.browse-main { display: grid; min-width: 0; }
.browse-main b { font-size: 1rem; }
.browse-meta { font-size: 0.82rem; color: var(--ink-2); }

.chk-row { display: flex; flex-wrap: wrap; gap: var(--s2); margin: var(--s3) 0; }
.chk {
  font-size: 0.82rem; font-weight: 600; padding: 5px 11px; border-radius: 999px;
  border: 1px solid var(--hairline); white-space: nowrap;
}
.chk--yes { background: var(--chip-green); color: var(--verified-text); border-color: transparent; }
.chk--yes::before { content: "\2713\00a0"; font-weight: 800; }
.chk b { font-weight: 700; font-variant-numeric: tabular-nums; }

.projects { display: grid; gap: var(--s4); grid-template-columns: 1fr; margin: var(--s4) 0; }
@media (min-width: 560px) { .projects { grid-template-columns: 1fr 1fr; } }
.project { margin: 0; background: var(--surface); border: 1px solid var(--hairline);
           border-radius: var(--r-md); overflow: hidden; }
.project img, .project-blank { display: block; width: 100%; aspect-ratio: 4 / 3;
                               object-fit: cover; background: var(--surface-alt); }
.project figcaption { padding: var(--s3) var(--s4); font-size: 0.9rem; color: var(--ink-2); }

/* Scoped under .reviews on purpose. The home page review wall already owns bare
   `.review` and `.review-stars`, it is defined later in this file so it wins, and
   its stars are sized for SVG icons rather than the text glyphs these use. Two
   rules with the same name quietly fighting is how a card ends up display:flex
   when nobody wrote that. */
/* The block above the review list: average, spread, and the four dimensions.
   `.rate-stars` rather than `.review-stars` on purpose. The home page wall owns
   bare `.review-stars`, it is defined later in this file so it wins, and it is
   sized for SVG icons rather than the text glyphs used here. */
.rate-sum {
  display: grid; gap: var(--s4); margin: var(--s4) 0 0;
  background: var(--surface); border: 1px solid var(--hairline);
  border-radius: var(--r-md); padding: var(--s4) var(--s5);
}
@media (min-width: 620px) {
  .rate-sum { grid-template-columns: minmax(0, 8rem) 1fr; align-items: start; gap: var(--s5); }
}
.rate-big { text-align: center; display: grid; gap: 2px; align-content: start; }
.rate-big b { font-size: 2.9rem; font-weight: 800; letter-spacing: -0.04em; line-height: 1;
              font-variant-numeric: tabular-nums; }
.rate-stars { color: var(--amber-text); letter-spacing: 2px; font-size: 0.95rem; }
.rate-count { font-size: 0.78rem; color: var(--ink-2); }
.rate-cols { display: grid; gap: var(--s4); min-width: 0; }
@media (min-width: 860px) { .rate-cols { grid-template-columns: 1fr 1fr; gap: var(--s5); } }
.rate-sum ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 7px; align-content: start; }
.rate-sum li {
  display: grid; grid-template-columns: 7.4rem minmax(0, 1fr) 2rem;
  align-items: center; gap: var(--s2); font-size: 0.8rem; color: var(--ink-2);
}
.rate-sum li i { display: block; height: 8px; border-radius: 999px; background: var(--surface-alt); }
.rate-sum li u { display: block; height: 100%; border-radius: 999px; background: var(--amber);
                 text-decoration: none; }
.rate-sum li b { text-align: right; color: var(--ink); font-weight: 700;
                 font-variant-numeric: tabular-nums; }

.reviews { list-style: none; padding: 0; margin: var(--s4) 0 0; display: grid; gap: var(--s3); }
.reviews .review { display: block; background: var(--surface); border: 1px solid var(--hairline);
                   border-radius: var(--r-md); padding: var(--s4); }
.reviews .review-stars { display: inline; color: var(--amber-text); letter-spacing: 1px; }
.reviews .review-date { font-size: 0.78rem; color: var(--ink-2); font-variant-numeric: tabular-nums; }
.reviews .review-job { margin: var(--s2) 0 var(--s2); font-weight: 700; font-size: 0.92rem; }
.reviews .review p { margin: 0; color: var(--ink-2); }
.reviews .review-head { display: flex; align-items: center; gap: var(--s3); margin-bottom: var(--s3); }
.reviews .review-who { flex: 1; min-width: 0; display: grid; }
.reviews .review-who b { font-size: 0.95rem; }

.hero-grid {
  display: grid; gap: clamp(var(--s6), 5vw, var(--s8));
  grid-template-columns: 1fr;
  align-items: center;
}
@media (min-width: 940px) {
  .hero-grid { grid-template-columns: 1.05fr 0.95fr; }
}

/* --------------------------------------------------------------------------
   8. Trades grid
   -------------------------------------------------------------------------- */

.trades { display: grid; gap: var(--s3); grid-template-columns: repeat(auto-fill, minmax(158px, 1fr)); }

.trade {
  display: flex; align-items: center; gap: var(--s3);
  padding: var(--s3) var(--s4) var(--s3) var(--s3);
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--r-md);
  color: var(--ink);
  font-weight: 700; font-size: 0.97rem;
  text-decoration: none;
  transition: transform .15s ease-out, box-shadow .15s ease-out, border-color .15s ease-out;
}
.trade:hover { transform: translateY(-2px); box-shadow: var(--shadow); color: var(--ink); }

.trade-chip {
  display: grid; place-items: center;
  width: 40px; height: 40px; flex: none;
  border-radius: var(--r-sm);
  background: color-mix(in srgb, var(--hue) var(--tint), transparent);
  color: var(--hue);
}
.trade-chip svg { width: 22px; height: 22px; }

.t-plumber   { --hue: var(--sky); }
.t-builder   { --hue: var(--clay); }
.t-cleaner   { --hue: var(--aqua); }
.t-assembly  { --hue: var(--lavender); }
.t-moving    { --hue: var(--peach); }
.t-electric  { --hue: var(--butter); }
.t-painter   { --hue: var(--rose); }
.t-garden    { --hue: var(--sage); }
.t-appliance { --hue: var(--mint); }
.t-rubbish   { --hue: var(--sand); }
.t-mechanic  { --hue: var(--lilac); }
.t-sidequest { --hue: var(--coral); }

/* --------------------------------------------------------------------------
   9. Shield / verification
   -------------------------------------------------------------------------- */

.shield-badge {
  display: inline-flex; align-items: center; gap: var(--s2);
  padding: 8px 16px 8px 12px;
  border-radius: 999px;
  background: var(--chip-green);
  color: var(--verified-text);
  font-weight: 700; font-size: 0.92rem;
}
.shield-badge svg { width: 19px; height: 19px; }

/* The identity orbit: one drawn tradesperson, four checks around them.
   Replaced the single oversized shield, which said "verification" without
   saying what was verified or who. */

.id-orbit {
  position: relative;
  width: min(100%, 340px);
  aspect-ratio: 1;
  margin-inline: auto;
  display: grid;
  place-items: center;
}

/* The portrait sits inside the orbit so the badges clear it on every side. */
.id-portrait {
  width: 68%;
  aspect-ratio: 1;
  height: auto;
  border-radius: 50%;
  box-shadow: var(--shadow-lift);
  /* The bag and the hat sit low-centre in the frame, so a plain centre crop cuts
     them in half. Favour the lower middle and the subject lands in the circle. */
  object-fit: cover;
  object-position: 50% 62%;
}

/* A hairline ring the badges sit on, so they read as a set rather than four
   things scattered near a circle. */
.id-orbit::before {
  content: "";
  position: absolute;
  inset: 8%;
  border-radius: 50%;
  border: 1px dashed color-mix(in srgb, var(--verified) 34%, transparent);
}

.id-mark { position: absolute; }
/* Four diagonals. The figure occupies the middle 68%, so the corners are the
   only places a badge can sit without covering a shoulder or the hat. */
.id-mark--a { top: 2%;  left: 50%; transform: translateX(-50%); }
.id-mark--b { top: 50%; right: 0;  transform: translateY(-50%); }
.id-mark--c { bottom: 2%; left: 50%; transform: translateX(-50%); }
.id-mark--d { top: 50%; left: 0;   transform: translateY(-50%); }

@media (prefers-reduced-motion: no-preference) {
  .id-mark { transition: transform .3s ease; }
  .id-orbit:hover .id-mark--a { transform: translateX(-50%) translateY(-4px); }
  .id-orbit:hover .id-mark--c { transform: translateX(-50%) translateY(4px); }
  .id-orbit:hover .id-mark--b { transform: translateY(-50%) translateX(4px); }
  .id-orbit:hover .id-mark--d { transform: translateY(-50%) translateX(-4px); }
}

/* --------------------------------------------------------------------------
   10. Phone mockups, always the app's real dark theme, per the brief.
   -------------------------------------------------------------------------- */

.phone {
  --p-canvas:#161B22; --p-surface:#1F2630; --p-alt:#2A323E; --p-line:#333C4A;
  --p-ink:#F4F7FA;   --p-ink2:#A2AEBE;
  /* Lifted from the app's #6B7787: at 9–10px on these surfaces that value is
     2.8:1. Still reads a step below ink2, but clears AA. */
  --p-ink3:#909DAE;
  --p-accent:#5B93FF; --p-wash:rgba(91,147,255,.22);
  --p-green:#4FD18C; --p-amber:#F6C453;
  /* Opaque tints for the chips that carry a label, same reason as --chip-accent:
     a translucent wash over the card surface lifts the plate and the label drops
     to 4.1:1. */
  --p-chip-accent:#192948; --p-chip-green:#1F3833;

  position: relative;
  width: 100%;
  max-width: 320px;
  margin-inline: auto;
  aspect-ratio: 320 / 660;
  padding: 11px;
  border-radius: 46px;
  background: linear-gradient(160deg, #3A424E, #1B2129 55%, #2C333D);
  box-shadow: 0 30px 60px -24px rgba(0,0,0,.55), 0 2px 0 rgba(255,255,255,.06) inset;
}
.phone-screen {
  position: relative;
  height: 100%;
  border-radius: 36px;
  overflow: hidden;
  background: var(--p-canvas);
  color: var(--p-ink);
  font-size: 12px;
  line-height: 1.45;
  display: flex;
  flex-direction: column;
}
.phone-island {
  position: absolute; top: 9px; left: 50%; transform: translateX(-50%);
  width: 82px; height: 24px; border-radius: 999px;
  background: #0B0E13; z-index: 4;
}
.phone-status {
  display: flex; align-items: center; justify-content: space-between;
  padding: 13px 20px 4px;
  font-size: 11px; font-weight: 700; color: var(--p-ink);
}
.phone-status .bars { display: flex; gap: 3px; align-items: flex-end; }
.phone-status .bars i { display: block; width: 3px; background: var(--p-ink); border-radius: 1px; }
.phone-status .bars i:nth-child(1){height:4px} .phone-status .bars i:nth-child(2){height:6px}
.phone-status .bars i:nth-child(3){height:8px} .phone-status .bars i:nth-child(4){height:10px}
.phone-status .batt {
  width: 20px; height: 10px; border: 1px solid var(--p-ink2); border-radius: 3px;
  position: relative; padding: 1px;
}
.phone-status .batt::after {
  content:""; display:block; height:100%; width:70%; background: var(--p-ink); border-radius: 1px;
}

.phone-body { flex: 1; overflow: hidden; padding: 6px 14px 0; }
/* A chat sits at the bottom of the screen, not the top, otherwise the frame
   reads as an empty app rather than a conversation scrolled to the latest. */
.phone-body--bottom { display: flex; flex-direction: column; justify-content: flex-end; padding-bottom: 4px; }
.phone-title { font-size: 19px; font-weight: 800; letter-spacing: -0.02em; margin: 6px 0 1px; }
.phone-sub { font-size: 11px; color: var(--p-ink2); margin-bottom: 12px; }

.p-card {
  background: var(--p-surface);
  border: 1px solid var(--p-line);
  border-radius: 16px;
  padding: 11px 12px;
  margin-bottom: 9px;
}
.p-field {
  background: var(--p-alt);
  border: 1px solid var(--p-line);
  border-radius: 12px;
  padding: 9px 11px;
  margin-bottom: 8px;
}
.p-label { font-size: 9.5px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; color: var(--p-ink3); margin-bottom: 3px; }
.p-value { font-weight: 700; font-size: 12.5px; }

.p-row { display: flex; align-items: center; gap: 9px; }
.p-row + .p-row { margin-top: 9px; }
.p-spread { display: flex; align-items: center; justify-content: space-between; gap: 8px; }

.p-avatar {
  width: 34px; height: 34px; flex: none; border-radius: 999px;
  display: grid; place-items: center;
  font-size: 12px; font-weight: 800;
  /* Mixed toward the phone's own canvas rather than `transparent`: over a card
     surface a translucent tint lightens the plate under the initials and drags
     them to 4.3:1. Opaque keeps the hue and clears AA. */
  background: color-mix(in srgb, var(--h) 24%, var(--p-canvas));
  color: var(--h);
}
.p-avatar--sq { border-radius: 11px; }

.p-name { font-weight: 700; font-size: 12.5px; display: flex; align-items: center; gap: 4px; }
.p-name svg { width: 12px; height: 12px; color: var(--p-green); flex: none; }
.p-meta { font-size: 10.5px; color: var(--p-ink2); }

.p-chips { display: flex; gap: 5px; flex-wrap: wrap; }
.p-chip {
  padding: 5px 10px; border-radius: 999px;
  background: var(--p-alt); border: 1px solid var(--p-line);
  font-size: 10.5px; font-weight: 700; color: var(--p-ink2);
}
.p-chip--on { background: var(--p-chip-accent); border-color: transparent; color: var(--p-accent); }
.p-chip--green { background: var(--p-chip-green); border-color: transparent; color: var(--p-green); }

.p-quote { font-size: 11.5px; color: var(--p-ink2); margin: 8px 0 0; }

.p-ballpark {
  display: inline-flex; align-items: baseline; gap: 5px;
  margin-top: 9px; padding: 5px 10px; border-radius: 999px;
  background: var(--p-alt); font-size: 11px; color: var(--p-ink2);
}
.p-ballpark b { color: var(--p-ink); font-size: 12px; }

.p-toggle {
  width: 38px; height: 22px; flex: none; border-radius: 999px;
  background: var(--p-accent); position: relative;
}
.p-toggle::after {
  content:""; position: absolute; top: 2px; right: 2px;
  width: 18px; height: 18px; border-radius: 999px; background: #fff;
}
.p-toggle--off { background: var(--p-alt); }
.p-toggle--off::after { right: auto; left: 2px; background: var(--p-ink3); }

.p-btn {
  display: block; text-align: center;
  padding: 11px; border-radius: 999px;
  background: var(--p-accent); color: #161B22;
  font-weight: 800; font-size: 12.5px;
  margin-top: 10px;
}
.p-btn--quiet { background: var(--p-alt); color: var(--p-ink2); }
button.p-btn { border: 0; width: 100%; font: inherit; cursor: pointer; }

.p-tabs {
  display: flex; justify-content: space-between;
  padding: 9px 16px 14px;
  border-top: 1px solid var(--p-line);
  background: var(--p-surface);
}
.p-tab { display: grid; justify-items: center; gap: 3px; color: var(--p-ink3); font-size: 9px; font-weight: 700; }
.p-tab svg { width: 18px; height: 18px; }
.p-tab--on { color: var(--p-accent); }

/* Chat */
.p-bubble {
  max-width: 78%; padding: 8px 11px; border-radius: 15px;
  font-size: 11.5px; margin-bottom: 7px; line-height: 1.4;
}
.p-bubble--them { background: var(--p-surface); border: 1px solid var(--p-line); border-bottom-left-radius: 5px; }
.p-bubble--me { background: var(--p-accent); color: #161B22; margin-left: auto; border-bottom-right-radius: 5px; font-weight: 600; }
.p-daymark { text-align: center; font-size: 10px; color: var(--p-ink3); margin: 4px 0 10px; }

/* Funnel */
.p-funnel { display: grid; gap: 7px; }
.p-funnel-row { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 8px; font-size: 11px; }
.p-funnel-bar { height: 26px; border-radius: 8px; background: var(--p-wash); display: flex; align-items: center; padding-left: 9px; color: var(--p-ink); font-weight: 700; font-size: 10.5px; }
.p-funnel-row b { font-size: 12px; }

/* Composite phone group */
.phone-row {
  display: grid;
  gap: var(--s5);
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  align-items: start;
}

/* --------------------------------------------------------------------------
   11. FAQ
   -------------------------------------------------------------------------- */

.faq { max-width: 820px; }
.faq details {
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--r-md);
  margin-bottom: var(--s3);
  overflow: hidden;
}
.faq summary {
  list-style: none;
  cursor: pointer;
  padding: var(--s4) var(--s5);
  font-weight: 700;
  font-size: 1.03rem;
  display: flex; align-items: center; justify-content: space-between; gap: var(--s4);
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after {
  content: "";
  width: 11px; height: 11px; flex: none;
  border-right: 2.5px solid var(--ink-3);
  border-bottom: 2.5px solid var(--ink-3);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform .18s ease-out;
}
.faq details[open] summary::after { transform: rotate(225deg) translate(-2px, -2px); }
.faq details[open] summary { padding-bottom: var(--s2); }
.faq .faq-body { padding: 0 var(--s5) var(--s5); color: var(--ink-2); }
.faq .faq-body p { font-size: 0.98rem; }

/* --------------------------------------------------------------------------
   12. Misc blocks
   -------------------------------------------------------------------------- */

.pill-row { display: flex; flex-wrap: wrap; gap: var(--s2); }
.pill {
  padding: 9px 15px; border-radius: 999px;
  background: var(--surface-alt); border: 1px solid var(--hairline);
  font-size: 0.88rem; font-weight: 700; color: var(--ink-2);
}
.pill--accent { background: var(--chip-accent); border-color: transparent; color: var(--accent-text); }

.stat-row { display: grid; gap: var(--s4); grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); }
.stat { padding: var(--s5); border-radius: var(--r-lg); background: var(--surface); border: 1px solid var(--hairline); }
.stat b { display: block; font-size: 1.45rem; letter-spacing: -0.02em; }
.stat span { color: var(--ink-2); font-size: 0.92rem; }

.timeline { list-style: none; padding: 0; margin: 0; max-width: var(--measure); }
.timeline li {
  position: relative;
  padding: 0 0 var(--s6) var(--s7);
  margin: 0;
}
.timeline li::before {
  content: "";
  position: absolute; left: 13px; top: 26px; bottom: 0;
  width: 2px; background: var(--hairline);
}
.timeline li:last-child { padding-bottom: 0; }
.timeline li:last-child::before { display: none; }
.timeline .dot {
  position: absolute; left: 0; top: 3px;
  width: 28px; height: 28px; border-radius: 999px;
  display: grid; place-items: center;
  background: var(--chip-accent); color: var(--accent-text);
  font-size: 0.8rem; font-weight: 800;
}
.timeline h3 { margin-bottom: var(--s1); }
.timeline p { color: var(--ink-2); }

.callout {
  border-radius: var(--r-lg);
  border: 1px solid var(--hairline);
  background: var(--surface-alt);
  padding: var(--s5);
}
/* Opaque on dark for the same reason the chips are: a link at accent-text on
   a translucent wash only reaches 3.6:1. */
.callout--accent { background: var(--chip-accent); border-color: transparent; }
.callout h3 { margin-bottom: var(--s2); }


.contact-card { display: flex; flex-wrap: wrap; gap: var(--s5); }
.contact-card > div { flex: 1 1 220px; }

/* .eyebrow is inline-block, so a short value sits on the label's line while a
 * long one wraps below it. With two channels side by side that reads as an
 * inconsistency rather than a wrap. Label always above its value. */
.contact-card .eyebrow { display: block; }
.contact-card a,
.contact-card .soon { font-size: 1.15rem; font-weight: 800; letter-spacing: -0.01em; }
.contact-card a { display: inline-block; padding-block: 9px; }

/* Not a link, because there is nothing to dial yet. Same weight as the address
 * beside it so the two channels read as a pair rather than one being an error.
 * `block` because .eyebrow is inline-block, and without it "Coming soon" is
 * short enough to sit on the label's line while the email address wraps below
 * its own, which makes the two channels look like different kinds of thing. */
.contact-card .soon { display: block; color: var(--ink-2); }

/* --------------------------------------------------------------------------
   13. CTA band + footer
   -------------------------------------------------------------------------- */

.cta-band { text-align: center; }
.cta-band .btn-row { justify-content: center; }
.cta-band p { margin-inline: auto; }

.site-footer {
  background: var(--surface);
  border-top: 1px solid var(--hairline);
  padding-block: var(--s8) var(--s6);
}
.footer-grid {
  display: grid; gap: var(--s6);
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  margin-bottom: var(--s7);
}
.footer-grid h3 { margin-bottom: var(--s3); font-size: 0.8rem; letter-spacing: 0.09em; text-transform: uppercase; color: var(--ink-2); }
.footer-grid ul { list-style: none; padding: 0; margin: 0; }
.footer-grid li { margin-bottom: 0; }
/* Padded to a real touch target rather than sized by the text.
   These were 19px tall with 17px between them, nineteen of them stacked in a
   column — the exact shape that produces a mis-tap on a phone. The padding is on
   the anchor, not the li, so the whole row is the target and not just the words. */
.footer-grid a {
  display: block; padding: 9px 0;
  color: var(--ink-2); text-decoration: none; font-size: 0.96rem;
}
.footer-grid a:hover { color: var(--ink); text-decoration: underline; }
/* Pull the column back level with its heading, since the first link now has
   padding above it. */
.footer-grid ul { margin-top: -9px; }
.footer-brand p { font-size: 0.94rem; color: var(--ink-2); max-width: 34ch; margin-top: var(--s3); }

.footer-base a { display: inline-block; padding: 10px 0; }

.footer-base {
  display: flex; flex-wrap: wrap; gap: var(--s4);
  justify-content: space-between; align-items: center;
  padding-top: var(--s5);
  border-top: 1px solid var(--hairline);
  color: var(--ink-2); font-size: 0.88rem;
}

/* --------------------------------------------------------------------------
   13b. Show, don't tell

   Components that carry a fact without a paragraph around it. Each one exists
   because it replaced prose: a number people can read in a glance beats a
   sentence containing that number, and a table beats two lists the reader has
   to hold in their head at once.
   -------------------------------------------------------------------------- */

/* Big figures. The number leads and the label follows, because the number is
   the thing worth remembering. */
.figures {
  display: grid; gap: var(--s3);
  grid-template-columns: repeat(auto-fit, minmax(148px, 1fr));
}
.figure {
  padding: var(--s5) var(--s4);
  border-radius: var(--r-lg);
  background: var(--surface); border: 1px solid var(--hairline);
  text-align: center;
}
.figure b {
  display: block;
  font-size: clamp(1.5rem, 1.2rem + 1vw, 1.95rem);
  line-height: 1.05; letter-spacing: -0.035em;
  color: var(--ink); font-variant-numeric: tabular-nums;
}
.figure span {
  display: block; margin-top: 6px;
  font-size: 0.88rem; line-height: 1.35; color: var(--ink-2);
}
.figure--accent b { color: var(--accent-text); }
.figure--green b { color: var(--verified-text); }

/* The money flow. Four beats, read left to right on a wide screen and top to
   bottom on a phone, with the connector drawn behind rather than between so it
   never fights the wrap. */
.flow {
  list-style: none; margin: 0; padding: 0;
  display: grid; gap: var(--s4);
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  counter-reset: flow;
}
.flow li {
  position: relative; margin: 0;
  padding: var(--s5) var(--s4) var(--s4);
  border-radius: var(--r-lg);
  background: var(--surface); border: 1px solid var(--hairline);
}
.flow li::before {
  counter-increment: flow; content: counter(flow);
  position: absolute; top: calc(-1 * var(--s3)); left: var(--s4);
  width: 32px; height: 32px; border-radius: 999px;
  display: grid; place-items: center;
  background: var(--btn-bg); color: var(--btn-ink);
  font-weight: 800; font-size: 0.94rem;
}
.flow h3 { font-size: 1rem; margin: 0 0 4px; }
.flow p { margin: 0; font-size: 0.92rem; color: var(--ink-2); }

/* Plan comparison. A table, because that is what a reader is doing: comparing
   two values of the same thing. Scrolls in its own box so the page never does. */
.compare-wrap { overflow-x: auto; margin-top: var(--s5); }
.compare {
  width: 100%; min-width: 460px;
  border-collapse: collapse; font-size: 0.95rem;
}
.compare th, .compare td {
  padding: var(--s3) var(--s4);
  border-bottom: 1px solid var(--hairline);
  text-align: left; vertical-align: top;
}
.compare thead th {
  font-size: 1.02rem; color: var(--ink);
  border-bottom-width: 2px;
}
.compare tbody th { color: var(--ink-2); font-weight: 600; width: 34%; }
.compare td { color: var(--ink); font-weight: 600; }
.compare .is-premium { background: var(--accent-wash); }
.compare tr:last-child th, .compare tr:last-child td { border-bottom: 0; }
.compare-head { display: flex; align-items: center; gap: var(--s2); }
.compare-head svg { width: 20px; height: 20px; flex: none; color: var(--amber); }

/* A price, sized like a price. */
/* The capacity, which is what the plan actually sells. Sits directly under the
   price so the two are read as a pair: this much money buys this much at once. */
.plan-slots {
  margin: 0 0 var(--s3);
  padding-top: var(--s3);
  border-top: 1px solid var(--hairline);
  color: var(--ink-2);
  font-size: 0.95rem;
}
.plan-slots b {
  color: var(--accent-text);
  font-size: 1.25rem;
  font-variant-numeric: tabular-nums;
  margin-right: 4px;
}

.pricetag { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; margin: var(--s3) 0; }
.pricetag b { font-size: 2.1rem; letter-spacing: -0.035em; line-height: 1; }
.pricetag span { color: var(--ink-2); font-size: 0.95rem; }

/* One line, one icon. What a four-item tick list used to be, minus the prose
   hanging off each item. */
.points { list-style: none; padding: 0; margin: var(--s4) 0 0;
          display: grid; gap: var(--s2); }
.points li {
  display: flex; align-items: center; gap: var(--s3);
  margin: 0; padding: var(--s3) var(--s4);
  border-radius: var(--r-md); background: var(--surface-alt);
  font-size: 0.95rem; color: var(--ink);
}
.points svg { width: 20px; height: 20px; flex: none; color: var(--accent-text); }
.points--green svg { color: var(--verified-text); }

/* --------------------------------------------------------------------------
   13b2. Photography

   The site had no photographs at all, which made it read as a diagram of a
   product rather than a way to get someone to your house. These are the rules
   that let a photo sit under text without costing any of the contrast the rest
   of the stylesheet was built to protect.

   The scrim is built from `--canvas`, so it inverts with the theme
   automatically: dark text gets a light veil, light text gets a dark one, and
   the measured contrast at the text is the same as it was with no photo at all.
   -------------------------------------------------------------------------- */

.hero--photo { position: relative; isolation: isolate; overflow: hidden; }

/* A real <img> rather than a CSS background: the preload scanner finds it, so
   it can be the LCP element instead of racing the stylesheet. */
.hero-photo {
  position: absolute; inset: 0; z-index: -2;
  width: 100%; height: 100%;
  object-fit: cover; object-position: 62% 55%;
}

.hero--photo::after {
  content: ""; position: absolute; inset: 0; z-index: -1;
  /* Measured, not guessed: sampling the composited pixels under the hero text
     showed the old ramp dropped to 1.7:1 at the right end of the lede. The
     fully-opaque band now covers the whole text column, and the photo reads in
     the last third where no text ever lands. */
  background: linear-gradient(
    98deg,
    var(--canvas) 0%,
    var(--canvas) 56%,
    color-mix(in srgb, var(--canvas) 88%, transparent) 74%,
    color-mix(in srgb, var(--canvas) 62%, transparent) 100%);
}
/* Below the split point the text sits over the middle of the photo, so the
   veil goes near-solid. Legibility first; the photo is atmosphere. */
@media (max-width: 900px) {
  .hero--photo::after {
    /* Measured: at 74% the lawn showed through under the sign-in line and took
       secondary ink to 3.53:1 in light theme. On a phone the photo is doing
       atmosphere, not information, so it gives way to the text. */
    background: linear-gradient(
      180deg,
      var(--canvas) 0%,
      color-mix(in srgb, var(--canvas) 96%, transparent) 60%,
      color-mix(in srgb, var(--canvas) 88%, transparent) 100%);
  }
  .hero-photo { object-position: 60% 40%; }
}

/* An in-content photo. Same corner radius and hairline as `.card`, because a
   photo floating with no frame in a page built entirely out of framed surfaces
   reads as a mistake. */
.media {
  position: relative;
  border-radius: var(--r-lg);
  overflow: hidden;
  border: 1px solid var(--hairline);
  box-shadow: var(--shadow);
  background: var(--surface-alt);
  aspect-ratio: 3 / 2;
}
.media img {
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
}
/* Dark theme darkens photographs slightly so a bright image doesn't punch a
   hole in a dark page. Same instinct as the sheen rule. */
html[data-theme="dark"] .media img { filter: brightness(.88) saturate(.95); }

/* ── Photos you can open ────────────────────────────────────────────────────
   Motivated, not decorative: this is a site about tradework, and the one thing
   a photo of tradework is for is looking at it closely. Every in-content photo
   is a real <button> that opens the full frame.
*/
button.media {
  display: block; width: 100%; padding: 0;
  font: inherit; color: inherit; cursor: zoom-in;
  transition: transform .25s cubic-bezier(.16,1,.3,1), box-shadow .25s ease-out;
}
button.media img { transition: transform .5s cubic-bezier(.16,1,.3,1); }
button.media:hover img,
button.media:focus-visible img { transform: scale(1.045); }
button.media:hover { box-shadow: var(--shadow-lift); }
button.media:active { transform: scale(.994); }

/* The affordance. Without it a photo that opens on click is a secret. */
.media-cue {
  position: absolute; right: var(--s3); bottom: var(--s3); z-index: 2;
  display: inline-flex; align-items: center; gap: 6px;
  padding: 7px 12px; border-radius: 999px;
  background: color-mix(in srgb, var(--canvas) 88%, transparent);
  border: 1px solid var(--hairline);
  color: var(--ink); font-size: .8rem; font-weight: 700;
  backdrop-filter: blur(8px);
  opacity: 0; transform: translateY(4px);
  transition: opacity .2s ease-out, transform .2s ease-out;
}
button.media:hover .media-cue,
button.media:focus-visible .media-cue { opacity: 1; transform: none; }
/* Touch has no hover, so the cue is simply always on. */
@media (hover: none) { .media-cue { opacity: 1; transform: none; } }
.media-cue svg { width: 15px; height: 15px; }

/* ── The viewer ─────────────────────────────────────────────────────────── */

.lightbox {
  position: fixed; inset: 0; z-index: 90;
  display: grid; place-items: center;
  padding: clamp(var(--s4), 4vw, var(--s7));
  background: color-mix(in srgb, var(--canvas) 82%, transparent);
  backdrop-filter: blur(14px) saturate(120%);
  animation: lb-in .18s ease-out both;
}
.lightbox img {
  max-width: min(1200px, 100%); max-height: 100%;
  width: auto; height: auto;
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-lift);
  animation: lb-rise .28s cubic-bezier(.16,1,.3,1) both;
}
.lightbox-cap {
  position: absolute; left: 0; right: 0; bottom: max(var(--s4), env(safe-area-inset-bottom));
  margin-inline: auto; max-width: 62ch; padding: 0 var(--s5);
  text-align: center; color: var(--ink-2); font-size: .92rem;
}
.lightbox-close {
  position: absolute; top: var(--s4); right: var(--s4);
  width: 46px; height: 46px; border-radius: 999px;
  display: grid; place-items: center;
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--hairline); box-shadow: var(--shadow);
  cursor: pointer;
}
.lightbox-close:hover { background: var(--surface-alt); }
.lightbox.is-going { animation: lb-out .16s ease-in both; }

@keyframes lb-in   { from { opacity: 0 } to { opacity: 1 } }
@keyframes lb-out  { from { opacity: 1 } to { opacity: 0 } }
@keyframes lb-rise { from { opacity: 0; transform: translateY(10px) scale(.985) } to { opacity: 1; transform: none } }

/* ── Hero drift ─────────────────────────────────────────────────────────────
   A slow parallax that gives the hero depth. Driven by a native scroll-driven
   animation rather than a scroll listener, so it runs on the compositor and
   costs nothing on the main thread. Browsers without it simply hold still.
*/
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .hero-photo {
      animation: hero-drift linear both;
      animation-timeline: view();
      animation-range: entry 0% exit 100%;
      will-change: transform;
    }
    @keyframes hero-drift {
      from { transform: scale(1.1) translateY(-1.8%); }
      to   { transform: scale(1.1) translateY(2.4%); }
    }
  }
}

@media (prefers-reduced-motion: reduce) {
  button.media, button.media img, .media-cue { transition: none; }
  button.media:hover img, button.media:focus-visible img { transform: none; }
  .lightbox, .lightbox img { animation: none; }
}

/* --------------------------------------------------------------------------
   13c. Polish pass

   Each rule here fixes something a design audit found, not something invented
   to look busy. Grouped so it can be read, argued with, or reverted whole.
   -------------------------------------------------------------------------- */

/* Orphans. `balance` is already on headings; body copy wants `pretty`, which
   only protects the last line and doesn't reflow whole paragraphs. */
p, li, figcaption, dd { text-wrap: pretty; }

/* A phone number broken across two lines stops being a phone number. Applies
   to the footer, the support card, the 404 and anywhere else one appears. */
a[href^="tel:"] { white-space: nowrap; }

/* Money and figures line up in a column or they aren't comparable. */
.pricetag b, .compare td, .figure b { font-variant-numeric: tabular-nums; }

/* A press should feel like a press. The old rule only cancelled the hover
   lift, which reads as nothing happening on a device that never hovered. */
.btn:active { transform: scale(.975); transition-duration: .06s; }
.trade:active, .card--tap:active { transform: scale(.99); }

/* CTAs in a row of cards land at whatever height the copy above them ends,
   which reads as a wonky baseline. Pinning them puts the buttons on one line
   without forcing the cards themselves to equal height. */
.card { display: flex; flex-direction: column; }
.card > .btn:last-child { margin-top: auto; align-self: flex-start; }
/* The block above a pinned CTA always carries its own bottom margin, so
   `auto` never collapses the gap to zero even when the card is exactly
   as tall as its content. */
.card > p:last-of-type { margin-bottom: var(--s4); }

/* Two card variants lay their contents out in a row, and both are declared
   further up this file than the `flex-direction: column` above. Same
   specificity, later wins, so the face that should sit beside the name sat on
   top of it and `align-items: center` then centred the whole card. The element
   selector is what settles it without moving either rule.
   Cards that are themselves anchors also inherit the prose underline, which is
   right for a link in a sentence and wrong for a whole tappable card. */
a.browse-card, a.demo-card { flex-direction: row; text-decoration: none; }
a.browse-card b, a.demo-card b { color: var(--ink); }
/* Same problem inside the plan cards: the price block is one line for Standard
   and two for Premium, so the paragraphs under them started at different
   heights. A min-height on the price settles the row. */
.pricetag { min-height: 2.6rem; align-items: baseline; }

/* Grain. Flat vector surfaces read as sterile at large sizes; a couple of
   percent of noise is enough to give the canvas a texture without anyone
   consciously noticing it. Fixed and inert, so it never intercepts a click
   and never scrolls out of alignment with itself. */
body::after {
  content: "";
  position: fixed; inset: 0; z-index: 1;
  pointer-events: none;
  opacity: .022;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
}
html[data-theme="dark"] body::after { opacity: .035; }
@media (prefers-reduced-motion: reduce) { body::after { display: none; } }

/* --------------------------------------------------------------------------
   14. Motion, restrained, once only, and off entirely on request.
   -------------------------------------------------------------------------- */

.reveal {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity .55s cubic-bezier(.22,.61,.36,1),
              transform .55s cubic-bezier(.22,.61,.36,1);
  transition-delay: calc(var(--i, 0) * 60ms);
}
.reveal.is-in { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }
  .reveal { opacity: 1; transform: none; }
  .btn:hover, .card--lift:hover, .trade:hover { transform: none; }
}

/* JS-off safety: if the observer never runs, content must still be visible. */
.no-js .reveal { opacity: 1; transform: none; }

/* --------------------------------------------------------------------------
   15. Print
   -------------------------------------------------------------------------- */

@media print {
  .site-header, .phone, .cta-band { display: none; }
  body { background: #fff; color: #000; }
}

/* --------------------------------------------------------------------------
   16. Anchors + in-page nav
   The header is sticky, so every anchor target needs to clear it or the
   heading you jumped to lands underneath the nav.
   -------------------------------------------------------------------------- */

section[id], div[id], ol[id], details[id] { scroll-margin-top: var(--anchor-top, 92px); }
main.has-subnav { --anchor-top: 142px; }   /* header + the sticky sub-nav */

/* Sticky in-page nav for the long pages. It sits under the header and tracks
   the section you're actually in, so the scroll-spy state is visible while
   you're reading rather than only once you've scrolled back to the top. */
.subnav {
  position: sticky; top: 68px; z-index: 60;
  background: color-mix(in srgb, var(--canvas) 90%, transparent);
  backdrop-filter: saturate(1.4) blur(14px);
  -webkit-backdrop-filter: saturate(1.4) blur(14px);
  border-bottom: 1px solid var(--hairline);
}
.subnav .pill-row {
  flex-wrap: nowrap;
  overflow-x: auto;
  padding-block: 11px;
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.subnav .pill-row::-webkit-scrollbar { display: none; }
.subnav .pill { white-space: nowrap; flex: none; }

/* Scroll-spy state for the pill rows on the long pages. */
.pill-row a.pill { text-decoration: none; transition: background-color .18s ease, color .18s ease, border-color .18s ease; }
.pill-row a.pill:hover { background: var(--surface); color: var(--ink); }
.pill-row a.pill[aria-current="true"] {
  background: var(--chip-accent);
  border-color: transparent;
  color: var(--accent-text);
}

/* --------------------------------------------------------------------------
   17. Four-taps walkthrough
   The fast path, demonstrated rather than described: four steps, a phone that
   follows them, and the trade grid on the page able to drive it.
   -------------------------------------------------------------------------- */

.walk {
  display: grid;
  gap: clamp(var(--s6), 4vw, var(--s7));
  grid-template-columns: 1fr;
  align-items: center;
}
/* The phone comes first in the DOM so it's the first thing you meet on a
   phone-sized screen. On desktop the steps take the lead column instead. */
@media (min-width: 940px) {
  .walk { grid-template-columns: 1fr 340px; align-items: center; }
  .walk > .walk-copy  { order: 1; }
  .walk > .walk-media { order: 2; }
}

.wt-steps { list-style: none; padding: 0; margin: 0; display: grid; gap: var(--s2); max-width: 520px; }
.wt-steps li { margin: 0; }

.wt-step {
  position: relative;
  display: flex;
  gap: var(--s4);
  align-items: flex-start;
  width: 100%;
  padding: 14px 18px 15px;
  border-radius: var(--r-md);
  border: 1px solid transparent;
  background: transparent;
  font: inherit;
  color: var(--ink-2);
  text-align: left;
  cursor: pointer;
  overflow: hidden;
  transition: background-color .18s ease, border-color .18s ease, color .18s ease,
              box-shadow .18s ease;
}
.wt-step:hover { background: var(--surface); color: var(--ink); }
.wt-step[aria-current="true"] {
  background: var(--surface);
  border-color: var(--hairline);
  color: var(--ink);
  box-shadow: var(--shadow);
}
/* Unreachable because nobody's in that trade yet. The copy still has to be
   readable, it's describing the product, not just labelling a dead control 
   so the cue is a hollow number, not dimmer text. */
.wt-step[disabled] { opacity: 1; cursor: default; color: var(--ink-2); }
.wt-step[disabled]:hover { background: transparent; color: var(--ink-2); }
.wt-step[disabled] .wt-num {
  background: transparent;
  border: 1.5px dashed var(--hairline);
  color: var(--ink-2);   /* the dashed ring carries the state; the digit stays readable */
}

.wt-num {
  display: grid; place-items: center;
  width: 30px; height: 30px; flex: none;
  border-radius: 999px;
  background: var(--surface-alt);
  color: var(--ink-2);
  font-size: 0.88rem; font-weight: 800;
  transition: background-color .18s ease, color .18s ease;
}
.wt-step[aria-current="true"] .wt-num { background: var(--btn-bg); color: var(--btn-ink); }

.wt-copy strong { display: block; color: inherit; font-size: 1.02rem; font-weight: 700; margin-bottom: 1px; }
.wt-copy span { display: block; font-size: 0.92rem; line-height: 1.5; }

/* Dwell indicator, only while the demo is playing itself. */
.wt-bar { position: absolute; left: 0; bottom: 0; height: 2px; width: 0; background: var(--accent); }
.walk.is-playing .wt-step[aria-current="true"] .wt-bar { animation: wt-dwell 2600ms linear forwards; }
@keyframes wt-dwell { from { width: 0 } to { width: 100% } }

.walk-note {
  display: flex; align-items: flex-start; gap: var(--s2);
  margin-top: var(--s4);
  padding: 14px 16px;
  border-radius: var(--r-md);
  background: var(--surface-alt);
  border: 1px solid var(--hairline);
  font-size: 0.92rem;
  color: var(--ink-2);
  max-width: 520px;
}
.walk-note svg { width: 18px; height: 18px; flex: none; margin-top: 3px; color: var(--accent-text); }
.walk-note strong { color: var(--ink); }

/* Screens inside the walkthrough phone */
.wt-screen { display: none; height: 100%; flex-direction: column; }
.wt-screen.is-on { display: flex; }
.wt-screen.is-on { animation: wt-fade .26s ease-out; }
@keyframes wt-fade { from { opacity: 0 } to { opacity: 1 } }

/* Category grid on screen one */
.wt-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
.wt-tile {
  display: grid; gap: 5px; justify-items: center;
  padding: 9px 3px 8px;
  border-radius: 13px;
  background: var(--p-surface);
  border: 1px solid var(--p-line);
  font: inherit;
  font-size: 8.5px; font-weight: 700; line-height: 1.25;
  color: var(--p-ink2);
  text-align: center;
  cursor: pointer;
  transition: border-color .15s ease, background-color .15s ease, color .15s ease;
}
.wt-tile .g {
  display: grid; place-items: center;
  width: 27px; height: 27px; border-radius: 9px;
  background: color-mix(in srgb, var(--h) 24%, transparent);
  color: var(--h);
}
.wt-tile .g svg { width: 16px; height: 16px; }
.wt-tile:hover { border-color: var(--p-ink3); color: var(--p-ink); }
.wt-tile.is-on { border-color: var(--p-accent); background: var(--p-chip-accent); color: var(--p-ink); }

/* A tasker row, and the same row used as a button */
.wt-row {
  display: flex; align-items: flex-start; gap: 9px; width: 100%;
  padding: 10px 11px;
  margin-bottom: 8px;
  border-radius: 15px;
  background: var(--p-surface);
  border: 1px solid var(--p-line);
  font: inherit; color: var(--p-ink);
  text-align: left; cursor: pointer;
  transition: border-color .15s ease;
}
.wt-row:hover { border-color: var(--p-ink3); }
.wt-row .grow { flex: 1; min-width: 0; }
.wt-chevron { width: 14px; height: 14px; flex: none; color: var(--p-ink3); align-self: center; }

.wt-actions {
  display: flex; gap: 8px; align-items: stretch;
  padding: 11px 14px 14px;
  border-top: 1px solid var(--p-line);
  background: var(--p-surface);
}
.wt-call-btn {
  display: grid; place-items: center; gap: 1px;
  width: 62px; flex: none;
  border-radius: 999px;
  background: var(--p-chip-accent);
  color: var(--p-accent);
  border: 0; font: inherit;
  font-size: 9.5px; font-weight: 800;
  cursor: pointer;
}
.wt-call-btn svg { width: 16px; height: 16px; }
.wt-msg-btn {
  flex: 1;
  border: 0; border-radius: 999px;
  background: var(--p-accent);
  color: #161B22;
  font: inherit; font-size: 12.5px; font-weight: 800;
  padding: 11px 10px;
  cursor: pointer;
  display: flex; align-items: center; justify-content: center; gap: 6px;
}
.wt-msg-btn svg { width: 15px; height: 15px; }

/* The call screen */
.wt-call {
  flex: 1;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 8px; text-align: center;
  padding: 0 24px 28px;
}
.wt-call .big {
  width: 76px; height: 76px; border-radius: 999px;
  display: grid; place-items: center;
  font-size: 25px; font-weight: 800;
  background: color-mix(in srgb, var(--h) 24%, transparent);
  color: var(--h);
  margin-bottom: 4px;
}
.wt-call .who { font-size: 15px; font-weight: 800; }
.wt-call .state { font-size: 11.5px; color: var(--p-ink2); }
.wt-call .privacy {
  margin-top: 14px; padding: 9px 12px;
  border-radius: 13px;
  background: var(--p-chip-green);
  color: var(--p-green);
  font-size: 10.5px; font-weight: 700; line-height: 1.4;
}
.wt-call-bar { display: flex; gap: 22px; justify-content: center; padding: 0 0 22px; }
.wt-call-bar span {
  display: grid; place-items: center;
  width: 46px; height: 46px; border-radius: 999px;
  background: var(--p-alt); color: var(--p-ink2);
}
.wt-call-bar span:last-child { background: #B0342A; color: #fff; }
.wt-call-bar svg { width: 20px; height: 20px; }

/* Trade tiles become the demo's remote control */
.trade { cursor: pointer; }
.trade[aria-current="true"] { border-color: var(--accent); box-shadow: var(--shadow); }

@media (prefers-reduced-motion: reduce) {
  .wt-screen.is-on, .walk.is-playing .wt-step[aria-current="true"] .wt-bar { animation: none; }
  .wt-bar { display: none; }
}

/* --------------------------------------------------------------------------
   15. Reviews

   The section is rendered from the database, so these styles have to look
   right in three states: loading, populated, and genuinely empty. The empty
   state is not a failure here, it is the honest answer for a new marketplace,
   so it gets real design rather than a shrug.
   -------------------------------------------------------------------------- */

.review-wall { display: grid; gap: var(--s4); grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }

.review {
  margin: 0; padding: var(--s5);
  border-radius: var(--r-lg);
  background: var(--surface); border: 1px solid var(--hairline);
  box-shadow: var(--shadow);
  display: flex; flex-direction: column; gap: var(--s3);
}
.review blockquote { margin: 0; font-size: 1rem; line-height: 1.55; }
.review figcaption { margin-top: auto; display: flex; flex-direction: column; gap: 2px;
                     color: var(--ink-2); font-size: .88rem; }
.review figcaption b { color: var(--ink); font-size: .95rem; }

.review-stars { display: inline-flex; gap: 3px; }
.review-stars .ic { width: 17px; height: 17px; color: var(--amber); fill: currentColor; stroke: none; }
.review-stars .ic.is-off { color: var(--ink-3); opacity: .35; }

/* Skeleton, shaped like the card it becomes so the section does not jump. */
.review-skel {
  height: 190px; border-radius: var(--r-lg);
  border: 1px solid var(--hairline); background: var(--surface);
  position: relative; overflow: hidden;
}
.review-skel::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(90deg, transparent, var(--sheen), transparent);
  transform: translateX(-100%);
  animation: review-shimmer 1.4s ease-in-out infinite;
}
@keyframes review-shimmer { to { transform: translateX(100%) } }

/* The empty state spans the whole grid rather than sitting in one column. */
@media (prefers-reduced-motion: reduce) { .review-skel::after { animation: none; } }

/* ==========================================================================
   16. The three checks
   Identity, business, trade licence. The marks themselves are drawn by
   assets/js/tl/badges.js, one drawing shared with both portals, so a shield
   here is the same object as a shield in the app. Everything below is the
   furniture around them.
   ========================================================================== */

.tl-badge { display: block; flex: none; overflow: visible; }

/* `flex`, not `inline-flex`: Chrome blockifies inline-level children of a
   <button>, so an inline-flex row inside one stacks into a column. */
.tl-badge-row {
  display: flex; width: fit-content; flex: none; align-items: center; gap: 5px;
}

/* The badge is decorative and hydrates in; every claim it stands next to is in
   the HTML. The slot holds its space so nothing jumps when the script lands. */
.badge-slot { display: inline-flex; align-items: center; justify-content: center;
              width: 56px; height: 56px; flex: none; }
.badge-slot--lg { width: 108px; height: 108px; }


/* --------------------------------------------------------------------------
   Verification explainer: the checks beside a profile showing where they land.
   The preview earns its place by showing an *unearned* badge, which is the one
   claim a row of green ticks cannot make.
   -------------------------------------------------------------------------- */

.verify-split { display: grid; gap: var(--s6); align-items: start; }
@media (min-width: 960px) {
  .verify-split { grid-template-columns: minmax(0, 1fr) minmax(300px, 400px); gap: var(--s7); }
  /* Sticky so the profile stays in view while the checks are read past it. The
     offset clears the header plus the sub-nav, same value as --anchor-top. */
  .verify-preview { position: sticky; top: 142px; }
}

.verify-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--s5); }
.verify-item { display: flex; gap: var(--s4); align-items: flex-start; }
.verify-item > .badge-slot { flex: none; margin-top: 2px; }
.verify-item h3 { margin: 0 0 var(--s2); display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s2); }
.verify-item p { margin: 0 0 var(--s2); color: var(--ink-2); }
.verify-item p:last-child { margin-bottom: 0; }

.verify-tag {
  font-size: 0.7rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
  padding: 3px 8px; border-radius: 999px;
  background: var(--surface-alt); color: var(--ink-2); white-space: nowrap;
}
.verify-tag--req { background: var(--chip-green); color: var(--verified-text); }

.verify-preview-label {
  font-size: 0.8rem; font-weight: 800; letter-spacing: 0.09em; text-transform: uppercase;
  /* ink-3 is the decoration token and lands at 2.6:1 here. Small uppercase text
     needs the readable one. */
  color: var(--ink-2); margin: 0 0 var(--s3);
}
.verify-preview-note { font-size: 0.9rem; color: var(--ink-2); margin: var(--s4) 0 0; }

/* The profile card. Skeleton bars for anything the reader should not try to
   read, real badges for the part that is the point. */
.profile-card {
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow);
  padding: var(--s4);
  overflow: hidden;
}

.pp-head { display: flex; gap: var(--s3); align-items: center; }
.pp-avatar {
  width: 54px; height: 54px; border-radius: 50%; flex: none;
  background: var(--surface-alt);
  border: 2px solid var(--verified);
}
.pp-name { flex: 1; min-width: 0; display: grid; gap: 7px; }
.pp-rating { display: flex; align-items: center; gap: 6px; font-size: 0.85rem; font-weight: 700; color: var(--ink); }
.pp-stars { color: var(--amber-text); letter-spacing: -1px; font-size: 0.8rem; }

.pp-gallery { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; margin: var(--s4) 0; }
.pp-gallery span { aspect-ratio: 4 / 3; border-radius: var(--r-sm); background: var(--surface-alt); }

.pp-body { display: grid; gap: 7px; margin-bottom: var(--s4); }

/* Skeleton bars. Decorative, and never carrying text a reader would miss. */
.sk { display: block; height: 9px; border-radius: 5px; background: var(--surface-alt); }
.sk--title { height: 13px; width: 62%; }
.sk--short { width: 45%; }
.sk--xs { height: 8px; width: 54px; }

.pp-verify {
  border: 1px solid var(--hairline);
  border-radius: var(--r-md);
  padding: var(--s3);
  background: color-mix(in srgb, var(--verified) 6%, var(--surface));
}
.pp-verify-head {
  margin: 0 0 var(--s3);
  font-size: 0.78rem; font-weight: 800; letter-spacing: 0.07em; text-transform: uppercase;
  color: var(--ink-2);
}
.pp-badges { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--s2); text-align: center; }
/* Four across only where there is room. Below that they go two-up rather than
   shrinking to the point where the silhouette stops being readable, which is the
   one thing this card exists to show. */
.pp-badges--four { grid-template-columns: repeat(2, 1fr); row-gap: var(--s3); }
@media (min-width: 420px) { .pp-badges--four { grid-template-columns: repeat(4, 1fr); } }

/* The two requirement paths. The badges carry it: two marks side by side is the
   whole rule, and the words underneath are a caption rather than an explanation. */
.req-pair { display: grid; gap: var(--s4); margin: 0 0 var(--s4); }
@media (min-width: 560px) { .req-pair { grid-template-columns: 1fr 1fr; } }
.req-pair > div {
  background: var(--surface); border: 1px solid var(--hairline);
  border-radius: var(--r-lg); padding: var(--s5) var(--s4);
  text-align: center; box-shadow: var(--shadow);
}
.req-who {
  margin: 0 0 var(--s4); font-size: 0.75rem; font-weight: 800;
  letter-spacing: 0.09em; text-transform: uppercase; color: var(--ink-2);
}
.req-marks { display: flex; justify-content: center; gap: var(--s3); margin-bottom: var(--s4); }
.req-what { margin: 0; font-size: 1.05rem; font-weight: 700; color: var(--ink); }
/* The separator is still text a screen reader reads and an eye has to resolve,
   so it takes the readable token rather than the decoration one. */
.req-what b { color: var(--ink-2); font-weight: 700; margin: 0 2px; }
.req-note {
  margin: 0 0 var(--s6); font-size: 0.92rem; color: var(--ink-2); text-align: center;
  max-width: 46ch; margin-inline: auto;
}
.pp-badge { display: grid; justify-items: center; gap: 3px; }
.pp-badge b { font-size: 0.72rem; font-weight: 700; color: var(--ink); line-height: 1.2; }
/* The state under each badge is the most important text in this card: it is what
   separates an earned badge from an empty setting. It does not get the faint token. */
.pp-badge i { font-size: 0.68rem; font-style: normal; color: var(--ink-2); }
.pp-register {
  margin: var(--s3) 0 0; padding-top: var(--s3);
  border-top: 1px solid var(--hairline);
  font-size: 0.74rem; color: var(--ink-2);
  font-variant-numeric: tabular-nums;
}
.pp-register b { color: var(--ink); font-weight: 700; }

.checks { display: grid; gap: var(--s4); }
@media (min-width: 900px) { .checks { grid-template-columns: repeat(3, 1fr); } }

.check { display: flex; flex-direction: column; gap: var(--s3); }
.check-head { display: flex; align-items: center; gap: var(--s3); }
.check-head h3 { margin: 0; }
.check-head .small { display: block; color: var(--ink-2); font-weight: 600; }

/* The limit sits at the same size as the promise above it. A trust mark without
   a stated limit is where the reader fills the gap in themselves, and they
   always fill it in too generously. */
.check-limit {
  margin: 0; padding-top: var(--s3); border-top: 1px solid var(--hairline);
  color: var(--ink-2); font-size: .92rem;
}

/* A number somebody can look up on the board's own register is a stronger claim
   than a tick they have to believe. */
.register {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--s1) var(--s3);
  padding: var(--s3); border-radius: var(--r-sm); background: var(--surface-alt);
}
.register b { font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
              font-size: .95rem; letter-spacing: -.01em; }
.register span { color: var(--ink-2); font-size: .85rem; }

.licence-note {
  display: flex; gap: var(--s2); align-items: flex-start;
  padding: var(--s3); border-radius: var(--r-sm);
  background: var(--bdg-wash-amber); color: var(--ink);
  font-size: .9rem;
}
.licence-note .ic { flex: none; width: 18px; height: 18px; color: var(--amber);
                    fill: currentColor; stroke: none; margin-top: 1px; }

/* The hero row on the trust page: three marks, all three always. */
.check-hero { display: flex; align-items: center; justify-content: center;
              gap: var(--s2); flex-wrap: wrap; }

/* ==========================================================================
   17. Legal pages
   Terms and the privacy policy. Long-form prose, so the job here is a narrow
   measure, generous vertical rhythm, and headings you can find by eye.
   ========================================================================== */

.legal { max-width: 72ch; }
.legal h2 {
  margin-top: var(--s8); padding-top: var(--s5);
  border-top: 1px solid var(--hairline);
  /* Same variable the general anchor rule uses. A hardcoded 120px used to sit
     here, which is less than the sticky sub-nav's 130px bottom edge, so every
     pill on these pages parked its heading just underneath the bar. */
  scroll-margin-top: var(--anchor-top, 92px);
}
.legal > h2:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
.legal h3 { margin-top: var(--s5); }
.legal p, .legal li { color: var(--ink-2); }
.legal strong { color: var(--ink); }
.legal ul, .legal ol { padding-left: 1.25em; }
.legal li { margin-bottom: var(--s2); }
.legal li::marker { color: var(--ink-3); }

/* Definition-style rows, for "what we collect and why". */
.legal dl { margin: var(--s4) 0 0; }
.legal dt { font-weight: 700; color: var(--ink); margin-top: var(--s4); }
.legal dd { margin: var(--s1) 0 0; color: var(--ink-2); }

.legal-meta {
  display: flex; flex-wrap: wrap; gap: var(--s2) var(--s5);
  margin-top: var(--s5); padding: var(--s3) var(--s4);
  border: 1px solid var(--hairline); border-radius: var(--r-md);
  background: var(--surface);
  font-size: .9rem; color: var(--ink-2);
}
.legal-meta b { color: var(--ink); }

/* The one block on these pages that is not yet a statement of fact. Amber, and
   deliberately hard to miss, because a placeholder that reads as a claim is
   worse than no page at all. */
.legal-todo {
  margin: var(--s5) 0;
  padding: var(--s4);
  border: 1px solid var(--amber);
  border-radius: var(--r-md);
  background: var(--bdg-wash-amber);
}
.legal-todo h3 { margin-top: 0; }
.legal-todo p, .legal-todo li { color: var(--ink); }
.legal-todo code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .9em; padding: 1px 5px; border-radius: 5px;
  background: color-mix(in srgb, var(--ink) 10%, transparent);
}

/* ==========================================================================
   The scroll-scrubbed hero, and the cinematic layer around it.
   Paired with assets/js/hero.js. The five media queries below must stay
   character for character identical to GATES in that file.
   ========================================================================== */

@font-face {
  font-family: 'Bricolage';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url(../fonts/bricolage-latin.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
                 U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
                 U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Bricolage';
  font-style: normal;
  font-weight: 400 800;
  font-display: swap;
  src: url(../fonts/bricolage-latin-ext.woff2) format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
                 U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF,
                 U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

:root {
  --tshadow: 0 1px 0 rgba(255,255,255,.7), 0 2px 14px rgba(255,255,255,.55);
  --ease-out: cubic-bezier(.22,.61,.36,1);
  --ease-soft: cubic-bezier(.4,0,.2,1);
}

.display { font-family: 'Bricolage', 'Figtree', ui-rounded, system-ui, sans-serif; }

/* One environment behind the whole page, so scrolling feels like moving through a
   place rather than past stacked sections. */
.daylight {
  position: fixed; inset: 0; z-index: -1; pointer-events: none;
  background:
    radial-gradient(120% 80% at 78% -10%, rgba(27,98,232,.10), transparent 60%),
    radial-gradient(90% 70% at 8% 8%, rgba(255,255,255,.85), transparent 62%);
  animation: daydrift 120s var(--ease-soft) infinite alternate -30s;
}
@keyframes daydrift { from { transform: translate3d(0,0,0) scale(1); }
                      to   { transform: translate3d(0,-2%,0) scale(1.06); } }

/* ── the pinned stage ─────────────────────────────────────────────────────── */

.hero-scrub { position: relative; height: 900vh; }
/* Nine screens of flicking is a long way on a phone for twelve seconds of footage.
   The JS reads the real height, so shortening it here just tightens the mapping. */
@media (max-width: 720px) { .hero-scrub { height: 620vh; } }

/* ── the scroll cue ───────────────────────────────────────────────────────── */

.scroll-cue {
  /* fixed, not absolute. The header is 69px and sits in flow, so before the stage
     sticks its foot is a header's worth below the fold — which is exactly the moment
     this cue has to be visible. Anchoring to the viewport sidesteps that entirely. */
  position: fixed; left: 50%; transform: translateX(-50%); z-index: 3;
  bottom: clamp(var(--s4), 3.5vh, 34px);
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  color: #0C1B33; pointer-events: none;
  font-size: .68rem; font-weight: 800; letter-spacing: .16em; text-transform: uppercase;
  text-shadow: var(--tshadow);
  transition: opacity .45s var(--ease-out);
}
.scroll-cue svg { width: 20px; height: 20px; animation: cue-bob 1.9s var(--ease-soft) infinite; }
@keyframes cue-bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(5px); } }
.scroll-cue.is-gone { opacity: 0; }
/* Static mode has nothing to scrub, so there is nothing to cue. */
.hero-stage.is-static .scroll-cue { display: none; }
@media (prefers-reduced-motion: reduce) { .scroll-cue svg { animation: none; } }
.hero-stage {
  /* dvh, not vh and not svh. vh is the tall viewport, so the foot of the stage sits
     below the fold while the URL bar is showing. svh is the short one, which fixes that
     but then leaves a bar of page background under the footage the moment the URL bar
     retracts. dvh is the only one that tracks what is actually on screen, so the stage
     fills it in both states. vh first, for anything that does not know dvh. */
  position: sticky; top: 0; height: 100vh; height: 100dvh; overflow: hidden;
  background: #EAF2FC;   /* the footage's own sky, not the theme canvas */
}
.hero-poster {
  position: absolute; inset: 0;
  background-image: url('../hero/hero-poster.jpg');
  background-size: cover; background-position: 50% 58%;
}
.hero-stage video {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  object-position: 50% 58%; opacity: 0; transition: opacity .5s var(--ease-out);
  will-change: transform; transform: translateZ(0);
}
.hero-stage.video-ready video { opacity: 1; }
.hero-stage.video-failed video { display: none; }

/* Layer 1 of the legibility system: one soft darkening over the whole frame, always
   on, so no frame is ever raw behind the page. Weighted to the top, where the nav is. */
.hero-scrim {
  position: absolute; inset: 0; pointer-events: none;
  background:
    linear-gradient(180deg, rgba(255,255,255,.42) 0%, rgba(255,255,255,.10) 26%, rgba(255,255,255,0) 46%);
}

.hero-ring {
  position: absolute; right: var(--s6); bottom: var(--s6); width: 34px; height: 34px;
  color: #fff; opacity: .85; transition: opacity .4s var(--ease-out);
}
.hero-ring circle { transform: rotate(-90deg); transform-origin: 50% 50%;
                    transition: stroke-dashoffset .12s linear; }
.hero-ring.is-done { opacity: 0; }

/* ── the caption bands ────────────────────────────────────────────────────── */

.band {
  position: absolute; inset: 0; display: grid; align-content: center;
  padding: 0 clamp(var(--s5), 7vw, 96px);
  color: #0C1B33; text-shadow: var(--tshadow); opacity: 0; will-change: opacity;
}
/* position + z-index are load bearing, not decoration.
   .band sets will-change: opacity, which makes it a stacking context. Inside one, an
   absolutely positioned ::before paints in the positioned-descendants layer, while a
   static grid item like this paints below it. So the wash below, whose whole job is to
   lighten the FOOTAGE, was painting over the headline and the button instead. Dark ink
   survived that reasonably; a saturated blue button under a 54% white veil turned pale,
   which is the washed-out Post a job button.
   Lifting the content one layer puts it above the wash, where it was always meant to be.
   The blur band already had position: relative for its own reasons, which is why it was
   the one band that never looked washed. */
.band-in { max-width: 44rem; margin: 0 auto; text-align: center;
           position: relative; z-index: 1; }
.band--settle .band-in { max-width: 47rem; }

/* Layer 2: a per-band scrim that deepens with the band's own progress, so the
   footage dims exactly while that band's words are on and nowhere else. It dies
   out by 76 percent so the corners stay live rather than going flat. */
.band::before {
  content: ""; position: absolute; inset: -4%; pointer-events: none;
  opacity: calc(.30 + .70 * var(--k, 1));
  background: radial-gradient(ellipse 84% 70% at 50% 50%,
    rgba(244,249,255,.40) 0%, rgba(244,249,255,.24) 48%, rgba(244,249,255,0) 78%);
}
.band--settle::before {
  background: radial-gradient(ellipse 86% 72% at 50% 48%,
    rgba(244,249,255,.54) 0%, rgba(244,249,255,.36) 48%, rgba(244,249,255,0) 78%);
}

.band h2 {
  font-family: 'Bricolage', 'Figtree', system-ui, sans-serif;
  font-weight: 700; letter-spacing: -.03em; line-height: 1.02;
  font-size: clamp(1.5rem, 3.1vw, 2.55rem); margin: 0;
}
.band p { margin: var(--s3) 0 0; font-size: clamp(.95rem, 1.25vw, 1.05rem);
          color: #35485F; max-width: 40ch; margin-inline: auto; }
/* Layer 3 off on buttons: a shadowed label reads as grime. */
/* Smaller than a page button. This one sits under a 40px headline on open footage,
   where the site's standard 15px/28px pill reads as oversized furniture rather than
   an invitation. Scoped to the band so the header and the rest of the site keep the
   full size. */
.band .btn { text-shadow: none; padding: 10px 22px; font-size: .92rem; }

/* Word and character entrances, all transform and opacity, all scrubbed off --k
   and fully reversible. */
.band .w { display: inline-block; white-space: pre; }
.band .c { display: inline-block; }
.e-drift .w { --kc: clamp(0, (var(--k,0) - var(--th,0)) * 2.8, 1);
              opacity: var(--kc);
              transform: translateY(calc((1 - var(--kc)) * -26px)); }
.e-scatter .c { --kc: clamp(0, (var(--k,0) - var(--th,0)) * 2.6, 1);
                opacity: var(--kc);
                transform: translate(calc((1 - var(--kc)) * var(--jx,0px)),
                                     calc((1 - var(--kc)) * var(--jy,0px)))
                           rotate(calc((1 - var(--kc)) * var(--jr,0deg))); }
.e-punch .w { --kc: clamp(0, (var(--k,0) - var(--th,0)) * 3, 1);
              --ks: clamp(0, (var(--k,0) - var(--th,0) - .12) * 2.8, 1);
              opacity: var(--kc);
              transform: scale(calc(.66 + .46 * var(--kc) - .12 * var(--ks))); }
.e-rise .w { --kc: clamp(0, (var(--k,0) - var(--th,0)) * 3, 1);
             opacity: var(--kc);
             transform: translateY(calc((1 - var(--kc)) * 30px)); }
.band--settle p  { opacity: clamp(0, (var(--k,0) - .5) * 3.4, 1); }
/* No separate fade on the button. It used to carry
   `opacity: clamp(0, (var(--k) - .7) * 4, 1)`, which held it invisible until the
   band was 70% through its ramp and only made it solid at 95%. Against a headline
   whose words are already rising, a button lagging behind at partial alpha is the
   ghosting that read as blur. It now shares the band's own opacity, like the text. */

/* Blur to sharp: two stacked copies crossfaded. The soft copy carries a STATIC
   blur, because animating `filter` is not compositor friendly. */
.e-blur .band-in { position: relative; }
.e-blur .soft { position: absolute; inset: 0; filter: blur(11px);
                opacity: calc(1 - var(--k,0)); }
.e-blur .sharp { opacity: var(--k,0); }

/* ── the static hero: a designed layout, not an apology ───────────────────── */

.hero-stage.is-static { position: relative; height: auto; min-height: 88vh; }
.hero-stage.is-static video, .hero-stage.is-static .hero-ring { display: none; }
.hero-stage.is-static .band { display: none; }
.hero-stage.is-static .hero-still { display: grid; }
.hero-still::before { content:""; position:absolute; inset:-10% -5%; pointer-events:none;
  background: radial-gradient(ellipse 96% 62% at 30% 46%,
    rgba(244,249,255,.94) 0%, rgba(244,249,255,.74) 52%, rgba(244,249,255,0) 82%); }
.hero-still > * { position: relative; z-index: 1; }
.hero-still { display: none; position: relative; z-index: 2; align-content: center;
              min-height: 84vh; padding: var(--s8) var(--s5); color: #0C1B33;
              text-shadow: var(--tshadow); }
.hero-still h1 { font-family: 'Bricolage','Figtree',system-ui,sans-serif; font-weight: 700;
                 letter-spacing: -.03em; line-height: 1.03; margin: 0;
                 font-size: clamp(1.9rem, 6.2vw, 2.9rem); max-width: 16ch; }
.hero-still p { margin: var(--s4) 0 var(--s5); font-size: 1rem; max-width: 34ch;
                color: #35485F; }
.hero-still .btn { text-shadow: none; }

/* Must stay character for character identical to GATES in assets/js/hero.js. */
@media (orientation: landscape) and (pointer: coarse) and (max-height: 560px),
       (prefers-reduced-motion: reduce) {
  .hero-scrub { height: auto; }
  .hero-stage { position: relative; height: auto; min-height: 88vh; }
  .hero-stage video, .hero-ring { display: none !important; }
  .band { display: none !important; }
  .scroll-cue { display: none !important; }
  .hero-still { display: grid; }
}

/* ── the signature: the slot rail ─────────────────────────────────────────── */

.slots { display: flex; gap: var(--s3); flex-wrap: wrap; margin: var(--s5) 0; }
.slot {
  --fill: 0;
  position: relative; flex: 1 1 8rem; min-width: 7rem; height: 60px;
  border-radius: 999px; border: 1.5px solid var(--hairline); background: var(--surface);
  overflow: hidden; display: grid; place-items: center;
  font-size: .8rem; font-weight: 700; color: var(--ink-2);
  transition: border-color .3s var(--ease-out), color .3s var(--ease-out);
}
.slot::before {
  content: ""; position: absolute; inset: 0; transform-origin: left center;
  transform: scaleX(var(--fill)); background: var(--accent); opacity: .16;
  transition: transform .12s linear;
}
.slot.is-held { border-color: var(--accent); color: var(--accent-text); }
.slot.is-done { border-color: var(--verified); color: var(--verified-text);
                background: var(--chip-green); }
.slot.is-done::before { transform: scaleX(1); background: var(--verified); opacity: .14; }
.slot-hint { font-size: .85rem; color: var(--ink-2); margin-top: var(--s3); }

/* One living element per section, at whisper level, paused off screen. */
.breathe.is-in { animation: breathe 7s var(--ease-soft) infinite alternate -2s; }
@keyframes breathe { from { transform: translateY(0); } to { transform: translateY(-5px); } }

/* `animation-play-state` is not inherited, so a value on a container never reaches
   nested elements or pseudo-elements. This shape cannot miss. */
body.paused *, body.paused *::before, body.paused *::after { animation-play-state: paused !important; }

@media (prefers-reduced-motion: reduce) {
  .daylight, .breathe.is-in { animation: none !important; }
  .band, .band .w, .band .c { opacity: 1 !important; transform: none !important; }
}

/* Step cards: the image is the card's face, so it sits flush and the caption
   carries the padding. Parallel elements get equal treatment, which is why all
   three steps have an image rather than two of them. */
/* `height: auto` is load bearing. The img carries height="1440" for layout
   stability, and without auto that attribute wins over aspect-ratio, which made
   every step image 1440px tall on a phone. `align-self: stretch` with auto width
   lets the column flex card size it, since an explicit width cancels stretch. */
.step-img { display: block; width: calc(100% + var(--s5) * 2); height: auto;
            max-width: none; aspect-ratio: 4 / 3; object-fit: cover;
            border-radius: var(--r-md);
            margin: calc(var(--s5) * -1) calc(var(--s5) * -1) var(--s4); }
.card figcaption h3 { margin: 0 0 var(--s2); }
.card figcaption p { margin: 0; }


/* The hero keeps its daylight world in both themes, so anything sitting on it has
   to be forced to the light treatment too. In dark mode the ghost button's label
   is near-white, which over a bright sky is an invisible button. */
.hero-still .btn--ghost, .band .btn--ghost {
  color: #0C1B33; border-color: rgba(12,27,51,.3); background: rgba(255,255,255,.6);
}
.hero-still .btn--ghost:hover, .band .btn--ghost:hover {
  border-color: rgba(12,27,51,.5); background: rgba(255,255,255,.8);
}
.hero-still .wordmark { color: #0C1B33; }
.hero-still .wordmark b { color: var(--l-accent); }
/* The mark takes the same blue as the "Task" half. Everywhere else `.brand-mark` fills
   with --accent-text, but this hero forces --l-accent on the b because it sits on a
   bright photo, and without this the mark and the word it is attached to were two
   different blues. */
.hero-still .wordmark .brand-mark { fill: var(--l-accent); }
/* The wordmark is a <p>, so `.hero-still p { font-size: 1rem }` above was beating
   `.wordmark--hero` on specificity and rendering the lockup at body size. It has been
   16px rather than the ~29px it was drawn at since the static hero was written; nobody
   caught it because this hero only appears for prefers-reduced-motion and for a phone
   held sideways. Same specificity as the p rule, so it wins on order. */
.hero-still .wordmark--hero { font-size: clamp(1.8rem, 5.5vw, 2.6rem); }

/* A dark chrome bar sitting on a bright sky photo was the last of the black hue.
   Over the hero the header frosts light in both themes; once it sticks past the
   hero it goes back to the theme's own chrome. */
body:has(.hero-scrub) .site-header:not(.is-stuck) {
  background: rgba(255,255,255,.74);
  border-bottom-color: transparent;
}
body:has(.hero-scrub) .site-header:not(.is-stuck) .brand,
body:has(.hero-scrub) .site-header:not(.is-stuck) .nav a,
body:has(.hero-scrub) .site-header:not(.is-stuck) .icon-btn { color: #0C1B33; }
body:has(.hero-scrub) .site-header:not(.is-stuck) .brand b { color: var(--l-accent); }
body:has(.hero-scrub) .site-header:not(.is-stuck) .brand-mark { fill: var(--l-accent); }
body:has(.hero-scrub) .site-header:not(.is-stuck) .icon-btn { border-color: rgba(12,27,51,.22); }


/* One accented word in a hero headline. The accent on the light wash measures
   about 5:1, which clears AA for text this size. */
.band .w.hl, .hero-still .hl { color: var(--l-accent); }
