/* v3-tokens.css — the light navy theme, as an opt-in override.
 *
 * Loaded AFTER css/handoff-base.css, which is what the live layouts actually
 * use. (css/design-base.css and css/parts/ are referenced only by base.njk,
 * which renders zero pages — they ship to nobody.)
 *
 * Everything here is scoped to [data-v3]. No unscoped selectors,
 * deliberately — anything unscoped here would reach all 67 pages, not just the
 * ones that opted in. A page joins the new theme with `v3: true` in its
 * frontmatter and leaves by deleting that line.
 *
 * handoff-base.css is 138 var() against 7 hardcoded colour declarations, and
 * names its tokens the same way this block does, so overriding these fourteen
 * re-themes a page almost completely.
 */

[data-v3]{
  /* SURFACES — light */
  --surface-base:   #FFFFFF;
  --surface-raised: #F1F5FB;
  --surface-panel:  #FFFFFF;

  /* TEXT.
     --text-tertiary is #646B76, not the #7A828E used in the homepage draft.
     On white that measured 3.88:1 and WCAG AA wants 4.5 for text under 18px.
     It carries the sub-bar, kickers, captions and the note beside the primary
     CTA, so the miss was on a lot of small type — the same reasoning, and the
     same token, that handoff-base.css already documents for the dark theme. */
  --text-primary:   #0A0A0B;
  --text-secondary: #464C56;
  --text-tertiary:  #646B76;

  /* ACCENT — one navy, from img/logo.svg. */
  --accent-core:    #0B2D5C;
  --accent-text:    #14417F;

  /* LINES — one value. */
  --line-soft:      #DCE3ED;
  --line-strong:    #DCE3ED;
  --line-media:     #DCE3ED;

  /* The sticky header and the primary button were hardcoded inline across the
     seven layouts; handoff-base.css now defines them as tokens so they flip here.
     Nav keeps 92% alpha because the header carries a backdrop blur. */
  --surface-nav:    rgba(255,255,255,.92);
  --text-inverse:   #FFFFFF;

  /* The primary CTA. The home page renders .btn on --accent-core; the other
     layouts wrote var(--text-primary) inline at every call site, so the same
     button was navy on / and black everywhere else. One token, one colour. */
  --cta-fill:       var(--accent-core);

  /* Bars: navy at low alpha, so the groove reads on the #F1F5FB meter card
     and the muted fill stays clearly lighter than the solid navy beside it. */
  --bar-track:      rgba(11,45,92,.10);
  --bar-prev:       rgba(11,45,92,.30);
  --bar-you:        rgba(11,45,92,.30);

  /* TYPE — the two v3 voices. --gut is layout, not theme; left alone. */
  --fd: "Outfit", system-ui, -apple-system, sans-serif;
  --fb: "IBM Plex Sans", system-ui, -apple-system, sans-serif;

  /* Aliases for the OTHER live stylesheet. css/parts/, which design-base.css
     imports and the four service-legacy pages render through, spells the font
     tokens --font-display/--font-body (and --f-display/--f-body in six places)
     rather than --fd/--fb. Same faces, different names. Harmless on
     handoff-base pages, which never read them. */
  --font-display:   "Outfit", system-ui, -apple-system, sans-serif;
  --font-body:      "IBM Plex Sans", system-ui, -apple-system, sans-serif;
  --f-display:      "Outfit", system-ui, -apple-system, sans-serif;
  --f-body:         "IBM Plex Sans", system-ui, -apple-system, sans-serif;
}

/* --------------------------------------------------------------------------
   Service hero field — ROLLOUT.md §Backgrounds.

   "Grid masked to the left third only, behind the h1", because service heroes
   are left-aligned and a centred mask would sit in dead space beside the aside
   panel. Static: nothing here moves.

   The hero is <section data-r="hero"> styled entirely inline, so an inline
   style would win over any declaration here — but pseudo-elements cannot be
   set inline, and `position` is not among the inline properties, so both of
   these apply cleanly. Scoped to [data-v3]; the 20 dark service pages that
   have not opted in never see it.
   -------------------------------------------------------------------------- */
[data-v3][data-layout="service"] [data-field]{position:relative;isolation:isolate}

[data-v3][data-layout="service"] [data-field]::before{
  content:"";position:absolute;inset:0;z-index:-1;pointer-events:none;
  background-image:radial-gradient(circle, rgba(11,45,92,.30) 1.6px, transparent 1.7px);
  background-size:26px 26px;
  -webkit-mask-image:radial-gradient(ellipse 44% 78% at 4% 44%, #000 18%, transparent 72%);
  mask-image:radial-gradient(ellipse 44% 78% at 4% 44%, #000 18%, transparent 72%);
}

/* Scrim: keeps the h1 crisp where it crosses the densest part of the grid. */
[data-v3][data-layout="service"] [data-field]::after{
  content:"";position:absolute;inset:0;z-index:-1;pointer-events:none;
  background:radial-gradient(ellipse 34% 40% at 14% 34%, rgba(255,255,255,.90) 38%, transparent 76%);
}

/* One column on phones, so the mask recentres and the grid loosens. */
@media(max-width:860px){
  [data-v3][data-layout="service"] [data-field]::before{
    background-size:22px 22px;
    -webkit-mask-image:radial-gradient(ellipse 72% 52% at 18% 26%, #000 14%, transparent 74%);
    mask-image:radial-gradient(ellipse 72% 52% at 18% 26%, #000 14%, transparent 74%);
  }
  [data-v3][data-layout="service"] [data-field]::after{
    background:radial-gradient(ellipse 60% 30% at 30% 22%, rgba(255,255,255,.90) 40%, transparent 78%);
  }
}

/* --------------------------------------------------------------------------
   Blog index and blog post — ROLLOUT.md §Backgrounds.

   "No field" for both, deliberately. Texture behind 32 index rows competes
   with the list, and nothing goes behind running text, ever. What they get
   instead is the flat canvas the tokens already give them, plus a hairline
   under the index h1 so the header still terminates.

   Belt and braces: the field rule above is already scoped to
   data-layout="service", and these two rules make the intent explicit rather
   than implicit, so a later batch that widens that selector fails loudly here
   instead of quietly painting dots behind a blog post.
   -------------------------------------------------------------------------- */
[data-v3][data-layout="guides"] [data-field]::before,
[data-v3][data-layout="guides"] [data-field]::after,
[data-v3][data-layout="post"]   [data-field]::before,
[data-v3][data-layout="post"]   [data-field]::after{content:none}

[data-v3][data-layout="guides"] [data-field]{
  border-bottom:1px solid var(--line-soft);
}

/* The post body sets its own measure and colour through [data-post-body] in
   handoff-base.css, which is already token-driven — it needs nothing here. */

/* --------------------------------------------------------------------------
   City / location field — ROLLOUT.md §Backgrounds.

   "Grid at tighter 18px spacing, masked to a soft circle behind the place
   name. Reads as a map at close zoom without being one — no invented
   geography." That last clause is the constraint that shapes this: no roads,
   no coastline, no pin, nothing that could be mistaken for a real map of a
   real town. A regular grid at a tighter pitch reads as "close zoom" and
   claims nothing.

   The hero here is the data-r="split" section holding the city eyebrow, the
   h1 and the lede. The circle sits behind the left column, where the place
   name is, not centred across the whole two-column row.

   inset is 0, not negative. A bleeding inset was tried first and it pushed
   the pseudo-element past the right edge, which scrolled every city page
   sideways at both 1440 and 390. It did not show up in an element sweep,
   because a ::before is not in querySelectorAll("*") — only
   documentElement.scrollWidth caught it. The mask fades to transparent well
   inside the box, so the bleed bought nothing anyway.
   -------------------------------------------------------------------------- */
[data-v3][data-layout="city"] [data-field]{position:relative;isolation:isolate}

[data-v3][data-layout="city"] [data-field]::before{
  content:"";position:absolute;inset:0;z-index:-1;pointer-events:none;
  background-image:radial-gradient(circle, rgba(11,45,92,.30) 1.5px, transparent 1.6px);
  background-size:18px 18px;
  -webkit-mask-image:radial-gradient(circle 300px at 13% 40%, #000 22%, transparent 70%);
  mask-image:radial-gradient(circle 300px at 13% 40%, #000 22%, transparent 70%);
}

/* Scrim, so the eyebrow and h1 stay crisp over the densest part of the circle. */
[data-v3][data-layout="city"] [data-field]::after{
  content:"";position:absolute;inset:0;z-index:-1;pointer-events:none;
  background:radial-gradient(ellipse 30% 34% at 13% 34%, rgba(255,255,255,.90) 36%, transparent 74%);
}

@media(max-width:860px){
  [data-v3][data-layout="city"] [data-field]::before{
    background-size:16px 16px;
    -webkit-mask-image:radial-gradient(circle 220px at 26% 20%, #000 18%, transparent 72%);
    mask-image:radial-gradient(circle 220px at 26% 20%, #000 18%, transparent 72%);
  }
  [data-v3][data-layout="city"] [data-field]::after{
    background:radial-gradient(ellipse 46% 22% at 30% 16%, rgba(255,255,255,.90) 38%, transparent 76%);
  }
}

/* --------------------------------------------------------------------------
   Trade field — ROLLOUT.md §Backgrounds, with two corrections.

   The spec says "grid at standard density, masked bottom-right, so the hero
   image gets the clean corner", on the reasoning that "trade pages lead with
   a photo". Measured against the actual build, both halves are wrong:

     * Trade heroes carry no photo. They are the same left-text / right-aside
       split as city. The photos live in the ledger and proof sections much
       further down the page.
     * Bottom-right is the LEAST free corner. The aside is align-self:end, so
       "What this page covers" occupies x 55.8-94.8%, y 61.5-83.1% of the hero
       box. A bottom-right mask would sit directly on that text.

   The genuinely clean area is the TOP-right: x 52-100%, y 0-61.5%, empty
   because the aside is bottom-aligned and the h1 stops at 52%. So the mask
   goes there. Standard 26px density as specified, which keeps it distinct
   from city's tighter 18px, and mirrored to the right so it does not read as
   a copy of the service page's left-masked field.

   Nothing sits under this one, so it needs no scrim.
   -------------------------------------------------------------------------- */
[data-v3][data-layout="trade"] [data-field]{position:relative;isolation:isolate}

[data-v3][data-layout="trade"] [data-field]::before{
  content:"";position:absolute;inset:0;z-index:-1;pointer-events:none;
  background-image:radial-gradient(circle, rgba(11,45,92,.30) 1.6px, transparent 1.7px);
  background-size:26px 26px;
  -webkit-mask-image:radial-gradient(ellipse 34% 38% at 79% 16%, #000 20%, transparent 72%);
  mask-image:radial-gradient(ellipse 34% 38% at 79% 16%, #000 20%, transparent 72%);
}

/* One column on phones: the aside drops below the CTA, so the whole right
   side disappears as a region. Move the grid to the top, loosen it slightly,
   and keep it clear of the h1. */
@media(max-width:860px){
  [data-v3][data-layout="trade"] [data-field]::before{
    background-size:22px 22px;
    -webkit-mask-image:radial-gradient(ellipse 54% 26% at 76% 8%, #000 14%, transparent 74%);
    mask-image:radial-gradient(ellipse 54% 26% at 76% 8%, #000 14%, transparent 74%);
  }
}

/* --------------------------------------------------------------------------
   Landing field. One page (/3d-web-design). Its hero is a centred 7fr/5fr
   split with align-items:center, so unlike service and trade there is no
   generous empty corner — the right column is vertically centred and fills
   its half. The grid therefore goes behind the RIGHT column at low density
   with a scrim, rather than trying to find a gap that is not there.
   -------------------------------------------------------------------------- */
[data-v3][data-layout="landing"] [data-field]{position:relative;isolation:isolate}

[data-v3][data-layout="landing"] [data-field]::before{
  content:"";position:absolute;inset:0;z-index:-1;pointer-events:none;
  background-image:radial-gradient(circle, rgba(11,45,92,.26) 1.5px, transparent 1.6px);
  background-size:24px 24px;
  -webkit-mask-image:radial-gradient(ellipse 38% 60% at 82% 50%, #000 16%, transparent 74%);
  mask-image:radial-gradient(ellipse 38% 60% at 82% 50%, #000 16%, transparent 74%);
}
[data-v3][data-layout="landing"] [data-field]::after{
  content:"";position:absolute;inset:0;z-index:-1;pointer-events:none;
  background:radial-gradient(ellipse 30% 40% at 82% 50%, rgba(255,255,255,.86) 34%, transparent 76%);
}
@media(max-width:860px){
  [data-v3][data-layout="landing"] [data-field]::before{background-size:20px 20px;
    -webkit-mask-image:radial-gradient(ellipse 60% 24% at 74% 10%, #000 14%, transparent 74%);
    mask-image:radial-gradient(ellipse 60% 24% at 74% 10%, #000 14%, transparent 74%);}
  [data-v3][data-layout="landing"] [data-field]::after{
    background:radial-gradient(ellipse 50% 16% at 74% 9%, rgba(255,255,255,.86) 36%, transparent 78%);}
}

/* Legal and the four legacy pages get no field. A privacy policy is a reading
   surface, and /contact and /book-online are conversion pages where texture
   behind a form is noise. Stated, not merely omitted. */
[data-v3][data-layout="legal"]  [data-field]::before,
[data-v3][data-layout="legal"]  [data-field]::after,
[data-v3][data-layout="legacy"] [data-field]::before,
[data-v3][data-layout="legacy"] [data-field]::after{content:none}

/* --------------------------------------------------------------------------
   Kill the ambient canvas animation on opted-in legacy pages.

   css/parts/00-tokens.css runs a scroll-driven animation on <body> whose
   keyframes hardcode the dark canvas:

       @keyframes ambient-shift{ 0%{background-color:oklch(12% 0.018 280)} ... }

   An animated value beats every normal declaration, so `body{background:
   var(--surface-base)}` never got a look in. /contact and /book-online
   rendered near-black body text on that dark canvas at 2.43:1 — the token
   override had correctly recoloured the TEXT and could not touch the
   BACKGROUND. Same shape of bug as the inline header colour in batch 2: a
   hardcoded value sitting somewhere the cascade cannot reach.

   The v3 canvas is flat white by design — MOTION.md allows no ambient
   background motion at all — so this is switched off rather than re-keyed.
   -------------------------------------------------------------------------- */
[data-v3] body{animation:none}

/* --------------------------------------------------------------------------
   Home page nav.

   The home page now includes _nav.njk like every other page, instead of the
   four in-page anchors and text wordmark it had drifted to. Two gaps had to
   be closed for that to look right here:

   1. The dropdown TRIGGER. _nav.njk uses <button>, which handoff-base.css
      styles — but the home page loads design-base.css, and css/parts/
      30-home.css styles the trigger as `.navdrop > .nav__link`. The button
      would otherwise render as a raw browser button in the middle of the nav.
      The panel itself needs nothing: .navdrop__panel matches by class in both
      stylesheets.

   2. The logo is now an <img>, not text, so the wordmark's font rules no
      longer apply and it needs a height instead.
   -------------------------------------------------------------------------- */
[data-layout="home"] .top .sitenav{display:flex;gap:26px;align-items:center}

[data-layout="home"] .navdrop>button{
  display:inline-flex;align-items:center;gap:6px;
  background:none;border:0;padding:0;cursor:pointer;
  color:var(--text-primary);
  font:var(--weight-body) var(--type-body-m)/1 var(--font-body);
}
[data-layout="home"] .navdrop>button:hover{color:var(--accent-core)}
[data-layout="home"] .navdrop>button::after{
  content:"";width:5px;height:5px;margin-top:-3px;flex:none;
  border-right:1px solid currentColor;border-bottom:1px solid currentColor;
  transform:rotate(45deg);opacity:.55;transition:transform .18s ease;
}
[data-layout="home"] .navdrop:hover>button::after,
[data-layout="home"] .navdrop:focus-within>button::after{transform:rotate(-135deg)}

[data-layout="home"] .top .logo{display:inline-flex;align-items:center;flex:none}
[data-layout="home"] .top .logo img{display:block;width:120px;height:auto}

/* The phone broke across two lines once the real menu took the width back.
   The other layouts already carry nowrap on their nav CTA group; the home
   nav never needed it while it only had four short anchors. */
[data-layout="home"] .top .right{white-space:nowrap}
[data-layout="home"] .top .tel{white-space:nowrap}

/* The mobile menu is a <details> in the include; the desktop row hides below
   1000px in the home stylesheet, so keep the two switchover points aligned. */
@media(max-width:1000px){
  [data-layout="home"] .top .sitenav{display:none}
}
@media(min-width:1001px){
  [data-layout="home"] .navmob{display:none}
}

/* --------------------------------------------------------------------------
   FEATURE ROWS — ported from slack.com/features/*.

   Used by the service, trade and city layouts. Scoped on .frow rather than on
   data-layout: the class only exists where a layout renders these rows, so the
   layout qualifier bought nothing and had to be widened every time another
   layout adopted the pattern.

   What Slack actually does, measured at a 1024 viewport on
   /features/workflow-automation:

     * a 50/50 split — 448px text column inside a 942px row
     * eyebrow: 14px, uppercase, ~0.8px tracking
     * heading: 32px/40px, 700
     * body: 18px/28px, one short paragraph
     * ONE piece of media per row, and the side ALTERNATES down the page
       (left, right, left, right...)
     * generous vertical rhythm, no rules between rows

   Ours was a ledger: `44px 1fr 340px`, image pinned right on every row. The
   alternation is the whole point — it is what stops a long page of rows
   reading as a list.

   Headings here are h3 (they sit under the section's h2), so the ladder is
   one step down from Slack's 32px rather than matching it outright.
   -------------------------------------------------------------------------- */
[data-v3] .fset__rows{
  max-width:var(--page, 1200px);
  margin-inline:auto;
  padding-inline:var(--gut);
  padding-bottom:clamp(56px,7vw,80px);
}

[data-v3] .frow{
  display:grid;
  grid-template-columns:1fr 1fr;
  gap:clamp(32px,5vw,72px);
  align-items:center;
  padding-block:clamp(40px,5.5vw,68px);
}

/* The alternation. Driven by a class the template computes, NOT by
   :nth-of-type(even) — which was the first attempt and was wrong: it counts
   every row, so a full-width text row falling between two media rows pushed
   both onto the same side. The `mediaOrdinal` filter counts only the rows
   that actually carry media. */
[data-v3] .frow--flip .frow__media{order:-1}

/* A row with no image is not a half-empty row — it is a full-width text row.
   44 of the 128 rows across these pages have no art yet. */
[data-v3] .frow--text{
  grid-template-columns:minmax(0,72ch);
  justify-content:start;
}

[data-v3] .frow__eyebrow{
  display:block;
  font:600 14px/1 var(--fb);
  letter-spacing:.06em;
  text-transform:uppercase;
  color:var(--accent-text);
  margin-bottom:14px;
}
[data-v3] .frow__eyebrow:empty{display:none}

[data-v3] .frow__copy h3{
  margin:0;
  font:600 clamp(1.375rem,2.2vw,1.75rem)/1.2 var(--fd);
  letter-spacing:-.018em;
  color:var(--text-primary);
  text-wrap:balance;
}
[data-v3] .frow__copy p{
  margin:14px 0 0;
  max-width:54ch;
  font:400 17px/1.7 var(--fb);
  color:var(--text-secondary);
}

[data-v3] .frow__media{
  margin:0;
  border:1px solid var(--line-media);
  border-radius:16px;
  overflow:hidden;
  background:var(--surface-panel);
}
[data-v3] .frow__media img{
  display:block;width:100%;height:auto;
}
/* "Illustration" label. Generated art is allowed on these pages only while it
   is visibly marked as generated; the template stamps this from the file path
   so a new illustration cannot arrive unlabelled. */
[data-v3] .frow__media figcaption{
  padding:8px 14px;
  font:500 12px/1.4 var(--fb);
  letter-spacing:.04em;text-transform:uppercase;
  color:var(--text-tertiary);
  border-top:1px solid var(--line-soft);
  background:var(--surface-base);
}

/* One column on phones. Media goes above the copy on every row — an
   alternating order is meaningless in a single column, and resetting it here
   stops the odd rows reading in a different order to the even ones. */
@media(max-width:860px){
  [data-v3] .frow{
    grid-template-columns:1fr;
    gap:24px;
    padding-block:clamp(32px,7vw,44px);
  }
  /* Media above the copy on EVERY row. Both selectors are listed because
     `.frow--flip .frow__media` (0,2,0) outranks `.frow__media` (0,1,0), so
     setting only the latter left the flipped rows stacking the other way and
     the column read copy-then-image on alternate rows. */
  [data-v3] .frow .frow__media,
  [data-v3] .frow--flip .frow__media{order:-1}
  [data-v3] .frow--text{grid-template-columns:1fr}
}

/* --------------------------------------------------------------------------
   Sticky mobile action bar, below 768px.

   Moved here 2026-09-19 from css/parts/35-home-v3.css, which only the home
   page and the four legacy pages load. The service, city, trade, post, guides,
   landing and legal pages load this file instead, and had no bar at all — so
   on a phone /local-seo offered a CTA at 2% of a 49,828px page and nothing
   again until 92%.

   Values are carried across unchanged, so the home page renders exactly as
   before; it simply reads them from one file now instead of two.
   -------------------------------------------------------------------------- */
[data-v3] .actionbar{position:fixed;left:0;right:0;bottom:0;z-index:70;display:none;gap:10px;
  padding:10px 14px calc(10px + env(safe-area-inset-bottom));
  background:rgba(255,255,255,.96);backdrop-filter:blur(8px);border-top:1px solid var(--line-soft)}
[data-v3] .actionbar a{display:inline-flex;align-items:center;justify-content:center;white-space:nowrap;
  padding:14px 12px;border-radius:999px;font:700 15px/1 var(--fb);text-decoration:none}
[data-v3] .actionbar .call{flex:1.45 1 auto;background:var(--accent-core);color:#fff}
[data-v3] .actionbar .td{flex:1 1 auto;border:1px solid var(--accent-core);color:var(--accent-core)}
@media(max-width:360px){[data-v3] .actionbar a{font-size:14px;padding:14px 8px}}
@media(max-width:768px){[data-v3] .actionbar{display:flex}[data-v3] body{padding-bottom:84px}}

/* --------------------------------------------------------------------------
   Mid-page CTA and the teardown sample. Both from the funnel audit.

   The CTA sits straight after the price block. It is a single line and a
   button, not a second form: the form is still the destination, and two forms
   on one page split the conversion rather than doubling it.
   -------------------------------------------------------------------------- */
[data-v3] .midcta{
  display:flex;flex-wrap:wrap;align-items:center;justify-content:center;
  gap:14px 20px;text-align:center;
  padding:clamp(28px,4vw,40px) var(--gut);
  border-bottom:1px solid var(--line-soft);
}
[data-v3] .midcta p{
  margin:0;flex:0 1 auto;
  font:600 clamp(1.0625rem,1.6vw,1.25rem)/1.4 var(--fd);
  color:var(--text-primary);text-wrap:balance;
}
[data-v3] .midcta a{
  flex:0 0 auto;padding:14px 26px;border-radius:999px;
  background:var(--cta-fill);color:var(--text-inverse);
  font:600 15px/1 var(--fd);text-decoration:none;white-space:nowrap;
}
[data-v3] .midcta span{
  flex:0 0 auto;font:400 13px/1.4 var(--fb);color:var(--text-tertiary);
}
@media(max-width:700px){
  [data-v3] .midcta{flex-direction:column;gap:12px}
  [data-v3] .midcta a{width:100%;max-width:340px}
}

/* The teardown sample: the offer, made visible. */
[data-v3] .tdsample{
  margin:28px 0 0;
  border:1px solid var(--line-media);border-radius:16px;overflow:hidden;
  background:var(--surface-panel);
}
[data-v3] .tdsample img{display:block;width:100%;height:auto}
[data-v3] .tdsample figcaption{
  padding:10px 14px;
  font:400 13px/1.4 var(--fb);color:var(--text-tertiary);
  border-top:1px solid var(--line-soft);
}
