/* ============================================================================
   LOCAL SITE GROWTH - v2 rebuild
   ----------------------------------------------------------------------------
   DESIGN PLAN (written before the code, per the frontend-design skill)

   Brief: a Christchurch web design service whose whole offer is "see the real
   first page of your website before you pay anything". No completed client
   work exists yet, so the site cannot lean on a portfolio, testimonials or
   case studies - inventing any of those is forbidden.

   Palette (5 values, pinned by the brand - CLAUDE.md mandates near-black,
   bone and gold as the ONLY accent, so this axis is not free):
     --ink        #0A0A0A  near-black ground
     --paper      #FFFFFF  pure white, the alternating band
     --bone       #F4F1EA  warm off-white for type on dark
     --gold       #F2C94C  the single accent
     --gold-deep  #7A5D00  the same accent made readable on white (measured:
                           #F2C94C on white is 1.7:1, which is unreadable)

   Type, three roles:
     display  Vollkorn 700/800 - warm, sturdy, a real italic for the accent word
     body     Outfit 300/400
     utility  Outfit 600, uppercase, tracked - labels and data only

   Layout: full-bleed alternating black and white bands. The distinctiveness is
   NOT in the bands, it is in the asymmetric editorial grid inside them: every
   section shares one left rail, and the right column carries the evidence. No
   centred card grids.

   SIGNATURE: the hero is a working first-page preview. The visitor picks their
   trade and types their business name, and a real page mockup assembles with
   their details in it. That is the offer performed rather than described, and
   it solves the no-portfolio problem honestly - instead of showing someone
   else's website, it shows a sketch of theirs.

   Boldness is spent entirely on that one element. Everything else is quiet.

   HONEST NOTE ON THE PALETTE: the frontend-design skill warns that AI design
   defaults to "warm cream near #F4F1EA with a high-contrast serif" and to
   "near-black with a single bright accent". That is this brand, hex for hex.
   The skill's own rule is that the brief wins where it pins an axis down, and
   CLAUDE.md pins this one hard. So the palette stays and the distinctiveness
   is earned on the axes that ARE free: the interactive signature, the
   asymmetric rail, and the type treatment.
========================================================================== */

@font-face{font-family:'Vollkorn';src:url('wf/vollkorn-700.woff2') format('woff2');font-weight:700;font-style:normal;font-display:swap}
@font-face{font-family:'Vollkorn';src:url('wf/vollkorn-800.woff2') format('woff2');font-weight:800;font-style:normal;font-display:swap}
@font-face{font-family:'Vollkorn';src:url('wf/vollkorn-700i.woff2') format('woff2');font-weight:700;font-style:italic;font-display:swap}
@font-face{font-family:'Outfit';src:url('wf/outfit-300.woff2') format('woff2');font-weight:300;font-display:swap}
@font-face{font-family:'Outfit';src:url('wf/outfit-400.woff2') format('woff2');font-weight:400;font-display:swap}
@font-face{font-family:'Outfit';src:url('wf/outfit-500.woff2') format('woff2');font-weight:500;font-display:swap}
@font-face{font-family:'Outfit';src:url('wf/outfit-600.woff2') format('woff2');font-weight:600;font-display:swap}

:root{
  --cta-radius:999px;
  /* The ticker's height lives here because TWO rules depend on it being
     the same number: the ticker itself, and the hero's min-height, which
     subtracts it so hero + ticker together are exactly one viewport tall
     and the ticker lands flush on the bottom edge at first paint
     (2026-09-06, Tomislav: "make the marquee visible straight away...
     exactly on the bottom edge on the first load"). If these two ever
     drift apart the ticker falls below the fold again. */
  --ticker-h:clamp(40px,4.4vw,54px);
  --ink:#0A0A0A;
  --ink-2:#141414;
  --paper:#FFFFFF;
  --bone:#F4F1EA;
  --gold:#F2C94C;
  --gold-deep:#7A5D00;

  --fg:var(--bone);
  --muted:rgba(244,241,234,.58);
  --hair:rgba(244,241,234,.14);
  --accent:var(--gold);

  --display:'Vollkorn',Georgia,serif;
  --body:'Outfit',ui-sans-serif,system-ui,-apple-system,'Segoe UI',sans-serif;

  --maxw:1220px;
  --gutter:clamp(20px,5vw,60px);
  --rail:clamp(20px,5vw,60px);

  --ease:cubic-bezier(.22,1,.36,1);
  --ease-out:cubic-bezier(0,0,.2,1);
}

*{box-sizing:border-box}
html{background:var(--ink);-webkit-text-size-adjust:100%;scroll-behavior:smooth}
body{
  margin:0;background:var(--ink);color:var(--fg);
  font-family:var(--body);font-weight:300;font-size:17px;line-height:1.6;
  -webkit-font-smoothing:antialiased;overflow-x:clip;
}
img,video{display:block;max-width:100%}
a{color:inherit;text-decoration:none}
button{font:inherit;color:inherit;background:none;border:0;cursor:pointer}
:focus-visible{outline:2px solid var(--accent);outline-offset:3px;border-radius:4px}

/* ---------------------------------------------------------------- BANDS
   Sections are full width so a background bleeds edge to edge; the CONTENT
   is what gets capped and centred. One padding formula for every band, so
   the left rail is identical the whole way down the page. */
.band{
  position:relative;
  padding-block:clamp(78px,10vw,140px);
  padding-inline:max(var(--gutter),calc((100% - var(--maxw)) / 2));
}
.band--paper{
  background:var(--paper);
  --fg:#0A0A0A;
  --muted:rgba(10,10,10,.62);
  --hair:rgba(10,10,10,.14);
  --accent:var(--gold-deep);
  color:#0A0A0A;
}
.band--ink{background:var(--ink)}

/* ------------------------------------------------------------- TYPOGRAPHY */
.eyebrow{
  margin:0 0 18px;font-family:var(--body);font-weight:600;font-size:11.5px;
  letter-spacing:.2em;text-transform:uppercase;color:var(--accent);
}
h1,h2,h3{font-family:var(--display);font-weight:700;margin:0;letter-spacing:-.02em}
h1{font-size:clamp(40px,6.2vw,78px);line-height:1.04}
h2{font-size:clamp(25px,4.2vw,52px);line-height:1.08;margin-bottom:20px}
h3{font-size:clamp(19px,1.8vw,23px);line-height:1.24;letter-spacing:-.015em}
em{font-style:italic;color:var(--accent)}
.lede{font-size:clamp(16.5px,1.3vw,19px);line-height:1.62;color:var(--muted);max-width:56ch;margin:0}
p{margin:0 0 16px}

/* ---------------------------------------------------------------- THE RAIL
   The asymmetric grid: content left, evidence right. Every band uses it, so
   the page has one spine rather than a series of centred blocks. */
.rail{display:grid;grid-template-columns:minmax(0,7fr) minmax(0,5fr);gap:clamp(30px,5vw,80px);align-items:start}
.rail--even{grid-template-columns:minmax(0,1fr) minmax(0,1fr)}
.rail__aside{padding-top:clamp(0px,2vw,14px)}

/* ------------------------------------------------------------------- NAV */
.nav{
/* Nav bar opacity raised .72 -> .82 on 2026-09-17, for contrast.
   The bar is translucent, so on a page whose first band is WHITE (every
   journal post, privacy, terms) it composited to #4f4f4f, and the bone links
   at .6 alpha measured 3.78:1 on it - under the 4.5:1 WCAG AA minimum. axe
   flagged it on 3 page types and was right: it was compositing correctly, not
   guessing. At .82 the same links measure 4.97:1.
   Chosen over brightening the links, because that would have changed the nav
   on the homepage too, where it sits on a dark hero and already passes.
   Over near-black, .72 and .82 are indistinguishable. */
  position:fixed;inset:0 0 auto 0;z-index:60;
  display:flex;align-items:center;gap:22px;
  padding:14px max(var(--gutter),calc((100% - var(--maxw)) / 2));
  background:rgba(10,10,10,.82);
  backdrop-filter:blur(20px) saturate(160%);
  -webkit-backdrop-filter:blur(20px) saturate(160%);
  border-bottom:1px solid rgba(244,241,234,.08);
  /* Only the MATERIAL changes on scroll, never the padding. Transitioning
     padding forces layout on every scroll tick; thickening the glass gives the
     same "settled in" feeling for free, on the compositor. */
  transition:background .3s var(--ease),backdrop-filter .3s var(--ease),border-color .3s var(--ease);
}
/* 2026-09-10. Scrolled, the bar went to 90% opacity, which over an interior
   page's ink band is indistinguishable from a solid black rectangle. The home
   page rarely showed it because the hero sits behind the bar and gives the
   glass something to carry. Interior pages had no such luck, so clicking any
   link from the menu landed you on a page with a black slab across the top.
   78% keeps the type legible over busy content and still reads as glass. The
   blur does the settling instead, which is free on the compositor. */
.nav.is-tight{
  background:rgba(10,10,10,.78);
  backdrop-filter:blur(30px) saturate(180%);
  -webkit-backdrop-filter:blur(30px) saturate(180%);
  border-bottom-color:rgba(244,241,234,.16);
}
.nav__mark{font-family:var(--display);font-weight:800;font-size:17px;color:var(--bone);letter-spacing:-.01em}
.nav__mark i{font-style:normal;color:var(--gold)}
/* Top-aligned with the CTA (2026-09-02, Tomislav: tidy up the logo against
   the button) rather than the container's default vertical centering - the
   logo runs up to 92px tall at full size and centering it made the CTA look
   like it was floating below the badge rather than sitting level with it. */
.nav__mark,.nav__cta,.nav__toggle{align-self:flex-start}
.nav__links{display:flex;gap:clamp(14px,2vw,28px)}
.nav__links a{font-size:14px;color:rgba(244,241,234,.6);transition:color .2s}
.nav__links a:hover{color:var(--gold)}
/* Matched to the hero CTA's size (2026-09-03) - was 10px/17px/13.5px,
   noticeably smaller than the hero button and inconsistent for what is
   the same action in two places on the page. Only visible on desktop;
   mobile hides .nav__cta entirely (see the max-width:980px block).
   .nav__cta.cta, not .nav__cta alone: same specificity trap already hit
   twice on this element (display, then colour) - the base .cta rule sits
   later in this file and wins any equal-specificity tie on padding/font-
   size too. Chaining both classes wins regardless of source order.
   Caught this time by actually checking computed style after the first,
   plausible-looking edit, not by assuming it landed. */
.nav__cta.cta{
  /* Same pill and finish as every other button (from base .cta); only the size is
     tuned for the header bar. No background override here - that would wipe the
     base gradient and leave the nav button flat. */
  font-weight:600;font-size:14px;padding:12px 22px;border-radius:var(--cta-radius,999px);
}
.nav__cta:active{transform:scale(.97)}
.nav__toggle{display:none}

/* ══════════════════════════════════════════════════ HEADER, BY WIDTH
   Consolidated 2026-09-03, replacing five separate, partly-contradictory
   margin-juggling passes that had accumulated across earlier sessions (the
   last two literally disagreed on which side the logo sat). One system now:
   desktop is a true three-zone grid, mobile is a toggled panel. */

/* DESKTOP (981px+): a real three-column grid, not auto-margins. Auto-margins
   only center something when the two side elements are equal width, and the
   logo (~54-92px) and the CTA button (~110-150px) never are - the links
   would sit closer to whichever side was narrower. 1fr/auto/1fr guarantees
   the centre column is genuinely centred on the header regardless of how
   wide the logo or the button are on any given day. */
@media (min-width:981px){
  .nav{ display:grid; grid-template-columns:1fr auto 1fr; }
  .nav__mark{ justify-self:start; }
  .nav__links{ justify-self:center; }
  .nav__cta{ justify-self:end; }

  /* BUG (2026-09-03): the mobile menu rebuild added the "+" markers, the
     masked-reveal wrapper, and a second CTA + vertical signature
     (.nav__menuFoot) as real DOM elements inside #navLinks, and only
     scoped their STYLING to the max-width:980px block - not their
     presence. On desktop they were still in the document with no rule
     hiding them, so the "+" signs, and a whole extra "Check my site"
     button plus rotated text, were rendering inline in the middle of the
     centred desktop nav. Reported back as "desktop header is a mess",
     correctly. Desktop gets the plain link text only, exactly as it was
     before the mobile redesign touched this markup. */
  .nav__linkNum{ display:none; }
  .nav__linkMask{ display:contents; }
  .nav__linkText{ font:inherit; color:inherit; }
  .nav__menuFoot{ display:none; }
}

/* MOBILE (980px and below), SECOND PASS (2026-09-03). First pass kept the
   CTA in the header row and gave the links a small, plain dropdown -
   flagged back as "way too boring and plain". Rebuilt as a genuine
   full-screen editorial takeover instead, and the CTA moved OUT of the
   header entirely (it lives inside the panel now, as the thing the whole
   menu builds toward - see .nav__menuFoot below). */
@media (max-width:980px){
  .nav{ gap:12px; }
  /* .nav__cta.cta, not .nav__cta alone: the base .cta{display:inline-flex}
     rule sits LATER in this file and, at equal specificity (both single-
     class selectors), source order wins - so display:none on .nav__cta by
     itself was silently losing to .cta's display:inline-flex. Chaining
     both classes matches the same element at higher specificity, which
     wins regardless of where either rule sits in the file. Found by
     testing the computed style, not assumed. */
  .nav__cta.cta{ display:none; }
  .nav__toggle{
    display:block; position:relative; width:32px; height:32px;
    margin-left:auto; padding:0; border:0; background:none; cursor:pointer;
    -webkit-tap-highlight-color:transparent;
  }
  .nav__toggle:active{ transform:scale(.9); }
  .nav__toggle span{
    position:absolute; left:5px; right:5px; height:2px; border-radius:2px;
    background:#F7F5F0;
    transition:transform .35s var(--ease-fluid), opacity .2s var(--ease-fluid), top .3s var(--ease-fluid);
  }
  .nav__toggle span:nth-child(1){ top:10px; }
  .nav__toggle span:nth-child(2){ top:15px; }
  .nav__toggle span:nth-child(3){ top:20px; }
  .nav.is-menu-open .nav__toggle span:nth-child(1){ top:15px; transform:rotate(45deg); }
  .nav.is-menu-open .nav__toggle span:nth-child(2){ opacity:0; }
  .nav.is-menu-open .nav__toggle span:nth-child(3){ top:15px; transform:rotate(-45deg); }

  /* The panel itself: solid, not translucent - a full-screen takeover
     should feel arrived, not like a glass sheet over the page underneath.
     A quiet gold glow from the top corner gives it depth without adding
     any new colour to the palette (still just --gold, at 12% opacity). */
  .nav__links{
    position:fixed; left:0; right:0; top:var(--navh,64px); z-index:60;
    height:calc(100dvh - var(--navh,64px));
    display:flex; flex-direction:column; gap:0; margin:0;
    padding:10px max(var(--gutter),22px) 28px;
    background:
      radial-gradient(140% 65% at 100% 0%, rgba(242,201,76,.12), transparent 62%),
      #070707;
    transform:translateY(-14px); opacity:0; visibility:hidden; pointer-events:none;
    transition:opacity .4s var(--ease-fluid), transform .4s var(--ease-fluid), visibility 0s .4s;
  }
  .nav.is-menu-open .nav__links{
    transform:translateY(0); opacity:1; visibility:visible; pointer-events:auto;
    transition:opacity .4s var(--ease-fluid), transform .4s var(--ease-fluid), visibility 0s;
  }

  /* Each link: a small gold index number, then the label at headline
     scale, in the display serif - large enough to be a statement, not a
     menu item. The label sits inside a masked line (overflow:hidden)
     exactly like .hero__h1 .ln/.w - the same technique this site already
     uses to bring the hero headline on screen, reused rather than
     reinvented for one component. */
  .nav__link{
    display:flex; align-items:baseline; gap:16px;
    padding:14px 2px; border-bottom:1px solid rgba(244,241,234,.10);
    text-decoration:none; -webkit-tap-highlight-color:transparent;
    transition:transform .15s var(--ease-out);
  }
  .nav__link:active{ transform:scale(.98); }
  /* The same diamond as the FAQ, the step animations and the post card. It
     was a "+", which read as "expand" on rows that navigate away rather than
     open. The mark is decorative and the label carries the meaning, so a
     shape is honest where a plus was not. The staggered fade is untouched. */
  .nav__linkNum{
    /* align-self, not margin. The row is align-items:baseline, and an empty
       block's baseline is its own bottom edge, so the diamond hung 9px below
       the label and no margin fixed it properly - the baseline just
       recalculated. Centring it against the line does. */
    flex:0 0 auto; align-self:center;
    width:9px; height:9px; background:var(--gold);
    transform:rotate(45deg);
    font-size:0; line-height:0;
    opacity:0; transition:opacity .5s var(--ease-fluid);
  }
  .nav__linkMask{ flex:1; overflow:hidden; display:block; }
  .nav__linkText{
    display:block; font-family:var(--display); font-weight:600;
    font-size:clamp(19px,5.2vw,23px); line-height:1.15; letter-spacing:-.005em;
    color:#F7F5F0;
    transform:translateY(115%) rotate(1.2deg); opacity:0;
    transition:transform .55s var(--ease-fluid), opacity .4s var(--ease-fluid);
  }
  .nav.is-menu-open .nav__linkNum{ opacity:.85; }
  .nav.is-menu-open .nav__linkText{ transform:translateY(0) rotate(0); opacity:1; }
  /* Each row a beat behind the last - the number arrives fractionally
     before its own label, so the cascade reads left-to-right as well as
     top-down. */
  .nav.is-menu-open .nav__links a:nth-child(1) .nav__linkNum{ transition-delay:.10s; }
  .nav.is-menu-open .nav__links a:nth-child(1) .nav__linkText{ transition-delay:.14s; }
  .nav.is-menu-open .nav__links a:nth-child(2) .nav__linkNum{ transition-delay:.17s; }
  .nav.is-menu-open .nav__links a:nth-child(2) .nav__linkText{ transition-delay:.21s; }
  .nav.is-menu-open .nav__links a:nth-child(3) .nav__linkNum{ transition-delay:.24s; }
  .nav.is-menu-open .nav__links a:nth-child(3) .nav__linkText{ transition-delay:.28s; }
  .nav.is-menu-open .nav__links a:nth-child(4) .nav__linkNum{ transition-delay:.31s; }
  .nav.is-menu-open .nav__links a:nth-child(4) .nav__linkText{ transition-delay:.35s; }

  /* Foot: the CTA that used to sit flat in the header, now the thing the
     whole panel builds toward - pinned to the bottom, large, gold, with a
     quiet vertical signature echoing .hero__edge at the far corner. */
  .nav__menuFoot{
    margin-top:auto; padding-top:22px;
    display:flex; align-items:flex-end; justify-content:space-between; gap:20px;
    opacity:0; transform:translateY(14px);
    transition:opacity .5s var(--ease-fluid), transform .5s var(--ease-fluid);
  }
  .nav.is-menu-open .nav__menuFoot{
    opacity:1; transform:translateY(0); transition-delay:.40s;
  }
  /* Colour needs its own fight: .nav--over .nav__links a{color:#F7F5F0}
     (global, further down this file) turns every <a> inside .nav__links
     white, including this one - the same class of bug already found once
     on the header CTA (display, not colour, that time). #0A0A0A !important
     is the pragmatic fix here rather than chasing another 4-class
     selector: .nav--over is a variant class that can combine in ways that
     keep shifting, and this is one property on one element, not worth a
     specificity arms race every time that class changes shape. */
  .nav__menuCta{
    flex:1; justify-content:center; padding:17px 22px; font-size:15.5px;
  }
  .nav__menuEdge{
    flex:0 0 auto; writing-mode:vertical-rl; transform:rotate(180deg);
    font-size:10px; letter-spacing:.3em; text-transform:uppercase;
    color:rgba(247,245,240,.34);
  }
}

.nav__scrim{
  /* z-index:35, not something closer to .nav's own 60: .nav also carries the
     .nav--over variant class (2026-09-03 bug, found by testing the click,
     not assumed), which cascades LATER in this file and sets z-index:40 on
     the same element - so the header's real effective stacking is 40, not
     60. Discovered because Playwright's own click retry logged the scrim
     intercepting a link tap that should have landed on the panel above it.
     35 stays safely under 40 regardless of which nav variant is active,
     rather than chasing the exact number nav--over happens to use today. */
  position:fixed; inset:0; z-index:35;
  background:rgba(4,4,4,.55);
  opacity:0; visibility:hidden; pointer-events:none;
  transition:opacity .32s var(--ease-fluid), visibility 0s .32s;
}
.nav__scrim.is-visible{
  opacity:1; visibility:visible; pointer-events:auto;
  transition:opacity .32s var(--ease-fluid), visibility 0s;
}
@media (min-width:981px){ .nav__scrim{ display:none; } }
body.nav-open{ overflow:hidden; }

@media (prefers-reduced-motion:reduce){
  .nav__links, .nav__linkText, .nav__linkNum, .nav__menuFoot, .nav__toggle span, .nav__scrim{
    transition-duration:.12s !important; transition-delay:0s !important;
  }
  .nav__links, .nav__linkText, .nav__menuFoot{ transform:none !important; }
}

/* ------------------------------------------------------------------ HERO */
.hero{
  position:relative;min-height:calc(100svh - var(--ticker-h));display:flex;align-items:center;
  padding-block:clamp(120px,15vw,170px) clamp(60px,8vw,100px);
  padding-inline:max(var(--gutter),calc((100% - var(--maxw)) / 2));
  overflow:hidden;
}
.hero__bg{position:absolute;inset:0;z-index:0}
.hero__bg video{width:100%;height:100%;object-fit:cover;
  transform:scale(calc(1 + var(--zoom,.22) * 1));opacity:0;
  animation:mediaIn 1200ms var(--ease-out) 200ms forwards;will-change:transform}
.hero__bg::after{
  content:"";position:absolute;inset:0;
  background:
    linear-gradient(to right,rgba(10,10,10,.95) 0%,rgba(10,10,10,.86) 42%,rgba(10,10,10,.5) 78%,rgba(10,10,10,.4) 100%),
    linear-gradient(to bottom,rgba(10,10,10,.8) 0%,rgba(10,10,10,0) 30%,rgba(10,10,10,0) 60%,rgba(10,10,10,.9) 100%);
}
@keyframes mediaIn{to{opacity:1}}
.hero__grid{position:relative;z-index:1;display:grid;
  grid-template-columns:minmax(0,1fr) minmax(0,clamp(320px,44vw,520px));
  gap:clamp(34px,5vw,70px);align-items:center;width:100%}
.hero h1 em{color:var(--gold)}
.hero .lede{margin:22px 0 32px;color:rgba(244,241,234,.86)}

/* THE BUTTON - PREMIUM PILL, ONE LOOK EVERYWHERE (2026-10-10). Tomislav: pill
   shape, a more premium look, and the same look on every button so they read as
   identical. This one base rule drives every .cta on the site; the nav, menu and
   submit variants only change size, never the shape or finish. */
.cta{
  position:relative; overflow:hidden; isolation:isolate;
  display:inline-flex; align-items:center; justify-content:center; gap:11px;
  background:var(--gold);
  /* Gold stays the only hue: the gradient is shading of #F2C94C, not a second
     colour - a lit top rolling to a seated base, which is what gives a flat fill
     the feel of a solid object instead of a sticker. */
  background-image:linear-gradient(180deg,#F8DC7B 0%,var(--gold) 50%,#ECC047 100%);
  /* White label with a three-layer dark halo behind it, exactly like the mobile
     menu "Get in touch" button (2026-10-10). The halo is what carries the text on
     the bright gold - the same gesture that carries the nav links over the hero. */
  color:#FFFFFF;
  text-shadow:0 0 1px rgba(6,9,12,.55), 0 1px 1px rgba(6,9,12,.72), 0 1px 6px rgba(6,9,12,.4);
  font-weight:600; font-size:clamp(15px,1.05vw,16.5px); line-height:1;
  letter-spacing:.005em;
  padding:16px 30px; border-radius:var(--cta-radius,999px); border:0; cursor:pointer;
  /* A bright top lip, a fine seated base, a tight contact shadow and a soft
     ambient one. Four layers read as depth; a single blurry glow reads as a web
     template. */
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,.60),
    inset 0 -1px 0 rgba(122,93,0,.22),
    0 1px 2px rgba(10,10,10,.18),
    0 10px 22px -10px rgba(10,10,10,.40);
  transition:transform .35s var(--m-spring), box-shadow .35s var(--m-spring), background-color .35s var(--m-spring);
}
/* One light passes across as it arrives, and again on hover. A glint, not a
   shine: brief, narrow, gone, and clipped to the pill by the overflow above. */
.cta::before{
  content:""; position:absolute; top:0; bottom:0; left:0; width:55%; z-index:2;
  background:linear-gradient(100deg,transparent 0%,rgba(255,255,255,.55) 50%,transparent 100%);
  transform:translateX(-220%); opacity:0; pointer-events:none;
}
@keyframes ctaGlint{ 0%{transform:translateX(-220%);opacity:0} 12%{opacity:1} 100%{transform:translateX(340%);opacity:0} }
.cta svg{ transition:transform .25s var(--ease); flex:none; position:relative; z-index:1 }
.cta:active{ transform:scale(.98) }
.cta:focus-visible{ outline:2px solid #0A0A0A; outline-offset:3px }
@media (hover:hover) and (pointer:fine){
  .cta:hover{
    transform:translateY(-2px);
    /* The premium hover: the pill lifts a touch higher, the contact shadow grows,
       and a soft warm gold bloom spreads behind it - classy, not neon. */
    box-shadow:
      inset 0 1px 0 rgba(255,255,255,.72),
      inset 0 -1px 0 rgba(122,93,0,.26),
      0 3px 7px rgba(10,10,10,.22),
      0 20px 36px -12px rgba(10,10,10,.52),
      0 0 26px -2px rgba(242,201,76,.52);
  }
  .cta:hover{ background-image:linear-gradient(180deg,#FBE394 0%,#F4CE57 52%,#EFC64E 100%); }
  .cta:hover svg{ transform:translateX(4px) }
  .cta:hover::before{ animation:ctaGlint .9s cubic-bezier(.4,0,.2,1) }
}

.hero__note{display:block;margin-top:18px;font-size:14.5px;color:rgba(244,241,234,.55)}
.hero__note a{border-bottom:1px solid var(--gold);padding-bottom:2px;color:var(--bone)}

/* ------------------------------------------------- SIGNATURE: THE PREVIEW
   The one bold element. A working sketch of the visitor's own first page,
   assembled from their trade and business name. Deliberately reads as a
   sketch, not a screenshot: this is an illustration of what they'd receive,
   and must never imply it is finished work for a real client. */
.demo{
  background:rgba(20,20,20,.82);
  backdrop-filter:blur(18px);-webkit-backdrop-filter:blur(18px);
  border:1px solid rgba(244,241,234,.16);
  border-radius:4px;overflow:hidden;
  box-shadow:0 30px 80px rgba(0,0,0,.5);
}
.demo__head{padding:14px 16px 12px;border-bottom:1px solid rgba(244,241,234,.1)}
.demo__label{font-size:10.5px;font-weight:600;letter-spacing:.18em;text-transform:uppercase;color:var(--gold)}
.demo__controls{display:grid;gap:10px;padding:14px 16px}
.demo__field{display:grid;gap:6px}
.demo__field span{font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:rgba(244,241,234,.5);font-weight:600}
.demo__field input,.demo__field select{
  font:inherit;font-size:14.5px;font-weight:400;color:var(--bone);
  background:rgba(244,241,234,.06);border:1px solid rgba(244,241,234,.16);
  border-radius:2px;padding:9px 11px;width:100%;
}
.demo__field select{appearance:none;background-image:linear-gradient(45deg,transparent 50%,rgba(244,241,234,.5) 50%),linear-gradient(135deg,rgba(244,241,234,.5) 50%,transparent 50%);background-position:calc(100% - 17px) 52%,calc(100% - 12px) 52%;background-size:5px 5px,5px 5px;background-repeat:no-repeat}
.demo__field input:focus,.demo__field select:focus{border-color:var(--gold);outline:none}

/* the rendered sketch */
.sheet{margin:4px 14px 16px;background:var(--bone);border-radius:2px;overflow:hidden;color:#0A0A0A}
.sheet__bar{display:flex;align-items:center;gap:8px;padding:9px 11px;background:#e7e3da;border-bottom:1px solid rgba(10,10,10,.09)}
.sheet__name{font-family:var(--display);font-weight:800;font-size:12.5px;letter-spacing:-.01em}
.sheet__nav{margin-left:auto;display:flex;gap:7px}
.sheet__nav i{display:block;width:16px;height:3px;border-radius:2px;background:rgba(10,10,10,.2)}
.sheet__hero{padding:16px 13px 15px;background:var(--sheet-tint,#dfe6ea);position:relative;overflow:hidden}
.sheet__eyebrow{font-size:8px;font-weight:600;letter-spacing:.16em;text-transform:uppercase;color:rgba(10,10,10,.55);margin-bottom:6px}
.sheet__h{font-family:var(--display);font-weight:800;font-size:19px;line-height:1.1;letter-spacing:-.02em;margin:0 0 8px}
.sheet__sub{font-size:10.5px;line-height:1.45;color:rgba(10,10,10,.66);margin:0 0 11px;max-width:30ch}
.sheet__btn{display:inline-block;background:#0A0A0A;color:var(--bone);font-size:9.5px;font-weight:600;padding:7px 12px;border-radius:2px}
.sheet__body{padding:13px;display:grid;grid-template-columns:repeat(3,1fr);gap:8px}
.sheet__card{background:rgba(10,10,10,.045);border-radius:2px;padding:9px 8px}
.sheet__card b{display:block;font-family:var(--display);font-size:10px;font-weight:700;margin-bottom:4px}
.sheet__card i{display:block;height:3px;border-radius:2px;background:rgba(10,10,10,.13);margin-bottom:3px}
.sheet__card i:last-child{width:62%}
.demo__foot{padding:0 16px 15px;font-size:12px;line-height:1.5;color:rgba(244,241,234,.5)}

/* ------------------------------------------------------------------ STEPS */
.steps{counter-reset:s;display:grid;gap:2px;margin-top:36px;border-top:1px solid var(--hair)}
.step{
  display:grid;grid-template-columns:clamp(88px,12vw,150px) minmax(0,1fr);
  gap:clamp(16px,3vw,40px);align-items:start;
  padding:26px 0;border-bottom:1px solid var(--hair);
}
.step__k{font-family:var(--body);font-weight:600;font-size:11.5px;letter-spacing:.16em;text-transform:uppercase;color:var(--accent);padding-top:5px}
.step p{margin:8px 0 0;color:var(--muted);font-size:16px;max-width:58ch}

/* ------------------------------------------------------------- INCLUDED */
.incl{list-style:none;margin:30px 0 0;padding:0;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1px;background:var(--hair);border:1px solid var(--hair)}
.incl li{background:var(--paper);padding:17px 18px;display:flex;gap:12px;align-items:flex-start;font-size:15.5px}
.band--ink .incl li{background:var(--ink)}
.incl svg{flex:0 0 auto;margin-top:4px;color:var(--accent)}
/* An odd number of items left the last cell empty, and because the grid's own
   background shows through the 1px gaps as hairlines, that empty cell rendered
   as a grey box that looked like a broken element. Five variables on the
   pricing page is what surfaced it. The last item spans instead. */
.incl li:last-child:nth-child(odd){grid-column:1 / -1}

/* ------------------------------------------------------------------- FAQ */
.faq{margin-top:22px;border-top:1px solid var(--hair)}
.faq__row{border-bottom:1px solid var(--hair)}
.faq__q{width:100%;display:flex;gap:20px;align-items:center;padding:21px 2px;text-align:left;
  font-size:clamp(16px,1.5vw,18.5px);font-weight:400;font-family:var(--display);transition:transform .12s var(--ease-out);}
.faq__q:active{transform:scale(.985)}
.faq__q span:first-child{flex:1}
/* The diamond (2026-09-10). It was a "+" that rotated into a cross, which is
   the same toggle every accordion on the internet uses. The diamond is
   already the site's own mark, on the seven step animations and on the post
   card eyebrow, so using it here makes it a signature rather than decoration.
   Closed it is a diamond; open it lands square.

   The "+" is still in the markup and is aria-hidden, so it is decorative and
   font-size:0 removes it without editing every FAQ on every page. */
.faq__mark{
  flex:0 0 auto; width:11px; height:11px;
  background:var(--accent);
  font-size:0; line-height:0;
  transform:rotate(45deg);
  transition:transform .34s var(--ease);
}
.faq__row.is-open .faq__mark{transform:rotate(0deg)}
/* A #F2C94C diamond at 11px on white is close to invisible. The site answers
   this the same way everywhere else it puts gold on paper. */
.band--paper .faq__mark{background:var(--gold-deep)}
.faq__a{overflow:hidden;height:0}
.faq__a div{padding:0 0 22px;max-width:64ch;color:var(--muted);font-size:16px}

/* ----------------------------------------------------------------- LOCAL */
.local__media{border:1px solid var(--hair);border-radius:3px;overflow:hidden;background:#111}
.local__media video{width:100%;height:auto;aspect-ratio:4/3;object-fit:cover;display:block}

/* ---------------------------------------------------------------- CLOSING */
.close h2{max-width:15ch}

/* One gap between a heading and its gold rule, matched to the hero's:
   .hero__h1 .rule sits ~5px into the foot of the slogan. Both masked
   heads - the free-audit head (.check__head) and this closing one -
   were dropping the rule ~22px low, because the default h2
   margin-bottom stacked under .m-rule's own margin-top. Zero the h2
   margin and pull the rule up to the same hug. */
.check__head h2,
.close h2{ margin-bottom:0 }
.check__head .m-rule,
.close .m-rule{ margin-top:-5px }
.close .m-rule{ margin-bottom:30px } /* breathing room before the lede */
.close__actions{display:flex;flex-wrap:wrap;gap:14px;margin-top:30px}

/* ---------------------------------------------------------------- FOOTER */
.foot{
  background:var(--ink);border-top:1px solid rgba(244,241,234,.12);
  padding:44px max(var(--gutter),calc((100% - var(--maxw)) / 2));
  display:flex;flex-wrap:wrap;gap:10px 34px;align-items:baseline;
  font-size:13.5px;color:rgba(244,241,234,.5);
}
.foot b{font-family:var(--display);font-weight:800;color:var(--bone);font-size:15px}

/* ---------------------------------------------------------------- REVEAL */
.js .rv{opacity:0;transform:translate3d(0,20px,0)}
.js .rv.in{opacity:1;transform:none;transition:opacity .7s var(--ease),transform .7s var(--ease)}

/* ------------------------------------------------------------ RESPONSIVE */
@media (max-width:980px){
  .rail,.rail--even{grid-template-columns:1fr;gap:28px}
  .hero__grid{grid-template-columns:1fr;gap:34px}
  /* .nav__links{display:none} removed 2026-09-03 - this predates the
     toggled panel further down the file, which sets its own display:flex
     for this breakpoint. Left in place across several nav rewrites because
     later, more specific-looking rules happened to cascade over it each
     time - until the redesign consolidated those away and this dead rule
     was suddenly the one left standing, hiding the panel completely. */
  .incl{grid-template-columns:1fr}
  .step{grid-template-columns:1fr;gap:6px;padding:22px 0}
  .step__k{padding-top:0}
}
@media (max-width:560px){
  body{font-size:16px}
  .sheet__body{grid-template-columns:1fr 1fr}
  .close__actions .cta{width:100%;justify-content:center}
}

/* --------------------------------------------------------- ACCESSIBILITY */
@media (prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  .js .rv,.js .rv.in{opacity:1;transform:none;transition:none}
  .hero__bg video{animation:none;opacity:1;transform:none}
  .cta,.nav__cta,.faq__mark,.nav{transition:none}
}
@media (prefers-reduced-transparency:reduce){
  .nav{background:rgba(10,10,10,.97);backdrop-filter:none;-webkit-backdrop-filter:none}
  .demo{background:#141414;backdrop-filter:none;-webkit-backdrop-filter:none}
}
@media (prefers-contrast:more){
  :root{--muted:rgba(244,241,234,.85);--hair:rgba(244,241,234,.4)}
  .band--paper{--muted:rgba(10,10,10,.85);--hair:rgba(10,10,10,.4)}
  .nav{background:#000;backdrop-filter:none}
}

/* ------------------------------------------------------------------ FORM
   Sits in the right column of the closing band. Deliberately plain: this is
   the moment a nervous person hands over their details, so nothing here is
   clever. Labels above fields, required marked in words not asterisks, and
   the trade carried over from the sketch so they do not retype it. */
.form{display:grid;gap:14px}
.form__prefill{
  font-size:13px;line-height:1.5;color:var(--muted);
  border-left:2px solid var(--gold);padding:8px 0 8px 12px;
}
.form__prefill b{color:var(--fg);font-weight:600}
.form__row{display:grid;grid-template-columns:1fr 1fr;gap:14px}
/* A row with one field in it. Business type lost its partner when suburb came
   out, and half a row with a gap beside it reads like something is missing. */
.form__row--one{grid-template-columns:1fr}
.field{display:grid;gap:6px}
.field > span{
  font-size:11.5px;font-weight:600;letter-spacing:.1em;text-transform:uppercase;
  color:var(--muted);display:flex;gap:7px;align-items:baseline;
}
.field > span i{font-style:normal;font-weight:400;letter-spacing:0;text-transform:none;
  font-size:12px;color:var(--muted);opacity:.75}
.field input,.field textarea{
  font:inherit;font-size:16px;font-weight:400;color:var(--fg);
  background:transparent;border:1px solid var(--hair);border-radius:2px;
  padding:11px 12px;width:100%;transition:border-color .18s var(--ease-out);
}
.field textarea{resize:vertical;min-height:84px}

/* The business type dropdown (2026-09-09). Native select chrome would break
   the row: browsers draw their own arrow, background and font. Reset it and
   draw one gold chevron so it sits level with the boxes beside it. */
.field select{
  font:inherit;font-size:16px;font-weight:400;color:var(--fg);
  background:transparent;border:1px solid var(--hair);border-radius:2px;
  padding:11px 34px 11px 12px;width:100%;
  appearance:none;-webkit-appearance:none;
  transition:border-color .18s var(--ease-out);
  background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none' stroke='%23F2C94C' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M1 1.5 6 6.5l5-5'/%3E%3C/svg%3E");
  background-repeat:no-repeat;background-position:right 12px center;
  cursor:pointer;
}
.field select:focus{
  outline:none; box-shadow:0 12px 28px -22px rgba(10,10,10,.5);
  background-color:rgba(242,201,76,.035);
  transition:border-color .18s var(--ease-out), box-shadow .25s var(--ease-out), background-color .25s var(--ease-out);
}
/* The empty first option is a blank box, not a grey instruction. */
.field select:invalid{color:var(--muted)}
.field select option{background:#101010;color:var(--fg)}
.field input:focus,.field textarea:focus{
  outline:none; box-shadow:0 12px 28px -22px rgba(10,10,10,.5);
  background:rgba(242,201,76,.035);
  transition:border-color .18s var(--ease-out), box-shadow .25s var(--ease-out), background-color .25s var(--ease-out);
}
/* No gold edge on focus (2026-09-05) - the border stays put. Same premium
   language the audit field already uses instead: a soft lift, an ambient
   shadow, and the label above brightening rather than the box outline. */
.field{ transition:transform .3s var(--m-spring); }
.field:focus-within{ transform:translateY(-1px); }
.field:focus-within > span{
  color:var(--accent); letter-spacing:.12em;
  transition:color .2s var(--ease-out), letter-spacing .2s var(--ease-out);
}
.field input[readonly]{opacity:.72;cursor:not-allowed}
.field input::placeholder,.field textarea::placeholder{color:var(--muted);opacity:.7}
.form__submit{margin-top:4px;justify-content:center;width:100%}
.form__submit[disabled]{opacity:.6;cursor:progress}
.form__status{margin:0;font-size:14px;line-height:1.5;min-height:1.2em}
.form__status.is-ok{color:var(--accent);font-weight:500}
.form__status.is-err{color:#B3261E}
.band--paper .form__status.is-err{color:#B3261E}
@media (max-width:560px){ .form__row{grid-template-columns:1fr} }

/* ============================================================ WHITE FIRST
   The hero is now paper, and every band alternates from there. A white hero
   changes the video's job completely: full-bleed footage behind black type is
   unreadable, so the footage becomes a contained panel sitting in the right
   column, framed like a plate in a magazine rather than a background. */
.hero--paper{
  background:var(--paper);
  --fg:#0A0A0A;
  --muted:rgba(10,10,10,.62);
  --hair:rgba(10,10,10,.14);
  --accent:var(--gold-deep);
  color:#0A0A0A;
  min-height:auto;
  padding-block:clamp(140px,16vw,200px) clamp(70px,9vw,110px);
}
/* The full-bleed background video is retired on a white hero. */
.hero--paper .hero__bg{display:none}
.hero--paper .hero h1 em,.hero--paper h1 em{color:var(--gold-deep)}
.hero--paper .hero__note{color:rgba(10,10,10,.55)}
.hero--paper .hero__note a{color:#0A0A0A;border-bottom-color:var(--gold-deep)}

/* The sketch panel now sits on white, so it needs its own weight rather than
   floating on a dark ground. */
.hero--paper .demo{
  background:#0F0F0F;
  border-color:rgba(10,10,10,.16);
  box-shadow:0 24px 60px rgba(10,10,10,.18);
}

/* The footage moves into the Local search band, which is now dark. */
.local__media{border-color:rgba(244,241,234,.16)}


/* Each face needs its own weight and tracking to sit right - one setting for
   all five would flatter the heavy ones and starve the light ones. */

@media (max-width:640px){    }

/* ------------------------------------------------- NAV ON A WHITE HERO
   A translucent dark bar over a white hero reads as grey mush, not glass.
   At the top the nav is now paper with dark type; once you scroll past the
   hero it becomes the dark glass it was designed as. Two clean states, no
   murky in-between. */
.nav:not(.is-tight){
  background:rgba(255,255,255,.86);
  backdrop-filter:blur(18px) saturate(150%);
  -webkit-backdrop-filter:blur(18px) saturate(150%);
  border-bottom:1px solid rgba(10,10,10,.1);
}
.nav:not(.is-tight) .nav__mark{color:#0A0A0A}
.nav:not(.is-tight) .nav__mark i{color:var(--gold-deep)}
.nav:not(.is-tight) .nav__links a{color:rgba(10,10,10,.62)}
.nav:not(.is-tight) .nav__links a:hover{color:#0A0A0A}

/* The hero lede needs more weight on white than it does on black: the same
   opacity that reads as calm on a dark ground reads as faded on paper. */
.hero--paper .lede{color:rgba(10,10,10,.72)}

/* ================================================== THE SCORE (signature)
   The new front door. It has to feel like a tool, not a lead-capture form,
   so it is set flush and plain: one field, one button, no card, no shadow.
   The result then arrives as a document rather than a popup. */
.score{margin:30px 0 0}
.score__label{
  display:block;font-size:11.5px;font-weight:600;letter-spacing:.14em;
  text-transform:uppercase;color:var(--muted);margin-bottom:9px;
}
.score__row{display:flex;gap:10px;align-items:stretch;max-width:520px}
.score__row input{
  flex:1;min-width:0;font:inherit;font-size:17px;font-weight:400;color:var(--fg);
  background:transparent;border:1px solid var(--hair);border-radius:2px;padding:14px 15px;
  transition:border-color .18s var(--ease-out);
}
.score__row input:focus{outline:none}
.score__row input::placeholder{color:var(--muted);opacity:.65}
/* Same object as every other CTA. Only nowrap differs, because it sits in a
   row beside an input and must not break onto two lines. */
.score__go{white-space:nowrap}
@media (min-width:981px){ .score__go{ font-size:14px; padding:12px 24px; align-self:center; } }
.score__go[disabled]{opacity:.6;cursor:progress}
.score__note{margin:10px 0 0;font-size:13.5px;color:var(--muted)}

.result{margin-top:26px;border-top:2px solid var(--fg);padding-top:20px;max-width:560px}
.result.is-loading{opacity:.6}
.result__head{display:flex;gap:20px;align-items:baseline;margin-bottom:16px}
.result__score{display:flex;align-items:baseline;gap:5px;font-family:var(--display)}
.result__score b{font-size:clamp(44px,5vw,60px);font-weight:700;line-height:1;letter-spacing:-.03em}
.result__score span{font-size:15px;color:var(--muted);font-family:var(--body)}
.result__verdict{display:grid;gap:3px}
.result__verdict b{font-size:15.5px;font-weight:600}
.result__verdict span{font-size:14.5px;color:var(--muted);line-height:1.45}

.result__list{list-style:none;margin:0;padding:0;display:grid;gap:1px;background:var(--hair);border:1px solid var(--hair)}
.result__list li{
  background:var(--paper);padding:11px 14px;display:grid;
  grid-template-columns:1fr auto;gap:4px 16px;align-items:baseline;
}
.band--ink .result__list li,.hero:not(.hero--paper) .result__list li{background:var(--ink)}
.result__list b{font-size:14.5px;font-weight:500}
.result__list span{font-size:13px;color:var(--muted);text-align:right}
/* Pass and fail are marked with a word-shaped cue as well as colour, so the
   result does not depend on colour vision alone. */
.result__list li::before{
  content:"";grid-row:1 / span 2;display:none;
}
.result__list li.ok b::before{content:"\2713\00a0\00a0";color:var(--accent);font-weight:700}
.result__list li.no b::before{content:"\00d7\00a0\00a0";color:#B3261E;font-weight:700}
.result__cta{margin-top:18px}
.result__cta:empty{display:none}

/* the construction animation, framed like a plate rather than a background */
.buildfig{margin:0}
.buildfig__v{
  width:100%;height:auto;display:block;background:#fff;
  aspect-ratio:900/1150;
}
.buildfig figcaption{
  margin-top:11px;font-size:12.5px;color:var(--muted);letter-spacing:.01em;
}

@media (max-width:640px){
  .score__row{flex-direction:column}
  .score__go{justify-content:center}
  .result__list li{grid-template-columns:1fr}
  .result__list span{text-align:left}
}

/* The hero columns align to the TOP, not the centre. The left column grows
   when a score result appears, and centring pushed the animation to the
   bottom of a very tall column, leaving a hole beside the headline. */
.hero--paper .hero__grid{align-items:start}
.hero--paper .rail__aside{padding-top:clamp(40px,7vw,96px);position:sticky;top:96px}
@media (max-width:980px){
  .hero--paper .rail__aside{position:static;padding-top:0}
}

/* ═══════════════════════════════════════════════════════════════════════
   HERO REBUILD  (2026-09-01)
   Full-bleed video after hapukulodge.com; agency structure and edge
   furniture after maverick-tourism.com. Motion and enclosure patterns from
   the high-end-visual-design skill: double-bezel containers, island CTAs
   with a nested icon, macro whitespace, and one custom easing curve used
   everywhere so the whole page moves with the same hand.
   ══════════════════════════════════════════════════════════════════════ */

:root{
  --ease-fluid: cubic-bezier(.32,.72,0,1);
  --shell: rgba(10,10,10,.05);
  /* NOT --hair. That token already existed and flips per band: light on ink,
     dark on paper. Overriding it globally here made every hairline on the
     dark bands invisible, which is why the ghost CTA in the local section
     rendered as bare text with no border. Bezels get their own token. */
  --bezel-hair: rgba(10,10,10,.10);
}

/* Film grain, animated (2026-09-03, studied from marsrouge.com's own
   noise layer - theirs cycles a texture on a steps() timer for a flicker
   rather than sitting static). Adapted rather than copied: their version
   swaps a whole PNG frame; this shifts the existing SVG noise via
   transform instead, so it stays GPU-composited (transform/opacity only,
   per the review-animations performance rule) with no second asset to
   load. Sized past 100% and inset negative so the jitter never reveals an
   edge. Fixed and non-interactive either way, so it never repaints on
   scroll regardless. */
.grain{
  position:fixed; inset:-8% -8% -8% -8%; z-index:60; pointer-events:none; opacity:.035;
  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");
  animation:grainFlicker 1s steps(6) infinite;
  will-change:transform;
}
@keyframes grainFlicker{
  0%,100%{transform:translate(0,0)}
  16%{transform:translate(-2%,-3%)}
  33%{transform:translate(3%,2%)}
  50%{transform:translate(-3%,3%)}
  66%{transform:translate(2%,-2%)}
  83%{transform:translate(-1%,1%)}
}
@media (prefers-reduced-motion:reduce){ .grain{ animation:none; } }

/* Custom cursor removed (2026-09-03): back to the normal OS pointer. */

/* MARQUEE  --  upgraded pass (2026-09-03).
   Still two rows, opposite directions, still only claims already made in
   real text above - built for depth now instead of a flat strip:
     - the rows are pitched fore / background: top row larger, warmer
       and brighter, bottom row smaller and dimmer, so the pair reads
       as two planes at different distances;
     - a gold four-point mark sits between phrases and slowly turns, so
       the rhythm carries a moving detail, not a static bullet;
     - the container is edge-masked, so phrases dissolve at both sides
       rather than being cut off by the overflow box;
     - two gold hairlines draw out from the centre on reveal - the same
       gesture as .m-rule elsewhere.
   Linear timing throughout: constant motion is not arriving anywhere.
   Speeds eased to 32s / 44s - calmer, and the two rows take even longer
   to drift back into visual sync. */
.ticker{
  position:relative; overflow:hidden; background:#0a0a0a;
  display:flex; align-items:center; justify-content:flex-start;
  height:var(--ticker-h);
}
.ticker::before,.ticker::after{
  content:""; position:absolute; left:50%; width:min(1100px,86vw); height:1px;
  background:linear-gradient(90deg,transparent,var(--gold) 18%,var(--gold) 82%,transparent);
  opacity:.24; transform:translateX(-50%) scaleX(0);
  transition:transform 1.4s var(--m-ease); z-index:2;
}
.ticker::before{ top:0; }
.ticker::after{ bottom:0; }
.ticker.in::before,.ticker.in::after{ transform:translateX(-50%) scaleX(1); }
[data-rv="marquee"]{ opacity:0; transition:opacity 1s var(--m-ease); }
[data-rv="marquee"].in{ opacity:1; }

/* One continuous track, flowing right to left (2026-09-05). The eight
   phrases are rendered twice in the markup and the track translates -50%,
   so the second set lands exactly where the first began and the loop has
   no seam. Duration is fixed rather than width-based, which keeps the
   speed constant at every viewport because the track's width comes from
   its content, not the screen. */
.ticker__track{
  display:flex; align-items:center; flex:none; white-space:nowrap;
  animation:tickerFlow 42s linear infinite; will-change:transform;
}
.ticker:not(.in) .ticker__track{ animation-play-state:paused; }
.ticker span{
  font-family:var(--body); font-style:normal; font-weight:500;
  font-size:clamp(10.5px,1.05vw,12.5px);
  letter-spacing:.2em; text-transform:uppercase;
  color:rgba(244,241,234,.52);
  padding-inline:clamp(15px,1.8vw,26px); flex:none;
}
/* FIX (2026-09-06): the hairline flickered, and the shape was the cause,
   not the speed. A 1px hard-edged vertical line translating horizontally
   lands on a fractional pixel on almost every frame, so the renderer keeps
   redistributing it across two columns and it strobes. A dot is round: it
   antialiases on all sides at once, so subpixel position shifts read as
   movement rather than as flashing. It is also the separator this site
   already uses, in the nav pill and in the footer. */
.ticker__sep{
  flex:none; display:block; width:3px; height:3px; border-radius:50%;
  background:var(--gold); opacity:.5;
}
@keyframes tickerFlow{
  from{ transform:translateX(0); }
  to{ transform:translateX(-50%); }
}
@media (prefers-reduced-motion:reduce){
  .ticker__track{ animation:none; }
  [data-rv="marquee"]{ opacity:1; }
  .ticker::before, .ticker::after{ transform:translateX(-50%) scaleX(1); transition:none; }
}

/* SEAL  --  upgraded pass (2026-09-03).
   Was a single rotating text ring over a static arrow. Now it behaves
   like the instrument the tool is:
     - an outer ring of real copy ("Free Audit - Instant Score") turning
       slowly one way;
     - an inner dashed track turning the other, for a machined dial feel;
     - a gauge arc that DRAWS ITSELF from empty to about three-quarters
       when the card is revealed - the seal performing the one thing the
       tool does, scoring, rather than only spinning;
     - the whole disc STAMPS in, scaled down and rotated, springing to
       rest, like a wax seal pressed onto the corner of the card;
     - on the card's pointer-tilt it rides on translateZ, lifting clear
       of the surface as the card moves.
   --gold-deep only: on the white paper band, plain --gold is the
   contrast failure the paper-band convention already designs around.
   pointer-events:none, aria-hidden. */
.bezel{ position:relative; }
.seal{
  position:absolute; top:-24px; right:-18px; z-index:3;
  width:clamp(76px,9.5vw,100px); height:clamp(76px,9.5vw,100px);
  pointer-events:none;
}
.seal__disc{
  position:absolute; inset:0; opacity:0;
  filter:drop-shadow(0 1px 0 rgba(255,255,255,.65)) drop-shadow(0 6px 14px rgba(10,10,10,.14));
}
.bezel[data-rv].in .seal__disc{ animation:sealStamp .9s var(--m-spring) .5s both; }
.seal__ring, .seal__dial, .seal__meter{ position:absolute; inset:0; width:100%; height:100%; }
.seal__ring{ animation:sealSpin 24s linear infinite; }
.seal__dial{ animation:sealSpinRev 18s linear infinite; }
.seal__text{
  font-family:var(--body); font-size:8.1px; font-weight:700;
  letter-spacing:.04em; text-transform:uppercase; fill:var(--gold-deep);
}
.seal__ticks{
  fill:none; stroke:var(--gold-deep); stroke-width:2; opacity:.4;
  stroke-dasharray:1 5; stroke-linecap:round;
}
.seal__gauge{
  fill:none; stroke:var(--gold-deep); stroke-width:3; stroke-linecap:round;
  transform-box:fill-box; transform-origin:center; transform:rotate(-90deg);
  stroke-dasharray:100; stroke-dashoffset:100;
  transition:stroke-dashoffset 1.3s var(--m-ease) 1s;
}
.bezel[data-rv].in .seal__gauge{ stroke-dashoffset:26; }
.seal__mark{
  position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
  color:var(--gold-deep);
}
.seal__mark svg{ transition:transform .3s var(--m-ease); }
@keyframes sealSpin{ to{ transform:rotate(360deg); } }
@keyframes sealSpinRev{ to{ transform:rotate(-360deg); } }
@keyframes sealStamp{
  0%{ opacity:0; transform:scale(.2) rotate(-38deg); }
  55%{ opacity:1; }
  100%{ opacity:1; transform:scale(1) rotate(0deg); }
}
@media (hover:hover) and (pointer:fine){
  .bezel:hover .seal__mark svg{ transform:translateX(3px); }
}
@media (max-width:640px){
  .seal{ top:-16px; right:-10px; width:64px; height:64px; }
}
@media (prefers-reduced-motion:reduce){
  .seal__ring, .seal__dial, .seal__disc{ animation:none !important; }
  .seal__disc{ opacity:1 !important; transform:none !important; }
  .seal__gauge{ transition:none; stroke-dashoffset:26; }
}

/* THE AUDIT CARD, IN MOTION
   The card is where someone decides whether to trust the tool with their
   address, so the motion is reassurance, not spectacle:
     - it tilts a few degrees toward the pointer (JS sets --brx / --bry,
       see app.js) and settles flat the instant a field inside is
       focused, so it never moves while someone is typing;
     - on reveal the field and note rise in just after the card lands, so
       the form assembles rather than snapping in whole;
     - focus lifts the input row on a soft shadow and lights the label
       and border in --gold-deep (paper-band contrast rule);
     - while the check runs, a --gold-deep sweep crosses the top edge of
       the plate: linear, indeterminate - the "still working" motion;
     - the result assembles head-first, then each finding in a short
       stagger, so it reads as being written out. */
.bezel__tilt{
  position:relative; transform-style:preserve-3d;
  transform:rotateX(var(--brx,0deg)) rotateY(var(--bry,0deg));
  transition:transform .5s var(--m-ease);
}
.bezel__tilt.is-tilting{ transition-duration:.1s; will-change:transform; }
@media (hover:hover) and (pointer:fine){
  .check .bezel{ perspective:1200px; }
  .check .seal{ transform:translateZ(42px); }
}
.bezel__core{ position:relative; }
.bezel__core:has(.result.is-loading)::before{
  content:""; position:absolute; left:0; right:0; top:0; height:2px;
  border-radius:21px 21px 0 0; overflow:hidden;
  background:linear-gradient(90deg,transparent,var(--gold-deep),transparent);
  background-size:42% 100%; background-repeat:no-repeat;
  animation:auditSweep 1.15s linear infinite;
}
@keyframes auditSweep{ from{ background-position:-42% 0; } to{ background-position:142% 0; } }
.bezel[data-rv].in .score__field{ animation:fieldRise .7s var(--m-ease) .2s both; }
.bezel[data-rv].in .score__note{ animation:fieldRise .7s var(--m-ease) .34s both; }
@keyframes fieldRise{ from{ opacity:0; transform:translateY(10px); } }
/* MATCHED TO THE ENQUIRY FORM (2026-09-06, on request). Every value below
   is the one .field uses, so entering the audit field and entering a form
   field are now the same gesture rather than two different ones.

   What went: a hard 3px gold ring and a border that snapped to gold. That
   is the browser's own default focus language wearing a brand colour - it
   arrives with an edge and it reads as validation. What replaced it is what
   the form does: the field lifts a single pixel, an ambient shadow appears
   under it, the inside warms very slightly, and the label above opens up.
   Nothing outlines anything. */
.score__row{
  border-radius:3px;
  transition:transform .3s var(--m-spring);
}
.score__row:focus-within{ transform:translateY(-1px); }
.score__row input{ transition:border-color .18s var(--ease-out); }
.score__row input:focus{
  outline:none;
  box-shadow:0 12px 28px -22px rgba(10,10,10,.5);
  background:rgba(242,201,76,.035);
  transition:border-color .18s var(--ease-out), box-shadow .25s var(--ease-out), background-color .25s var(--ease-out);
}
/* Opens up by .02em, the same as the form's label. It used to tighten from
   .14em to .10em - the same moment, moving the opposite way. */
.score__field:focus-within .score__label{
  color:var(--accent); letter-spacing:.16em;
  transition:color .2s var(--ease-out), letter-spacing .2s var(--ease-out);
}
.result:not(.is-loading) .result__head > *{ animation:fieldRise .55s var(--m-ease) both; }
.result:not(.is-loading) .result__verdict{ animation-delay:.08s; }
.result:not(.is-loading) .result__score b{ animation:scoreRise .6s var(--m-spring) both; }
.result:not(.is-loading) .result__list li{ animation:fieldRise .5s var(--m-ease) both; }
.result:not(.is-loading) .result__list li:nth-child(1){ animation-delay:.12s; }
.result:not(.is-loading) .result__list li:nth-child(2){ animation-delay:.19s; }
.result:not(.is-loading) .result__list li:nth-child(3){ animation-delay:.26s; }
.result:not(.is-loading) .result__list li:nth-child(4){ animation-delay:.33s; }
.result:not(.is-loading) .result__list li:nth-child(5){ animation-delay:.4s; }
.result:not(.is-loading) .result__list li:nth-child(6){ animation-delay:.47s; }
.result:not(.is-loading) .result__list li:nth-child(n+7){ animation-delay:.54s; }
.result:not(.is-loading) .result__cta{ animation:fieldRise .55s var(--m-ease) .5s both; }
@keyframes scoreRise{ from{ opacity:0; transform:translateY(6px) scale(.92); } }
@media (prefers-reduced-motion:reduce){
  .bezel__tilt{ transform:none !important; transition:none; }
  .bezel__core:has(.result.is-loading)::before{ animation:none; opacity:.5; }
  .bezel[data-rv].in .score__field,
  .bezel[data-rv].in .score__note,
  .result:not(.is-loading) .result__head > *,
  .result:not(.is-loading) .result__score b,
  .result:not(.is-loading) .result__list li,
  .result:not(.is-loading) .result__cta{ animation:none !important; }
  /* The lift is motion and goes; the shadow, the warmth and the label are
     not motion and stay, so the field still tells you where you are.
     .field is listed alongside it - the enquiry form never had this rule,
     and matching the two by taking the audit's away would have been the
     wrong direction. */
  .score__row:focus-within,
  .field:focus-within{ transform:none; }
}

/* ─────────────────────────────────────────────────────────── HERO */
/* Height reserves the ticker's row so hero + ticker together are exactly
   one viewport, putting the ticker flush on the bottom edge at first
   paint rather than one scroll below it (2026-09-06).

   svh, not dvh: svh is the viewport at its SMALLEST, i.e. a phone with the
   browser chrome fully expanded, which is precisely the state the page is
   in on first load. dvh is the LIVE value, so it grows the moment mobile
   chrome retracts - the hero would then be taller than the screen was when
   it painted, and the ticker would sit just under the fold again, which is
   the exact bug being fixed. svh also stops the hero resizing mid-scroll
   on mobile. */
.hero{
  position:relative; min-height:calc(100svh - var(--ticker-h)); display:flex; align-items:flex-end;
  padding:0 0 clamp(64px,8vw,120px); overflow:hidden; isolation:isolate;
  background:#0a0a0a;
}
.hero__media{position:absolute; inset:0; z-index:0}
.hero__media picture{position:absolute; inset:0; display:block}
.hero__poster,.hero__video{
  position:absolute; inset:0; width:100%; height:100%; object-fit:cover;
  transform:scale(1.04);
}
.hero__video{opacity:0; transition:opacity 1.1s var(--ease-fluid)}
.hero__media.is-playing picture{opacity:0; transition:opacity 1.1s var(--ease-fluid)}
.hero__video.is-in{opacity:1}
/* Legibility is bought with a gradient, not by flattening the whole picture,
   so the top third of the frame stays open. */
/* Two gradients, not one. The vertical pass holds the nav and the bottom
   edge; the horizontal pass darkens the COLUMN the type sits in, which is how
   both reference sites keep a headline readable over moving footage. Measured
   rather than eyeballed: gold on the busiest frame of this clip was 3.22:1
   with a single gradient, which only just clears the 3:1 large-text minimum
   and would fail as the tram moves through. */
/* THE BLACK COMES BACK AS FAR AS THE WORDS ALLOW (2026-09-21).
   Tomislav: "on desktop view the hero section is a bit too dark somehow, it
   needs to be more bright and add some color to it", and he chose to let the
   footage itself through rather than tint anything, so no colour is added here.
   The colour is the clip's, and it was being painted over.

   Measured before touching it, over ten frames of the real clip at three
   widths: the headline sat between 6.6:1 and 8.7:1 against a floor of 3:1, and
   the lede between 9.3:1 and 9.7:1 against 4.5:1. Two to three times darker
   than legibility asks for, which is why it reads heavy.

   Also measured: the footage behind every one of those boxes reaches pure white
   on some frame. So the scrim is sized against 255, not against an average, and
   these are the numbers that fall out of it - a background may reach about 138
   under the headline and about 90 under the lede before those floors break.

   WHERE THE DARK GOES. Thinning both passes evenly was the first attempt and it
   failed its own measurement: the headline fell to 2.99:1 and its gold words to
   2.45:1, under the 3:1 floor. The headline is the binding constraint, not the
   lede - it is 15 characters of 165px type, so at 1920 it reaches most of the
   way across the frame and up into the open part of it, and gold is the tightest
   colour on the page.

   So the weight moved rather than reduced. The foot of the frame, where the
   headline and the lede live, is held harder than before. Everything above 74%
   of the height carries no vertical scrim at all, against 10% to 55% before, and
   the right sixth carries none horizontally. The sky, the water and the light
   are what come back. */
.hero__scrim{
  position:absolute; inset:0;
  background:
    linear-gradient(to right, rgba(8,8,8,.76) 0%, rgba(8,8,8,.58) 32%,
                    rgba(8,8,8,.18) 66%, rgba(8,8,8,0) 88%),
    linear-gradient(to top, rgba(8,8,8,.92) 0%, rgba(8,8,8,.68) 20%,
                    rgba(8,8,8,.22) 46%, rgba(8,8,8,0) 74%);
}
/* THE NAVIGATION GETS ITS OWN BAND, BECAUSE IT WAS ALREADY FAILING (2026-09-21).
   Not part of the brightening: the same measurement found the nav links at
   3.85:1 at 1280 and 3.97:1 at 1440 over a bright frame, against a 4.5:1 floor.
   They were leaning on the top of the vertical pass, which is a poor place to
   carry them - it darkens the whole sky to protect five words.
   A short band at the head of the frame does the same job in 160px, and the sky
   below it is free. */
.hero__scrim::before{
  content:""; position:absolute; left:0; right:0; top:0; height:190px;
  /* .80/.52 measured 4.52:1 against a 4.5:1 floor - passing, but two
     hundredths is not a margin, and the next frame of the clip could take it
     under. These read 5.1:1 and cost nothing visible: the band is 190px tall
     and the sky below it is untouched either way. */
  background:linear-gradient(to bottom, rgba(8,8,8,.84) 0%, rgba(8,8,8,.58) 45%,
                             rgba(8,8,8,0) 100%);
  pointer-events:none;
}
@media (max-width:900px){ .hero__scrim::before{ display:none } }
@media (max-width:900px){
  /* No room for a text column on a phone, so the whole frame carries it. */
  .hero__scrim{
    background:linear-gradient(to top, rgba(8,8,8,.94) 0%, rgba(8,8,8,.78) 38%,
                               rgba(8,8,8,.52) 66%, rgba(8,8,8,.66) 100%);
  }
}
.hero__inner{
  position:relative; z-index:1; width:100%;
  /* FIX (2026-09-02): this used to run its own independent max-width
     (1400px) and padding clamp, decoupled from the --gutter/--maxw rail
     system every other section and the nav use. The two formulas only
     nearly agreed by coincidence around a 1440px viewport - at 1920px the
     header logo sat about 25px left of the headline, and it grew wider on
     bigger screens. Now the hero's left edge is computed with the exact
     same formula as .nav's padding, so the logo and "Get Found." share one
     vertical line at every width. */
  padding-inline:max(var(--gutter),calc((100% - var(--maxw)) / 2));
}
.hero__h1{
  color:var(--bone); margin:20px 0 0; max-width:15ch;
  font-size:clamp(61px,11.2vw,165px); line-height:.94; letter-spacing:-.035em;
}
.hero__h1 em{font-style:normal; color:var(--gold)}
.hero__lede{
  color:rgba(247,245,240,.78); max-width:52ch; margin:26px 0 40px;
  font-size:clamp(16px,1.35vw,19px); line-height:1.62;
}

/* Maverick's vertical edge label. Hidden where there is no room for it. */
.hero__edge{
  position:absolute; left:26px; bottom:120px; z-index:1;
  writing-mode:vertical-rl; transform:rotate(180deg);
  font-size:11.5px; letter-spacing:.34em; text-transform:uppercase;
  color:rgba(247,245,240,.42);
}
@media (max-width:1180px){ .hero__edge{display:none} }


/* Nav sits over the video, and only gains a surface once you scroll. */
.nav--over{
  position:fixed; top:0; left:0; right:0; z-index:40;
  background:transparent !important; border-bottom:1px solid transparent;
  transition:background .5s var(--ease-fluid), border-color .5s var(--ease-fluid),
             backdrop-filter .5s var(--ease-fluid);
}
.nav--over .nav__mark,.nav--over .nav__links a{color:#F7F5F0}
.nav--over.is-tight{
  background:rgba(10,10,10,.82) !important; backdrop-filter:blur(18px) saturate(140%);
  border-bottom-color:rgba(247,245,240,.10);
}

/* ─────────────────────────────── ISLAND CTA (button in button) */
.nav__cta.nav__cta
/* ─────────────────────────────── DOUBLE BEZEL (tray + plate) */
.bezel{
  background:var(--shell); border:1px solid var(--bezel-hair);
  border-radius:30px; padding:9px;
}
.bezel--tight{padding:7px; border-radius:22px}
.bezel__core{
  background:#fff; border-radius:21px; padding:clamp(24px,3.4vw,44px);
  box-shadow:inset 0 1px 1px rgba(255,255,255,.9), 0 24px 60px -34px rgba(10,10,10,.34);
}
.bezel--tight .bezel__core{padding:0; border-radius:15px; overflow:hidden}

/* ─────────────────────────────── THE CHECK band */
.check{padding-block:clamp(88px,11vw,168px)}
.check__hooks{
  display:flex; flex-wrap:wrap; justify-content:center; gap:12px 28px;
  max-width:980px; margin:clamp(28px,4vw,44px) auto 0;
  padding-inline:clamp(22px,6vw,120px);
  list-style:none;
}
.check__hooks li{
  position:relative; padding-left:20px; font-size:13.5px;
  color:rgba(10,10,10,.62); letter-spacing:.01em;
}
.check__hooks li::before{
  content:""; position:absolute; left:0; top:.5em; width:9px; height:9px;
  border-radius:50%; background:var(--gold);
  transform:translateY(-50%);
}
@media (max-width:640px){
  .check__hooks{ gap:10px 20px; justify-content:flex-start }
  .check__hooks li{ font-size:13px }
}
.check__head{max-width:1400px; margin:0 auto clamp(38px,4.4vw,64px); padding-inline:clamp(22px,6vw,120px)}
.check .bezel{max-width:980px; margin-inline:auto; margin-left:clamp(22px,6vw,120px); margin-right:clamp(22px,6vw,120px)}
@media (min-width:1220px){ .check .bezel{margin-inline:auto} }

/* Two lines at every width (2026-09-06), centred against each other. A
   999px radius belongs to a single-line pill; on two lines it bows the
   sides, so the corner softens to a radius that holds its edges. */
.eyebrow--pill{
  display:inline-block; padding:9px 17px; border-radius:14px;
  background:rgba(247,245,240,.10); border:1px solid rgba(247,245,240,.18);
  color:rgba(247,245,240,.82); backdrop-filter:blur(8px);
  font-size:10.5px; letter-spacing:.22em;
  text-align:center; line-height:1.55;
}


@media (prefers-reduced-motion:reduce){
      .hero__video{transition:none}
}

@media (max-width:900px){
  .hero{align-items:flex-end; padding-bottom:76px}
  .hero__h1{max-width:none}
  .bezel{border-radius:24px; padding:7px}
  .bezel__core{border-radius:17px}
  .check .bezel{margin-inline:18px}
}

/* The wordmark inherited the dark ink colour and vanished against the film.
   Raised specificity rather than adding another !important. */
.nav.nav--over .nav__mark{color:#F7F5F0}
.nav.nav--over .nav__links a{color:rgba(247,245,240,.82)}
@media (hover:hover) and (pointer:fine){
  .nav.nav--over .nav__links a:hover{color:var(--gold)}
}

/* Serif display faces need more air than a grotesk at hero scale. The shared
   tracking (-.022em) was set for Cabinet Grotesk and pulls Newsreader's
   letterforms into each other at 120px. Loosened for the serif options only,
   and the optical size axis is set so the face uses its display cut rather
   than the text cut. */

/* "Chosen" set in italic. Only the two families that ship a REAL italic get
   it - Newsreader 700i and Instrument Serif 400i. The grotesk options have no
   italic cut, and letting the browser synthesise one produces a mechanical
   slant that looks broken at 120px, so they stay upright.
   The full stop stays roman, which is the convention: punctuation following
   an italic word belongs to the sentence, not to the word. */


/* ═══════════════════════════════════════════════════════════════════════
   HERO ENTRANCE  (2026-09-01)
   Mask wipes, not fades. A word rising out of nothing reads as deliberate;
   a word fading in reads as a page still loading. One easing curve for the
   whole sequence so it moves like one hand wrote it.
   ══════════════════════════════════════════════════════════════════════ */

/* Smaller. The old clamp topped out at 124px, which swallowed the frame and
   left no room for the city behind it. */
.hero__h1{
  font-size:clamp(49px,7.47vw,112px);
  line-height:1.02;
  max-width:18ch;
}

.hero__h1 .ln{
  display:block;
  overflow:hidden;
  /* Room for descenders and for the italic's overhang, or the mask clips them. */
  padding:.04em .12em .1em 0;
  margin-left:-.02em;
  position:relative;
}
.hero__h1 .ln--gold{color:var(--gold)}
.hero__h1 .w{
  display:inline-block;
  /* FIX (2026-09-06, "it still stutters"): the rotation was the stutter.
     translate on its own is handled by the compositor with no repaint;
     add a rotation and every frame has to re-rasterise the glyphs at a
     new angle, which at hero size is expensive enough to drop frames on
     exactly the beat the eye is watching. translate3d and an explicit
     opacity hint keep each word on its own layer for the whole move. */
    transform:translate3d(0,118%,0) scale(.965);
  transform-origin:0 100%;
  opacity:0;
  will-change:transform,opacity;
  backface-visibility:hidden;
}
/* The bar of light that crosses each line just ahead of its words. Absolute,
   so it never touches layout; clipped by .ln's own overflow, so it simply
   leaves at the right edge rather than needing to be shrunk back. Bright at
   the leading edge and falling away behind it, which is what makes it read
   as a pass of light rather than a rectangle sliding by. */
.hero__h1 .ln__sweep{
  position:absolute; left:0; right:0; bottom:.14em; height:1.5px;
  background:linear-gradient(90deg,
    rgba(242,201,76,0) 0%,
    rgba(242,201,76,.22) 42%,
    rgba(242,201,76,.75) 84%,
    var(--gold) 100%);
  transform:translateX(0) scaleX(0);
  transform-origin:0 50%;
  will-change:transform;
  pointer-events:none;
}

/* The gold hairline that closes the slogan. */
.hero__h1 .rule{
  position:absolute; left:0; bottom:.16em; height:2px; width:min(38%,240px);
  background:linear-gradient(90deg,var(--gold),rgba(242,201,76,0));
  transform:scaleX(0); transform-origin:left center; opacity:0;
}

/* A curtain over the film, so the city is revealed rather than just present. */
.hero__veil{
  position:absolute; inset:0; z-index:1; background:#070707; pointer-events:none;
}


.hero__edge{opacity:0; transform:translateY(-18px)}

/* ── the sequence ─────────────────────────────────────────────────────── */
/* FIX (2026-09-02, review-animations bar): reported as "stuttering, not
   smooth". Measured, not assumed - a real-frame trace during this exact
   sequence showed genuine dropped frames (11 of 220 over 20ms), but the
   bigger issue was structural: .hero__inner was SCALING (1.03 -> 1) for
   2.4s while its own child .w words were independently translating and
   rotating on their own staggered timeline, almost fully overlapping in
   time. Two transform animations compounding on the same element tree is
   the exact defect this project's own history already diagnosed and fixed
   once, on the local-search video (commit 4e0a140: "a video cannot carry a
   second movement on top of its own without reading as warping" - a block
   of already-animating text has the identical problem). heroSettle no
   longer touches transform at all, only opacity - the words now carry all
   of the real motion, uncontested.
   Retimed for "a bit more slow and very smooth" per request: durations up
   from 1.9s to 2.2s, and the stagger rhythm evened out - a consistent .20s
   between words in the same line, .30-.35s on the breath between lines
   (the old rhythm jumped .18/.26/.18/.32/.28, an uneven cascade that reads
   as a stumble even without dropping a single frame). */
@keyframes heroWord{
  0%{ transform:translate3d(0,118%,0) scale(.965); opacity:0 }
  /* Opacity is finished early and deliberately. The line's own mask is what
     performs the reveal; a fade running the full length of the move on top
     of it just makes the letters look thin while they travel. */
  28%{ opacity:1 }
  100%{ transform:translate3d(0,0,0) scale(1); opacity:1 }
}
/* Grows from the left, then leaves to the right. Two phases and one fixed
   origin, which is the whole reason this is cheap: no animated
   transform-origin, no clip-path, nothing that would pull it back onto the
   main thread. */
@keyframes heroSweep{
  0%{ transform:translateX(0) scaleX(0) }
  38%{ transform:translateX(0) scaleX(1) }
  100%{ transform:translateX(103%) scaleX(1) }
}
@keyframes heroVeil{to{opacity:0}}
@keyframes heroRule{to{transform:scaleX(1);opacity:1}}
@keyframes heroRise{from{opacity:0;transform:translateY(22px)}to{opacity:1;transform:none}}
@keyframes heroEdge{to{opacity:1;transform:none}}

/* SLOWED BY HALF (2026-09-06, on request). Every duration and every delay
   below is exactly twice what it was, applied by scaling the block rather
   than by retyping it - so the proportions the sequence was built with are
   untouched: the sweep still leads its own line's words by the same
   fraction, the two words in a line still sit the same distance apart
   relative to everything else, and the breath between lines is unchanged in
   shape. Retyping thirty-one numbers by hand is precisely how a cascade
   picks up the uneven stumble this sequence was rebuilt to get rid of.
   Stagger inside a line is now 140ms and the whole entrance settles at
   6.9s. Both are past the `animate` skill's bands, knowingly and on an
   explicit instruction, which the skill's own precedence note allows. */
.hero.is-lit .hero__veil{animation:heroVeil 3.2s var(--ease-fluid) .2s forwards}
.hero.is-lit .eyebrow--pill{animation:heroRise 2.2s var(--ease-fluid) .36s both}

/* THE SEQUENCE (rebuilt 2026-09-06 to the `animate` skill, then slowed by
   half on request - the numbers below are the doubled ones).
   Per line: the bar of light is released first, the two words follow 140ms
   apart just behind its leading edge, and the next line begins 640ms later.
   The shape is what was designed and is unchanged; only the clock moved.
   Word travel is 2.1s, and because the stagger is far shorter than the
   travel the two words in a line still swell together as one line rather
   than arriving as two separate events - which was the whole point of the
   rebuild, and is what the old 240ms stagger destroyed. */
.hero.is-lit .ln:nth-child(1) .ln__sweep{animation:heroSweep 2.3s var(--ease-fluid) .6s both}
.hero.is-lit .ln:nth-child(1) .w:nth-child(1){animation:heroWord 2.1s var(--ease-fluid) .92s forwards}
.hero.is-lit .ln:nth-child(1) .w:nth-child(2){animation:heroWord 2.1s var(--ease-fluid) 1.06s forwards}

.hero.is-lit .ln:nth-child(2) .ln__sweep{animation:heroSweep 2.3s var(--ease-fluid) 1.24s both}
.hero.is-lit .ln:nth-child(2) .w:nth-child(1){animation:heroWord 2.1s var(--ease-fluid) 1.56s forwards}
.hero.is-lit .ln:nth-child(2) .w:nth-child(2){animation:heroWord 2.1s var(--ease-fluid) 1.7s forwards}

.hero.is-lit .ln:nth-child(3) .ln__sweep{animation:heroSweep 2.3s var(--ease-fluid) 1.88s both}
.hero.is-lit .ln:nth-child(3) .w:nth-child(1){animation:heroWord 2.1s var(--ease-fluid) 2.2s forwards}
.hero.is-lit .ln:nth-child(3) .w:nth-child(2){animation:heroWord 2.3s var(--ease-fluid) 2.34s forwards}

/* The one stroke that stays, under the word the whole slogan is aiming at. */
.hero.is-lit .rule{animation:heroRule 2.2s var(--ease-fluid) 3.44s forwards}

.hero.is-lit .hero__lede{animation:heroRise 2.2s var(--ease-fluid) 3.9s both}
.hero__inner .cta{opacity:0}
.hero.is-lit .hero__inner .cta{animation:heroRise 2s var(--ease-fluid) 4.3s both}
.hero.is-lit .hero.is-lit .hero__edge{animation:heroEdge 2.2s var(--ease-fluid) 4.7s forwards}

/* And anyone without JavaScript gets the same (2026-09-17). Identical bug to
   the reduced-motion one fixed below on 2026-09-06, in a second place nobody
   looks: .is-lit is added by JavaScript, so with scripting off the hero sat
   at its opening frame forever and the only call to action on the homepage
   was invisible. Found by tools/check-no-javascript.mjs, not by eye - the
   same way the first one was found. */
html:not(.js) .hero__h1 .w{transform:none;opacity:1}
html:not(.js) .hero__h1 .ln__sweep{display:none}
html:not(.js) .hero__inner .cta{opacity:1}
html:not(.js) .hero__h1 .rule{transform:scaleX(1);opacity:1}
html:not(.js) .hero__inner{transform:none;opacity:1}
html:not(.js) .hero__veil{opacity:0}
html:not(.js) .hero__edge{opacity:1;transform:none}
html:not(.js) .hero__lede{opacity:1;transform:none}

/* Anyone who asked for less motion gets the finished state immediately. */
@media (prefers-reduced-motion:reduce){
  .hero__h1 .w{transform:none;opacity:1}
  .hero__h1 .ln__sweep{display:none}
  /* FIX (2026-09-06): the CTA starts at opacity 0 and is brought in by its
     own animation, and this block switches every hero animation off - so
     anyone who asks for reduced motion was being handed a hero with no
     visible button at all. Caught by an automated check, not by eye. */
  .hero__inner .cta{opacity:1}
  .hero__h1 .rule{transform:scaleX(1);opacity:1}
  .hero__inner{transform:none;opacity:1}
  .hero__veil{opacity:0}
  .hero__edge{opacity:1;transform:none}
  .hero.is-lit *{animation:none !important}
}

/* The base nav carries backdrop-filter: blur(20px) saturate(160%) at all
   times. Making the background transparent over the film was not enough - the
   filter kept blurring and over-saturating the sky behind it, which drew a
   hard bright band exactly the height of the nav. The filter now only exists
   once the nav actually has a surface to justify it. */
.nav.nav--over:not(.is-tight){
  backdrop-filter:none;
  -webkit-backdrop-filter:none;
}

/* ── Type, settled ──────────────────────────────────────────────────────
   Newsreader for display, Switzer for text. Chosen 2026-09-01 after the five
   pairings were compared on the real page rather than on a specimen sheet. */
:root{--display:'Newsreader',Georgia,serif;--body:'Switzer',ui-sans-serif,system-ui,sans-serif}
h1,h2,h3{font-weight:700;letter-spacing:-.022em}
/* A serif display face needs more air than a grotesk at hero scale, and its
   display optical cut rather than its text cut. */
.hero__h1{letter-spacing:-.012em;font-variation-settings:"opsz" 44;line-height:.98}
/* "Chosen" uses the genuine 700 italic, never a synthesised slant. */
.hero__h1 em i{font-style:italic;margin-right:.06em}

/* ── Hero block, tidied ───────────────────────────────────────────────────
   Measured before changing anything: the four elements ended at x=413, 538,
   673 and 288. The lede overhung the headline by 135px, so the block had no
   right edge at all - it just straggled.

   Everything now shares one measure. The lede carries its own line break
   (same as the live site) so the two lines balance rather than wrapping
   wherever the container happens to end, and the CTA sits on the same
   baseline grid as the rest. */
/* FIX (2026-09-03): reported as squashed on both breakpoints, and it was -
   .hero__inner is plain block layout, and adjacent block margins COLLAPSE:
   the eyebrow's 16px bottom margin and the headline's 20px top margin were
   never adding to 36px, they were collapsing to max(16,20)=20px. Flex
   column stops that collapse outright (flex children never collapse
   margins against each other), so the existing numbers below now apply in
   full rather than being silently eaten - and they are also increased on
   top of that, for the airier, more considered rhythm that was asked for. */
.hero__inner{ display:flex; flex-direction:column; align-items:flex-start; }
.hero__inner > *{ margin-left:0; margin-right:0; }
.hero__inner .eyebrow--pill{ margin-bottom:30px; }
.hero__h1{ max-width:13ch; margin-top:0; }
.hero__lede{
  max-width:46ch;
  margin:34px 0 44px;
  text-wrap:balance;
}
/* Superseded. This used to hide every break on a phone, from when the subline
   was two lines everywhere. The phone now wants FOUR specific lines, and this
   rule was silently killing all three breaks because `.hero__lede br` outranks
   `.br-m`. The break behaviour now lives entirely with the .br-m class. */

/* ── The real logo, and a hero that clears it ─────────────────────────────
   Two faults visible on a short desktop viewport: the eyebrow pill was
   sitting level with the wordmark and touching it, and the whole block was
   jammed against the top because the hero anchored its content to the BOTTOM
   of 100dvh - on a short screen a tall headline pushes up into the nav.

   The block is now vertically centred with enough top padding to clear the
   nav at its full height, so it can never collide again regardless of
   viewport height. */
.nav__mark img{
  display:block; height:clamp(54px,5.4vw,92px); width:auto;
  /* The badge has its own dark background baked in, so against dark video it
     reads as a hole rather than a mark. A hairline and a slight radius give it
     an edge, and a soft shadow lifts it off the film. */
  border-radius:6px;
  box-shadow:0 0 0 1px rgba(244,241,234,.14), 0 6px 18px -8px rgba(0,0,0,.7);
  transition:box-shadow .35s var(--m-spring), transform .35s var(--m-spring);
}
/* Lowered 2026-09-03 (was 12px) - reported as sitting too high/cramped
   against the top edge. --navh is measured live off this element (see
   motion.js), so the hero's own clearance below adjusts automatically -
   nothing to retune there by hand. */
.nav{ padding-block:20px; }

.hero{
  align-items:center;
  padding-top:clamp(120px,11vw,168px);
  padding-bottom:clamp(56px,7vw,104px);
}
/* Below this height the hero would rather scroll than crush the type. */
@media (max-height:720px) and (min-width:901px){
  /* Minimum raised 112px -> 140px (2026-09-03): nav grew taller when its own
     padding was increased for the "header sits too high" fix, and this
     short-viewport clamp's floor was no longer comfortably above it -
     measured a real overlap at 1440x700 before catching it here. */
  .hero{ align-items:flex-start; padding-top:clamp(140px,13vh,150px); }
}
@media (max-width:900px){
  .hero{ align-items:flex-end; padding-top:104px; padding-bottom:72px; }
  .nav__mark img{ height:44px; }
}


/* ── Hero type, brought down a size ───────────────────────────────────────
   It was topping out at 84px, which filled the frame edge to edge and left
   the block feeling crammed rather than composed. Smaller type on the same
   measure gives the section air, which is what makes it read as expensive. */
.hero__h1{
  font-size:clamp(44px,5.87vw,88px);
  line-height:1.04;
  max-width:14ch;
}
.hero__lede{
  font-size:clamp(15px,1.15vw,17.5px);
  line-height:1.66;
  /* Measured, not guessed: at 44ch the first line broke early and ended 163px
     short of the headline. 52ch lets 'Custom web design that presents your
     business' sit on one line, which is where the desktop break belongs. */
  max-width:52ch;
  /* Widened 2026-09-03 (was 22px/34px) - reported as squashed, and the flex
     column fix further up stopped these being silently collapsed against
     the headline's margin, so they now read as the real gap they always
     claimed to be, plus some. */
  margin:34px 0 44px;
}
.eyebrow--pill{ font-size:10px; letter-spacing:.2em; }
.hero__inner .cta{
  /* Smaller on the hero specifically, not sitewide - the button had been
     matching the section CTAs' full size, which read as loud next to a
     headline this much larger. A quieter next-step, not a shout, is the
     more premium choice. */
  padding:13px 22px; font-size:14px;
}

/* ── Mobile line breaks ─────────────────────────────────────────────────── */
.br-m{ display:none; }
@media (max-width:640px){
  .br-m{ display:inline; }
  .hero__inner .eyebrow--pill{ margin-bottom:24px; }
  /* All four breaks apply on a phone; the desktop one is simply the second
     of the four, so nothing needs hiding. */
  .hero__lede{ max-width:none; font-size:16.5px; line-height:1.62; margin:26px 0 36px; }
  .hero__h1{ font-size:clamp(28px,8.4vw,40px); max-width:none; }
  .hero__inner .cta{ padding:14px 24px; font-size:14.5px; }
}

/* ── More motion on a phone ───────────────────────────────────────────────
   Touch has no hover, so the whole hover layer of the design is simply absent
   on a phone. Scroll is the only input, so scroll has to carry more.

   Everything below is transform and opacity only, driven by one rAF-batched
   pass, and it all switches off for reduced motion. */
@media (max-width:900px){
  /* Reveals travel further and land slower, because a phone shows one thing
     at a time and can afford the drama. */
  /* No blur on a phone. Profiling put every long frame exactly where a
     section enters - #how, #included, #local, #questions - because a dozen
     elements start animating filter:blur() at once and blur is repainted per
     frame rather than composited. Travel and opacity alone are free, so the
     motion gets BIGGER here to compensate for losing the softness. */
  [data-rv]{ filter:none !important; }
  [data-rv="up"]    { transform:translate3d(0,86px,0); }
  [data-rv="left"]  { transform:translate3d(-72px,28px,0) rotate(-1.4deg); }
  [data-rv="right"] { transform:translate3d(72px,28px,0) rotate(1.4deg); }
  [data-rv="tilt-l"]{ transform:translate3d(-54px,64px,0) rotate(-2.8deg); }
  [data-rv="tilt-r"]{ transform:translate3d(54px,64px,0) rotate(2.8deg); }
  [data-rv="scale"] { transform:translate3d(0,64px,0) scale(.88); }
  :root{ --m-dur:1.5s; --m-step:150ms; }

  /* No scroll-driven scale on video. A still photograph can carry a slow
     zoom and look expensive; a video is already moving, so a second movement
     on top of it reads as the picture warping. */
  .local__media, .buildfig{ overflow:hidden; }

  /* Section bands lift very slightly as they arrive, so scrolling feels like
     moving through layers rather than past a list. */
  .band{ transform:translate3d(0,var(--lift,0),0); }
}

/* A thin gold line across the top showing how far through you are. It costs
   one element and gives a phone the sense of a document with a length. */
.progress{
  position:fixed; top:0; left:0; height:2px; width:100%; z-index:70;
  background:var(--gold); transform:scaleX(0); transform-origin:left center;
  pointer-events:none;
}
/* Runs on every viewport now (2026-09-05) - see the standalone progress IIFE in motion.js. */

@media (prefers-reduced-motion:reduce){
  .band{ transform:none !important; }
  .progress{ display:none; }
}


/* ── Hero, centred between the header and the bottom of the section ───────
   It was bottom-anchored on mobile, which pushed everything low. The section
   now reserves exactly the header's height at the top and the same amount at
   the bottom, so the block sits on the true optical centre. */
@media (max-width:900px){
  .hero{
    align-items:center;
    /* Reserve the header at the top only. Matching it at the BOTTOM shrank the
       centring box from the wrong end and pushed the block high, leaving 212px
       of dead space below against 116px above. With bottom padding at zero the
       content centres on the true midpoint between the header and the end of
       the section. */
    padding-top:var(--navh,118px);
    padding-bottom:0;
  }
}

/* ── More motion on a phone ───────────────────────────────────────────────
   Touch has no hover, so these carry the weight instead. All transform and
   opacity, all switched off under reduced motion. */
@media (max-width:900px){
  /* Each heading line arrives on its own beat rather than as a block. */
  [data-rv="mask"] .mk > span{ transition-duration:1.6s; }

  /* Cards and rows tilt in from alternating sides with a little more travel,
     because a phone shows one at a time and can carry the drama. */
  .step{ transform-origin:left center; }
  .incl li{ transform-origin:left center; }

  /* A gold edge wipes down the left of each dark band as it enters. */
  .band--ink{ position:relative; }
  .band--ink::before{
    content:""; position:absolute; left:0; top:0; bottom:0; width:2px;
    background:linear-gradient(to bottom, transparent, var(--gold), transparent);
    transform:scaleY(var(--edge,0)); transform-origin:top center;
    opacity:.55; pointer-events:none;
  }

  /* The hero settles as you scroll away from it rather than just leaving.
     FIX (2026-09-06): this used to say `transform, opacity`. The opacity
     half was left behind when the scroll fade was removed for costing 8
     long frames, and on its own it was doing real damage: will-change on
     opacity forces a group opacity layer permanently, and a group layer
     flattens every descendant into one texture - which meant the six
     hero words had no compositor layers of their own on a phone, and
     their reveal was being drawn on the main thread. Transform only. */
  .hero__inner{ will-change:transform; }
}

@media (prefers-reduced-motion:reduce){
  .band--ink::before{ display:none; }
}

/* ── Hero block sits a little above centre ────────────────────────────────
   Dead centre measured correctly but read slightly low, because the eye reads
   the optical centre as higher than the mathematical one. Padding at the foot
   lifts the block without breaking the centring logic. */
@media (max-width:900px){
  .hero{ padding-bottom:clamp(70px,13vh,120px); }
}
@media (min-width:901px){
  .hero{ padding-bottom:clamp(96px,11vh,150px); }
}


/* ── Footer, in the live site's shape ────────────────────────────────────
   A centred column: mark, business block, links, contact, then a ruled legal
   row. Same order and same restraint as the live one. */
.foot{
  background:#080808; border-top:1px solid rgba(244,241,234,.10);
  padding:clamp(56px,7vw,88px) clamp(22px,6vw,120px) clamp(40px,5vw,60px);
  display:flex; flex-direction:column; align-items:center; gap:26px;
  text-align:center;
}
.foot__mark img{
  display:block; height:clamp(80px,8vw,112px); width:auto; border-radius:8px;
  transition:transform .5s var(--m-spring);
}
.foot__mark img{ transition:transform .5s var(--m-spring), box-shadow .4s var(--m-spring); }
/* A golden edge when you hover the logo, anywhere on the site (2026-10-10). */
@media (hover:hover) and (pointer:fine){
  .foot__mark:hover img{ transform:scale(1.04); box-shadow:0 0 0 2px var(--gold), 0 0 18px -2px rgba(242,201,76,.5); }
  .nav__mark:hover img{ transform:scale(1.06); box-shadow:0 0 0 2px var(--gold), 0 0 16px -2px rgba(242,201,76,.55), 0 6px 18px -8px rgba(0,0,0,.7); }
}

.foot__address{
  font-style:normal; color:rgba(244,241,234,.62);
  font-size:13.5px; line-height:1.75;
}
.foot__address b{ display:block; color:#F4F1EA; font-weight:600; margin-bottom:2px; }

.foot__nav{ display:flex; flex-wrap:wrap; justify-content:center; gap:10px 26px; }
.foot__nav a{
  color:rgba(244,241,234,.72); font-size:14px;
  transition:color .4s var(--m-spring);
}
@media (hover:hover) and (pointer:fine){ .foot__nav a:hover{ color:var(--gold); } }

.foot__contact{
  display:flex; flex-wrap:wrap; justify-content:center; align-items:center; gap:12px;
  font-size:13.5px; color:rgba(244,241,234,.5);
}
.foot__contact a{ color:rgba(244,241,234,.72); transition:color .4s var(--m-spring); }
@media (hover:hover) and (pointer:fine){ .foot__contact a:hover{ color:var(--gold); } }

.foot__legal{
  width:100%; margin-top:8px; padding-top:22px;
  border-top:1px solid rgba(244,241,234,.08);
}
/* Footer legal text and links: alpha raised 2026-09-17 to clear WCAG AA.
   axe measured the copyright line at 2.6:1 and the Privacy/Terms links at
   3.98:1 against the 4.5:1 minimum for text this size. .55 measures 5.66:1
   on this background, which keeps the line clearly secondary without
   failing. We report contrast on customers' sites, so failing it here is
   the tool failing its own test. */
.foot__legal p{ margin:0; color:rgba(244,241,234,.55); font-size:12px; line-height:1.7; }

/* ── Contain the sideways reveals ─────────────────────────────────────────
   The directional entrances translate elements up to 78px left or right.
   Transforms do not affect layout, but they DO extend the scrollable area, so
   the page grew to 448px inside a 390px viewport and everything - including
   the header CTA - was pushed off the right edge. That is what made the
   button look squashed into the corner: the page was wider than the screen.

   Clipped on the x axis only. overflow-y must stay visible or position:sticky
   stops working. */
/* overflow-y explicit on both (2026-09-06) - the comment above already
   warned "overflow-y must stay visible or position:sticky stops working"
   but only overflow-x was ever actually declared. Per spec, declaring just
   one axis computes the OTHER axis to auto, not visible - so body was
   quietly its own scroll container the whole time, and any position:sticky
   element anywhere on the page had zero working "stick" range. Found while
   building the journey section's per-phase sticky labels: the sticky top
   moved in exact 1:1 lockstep with scroll instead of holding, which is
   the signature of this exact bug. */
/* overflow-x:clip, not hidden (2026-09-06) - CSS cannot actually pair
   overflow-x:hidden with overflow-y:visible; per spec, whichever axis is
   "visible" gets silently promoted to "auto" the moment its sibling axis
   is anything other than visible, no matter how explicitly it's declared.
   That auto-promotion is what was quietly turning body into its own
   independent scroll container, which broke position:sticky everywhere
   on the page (found while building the journey section's per-phase
   sticky labels: the sticky top moved in exact 1:1 lockstep with scroll
   instead of holding). overflow:clip is exempt from that promotion rule -
   it clips the x axis without forcing y into a scroll container - which
   is what the very first body{} rule already used (overflow-x:clip); this
   later rule was overriding it back to hidden and reintroducing the bug. */
html{ overflow-x:clip; }
body{ overflow-x:clip; position:relative; width:100%; }


/* ══════════════════════════════════════════ SERVICE PAGES (2026-09-03)
   Interior pages reuse .band/.rail/.eyebrow/.incl/.cta as-is - no parallel
   design system. Two additions only: inline links inside body copy (the
   homepage's a{color:inherit} makes a cross-link invisible without this),
   and the services hub's card grid, built on the same hairline-grid pattern
   as .incl rather than inventing a new one. */

.lede a, .step p a, .faq__a a, .svc__item p a{
  color:inherit; border-bottom:1px solid var(--accent); padding-bottom:1px;
  transition:opacity .2s var(--ease);
}
.lede a:hover, .step p a:hover, .faq__a a:hover, .svc__item p a:hover{ opacity:.72; }

/* ══ FURTHER READING (2026-09-19) ════════════════════════════════════════════
   Tomislav: these are on the bottom of a few pages now and they need to be more
   obvious that they are links, in our style.

   He was right, and the cause is in this file. The block was a plain <p>, and a
   plain <p> is not .lede, .step p, .faq__a or .svc__item p - the four selectors
   above are the only places an inline link gets a colour. Everywhere else
   a{color:inherit;text-decoration:none} applies, so the link was rendered as
   ordinary body text: nothing to see, nothing to click, no reason to try.

   It reads as a row now rather than a sentence with something hidden in it.
   Hairline rules top and bottom like .incl and .svc elsewhere on the site, the
   title carrying a gold underline, a gold arrow sitting at the end of the row,
   and the whole row clickable. Obvious without being loud: the gold and the
   arrow do the work that a blue underline does on an ordinary page.

   The row is 44px of tap target at its shortest, on the smallest phone. */
.fread{ list-style:none; margin:16px 0 0; padding:0; max-width:54ch; border-top:1px solid var(--hair); }
.fread li{ border-bottom:1px solid var(--hair); }
.fread a{
  display:grid; grid-template-columns:1fr auto; align-items:center; gap:16px;
  padding:15px 2px; min-height:44px;
  color:inherit; text-decoration:none;
}
.fread__t{
  display:inline; font-family:var(--display); font-size:clamp(17px,1.7vw,20px);
  line-height:1.3; letter-spacing:-.01em;
  border-bottom:1px solid var(--accent); padding-bottom:2px;
  transition:color .2s var(--ease), border-color .2s var(--ease);
}
.fread__d{
  display:block; margin-top:7px; font-size:14.5px; line-height:1.5;
  color:var(--muted); max-width:46ch;
}
.fread__go{
  flex:none; color:var(--accent); opacity:.85;
  transition:transform .22s var(--ease), opacity .2s var(--ease);
}
@media (hover:hover) and (pointer:fine){
  .fread a:hover .fread__t{ color:var(--accent); }
  .fread a:hover .fread__go{ transform:translateX(5px); opacity:1; }
}
.fread a:focus-visible{ outline:2px solid var(--accent); outline-offset:3px; }
@media (prefers-reduced-motion:reduce){
  .fread__t,.fread__go{ transition:none; }
}

/* Plain (non-hero) page intro. The fixed .nav already carries its own
   background when it lacks the --over modifier (see .nav base rule), so this
   only needs the band's own top padding to clear it - confirmed clear at
   320-2560px, no extra offset required. */
.pagehead{ padding-top:calc(clamp(78px,10vw,140px) + 58px); }

/* THE DIVIDERS ARE BORDERS, NOT A 1px GAP (2026-10-01). The lines between cards used to be the 1px grid gap showing the container's
   tint. The columns are fractional widths, so that gap lands between device pixels and the browser paints it as one pixel, two pixels,
   two faint ones, or nothing, depending on the screen width and pixel density. Tomislav saw the line between Website Redesign and SEO
   Services missing. A border is never rounded below one device pixel, so every divider draws on every screen. */
.svc{
  list-style:none; margin:40px 0 0; padding:0;
  display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:0;
  background:transparent; border-top:1px solid var(--hair); border-right:1px solid var(--hair);
}
@media (max-width:1080px){ .svc{ grid-template-columns:repeat(2,minmax(0,1fr)); } }
/* Three cards in two columns leave the fourth cell empty. It was always tinted like the lines, which closed the frame; the tint now belongs to
   the cell itself, so it cannot leak between cards. Only when the grid holds exactly three cards, and only while it is two columns wide. */
@media (min-width:721px) and (max-width:1080px){ .svc:has(.svc__item:nth-child(3):last-child)::after{ content:""; background:var(--hair); border-left:1px solid var(--hair); border-bottom:1px solid var(--hair); } }
.svc__item{
  background:var(--ink); padding:clamp(28px,3.2vw,40px);
  display:flex; flex-direction:column; gap:14px;
  border-left:1px solid var(--hair); border-bottom:1px solid var(--hair);
}
.band--paper .svc__item{ background:var(--paper); }
.svc__item h3{ margin:0; }
.svc__item p{ margin:0; color:var(--muted); font-size:15.5px; line-height:1.62; }
.svc__item .svc__link{
  margin-top:2px; font-size:13.5px; font-weight:600; color:var(--accent);
  display:inline-flex; align-items:center; gap:6px; align-self:flex-start;
  border-bottom:1px solid transparent; transition:border-color .2s var(--ease);
}
.svc__item .svc__link:hover{ border-color:var(--accent); }
.svc__item .svc__link svg{ transition:transform .3s var(--ease); }
.svc__item .svc__link:hover svg{ transform:translateX(3px); }
@media (max-width:720px){ .svc{ grid-template-columns:1fr; } }

/* Two-path comparison (website care: managed vs independent). Same hairline-
   grid language as .svc, just two cells instead of four. */
.paths{
  list-style:none; margin:36px 0 0; padding:0;
  display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:1px;
  background:var(--hair); border:1px solid var(--hair);
}
.paths li{
  background:var(--ink); padding:clamp(26px,3vw,34px);
}
.band--paper .paths li{ background:var(--paper); }
.paths h3{ margin:0 0 10px; }
.paths p{ margin:0; color:var(--muted); font-size:15.5px; line-height:1.62; }
@media (max-width:720px){ .paths{ grid-template-columns:1fr; } }

/* Interior-page nav fix (2026-09-03). The old "white hero" nav treatment
   above (.nav:not(.is-tight), written for a .hero--paper design this site no
   longer uses) is unscoped, so it silently applied its light-bar/dark-text
   styling to any .nav lacking .is-tight - including these new pages' plain,
   always-solid header, which has no .hero--paper ancestor to make that
   correct. Rather than touch the legacy rule (still load-bearing if that
   white-hero direction ever comes back), this beats it outright on
   specificity for pages that opt in with .nav--solid, restoring the intended
   dark-glass bar with light type, while still respecting .is-tight's own
   scroll-thickening. */
.nav.nav--solid:not(.is-tight){
  background:rgba(10,10,10,.82);
  backdrop-filter:blur(20px) saturate(160%);
  -webkit-backdrop-filter:blur(20px) saturate(160%);
  border-bottom-color:rgba(244,241,234,.08);
}
.nav.nav--solid:not(.is-tight) .nav__mark{color:var(--bone)}
.nav.nav--solid:not(.is-tight) .nav__mark i{color:var(--gold)}
.nav.nav--solid:not(.is-tight) .nav__links a{color:rgba(244,241,234,.6)}
.nav.nav--solid:not(.is-tight) .nav__links a:hover{color:var(--gold)}

/* Service card polish (2026-09-03, second pass). The whole tile is now the
   hit target (a stretched link over the card), not just the small "Read
   more" line - a text link buried in a paragraph is a worse target than the
   card design implies. Hover state (desktop pointer only, matching every
   other hover treatment on this site) lifts the tile a little and warms the
   surface, driven by the same --ease token everything else uses. .paths
   deliberately does NOT get this: those two cells are informational, not
   links, and a hover glow on something you can't click would be a lie the
   interface tells. */
.svc__item{
  position:relative;
  transition:transform .3s var(--ease), background-color .3s var(--ease);
}
.svc__item .svc__link::after{ content:""; position:absolute; inset:0; }
@media (hover:hover) and (pointer:fine){
  .svc__item:hover{ transform:translateY(-3px); background:rgba(242,201,76,.07); }
  .band--paper .svc__item:hover{ background:rgba(122,93,0,.05); }
  .svc__item:hover .svc__link{ border-color:var(--accent); }
  .svc__item:hover .svc__link svg{ transform:translateX(3px); }
}

/* ══════════════════════════════ PAGEHEAD, second pass (2026-09-03)
   First pass reused the section-intro pattern (small eyebrow, body-size h1,
   nothing in the right column) verbatim from the middle of the homepage -
   correct components, but next to the real hero it read thin: no glow, no
   edge label, a headline sized like a section title rather than an opening
   line. This gives the opener some of the hero's own weight - a soft gold
   vignette (decorative gradient, not a photo - the brand bans stock imagery
   but not light), a bigger headline, and the same vertical edge mark the
   hero itself carries - without pretending it is the video hero, which it
   structurally cannot be (no footage exists for four different services). */
.pagehead{ position:relative; overflow:hidden; }
.pagehead::before{
  content:""; position:absolute; inset:0; z-index:0; pointer-events:none;
  background:radial-gradient(ellipse 880px 520px at 12% -10%, rgba(242,201,76,.12), transparent 62%);
}
.pagehead .rail{ position:relative; z-index:1; }
.pagehead h1{ font-size:clamp(48px,7.4vw,104px); line-height:.98; }
.pagehead__edge{
  position:absolute; left:26px; bottom:56px; z-index:1;
  writing-mode:vertical-rl; transform:rotate(180deg);
  font-size:11.5px; letter-spacing:.34em; text-transform:uppercase;
  color:rgba(247,245,240,.34);
}
@media (max-width:1180px){ .pagehead__edge{ display:none; } }

/* Service card redesign (2026-09-04, studied from platocreative.co.nz's own
   services grid and pushed further, not copied outright - theirs is a plain
   agency column list; this keeps that column's scannable rhythm (title,
   one-line promise, three concrete specifics, one link out) but renders it
   in this site's own language: hairline grid, gold accent, serif title,
   arrow-marked bullets instead of their plain text list. Paragraph-per-card
   was too slow to scan next to this - a visitor should get the shape of
   all four services in the time it takes to read one paragraph, not four. */
.svc__item h3{
  font-size:clamp(21px,2.1vw,27px); line-height:1.04; margin:0 0 12px;
}
.svc__item .svc__line{
  margin:0; font-size:15px; color:var(--fg); font-weight:600; line-height:1.4;
}
.svc__bullets{
  list-style:none; margin:14px 0 0; padding:0;
  display:flex; flex-direction:column; gap:7px;
}
.svc__bullets li{
  font-size:13.5px; color:var(--muted); line-height:1.4;
  display:flex; align-items:baseline; gap:8px;
}
.svc__bullets li::before{
  content:"\2192"; color:var(--accent); font-size:11px; flex:none;
  transform:translateY(-1px);
}
.svc__item .svc__link{ margin-top:18px; }

/* Hero lede width, locked to the real headline (2026-09-05). Three earlier
   passes each redeclared .hero__lede's max-width as an unrelated static ch
   value (52ch, then 46ch, then 52ch again for desktop; max-width:none on
   mobile), none of which track .hero__h1's own responsive font-size - measured
   live, the lede rendered 66-125px wider than the headline above it at every
   breakpoint, on both desktop and mobile. --hero-textw is the headline's real,
   current rendered width (see motion.js), so this simply cannot drift out of
   sync again. Appended last on purpose: same specificity as every earlier
   .hero__lede rule (including the mobile media-query one), so it wins on
   source order without needing !important or touching the older rules. */
.hero__lede{ max-width:var(--hero-textw, 44ch); }

/* ══════════════════════════════════════ FOOTER, premium pass (2026-09-05)
   Brought up to parity with the live site (phone, Facebook, Privacy/Terms -
   real links to the live site's own legal pages, since this concept has no
   pages of its own for them and duplicating real legal text here would be
   the kind of drift CLAUDE.md already warns about) and given the same
   quiet-premium treatment as the rest of the rebuild: a hairline gold rule
   instead of a hard edge, icons that match the site's own stroke language
   (2px round-cap, the same family as every arrow and check already on the
   page) rather than a generic icon font, and a small lift on hover instead
   of a flat colour swap. */
.foot{ position:relative; }
.foot__rule{
  position:absolute; top:0; left:50%; translate:-50% 0;
  width:min(220px,60%); height:1px;
  background:linear-gradient(90deg, transparent, rgba(242,201,76,.55), transparent);
}
.foot__contact{ margin-top:2px; }
.foot__contactLink{
  display:inline-flex; align-items:center; gap:8px;
  color:rgba(244,241,234,.72); transition:color .3s var(--m-spring);
}
.foot__contactLink svg{
  color:var(--gold); flex:none;
  transition:transform .3s var(--m-spring), color .3s var(--m-spring);
}
@media (hover:hover) and (pointer:fine){
  .foot__contactLink:hover{ color:var(--bone); }
  .foot__contactLink:hover svg{ transform:scale(1.14) translateY(-1px); }
}
.foot__legalLinks{ margin:6px 0 0; }
/* The footage credit. Quieter than the legal links beside it, because it is an
   acknowledgement rather than something anyone came here to click. */
.foot__credit{
  margin:10px 0 0;
  color:rgba(244,241,234,.42);
  font-size:12px;
  line-height:1.7;
  /* Held to a readable measure, and centred with the legal lines above it rather
     than left where the max-width would otherwise park it. */
  max-width:46ch;
  margin-inline:auto;
  text-align:center;
}
.foot__credit a{ color:rgba(244,241,234,.62); border-bottom:1px solid rgba(242,201,76,.34); padding-bottom:1px; }
@media (hover:hover) and (pointer:fine){ .foot__credit a:hover{ color:var(--gold); border-bottom-color:var(--gold); } }
.foot__legalLinks a{
  color:rgba(244,241,234,.55); transition:color .3s var(--m-spring);
}
@media (hover:hover) and (pointer:fine){
  .foot__legalLinks a:hover{ color:var(--gold); }
}
.foot__legalLinks + p{ margin-top:6px; }
/* The credit is a .foot__legal p, so `.foot__legal p{margin:0}` (higher
   specificity) was cancelling its margin-inline:auto and parking it at the
   bottom-left on desktop instead of centred under Privacy/Terms. This wins on
   specificity: centred, and with room above it so it is not squashed against
   the links. A bit more room on phones. (2026-10-10) */
.foot__legal .foot__credit{ margin:18px auto 0; }
@media (max-width:640px){ .foot__legal .foot__credit{ margin-top:26px; } }

/* The mark deserves the same lift the rest of the site gives an image on
   hover, not just a flat scale - a touch of shadow so it reads as lifting
   off the page rather than just growing. */
@media (hover:hover) and (pointer:fine){
  .foot__mark:hover img{
    transform:scale(1.05) translateY(-2px);
    filter:drop-shadow(0 8px 18px rgba(242,201,76,.18));
  }
}

/* ══════════════════════════ INTERACTIVE LAYER, second pass (2026-09-05)
   Three additions, each reusing a pattern already proven elsewhere on the
   page rather than inventing new motion language - "another layer", not a
   different site. Restrained on purpose: the custom cursor was flagged as
   "too much" earlier and removed outright, so nothing here runs
   continuously or draws attention on its own. Every effect is dormant
   until a visitor's pointer or scroll actually triggers it, desktop
   pointer devices only where relevant, and all three collapse to nothing
   under prefers-reduced-motion. */

/* 1. Service-card tilt. Exactly the audit card's own pointer-tilt
   (rotateX/rotateY driven by --brx/--bry, set in app.js, transform only),
   extended to the services grid and merged with the card's existing hover
   lift/tint so the two motions read as one gesture instead of fighting
   each other for the transform property. */
.svc{ perspective:1400px; }
.svc__item{ transform-style:preserve-3d; transition:background-color .3s var(--ease); }
/* Specificity note: .svc__item carries data-rv="pop" for its scroll-reveal,
   and motion.css's [data-rv].in{transform:none} (attribute+class, loaded
   after this file) would otherwise win the transform property outright once
   the card has landed - exactly the gap the audit card's own tilt element
   never hits, because that one is a plain nested div with no data-rv of its
   own. Three selector components (.svc, .svc__item, [data-rv]) beats
   [data-rv].in's two regardless of load order, so the tilt keeps working
   after the reveal finishes rather than freezing flat the moment it lands. */
.svc .svc__item[data-rv]{
  transform:translateY(0) rotateX(var(--brx,0deg)) rotateY(var(--bry,0deg));
  transition:transform .5s var(--m-ease), background-color .3s var(--ease);
}
.svc .svc__item[data-rv].is-tilting{ transition-duration:.1s; will-change:transform; }
@media (hover:hover) and (pointer:fine){
  .svc .svc__item[data-rv]:hover{
    transform:translateY(-3px) rotateX(var(--brx,0deg)) rotateY(var(--bry,0deg));
    background:rgba(242,201,76,.07);
  }
}
@media (prefers-reduced-motion:reduce){ .svc .svc__item[data-rv]{ transform:none !important; transition:none; } }

/* 2. Magnetic buttons. Primary CTAs only (.cta, not the small text links),
   pull a few pixels toward the pointer within a short radius and spring
   back - the same lerp-based easing already used for the hero video swap
   timing elsewhere in app.js, applied to a translate instead. Off on
   touch, since there is no hover/pointer proximity to react to. */
@media (hover:hover) and (pointer:fine){
  .cta{ transition:transform .12s var(--ease-out), background-color .2s var(--ease-out); }
  .cta.is-magnet{ transition:transform .15s var(--ease-out); }
}

/* 3. Scroll progress, universal. .progress itself already existed for
   mobile only (see the "how far through" comment above); it is now
   created and driven by its own small IIFE in motion.js that runs on
   every viewport, so the mobile-only band-lift/ink-edge/hero-drift IIFE
   below no longer needs to own it. */

/* ══════════════════════════════════ THE JOURNEY (2026-09-06, rebuilt a
   second time as three independent per-phase sticky blocks)
   Tomislav asked for the left side to "expand and follow the right side
   so both sides are visibly expanding at the same time... same exact
   amount of space" - one sticky nav spanning the WHOLE section (the
   previous build) can only ever show discrete state changes inside a
   fixed-height box, never actually grow. The fix is structural, not a
   bigger JS effort: three separate blocks, one per phase, each its own
   CSS grid row. The phase label is position:sticky WITHIN its own block
   only, so it is pinned only for exactly as long as its own block's
   right-hand content takes to scroll past - a block with three steps
   naturally holds its label on screen for three steps' worth of
   scrolling, a block with one step for one step's worth. That IS "the
   same exact amount of space" on both sides; no JS coordinates it because
   none needs to - it falls out of two elements sharing one grid row. */
.journey__head{ max-width:64ch; }
.journey__head .lede{ margin-top:22px; }

.journey__phases{ margin-top:clamp(56px,7vw,88px); }
/* align-items must stay at its default (stretch), NOT "start". A grid item
   with align-items:start shrinks its box to its own content height, which
   quietly caps how far position:sticky can travel inside it - the sticky
   label was unsticking almost immediately because its own box was only as
   tall as itself, not as tall as the row. Stretch makes the sticky column's
   grid cell match the taller content column's height, giving the label
   real room to stay pinned while its own phase's steps scroll past. */
.journey__phaseBlock{
  display:grid; grid-template-columns:minmax(0,3fr) minmax(0,7fr);
  gap:clamp(30px,5vw,80px);
}
.journey__phaseBlock + .journey__phaseBlock{ margin-top:clamp(40px,6vw,72px); }

/* The grid item that stretches to match the row (the taller content
   column) is THIS cell, not the sticky element itself - if the sticky
   element's own box were the one stretched, its box would already be
   exactly as tall as the available scroll room, leaving no slack for it
   to actually travel while "stuck". Keeping .journey__phaseSticky short
   and nested one level inside the stretched cell is what gives it real
   room to hold position while the phase's own steps scroll past. */
.journey__phaseStickyCell{ align-self:stretch; }
.journey__phaseSticky{
  position:sticky; top:calc(var(--navh,80px) + 48px);
}
.journey__phaseStickyNum{
  display:block; font-size:12px; letter-spacing:.16em; color:var(--muted);
  margin-bottom:10px;
}
.journey__phaseStickyName{
  font-family:var(--display); font-weight:700; letter-spacing:-.01em;
  font-size:clamp(30px,3.4vw,44px); line-height:1.05; margin:0;
}
/* The rail under each phase name grows only across that phase's own
   content height (see the per-block scroll IIFE in motion.js) - a short
   fill for the one-step phase, a longer one for the three-step phases,
   in direct proportion, which is the "same exact amount of space" made
   visible rather than just structural. */
.journey__phaseRail{
  position:relative; width:26px; height:2px; margin-top:22px;
  background:var(--hair); border-radius:2px; overflow:hidden;
}
.journey__phaseRailFill{
  position:absolute; top:0; left:0; height:100%; width:100%;
  background:var(--gold); transform-origin:left center; transform:scaleX(0);
  border-radius:2px; will-change:transform;
}

.journey__phaseContent{ display:flex; flex-direction:column; }
/* Hover feedback - "shows which step you are at on every move of the
   mouse". Border stays reserved at all times (transparent by default) so
   the colour change on hover costs no layout shift. This element does
   not carry [data-rv] itself (the reveal happens on the children inside
   .journey__stepInner, staggered), so it never collides with motion.css's
   [data-rv]{transition-property:...} rule. */
.journey__step{
  position:relative; padding:30px 20px 30px 34px;
  border-bottom:1px solid var(--hair); border-left:2px solid transparent;
  transition:border-color .3s var(--ease), background-color .3s var(--ease);
}
.journey__step:first-child{ padding-top:0; }
.journey__step:last-child{ border-bottom:none; padding-bottom:0; }
@media (hover:hover) and (pointer:fine){
  .journey__step:hover{
    border-left-color:var(--accent); background:rgba(242,201,76,.045);
  }
  .band--paper .journey__step:hover{ background:rgba(122,93,0,.035); }
}
.journey__stepNum{
  position:absolute; left:2px; top:32px;
  /* 11px, and it did not scale at any width, which made it the smallest text
     in the section on every screen. 14px is still clearly subordinate to the
     23px step title beside it and is readable without leaning in. The step's
     left padding grew with it, or a wider numeral would have closed the gap
     to the title. */
  font-size:14px; letter-spacing:.04em; color:var(--muted);
  font-variant-numeric:tabular-nums;
}
.journey__step h3{
  display:inline-flex; align-items:center; gap:10px; margin-bottom:14px;
}
.journey__step h3::after{
  content:""; width:7px; height:7px; border:1.5px solid var(--accent);
  border-right:none; border-top:none; transform:rotate(-45deg); flex:none;
}
.journey__fig{ margin:24px 0 0; max-width:340px; }
/* The step-video pair (2026-09-15) needs the FRAME itself to hold its own
   size, because every video inside it is now position:absolute (so any of
   them can be freely created and discarded without ever being the one
   in-flow child a parent's auto-height depended on - see app.js for why).
   Scoped to this figure only: .bezel__core is shared by other components
   that must not gain an aspect-ratio they never asked for. */
.journey__fig .bezel__core{ aspect-ratio:900/1150; }
/* Remotion-rendered step videos (2026-09-06) - same aspect ratio as the
   existing .buildfig__v (900/1150) so all step figures on this page share
   one frame shape regardless of which step they belong to. */
.stepfig__v{ width:100%; height:auto; display:block; aspect-ratio:900/1150;
  /* The video's own paper tone, NOT --ink. It used to be #0A0A0A behind a
     near-white video, so every loop restart and decode gap flashed black
     and every unclipped corner was black rather than invisible. */
  background:#F6F3EC; }
.journey__fig figcaption{
  margin-top:11px; font-size:12.5px; color:var(--muted); letter-spacing:.01em;
}

@media (max-width:900px){
  .journey__phases{ margin-top:clamp(40px,7vw,56px); }
  .journey__phaseBlock{ grid-template-columns:1fr; gap:18px; }
  .journey__phaseSticky{
    position:static; display:flex; align-items:baseline; gap:12px;
    padding-bottom:12px; border-bottom:1px solid var(--hair);
  }
  .journey__phaseStickyNum{ margin:0; order:2; }
  .journey__phaseStickyName{ font-size:22px; order:1; }
  .journey__phaseRail{ display:none; }
  .journey__step{ padding-left:30px; }
  .journey__stepNum{ top:0; }
}

/* ── Short viewports, LAST so it wins ────────────────────────────────────
   Must live at the end of the file: there are six separate .hero /
   .hero__h1 rules above (including two width-based padding-bottom media
   queries near the footer block), and media queries carry no extra
   specificity, so source order is the only thing that decides. Placed
   earlier, this was silently overridden and the ticker stayed below the
   fold on a short window.

   Why it exists: the hero reserves the ticker's row via min-height, but
   min-height cannot stop the hero's own CONTENT from exceeding what is
   left. When it does, the hero grows and pushes the ticker back under the
   fold - the exact bug the reserve was added to fix. On short screens the
   hero's rhythm is therefore scaled to viewport HEIGHT (vh), which is what
   actually constrains it, rather than width. Measured at 1280x600,
   1366x650 and 740x360. */
/* Threshold raised 700px -> 800px (2026-09-06). A 1366x768 laptop is one
   of the most common screens there is, and it was getting full desktop
   spacing on a viewport that cannot carry it: measured with the block
   filling the band edge to edge and 10px of slack in the entire section,
   which leaves nothing for the centring to work with. */
/* Threshold raised 800px -> 880px on 2026-09-17. A 1440x820 window fell in a
   gap: too tall for this block, too tall for the 720px nav rule below it, so
   it got full-size everything and the ticker was cut by 9px. Untightened, the
   hero needs 829px at 1440 and 862px at 1920, so the relief has to start
   before 830, not at 800. Only the MARGINS are involved here; the headline
   size still steps at 800, because tightening the rhythm is invisible and
   shrinking the type is not. */
@media (max-height:880px){
  /* Retuned 2026-09-06 for the four-line subline. Top pad carried 140px of
     nav clearance against a 70px nav, which is the cheapest 30px on the
     screen; the eyebrow's 30px gap is a tall-hero rhythm that a 600px
     viewport cannot afford either. */
  /* Padding is not touched here any more - it is the nav's real measured
     height, and a short screen still has the same nav. Only the type
     tightens. */
  /* Spacing applies at any width - a short screen is a short screen.
     The SIZE does not: this rule was written for short desktop windows,
     and on a 360x640 phone it was beating the phone tier and handing a
     small screen a LARGER headline than a 390px one. Scoped below. */
  .hero__h1{ margin-top:8px; }
  .hero__inner .eyebrow--pill{ margin-bottom:16px; }
  .hero__lede{ margin:10px 0 16px; font-size:14.5px; line-height:1.45; }
  .hero__edge{ display:none; }
}

/* A landscape phone has less height than the hero block has content, so
   everything steps down one more notch rather than pushing the strip off
   the fold.

   RETUNED 2026-09-06, when the headline went up by a third and this was the
   only viewport that could not absorb it - the strip landed 58px below the
   fold. The header turned out to be the real problem rather than the type:
   at 740x360 it stood 94px tall, a quarter of the whole screen, because the
   logo clamp has a 54px floor and the nav wraps 40px of padding around it.
   A header that size on a landscape phone is wrong on its own terms, quite
   apart from what it does to the fold. Shrinking it gives back most of the
   58px, and --navh is measured live so the hero's own top padding follows
   it down with nothing to keep in sync by hand. */
@media (max-height:520px){
  .nav{ padding-block:9px; }
  .nav__mark img{ height:40px; }
  .hero__h1{ font-size:clamp(30px,7.9vh,56px); margin-top:2px; }
  .hero__inner .eyebrow--pill{ margin-bottom:6px; }
  .hero__lede{ margin:6px 0 8px; font-size:12px; line-height:1.28; }
  .hero__inner .cta{ padding:8px 16px; font-size:12.5px; }
}


/* ══════════════════════════════════════════════ JOURNAL (2026-09-04)
   The blog, renamed. Editorial rather than card-grid: a numbered list on
   hairlines, the same device the How It Works section already uses, so
   the section reads as part of this site rather than a bolted-on blog
   theme. 29 posts across 7 categories, so a quiet text filter earns its
   place; it is plain buttons with a gold underline, not pills. */
.journal__head{ padding-bottom:clamp(48px,6vw,72px); }
.journal__head h1{ margin-top:18px; max-width:16ch; }
.journal__head .lede{ margin-top:24px; max-width:62ch; }

.jfilters{
  display:flex; flex-wrap:wrap; gap:8px 26px;
  padding-bottom:clamp(26px,3vw,38px); border-bottom:1px solid var(--hair);
}
.jfilter{
  padding:6px 0; font-size:13.5px; letter-spacing:.02em; color:var(--muted);
  border-bottom:2px solid transparent; transition:color .25s var(--ease), border-color .25s var(--ease);
}
.jfilter span{ font-size:11.5px; opacity:.6; margin-left:3px; font-variant-numeric:tabular-nums; }
.jfilter.is-on{ color:var(--fg,#0A0A0A); border-bottom-color:var(--accent); }
@media (hover:hover) and (pointer:fine){ .jfilter:hover{ color:var(--fg,#0A0A0A); } }

.jlist{ list-style:none; margin:0; padding:0; }
.jlist__item{ border-bottom:1px solid var(--hair); }
.jlist__link{
  display:grid; grid-template-columns:auto minmax(0,1fr) auto; gap:clamp(16px,3vw,40px);
  align-items:start; padding:clamp(24px,3vw,34px) 0;
  border-left:2px solid transparent; padding-left:0;
  transition:border-color .3s var(--ease), padding-left .3s var(--ease), background-color .3s var(--ease);
}
@media (hover:hover) and (pointer:fine){
  .jlist__link:hover{ border-left-color:var(--accent); padding-left:18px; background:rgba(122,93,0,.028); }
  .jlist__link:hover .jlist__arrow{ transform:translateX(5px); color:var(--accent); }
}
.jlist__num{
  font-size:11.5px; letter-spacing:.06em; color:var(--muted);
  font-variant-numeric:tabular-nums; padding-top:7px;
}
.jlist__body{ display:flex; flex-direction:column; gap:8px; min-width:0; }
.jlist__meta{
  font-size:11.5px; letter-spacing:.14em; text-transform:uppercase; color:var(--accent);
}
.jlist__title{
  font-family:var(--display); font-weight:700; letter-spacing:-.02em;
  font-size:clamp(21px,2.3vw,30px); line-height:1.16;
}
.jlist__desc{ color:var(--muted); max-width:64ch; font-size:15.5px; line-height:1.6; }
.jlist__arrow{ color:var(--muted); padding-top:5px; transition:transform .3s var(--ease), color .3s var(--ease); }
.jlist__empty{ padding:40px 0; color:var(--muted); }
@media (max-width:640px){
  .jlist__link{ grid-template-columns:auto minmax(0,1fr); }
  .jlist__arrow{ display:none; }
}

/* ── Post page ─────────────────────────────────────────────────────── */
.post__head{ max-width:68ch; }
.post__head h1{
  font-size:clamp(31px,4.6vw,58px); line-height:1.06; letter-spacing:-.028em; margin-top:14px;
}
.post__head .eyebrow a{ color:inherit; border-bottom:1px solid var(--accent); }
.post__meta{
  margin:20px 0 0; font-size:13px; letter-spacing:.03em; color:var(--muted);
}
.post .m-rule{ margin-top:clamp(28px,4vw,40px); }

/* Article body. One measure, generous leading, and headings that step
   down clearly so a 2000-word guide stays scannable. */
.prose{ max-width:68ch; margin-top:clamp(34px,4.5vw,52px); }
.prose > * + *{ margin-top:1.15em; }
.prose p{ color:rgba(10,10,10,.78); font-size:17px; line-height:1.72; }
.prose h2{
  font-size:clamp(24px,2.9vw,34px); line-height:1.2; letter-spacing:-.02em;
  margin-top:2em; margin-bottom:.1em;
}
.prose h3{
  font-size:clamp(19px,2.1vw,23px); line-height:1.25; letter-spacing:-.015em;
  margin-top:1.7em; margin-bottom:.1em;
}
.prose strong{ font-weight:600; color:#0A0A0A; }
.prose a{
  color:inherit; border-bottom:1px solid var(--accent); padding-bottom:1px;
  transition:opacity .2s var(--ease);
}
.prose a:hover{ opacity:.7; }
.prose ul,.prose ol{ padding-left:1.15em; color:rgba(10,10,10,.78); }
.prose li{ margin-top:.5em; font-size:17px; line-height:1.68; }
.prose li::marker{ color:var(--accent); }
.prose hr{
  border:0; height:1px; background:var(--hair); margin:2.6em 0;
}
.prose blockquote{
  margin-left:0; padding-left:22px; border-left:2px solid var(--accent);
  color:rgba(10,10,10,.72); font-style:italic;
}
.prose table{ width:100%; border-collapse:collapse; font-size:15.5px; }
.prose th,.prose td{ text-align:left; padding:11px 12px; border-bottom:1px solid var(--hair); }
.prose th{ font-weight:600; letter-spacing:.02em; }
.prose code{
  font-size:.92em; background:rgba(10,10,10,.05); padding:2px 6px; border-radius:4px;
}

.post__close .lede{ margin-top:20px; }
.post__more{ list-style:none; margin:18px 0 0; padding:0; display:flex; flex-direction:column; gap:2px; }
.post__more li{ border-top:1px solid var(--hair); }
.post__more a{ display:flex; flex-direction:column; gap:5px; padding:16px 0; }
.post__moreCat{ font-size:11px; letter-spacing:.14em; text-transform:uppercase; color:var(--accent); }
.post__moreTitle{
  font-family:var(--display); font-weight:700; font-size:17px; line-height:1.25; letter-spacing:-.012em;
}
@media (hover:hover) and (pointer:fine){ .post__more a:hover .post__moreTitle{ color:var(--accent); } }


/* ══════════════════════════════════════════════ PRICING (2026-09-04)
   Built because the live /pricing page ranks position 78 on 340
   impressions while /blog/website-cost-nz, same intent, ranks 32 on 382.
   The difference is that one publishes numbers and the other declines to.
   So the number leads here, at display scale, and the reasoning follows
   it. Numbered tiers reuse the site's existing house device (the journey
   steps, the journal list) rather than inventing a card grid. */
.price__head{
  /* Explicit top padding so the eyebrow clears the fixed nav. The band's
     default top padding is close on mobile but not reliably clear of it. */
  padding-top:calc(var(--navh,80px) + clamp(34px,6vw,60px));
  padding-bottom:clamp(52px,7vw,84px);
}
.price__head h1{
  margin-top:18px; font-size:clamp(34px,5.4vw,72px); line-height:1.03;
  letter-spacing:-.03em; max-width:16ch;
}
.price__head .lede{ margin-top:26px; max-width:60ch; }

/* The anchor figure. This is the one moment on the site where a number is
   allowed to behave like a headline. */
.price__anchor{ padding-block:clamp(56px,7vw,96px); }
.price__big{
  font-family:var(--display); font-weight:700; color:#0A0A0A;
  font-size:clamp(46px,7.2vw,104px); line-height:1; letter-spacing:-.04em;
  margin:14px 0 0; display:flex; align-items:baseline; flex-wrap:wrap; gap:.24em;
}
.price__big span{
  font-family:var(--body); font-weight:500; font-style:italic;
  font-size:clamp(15px,1.5vw,21px); letter-spacing:0; color:var(--muted);
}

.price__bandsHead{ max-width:64ch; }
.price__bandsHead h2{ margin-top:16px; }
.price__bandsHead .lede{ margin-top:22px; }

.tiers{ list-style:none; margin:clamp(44px,6vw,72px) 0 0; padding:0; counter-reset:none; }
.tier{
  display:grid; grid-template-columns:auto minmax(0,1fr);
  gap:clamp(18px,3vw,44px);
  padding:clamp(28px,3.4vw,40px) 0;
  border-top:1px solid var(--hair);
}
.tier:last-child{ border-bottom:1px solid var(--hair); }
.tier__num{
  font-size:12px; letter-spacing:.06em; color:var(--muted);
  font-variant-numeric:tabular-nums; padding-top:9px;
}
.tier__body{ min-width:0; }
.tier__top{
  display:flex; align-items:baseline; justify-content:space-between;
  gap:18px; flex-wrap:wrap; margin-bottom:14px;
}
.tier__top h3{
  font-size:clamp(21px,2.4vw,30px); letter-spacing:-.02em; line-height:1.15;
  display:flex; align-items:center; gap:12px; flex-wrap:wrap;
}
.tier__price{
  font-family:var(--display); font-weight:700; font-size:clamp(17px,1.8vw,23px);
  letter-spacing:-.01em; color:var(--accent); white-space:nowrap;
}
.tier__flag{
  font-family:var(--body); font-size:10.5px; font-weight:700;
  letter-spacing:.16em; text-transform:uppercase; color:#0A0A0A;
  background:var(--gold); padding:5px 10px; border-radius:20px;
}
.tier p{ color:var(--muted); max-width:64ch; margin:0; }
.tier p + p{ margin-top:10px; }
.tier__note{ font-size:15.5px; }
.tier__note b{ color:var(--fg,#F4F1EA); font-weight:600; }
.tier__split{
  list-style:none; margin:16px 0; padding:0;
  display:flex; flex-direction:column; gap:10px;
  border-left:2px solid var(--hair); padding-left:20px;
}
.tier__split li{ color:var(--muted); font-size:15.5px; line-height:1.6; max-width:60ch; }
.tier__split b{
  color:var(--accent); font-weight:700; font-family:var(--display);
  margin-right:8px; white-space:nowrap;
}
/* The band LSG actually works in gets the accent, so the eye lands on it
   without the page having to say "this one" in words. */
.tier--ours{ border-left:2px solid var(--accent); padding-left:clamp(16px,2vw,26px); }

.steps{ list-style:none; margin:0; padding:0; display:flex; flex-direction:column; }
.steps li{
  display:grid; grid-template-columns:auto minmax(0,1fr); gap:16px;
  padding:18px 0; border-bottom:1px solid var(--hair);
}
.steps li:last-child{ border-bottom:none; }
.steps__n{
  font-size:11.5px; letter-spacing:.06em; color:var(--accent);
  font-variant-numeric:tabular-nums; padding-top:4px;
}
.steps p{ margin:0; color:var(--muted); font-size:15.5px; line-height:1.62; }

@media (max-width:700px){
  .tier{ grid-template-columns:1fr; gap:10px; }
  .tier__num{ padding-top:0; }
  .tier__top{ flex-direction:column; gap:6px; }
  .price__big{ gap:.16em; }
}

/* Sources block. A pricing page that cites is a pricing page that can be
   defended in a room, which is the whole point (2026-09-05). */
.sources{
  margin-top:clamp(40px,5vw,64px); padding-top:clamp(26px,3vw,36px);
  border-top:1px solid var(--hair); max-width:74ch;
}
.sources__head{
  font-size:11.5px; letter-spacing:.18em; text-transform:uppercase;
  color:var(--accent); margin:0 0 12px;
}
.sources__note{ color:var(--muted); font-size:14.5px; line-height:1.62; margin:0 0 18px; max-width:66ch; }
.sources__list{ list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:9px; }
.sources__list li{ font-size:14.5px; line-height:1.5; }
.sources__list a{ color:inherit; border-bottom:1px solid var(--accent); padding-bottom:1px; }
.sources__list a:hover{ opacity:.72; }
.sources__list span{ color:var(--muted); }
.sources__date{ margin:18px 0 0; font-size:13px; color:var(--muted); }

/* ══════════════════════════════════════════════ SEO SERVICE (2026-09-05)
   The compare block carries the whole point of the page: local search and
   SEO sound alike, so the difference is shown side by side rather than
   explained in a paragraph nobody finishes. */
.compare{ list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:2px; }
.compare li{
  padding:22px 0; border-top:1px solid var(--hair);
}
.compare li:last-child{ border-bottom:1px solid var(--hair); }
.compare__tag{
  display:inline-block; font-size:10.5px; font-weight:700; letter-spacing:.18em;
  text-transform:uppercase; color:var(--muted); margin-bottom:10px;
}
.compare__tag--on{
  color:#0A0A0A; background:var(--gold); padding:5px 10px; border-radius:20px;
}
.compare p{ margin:0; color:var(--muted); font-size:15.5px; line-height:1.66; max-width:56ch; }
.compare a{ color:inherit; border-bottom:1px solid var(--accent); padding-bottom:1px; }
.compare a:hover{ opacity:.72; }

/* A fifth card in a two-column grid would leave a dead cell showing the
   hairline background, so the ongoing service takes the full row instead.
   It reads as deliberate rather than left over, and suits a service that
   follows the build rather than sitting beside it. */
.svc__item--wide{ grid-column:1 / -1; }
.svc__item--wide .svc__bullets{ display:flex; flex-wrap:wrap; gap:8px 26px; }


/* ══════════════════════ HERO, CENTRED FOR REAL (2026-09-06) ══════════════
   Reported as squashed against the logo, and it measured that way: 68px of
   space above the block against 130px below it at 1440x900. The cause was
   never the numbers themselves but the shape of the box - padding-top and
   padding-bottom were two unrelated clamps, and align-items:center centres
   content inside the CONTENT box, so an asymmetric box centres against
   nothing you can see.

   The content box now starts at the exact bottom of the header and ends at
   the exact top of the strip. Centre then means what it says. --navh is
   measured live off the real header in motion.js, including the two-row
   wrap it does on a phone, so this tracks the logo at every size with
   nothing to keep in sync by hand.

   Placed last deliberately: media queries carry no extra specificity, so
   this plain rule beats every earlier .hero variant on source order alone,
   without a single !important. */
.hero{
  align-items:center;
  padding-top:var(--navh,118px);
  /* The block is centred between the header's bottom edge and the top of
     the marquee, exactly. padding-top reserves the header's box; this
     correction takes off the difference between that box and the header's
     real visible edge, which is 20px of padding under the logo. Solved and
     verified in motion.js, because the number depends on the logo's clamped
     height at the current viewport. Defaults to 0, so without JS the
     behaviour is the old, safe one. */
  padding-bottom:var(--hero-lift,0px);
}


/* ═══════════════ EYEBROW, SOLVED TO THE HEADLINE (2026-09-06) ════════════
   One line, ending exactly on the same right edge as the headline below it
   and never overhanging it by so much as a pixel.

   No fixed font-size can hold that. The headline is a clamp that moves with
   the viewport, the eyebrow is a fixed string, and the two share no width
   formula - so the size is measured and solved live in motion.js rather
   than guessed here.

   The only thing this rule has to guarantee is that the solve is possible:
   padding and tracking are both in em, so the pill's width is a straight
   linear function of its font-size, which is what makes a two-point
   measurement enough to land on the target exactly. Put anything in px
   here and the answer stops being exact.

   Last in the file on purpose - media queries carry no extra specificity,
   so this beats the earlier .eyebrow--pill variants on source order. */
.eyebrow--pill{
  display:inline-block;
  white-space:nowrap;
  padding:.78em var(--eyebrow-pad, 1.4em);
  border-radius:999px;
  background:rgba(247,245,240,.10);
  border:1px solid rgba(247,245,240,.18);
  color:rgba(247,245,240,.82);
  backdrop-filter:blur(8px);
  -webkit-backdrop-filter:blur(8px);
  font-size:var(--eyebrow-fs, 10.5px);
  letter-spacing:var(--eyebrow-tr, .22em);
  line-height:1.05;
}
/* The middot returns with the single line, and keeps its own air rather
   than inheriting the tracking meant for the words either side of it. */
.eyebrow--pill .sep{ display:inline; letter-spacing:0; opacity:.5; }


/* ══════════════════════════ ADD-ONS (2026-09-07) ═════════════════════════
   Deliberately not cards. Three services and two extras cannot be told
   apart in a flat grid of five equal boxes - that is the shape the old
   services section had, and it was why a monthly extra sat at the same
   weight as the build itself. Hairline rows read as a list of extras,
   which is what these are.

   One row is one link. The whole row is the hit area rather than the
   heading alone, so it behaves the way a row of this kind is expected to.
   The arrow moves on hover, gated to real pointers so a tap does not fire
   a hover state that then sticks. */
.addons__label{ margin:clamp(48px,6vw,80px) 0 0; }
.addons{
  list-style:none; margin:14px 0 0; padding:0;
  border-top:1px solid var(--hair);
}
.addons > li{ border-bottom:1px solid var(--hair); }
.addons a{
  display:grid; grid-template-columns:minmax(0,4fr) minmax(0,7fr) auto;
  gap:clamp(14px,3vw,44px); align-items:baseline;
  padding:clamp(20px,2.3vw,30px) 0;
  color:inherit; text-decoration:none;
}
.addons h3{
  margin:0; font-size:clamp(19px,1.9vw,26px); letter-spacing:-.015em; line-height:1.15;
}
.addons p{ margin:0; color:var(--muted); font-size:15px; line-height:1.62; }
.addons__go{
  align-self:center; color:var(--accent); display:inline-flex;
  transition:transform .3s var(--ease-fluid);
}
@media (hover:hover) and (pointer:fine){
  .addons a:hover .addons__go{ transform:translateX(6px); }
  .addons a:hover h3{ color:var(--accent); }
}
.addons h3{ transition:color .2s var(--ease-out); }
@media (max-width:720px){
  .addons a{ grid-template-columns:1fr auto; gap:8px 16px; }
  .addons p{ grid-column:1 / -1; }
}
@media (prefers-reduced-motion:reduce){
  .addons__go{ transition:none; }
}


/* ═══════════════ ONE TYPE SCALE, EVERY PAGE (2026-09-07) ═════════════════
   Measured at 1440x900 before this: five H1 sizes across the site, with the
   interior pages at 104px against the hero's 84.5px - bigger than the thing
   they are subordinate to. Five H3 sizes. One H2 out on its own.

   The hero's four responsive steps are mirrored onto the other H1s rather
   than overridden. .hero__h1 is redefined inside three media queries so it
   fits above the fold, and a plain rule here would beat all three on source
   order and quietly undo that. Mirroring also means an interior H1 still
   matches the hero on a 768px-tall laptop, not only on a tall screen.

   Colour is deliberately NOT collapsed to one value: headings take theirs
   from the band, bone on ink and ink on paper, and the eyebrow is gold on
   ink but gold-deep on paper. One value would put gold on white. */
/* TWO JOBS, TWO SIZES (2026-09-17). The rule above made every H1 the same
   NUMBER as the hero, and that was right as far as it went. It is not the
   same thing as making them look equal.

   Measured at 1440 before this change: every H1 on the site was 84.5px, and
   the rendered headline block ran from 104px tall on /terms to 456px on a
   journal post. The hero was designed at that size for two-word lines - "Get
   Found. / Build Trust. / Be Chosen." - while a journal title is a sentence,
   so it wrapped to five lines and became a wall. Tomislav: "the headlines of
   some pages are enormous (Journal, Services, Custom Web Design)". All three
   were multi-line.

   So the size now follows the job:
     hero           84.5px   the brand statement, one page, unchanged
     page heads     64.1px   about, services, service pages, locations
     article titles 51.8px   journal posts, privacy, terms - these are
                             sentences, not statements

   After: blocks run 64-299px and the tallest headline on the site is the
   homepage hero, which is the hierarchy it should have had. Verified at six
   viewport shapes, not just desktop - and all four media queries below had to
   move too, because a short-window rule left behind would have silently
   restored the old size on any laptop under 800px tall. */
.pagehead h1, .price__head h1, .journal__head h1, .post__head h1{
  line-height:1.04;
  letter-spacing:-.012em;
}
.pagehead h1, .price__head h1, .journal__head h1{ font-size:clamp(38px,4.45vw,66px); }
.post__head h1{ font-size:clamp(32px,3.6vw,54px); }
/* On a phone every page opens at the same size as the home hero. It used
   to be 44.7px here against the hero's 32.8px at 390px, a 36% gap that does
   not exist on desktop, where both are 84.5px. Same 8.4vw scale now, so they
   track each other at every width, and the 44px cap meets the 641px rule
   with no step. */
@media (max-width:640px){
  .pagehead h1, .price__head h1, .journal__head h1{ font-size:clamp(30px,8.4vw,44px); }
  /* Article titles step down here too. A phone has the least room for a
     five-line heading, so this is the width where it matters most. */
  .post__head h1{ font-size:clamp(27px,7.2vw,38px); }
}

/* Short-viewport sizes for real desktop windows and landscape phones only,
   never a narrow portrait screen that happens to be short.

   Without the min-width guard a 360x780 Pixel was being handed a 68.6px
   headline - 19% of its width - while a 390x844 iPhone got 44.7px on the
   same page. The hero was guarded against exactly this on 2026-09-08 and
   these four selectors were left behind, which is a category fixed in one
   place instead of everywhere it applies. */
@media (max-height:800px) and (min-height:521px) and (min-width:641px){
  .pagehead h1, .price__head h1, .journal__head h1{ font-size:clamp(32px,6.6vh,60px); }
  .post__head h1{ font-size:clamp(28px,5.4vh,50px); }
}
@media (max-height:520px) and (min-width:641px){
  .pagehead h1, .price__head h1, .journal__head h1{ font-size:clamp(26px,5.9vh,42px); }
  .post__head h1{ font-size:clamp(24px,4.8vh,36px); }
}

/* One H2. The journal's prose H2 was the only one out of step; its top
   margin comes back proportionally so a bigger heading does not open a
   hole in the middle of an article. */
h2, .prose h2{
  /* 30px was the floor and every phone sat on it, because 4.2vw does not
     overtake 30px until 714px wide. That put the section headline at 0.91 of
     the H1 on a 390px phone and 0.99 on a 360px one, against 0.62 on
     desktop: no hierarchy left. 25px restores the step down and changes
     nothing from 595px up, so tablet and desktop are untouched. */
  font-size:clamp(25px,4.2vw,52px);
  line-height:1.08;
  letter-spacing:-.022em;
}
.prose h2{ margin-top:1.15em; margin-bottom:.16em; }

/* One H3. Service cards, pricing tiers, add-on rows and article subheads
   were running 23, 26, 27 and 30. */
h3, .prose h3, .svc__item h3, .tier__top h3, .addons h3{
  font-size:clamp(19px,1.8vw,23px);
  line-height:1.24;
  letter-spacing:-.015em;
}


/* The short-viewport headline size, for real desktop windows only. Phones
   keep the width-based tier: a 360px-wide screen being 640px tall does not
   mean it can carry a 56px headline, and before this it was being handed
   one. */
@media (max-height:800px) and (min-height:521px) and (min-width:641px){
  .hero__h1{ font-size:clamp(37px,8.8vh,80px); }
}


/* ═══════ ROUNDED VIDEO CORNERS THAT SURVIVE A PHONE (2026-09-08) ═════════
   Measured first: .stepfig__v had border-radius 0 at every width and was
   leaning on .bezel__core's overflow:hidden to cut its corners. A playing
   video sits on its own compositor layer, so an ancestor's rounded clip is
   applied during paint and the layer lands on top of it - square corners on
   mobile, and a flicker as the compositor re-resolves the clip against a
   surface that changes every frame.

   So the video rounds itself, and the container gets a layer of its own so
   the clip is composited rather than painted. Belt and braces on purpose:
   either one alone still leaves a browser that gets it wrong. */
.stepfig__v{
  border-radius:15px;      /* .bezel--tight is 22px with 7px of padding */
  overflow:hidden;
  /* clip-path, not just border-radius. A decoding video sits on its own
     compositor layer, and that layer is precisely what an ancestor's
     overflow clip fails to reach. clip-path is applied by the compositor to
     the element's own layer, so it holds where the other two do not.
     border-radius stays for anything without clip-path support. */
  clip-path:inset(0 round 15px);
  -webkit-clip-path:inset(0 round 15px);
}
.bezel--tight .bezel__core,
.local__media{
  transform:translateZ(0);
  -webkit-transform:translateZ(0);
  backface-visibility:hidden;
  -webkit-backface-visibility:hidden;
  isolation:isolate;
}
.local__media video{ border-radius:2px; }

/* The phone tier set .bezel to 24px, which beats .bezel--tight's 22px on
   source order while the inner radius stayed at 15px. 24 less 7px of
   padding is 17, so the two curves disagreed. The tight bezel holds one
   radius at every width now, and 22 less 7 is exactly 15. */
.bezel--tight{ border-radius:22px; }


/* ═════════════ THE AUDIT REPORT, grouped (2026-09-08) ════════════════════
   Four states, and the fourth is the point. "unknown" means the check could
   not determine an answer - JSON-LD injected by JavaScript, images set in
   CSS - and it is deliberately neutral. Showing it as a failure would be
   inventing a finding, which is the exact thing the audit rewrite was for. */
.result__groups{ display:grid; gap:clamp(20px,2.4vw,30px); }
.rgroup__title{
  margin:0 0 4px; font-size:12px; font-weight:600; letter-spacing:.18em;
  text-transform:uppercase; color:var(--accent); font-family:var(--body);
}
/* Explicit colour rather than var(--muted): whatever scope this panel sits
   in resolved it to #a4a4a4, which is 2.49:1 on white. Stated here so it
   cannot drift with a token defined somewhere else. 7.1:1. */
.rgroup__note{ margin:0 0 12px; font-size:13px; line-height:1.55; color:rgba(10,10,10,.68); max-width:62ch; }
/* The colour above assumes a white band - true everywhere this panel
   has ever appeared until the /check page put it on black. Same fix
   shape as .rf--info/.rf--unknown just below it: bone, not another new
   colour. */
.band--ink .rgroup__note{ color:rgba(244,241,234,.68); }
.result__list li.rf{
  display:grid; grid-template-columns:auto minmax(0,1fr); gap:12px;
  align-items:start; padding:12px 15px;
}
/* The old list right-aligned its second column. These findings read as
   sentences, so they start where the eye already is. */
.result__list li.rf span{ text-align:left; }
.result__list li.rf b{ font-size:14.5px; font-weight:600; }
.rf__mark{
  width:19px; height:19px; border-radius:50%; flex:none; margin-top:2px;
  display:inline-flex; align-items:center; justify-content:center;
  font-style:normal; font-size:11px; font-weight:700; line-height:1;
}
.rf--ok .rf__mark{ background:rgba(34,139,84,.14); color:#1E7A46; }
.rf--warn .rf__mark{ background:rgba(122,93,0,.14); color:var(--gold-deep); }
.rf--fail .rf__mark{ background:rgba(179,38,30,.12); color:#B3261E; }
.rf--unknown .rf__mark{ background:rgba(10,10,10,.08); color:var(--muted); }
.band--ink .rf--ok .rf__mark{ background:rgba(74,222,128,.16); color:#4ADE80; }
.band--ink .rf--warn .rf__mark{ background:rgba(242,201,76,.16); color:var(--gold); }
.band--ink .rf--fail .rf__mark{ background:rgba(248,113,113,.16); color:#F87171; }
/* "info" reports, it does not judge, and must not look like "unknown" -
   which has to keep meaning "we could not determine this". */
.rf--info .rf__mark{ background:rgba(10,10,10,.06); color:var(--fg); }
.band--ink .rf--info .rf__mark{ background:rgba(244,241,234,.10); }
.band--ink .rf--unknown .rf__mark{ background:rgba(244,241,234,.12); }
.rf b{ display:block; }
.rf span{ display:block; margin-top:2px; font-size:13.5px; line-height:1.55; color:var(--muted); }
.result__stamp{
  margin:18px 0 0; font-size:12.5px; line-height:1.6; color:var(--muted);
}
.result__score span{ max-width:14ch; line-height:1.2; }


/* The deep pass takes up to a minute, so its pending state has to look
   deliberate rather than stuck. A quiet pulsing rule, gated on reduced
   motion, and the note explains what is happening and that nothing above
   will change. */
.rgroup--pending .rgroup__title{ position:relative; padding-left:20px; }
.rgroup--pending .rgroup__title::before{
  content:""; position:absolute; left:0; top:50%; width:11px; height:11px;
  margin-top:-5px; border-radius:50%; background:var(--accent);
  animation:deepPulse 1.6s var(--ease-fluid) infinite;
}
@keyframes deepPulse{ 0%,100%{ opacity:.25; transform:scale(.7) } 50%{ opacity:1; transform:scale(1) } }
.rgroup__retry{
  margin-top:12px; font:inherit; font-size:13.5px; font-weight:600;
  color:var(--accent); background:none; border:0; border-bottom:1px solid var(--accent);
  padding:0 0 2px; cursor:pointer;
}
@media (prefers-reduced-motion:reduce){
  .rgroup--pending .rgroup__title::before{ animation:none; opacity:.7; }
}

/* THE LIVE LOG (2026-09-29). Every group title already marks a real pass -
   score, render, sitewide, deep - landing as it actually finishes, in real
   time, not a simulated delay. This gives that real sequence a console
   identity instead of a plain heading: a monospace face and a "> " mark
   in front of every line, so the report reads as something that ran rather
   than something that loaded. Scoped to .result__groups only - this is the
   audit tool's own vocabulary, not a sitewide type change. */
.result__groups .rgroup__title{
  font-family:ui-monospace,'SF Mono','Cascadia Code','Courier New',monospace;
  letter-spacing:.02em;
}
.rgroup:not(.rgroup--pending) .rgroup__title{ position:relative; padding-left:20px; }
.rgroup:not(.rgroup--pending) .rgroup__title::before{
  content:">"; position:absolute; left:0; top:-1px; color:var(--accent);
  font-weight:700;
}
.rgroup--first .rgroup__title::before{ color:var(--gold-deep,#7A5D00); }
/* The one line genuinely running right now gets the cursor - not every
   line, or it stops meaning "this one, active". Reduced motion keeps the
   mark but drops the blink, same pattern as the pulse dot above it. */
.rgroup--pending .rgroup__title::after{
  content:""; display:inline-block; width:7px; height:1em; margin-left:6px;
  vertical-align:-2px; background:var(--accent); animation:logBlink 1s steps(1) infinite;
}
@keyframes logBlink{ 50%{ opacity:0; } }
@media (prefers-reduced-motion:reduce){
  .rgroup--pending .rgroup__title::after{ animation:none; opacity:.6; }
}

/* ── The shortlist and the head to head (2026-09-09) ──────────────────────
   Plato and Semrush both open with a verdict and close with a comparison.
   Ours leads with the three things we would start on, because fifty-seven
   findings is a list and three in order is advice. */
.rgroup--first { border-left: 2px solid var(--gold, #F2C94C); padding-left: 1.4rem; }
/* gold-deep, not gold: this panel is on a paper band and #F2C94C measures
   1.58:1 on white. Found by running axe against a real audit result on
   2026-09-18 - the page-level accessibility check never saw it, because the
   report does not exist until an audit has run. This is the report a
   customer reads, so it matters more than any button on the site. */
.rgroup--first .rgroup__title { color: var(--gold-deep, #7A5D00); }
.result__list--first { counter-reset: rfirst; }
.result__list--first .rf { counter-increment: rfirst; align-items: baseline; }
.result__list--first .rf::before {
  content: "0" counter(rfirst);
  font-variant-numeric: tabular-nums;
  color: var(--gold, #F2C94C);
  opacity: .55;
  margin-right: .85rem;
  font-size: .82em;
  letter-spacing: .04em;
}
.rcompare__row { display: flex; gap: .6rem; flex-wrap: wrap; margin-top: .9rem; }
.rcompare__input {
  flex: 1 1 14rem; min-width: 0;
  background: transparent;
  border: 1px solid color-mix(in srgb, currentColor 22%, transparent);
  border-radius: 2px;
  padding: .7rem .85rem;
  color: inherit; font: inherit; font-size: .95rem;
}
.rcompare__input:focus-visible { outline: 2px solid var(--gold, #F2C94C); outline-offset: 2px; }
.rcompare__go {
  flex: 0 0 auto;
  background: var(--gold, #F2C94C); color: #0a0a0a;
  border: 0; border-radius: 2px;
  padding: .7rem 1.3rem;
  font: inherit; font-size: .95rem; font-weight: 600;
  cursor: pointer;
}
.rcompare__go:disabled { opacity: .5; cursor: default; }
@media (max-width: 480px) {
  /* Stacking turns the main axis vertical, which turns a flex-basis of 14 or
     15rem from a sensible minimum WIDTH into a 15rem tall input box. Measured
     2026-09-10 on a 390px screen: the email field rendered 240px tall. The
     comparison box had the same defect and had been shipped with it, so both
     are corrected here rather than only the new one. */
  .rcompare__row { flex-direction: column; }
  .rcompare__input, .rcompare__go { flex: 0 0 auto; width: 100%; }
}

/* ═══════ EVERY LINK LANDS ON THE SAME LINE (2026-09-10) ═════════════════
   Measured before this existed, on a 390px phone with the header ending at
   85px, the gap from the header's bottom edge to the top of the eyebrow was:

       #how / #local / #questions / #start    -12   behind the header
       journal                                -12   behind the header
       pricing                                +29
       services                               +46

   Four answers, two of them hiding the thing you had just navigated to.

   The eyebrow is the first child of every landing section, so it sits at
   exactly that section's top padding, and three separate rules were setting
   that padding. Only one of them knew the fixed header was there.

   One number drives both mechanisms now. Page heads reserve header + gap as
   padding, so a fresh page load puts the eyebrow one gap below the bar.
   Anchors cannot use padding, because a section's padding is also its
   breathing room when you scroll past it normally, so they use
   scroll-margin-top, which moves where an anchor jump stops and changes no
   layout at all. It is pulled back by exactly the band's own padding, so it
   finishes on the same line as the page heads.

   Last in the file so it beats .pagehead at 1753, .pagehead at 1845 and
   .price__head at 2313 without fighting any of them on specificity. */
:root{ --land-gap:clamp(24px,2.8vw,40px); }

.band[id]{
  scroll-margin-top:calc(var(--navh,85px) + var(--land-gap) - clamp(78px,10vw,140px));
}

.pagehead, .price__head, .journal__head, .post__head{
  padding-top:calc(var(--navh,85px) + var(--land-gap));
}

/* A finished pass's slot. It keeps its position in the flow and carries no
   height of its own, so emptying it moves nothing below it. */
.rgroup-slot{ display:contents; }

/* The reserved shortlist rows (2026-09-10).
   Sized to what a real row occupies, not to a token line. The shell exists so
   the shortlist never has to be inserted above a reader once the ranking is
   ready; if the shell is shorter than its eventual contents then filling it
   pushes the whole report down and the reservation achieves nothing. Measured
   before this rule existed: shell 290px, filled 578px, a 288px shove. A real
   row is a label and about three lines of detail. */
.rgroup--first.is-waiting .rgroup__note{ opacity:.6 }
.rf--skeleton{ opacity:.22; pointer-events:none }
.rf--skeleton b{ display:block; height:1.05em; width:38%; background:currentColor; border-radius:2px }
.rf--skeleton span{ display:block; height:6em; width:92%; margin-top:.55em; background:currentColor; border-radius:2px }

/* The two are made to match by construction rather than by tuning a number.
   The skeleton reserves four lines of detail; the real row is clamped to four
   lines. Chasing a fixed pixel height was never going to hold, because the
   text length changes with every site audited. */
.result__list--first .rf span{
  display:-webkit-box; -webkit-line-clamp:4; -webkit-box-orient:vertical;
  overflow:hidden;
}


/* ── THE REPORT BY EMAIL (2026-09-10) ────────────────────────────────────
   Sits at the foot of the finished report. Built from the comparison box's
   parts so the two things at the end of a report look like one family, and
   coloured off currentColor so it works on the ink panel and the paper one
   without a second set of rules. */
.rmail {
  margin-top: 1.7rem; padding-top: 1.5rem;
  border-top: 1px solid color-mix(in srgb, currentColor 15%, transparent);
}
/* h4 -> h3 on 2026-09-17 to close a heading-order jump (h2 straight to h4).
   It had been leaning on the h4 default for font and line-height, and h3
   carries the display serif and a tighter line-height, so both are pinned
   to keep this rendering byte-identical. Verified by measuring computed
   styles before and after, not by looking at it. */
.rmail__title { margin: 0; font-size: 1.06rem; letter-spacing: -.01em;
  font-family: var(--body); line-height: 1.6; }
.rmail__note { margin: .35rem 0 0; font-size: .92rem; opacity: .72; max-width: 46ch; }
.rmail__row { display: flex; gap: .6rem; flex-wrap: wrap; margin-top: .95rem; }
.rmail__input {
  flex: 1 1 15rem; min-width: 0;
  background: transparent;
  border: 1px solid color-mix(in srgb, currentColor 22%, transparent);
  border-radius: 2px;
  padding: .7rem .85rem;
  color: inherit; font: inherit; font-size: .95rem;
}
.rmail__input:focus-visible { outline: 2px solid var(--gold, #F2C94C); outline-offset: 2px; }
.rmail__go {
  flex: 0 0 auto;
  background: var(--gold, #F2C94C);
  background-image:linear-gradient(180deg,#F8DC7B 0%,var(--gold) 50%,#ECC047 100%);
  color:#FFFFFF;
  text-shadow:0 0 1px rgba(6,9,12,.55), 0 1px 1px rgba(6,9,12,.72), 0 1px 6px rgba(6,9,12,.4);
  border: 0; border-radius: var(--cta-radius,999px);
  padding: .72rem 1.3rem;
  font: 600 .9rem/1 var(--body, inherit);
  letter-spacing: .02em; cursor: pointer;
  box-shadow:inset 0 1px 0 rgba(255,255,255,.55), 0 1px 2px rgba(10,10,10,.18), 0 8px 18px -10px rgba(10,10,10,.40);
}
.rmail__go:disabled { opacity: .5; cursor: default; }
.rmail__opt {
  display: flex; align-items: flex-start; gap: .55rem;
  margin-top: .8rem; font-size: .9rem; opacity: .82; cursor: pointer;
}
.rmail__opt input { margin: .18rem 0 0; accent-color: var(--gold, #F2C94C); flex: 0 0 auto; }
/* A field only a robot fills in. Off screen rather than display:none, since
   some bots skip anything hidden outright. */
.rmail__hp { position: absolute; left: -9999px; width: 1px; height: 1px; opacity: 0; }
.rmail__msg { margin: .75rem 0 0; font-size: .9rem; min-height: 1.25em; }
.rmail__msg.is-bad { color: #d8604f; }
.rmail--sent .rmail__row, .rmail--sent .rmail__opt { display: none; }

/* Stacked, the flex basis has to go.
 *
 * Turning the row into a column turns a flex-basis of 15rem from a sensible
 * minimum WIDTH into a 15rem tall box. Measured on a 390px screen: the email
 * field rendered 240px tall. This lives at the end of the file on purpose,
 * because the .rmail rules above are the last word on that property and a
 * media query earlier in the file loses to them at equal specificity, which
 * is exactly how the first attempt at this fix silently did nothing. */
@media (max-width: 480px) {
  .rmail__row { flex-direction: column; }
  .rmail__input, .rmail__go { flex: 0 0 auto; width: 100%; }
}

/* 44px minimum, because that is the number our own audit fails other people
   for. Measured 2026-09-10: this button rendered 37px tall, and the
   comparison box's button the same. Shipping a control we would flag on a
   customer's site is the one thing this report cannot afford to do. */
.rmail__go, .rcompare__go { min-height: 44px; }
.rmail__input, .rcompare__input { min-height: 44px; }


/* ── MORE RESULTS WAITING (2026-09-10) ───────────────────────────────────
   Sits above the fold, out of the way, and only while something is actually
   waiting. Deliberately not a toast that fades: it is a control, it stays
   until it is used or until scrolling back to the report makes it
   unnecessary. */
.rwait{
  position: fixed; z-index: 60;
  left: 50%; bottom: max(18px, env(safe-area-inset-bottom, 0px));
  transform: translateX(-50%);
  display: inline-flex; align-items: center; gap: .6rem;
  min-height: 44px; padding: .55rem .95rem;
  background: var(--ink, #0A0A0A); color: var(--bone, #F4F1EA);
  border: 1px solid rgba(244,241,234,.16); border-radius: 999px;
  box-shadow: 0 10px 34px rgba(0,0,0,.42);
  font: 600 .88rem/1 var(--body, inherit); letter-spacing: .01em;
  cursor: pointer;
  animation: rwaitIn .28s cubic-bezier(.2,.7,.3,1) both;
}
.rwait[hidden]{ display: none; }
.rwait__mark{
  width: 9px; height: 9px; flex: 0 0 auto;
  background: var(--gold, #F2C94C); transform: rotate(45deg);
}
.rwait__go{
  color: var(--gold, #F2C94C); text-transform: uppercase;
  letter-spacing: .12em; font-size: .74rem;
  border-bottom: 1px solid currentColor; padding-bottom: 2px;
}
@keyframes rwaitIn{ from{ opacity:0; transform: translate(-50%, 14px) } to{ opacity:1; transform: translate(-50%, 0) } }
@media (prefers-reduced-motion: reduce){ .rwait{ animation: none } }


/* ── NO 3D AROUND A REPORT (2026-09-10) ──────────────────────────────────
   The audit card tilts toward the pointer, which is a good few degrees of
   physicality on an empty 340px card and a disaster on the same card with a
   five thousand pixel report inside it: the bottom edge swings about 270px
   through space and the whole subtree is repainted as one 3D layer, which
   measured 26 frames over 50ms on a 1440 desktop.

   Two halves. The script stops feeding it angles past 600px tall, and the
   perspective comes off the moment there is a result to read, so the browser
   is not holding a five thousand pixel 3D layer at all. Both are desktop
   pointer concerns only; a phone never had either. */
.bezel__tilt.is-flat{
  transform: none !important;
  transition: none;
  will-change: auto;
}
@media (hover:hover) and (pointer:fine){
  .check .bezel:has(.result:not([hidden])){ perspective: none; }
}


/* ── LOOPING DECORATION HOLDS STILL WHILE THE PAGE MOVES (2026-09-10) ─────
   Measured, not assumed: the grain alone was worth 90 frames across a 20
   second desktop scroll, and it is a fixed full-viewport layer repainting six
   times a second for ever. None of these carry information and none of them
   can be read while the page is moving. The marquee is not in this list on
   purpose - it is the one loop somebody might be watching, and freezing it
   would read as broken. */
/* animation:none on the grain, not animation-play-state:paused. A paused
   animation is still a live animation as far as the compositor is concerned,
   and measured it saved nothing; removing it outright measured 943 frames
   against 853. The grain is random noise, so restarting it is invisible. The
   two seal rings are a continuous rotation, where restarting WOULD be
   visible, so those are paused rather than removed. */
.is-scrolling .grain{ animation: none; }
.is-scrolling .seal__ring,
.is-scrolling .seal__dial{ animation-play-state: paused; }

/* And the marquee stops when it is nowhere near the screen, which the
   original rule missed: it paused only until the ticker was first seen, then
   ran for ever whether it was on screen or not. */
.ticker.in:not(.is-live) .ticker__track{ animation-play-state: paused; }


/* ── SOMETHING A THUMB CAN HIT (2026-09-11) ──────────────────────────────
   Measured with the same rule the audit applies to customers: 15 controls
   under 44px on a phone, 20 on desktop, the hamburger among them at 32x32.

   Two numbers, and only one of them is a standard:
     WCAG 2.2 criterion 2.5.8 Level AA   24x24 CSS px   the standard
     Apple Human Interface Guidelines    44x44 pt       a convention
   The typographic links here were 22px tall, which fails the STANDARD by two
   pixels. Padding fixes that invisibly, because padding grows the hit box and
   not the type. Taking every text link to Apple's 44 would need eleven pixels
   above and below each one, which moves the footer, the nav and the cards -
   a design change, not a fix, and not made here.

   The hamburger is a control rather than a line of type, so it gets the full
   44 as padding, with the three bars untouched. */

.nav__toggle{
  min-width:44px; min-height:44px;
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:5px; padding:6px;
}

/* 24px minimum on the standalone text links, bought with padding so nothing
   moves. Line height already carries part of it, so the padding is small. */
.foot__nav a,
.foot__contactLink,
.svc__item .svc__link{
  padding-block:2px;
  min-height:24px;
}

/* The two that were within a pixel or four of 44 are simply rounded up. */
.nav__cta, .hero .cta{ min-height:44px; }

/* The desktop nav row and the logo link, the last two under the 24px WCAG
   floor. Padding on the row, not the type, so the bar does not move; the logo
   measured 43.98 and rounded to 44, which is inside the rule by luck rather
   than by intent. */
.nav__links a{ padding-block:2px; min-height:24px; display:inline-flex; align-items:center; }
.nav__mark{ min-width:44px; min-height:44px; }


/* ── APPLE'S 44px ON EVERY STANDALONE LINK (2026-09-11) ──────────────────
   Tomislav asked for the full 44, not the 24px WCAG minimum these already
   met. The type does not change: only the box around it grows, so the words
   stay exactly the size and weight they were and only the area a thumb can
   land on gets bigger.

   Measured before: footer 603px on a phone, 593px on desktop, links 26px.
   The gap is shown in the commit message so the cost is on the record. */
.foot__nav a,
.foot__contactLink,
.svc__item .svc__link,
.nav__links a{
  min-height:44px;
  padding-block:9px;
  display:inline-flex;
  align-items:center;
}
/* 44 in BOTH directions, which is what the guideline says and what our own
   audit measures. "About" is a short enough word to come out 38px wide. */
.foot__nav a{ min-width:44px; justify-content:center; }

/* The footer's rows carry the extra height, so the gaps between them come in
   to compensate and the block does not simply grow by the difference. */
.foot__nav{ row-gap:0; }
.foot__contact{ row-gap:0; }


/* ═══════════════════════════════════════════════════════════════════════
   THE STEP REVEALS (2026-09-15) - replaces the Remotion-rendered videos
   ═══════════════════════════════════════════════════════════════════════
   Five rounds of fixing the pre-rendered step videos each found and fixed
   something real, and each one was still reported as freezing on a real
   phone afterwards - which is what pointed at the technology rather than
   the timing: a video decoder can behave differently on real mobile
   hardware than in a desktop test, no matter how carefully the render or
   the swap logic is measured. See app.js for the full account.

   This replaces the video with the exact technique the hero headline
   above it already runs in production on this same page: a masked line
   rising into place behind a bar of light, using the SAME heroWord and
   heroSweep keyframes (not new ones), cycling through three short acts
   per step. No decoder anywhere, so nothing left that can stall on one. */
.actstage{
  position:relative; width:100%; aspect-ratio:900/1150; overflow:hidden;
  background:linear-gradient(168deg, #FCFBF8 0%, #F6F3EC 52%, #EEEADE 100%);
}
/* THE HAIRLINES ARE GONE (2026-09-21).
   Tomislav, twice: "it still shoots like a grey line across cards, it looks
   like it glitches still on all cards", then "on both mobile and desktop view
   are still showing those grey lines shooting across".

   .actstage__cols used to paint a 1px vertical line every 46px across the whole
   card at 4.3% black. Measured on the live page: 326x417 of it on a desktop
   card, 236x302 on a phone, on every one of the seven. The row slides while it
   is being read, so the pattern travelled across the window with the card, and
   a 1px line at 4% alpha does not travel cleanly at sub-pixel offsets - it
   shimmers. Same element on both widths, which is why fixing the sweep made no
   difference to what he was seeing.

   The element is gone from the markup too. The paper gradient, the vignette and
   the ghost numeral carry the texture on their own, and none of them has an
   edge to shimmer. */
.actstage__vignette{
  position:absolute; inset:0; pointer-events:none; z-index:0;
  background:radial-gradient(78% 62% at 42% 46%, rgba(10,10,10,0) 55%, rgba(10,10,10,.07) 100%);
}
/* Oversized ghost numeral, the same editorial texture the video composition
   used - static here rather than drifting, since the drift was never the
   point, the type behind the type was. */
.actstage__numeral{
  position:absolute; right:-2%; bottom:-6%; z-index:0;
  font-family:var(--display); font-weight:700; font-size:150px; line-height:1;
  color:var(--ink); opacity:.05; pointer-events:none; user-select:none;
}
.actstage__acts{ position:relative; width:100%; height:100%; z-index:1; }
/* ONE FRAME ON THE CARD AT A TIME (2026-09-21).
   Tomislav: "on all 7 cards there is a glitch on them, while they're all giving
   those words/lines out... there's some kind of squares and rectangles glitching
   / flashing around".

   Measured, 163 frames over five seconds on one card: THREE times in that five
   seconds, for 328ms, 334ms and 337ms, two acts were painted at once - and the
   act on its way out was still at 90% opacity while its own two lines read
   opacity 0 and had already dropped 40px. So a third of a second, three times
   every five seconds, on every card, the reader saw a gold diamond and an
   eyebrow with no headline under them, with the next act's gold diamond fading
   up on the same spot and a half-finished gold sweep bar vanishing mid-stroke.

   Both halves of that are fixed here. The frame leaving keeps its words while
   it goes (see the base state below), and the two frames no longer share the
   screen: the old one goes in .14s, the new one starts after it. */
.actstage__act{
  position:absolute; inset:0;
  display:flex; flex-direction:column; justify-content:center; gap:12px;
  padding:11% 9%;
  opacity:0; transition:opacity .14s linear;
  pointer-events:none;
}
.actstage__act.is-on{ opacity:1; transition:opacity .22s linear .14s; }
.actstage__eyebrow{
  display:flex; align-items:center; gap:8px; margin:0 0 2px;
  font-family:var(--body); font-weight:700; font-size:11px;
  letter-spacing:.16em; text-transform:uppercase; color:var(--gold-deep);
}
/* The same mark that replaced the dash on the original Remotion video
   eyebrows (see the old remotion/src/StepTypography.jsx) - reused here
   rather than reinvented. */
.actstage__mark{
  width:9px; height:9px; flex:none;
  background:linear-gradient(135deg, var(--gold) 0%, var(--gold-deep) 100%);
  transform:rotate(45deg);
  box-shadow:0 1px 3px rgba(122,93,0,.28);
}
/* Was an h4 until 2026-09-17, changed to h3 to close a heading-order jump
   (the section h2 went straight to h4). The actstage sits inside a
   .journey__step, and `.journey__step h3` sets display:inline-flex and a
   14px bottom margin - which laid the two lines of this heading side by side
   instead of stacking them, and cost 5px of height. Caught by measuring the
   computed box before and after, not by looking at it.

   Scoped with .journey__step so it outranks that rule instead of relying on
   source order, and both properties are now stated rather than inherited. */
.actstage__h,
.journey__step .actstage__h{
  margin:0; font-family:var(--display); font-weight:700; letter-spacing:-.02em;
  color:var(--ink); font-size:30px; line-height:1.14;
  display:block; gap:0;
}
/* .journey__step h3::after draws a gold chevron after the step headings. The
   actstage headings are not step headings, they are frames of an animation,
   so the chevron is cancelled rather than inherited. */
.journey__step .actstage__h::after{ content:none; }
/* The one three-line act (step-live, "Then / Live.") is a single short
   word - sized up the way the video composition did, rather than sitting
   small and lonely at the 2-line size. */
.actstage__h--big{ font-size:42px; }

/* THE REVEAL ITSELF - .hero__h1's own .ln/.w/.ln__sweep technique, scoped
   to this smaller stage and reusing the SAME @keyframes heroWord and
   heroSweep already defined above for the hero. Each act only ever has
   one or two lines (never three, unlike the full hero headline), so only
   :nth-child(1) and :nth-child(2) need their own delay. */
/* THE RULE BELONGS TO THE WORD, NOT TO THE CARD (2026-09-21).
   The line box ran the full width of the card, so the gold rule under a short
   line carried on past the end of it and read as a stray line lying on the
   paper. width:fit-content keeps the lines stacked - they are still blocks, and
   inline-block laid them side by side once before - while ending the box where
   the words end. */
.actstage__ln{ display:block; width:fit-content; overflow:hidden; position:relative; padding-bottom:.16em; }
/* THE RESTING STATE IS THE FINISHED ONE, NOT THE HIDDEN ONE (2026-09-21).
   It used to be the hidden one, so the moment an act lost .is-on its animation
   went with it and the line fell back to 118% below the mask at opacity 0 -
   instantly, while the act itself was still nine tenths visible. That snap is
   what emptied the card during every changeover.
   The animations below now fill BOTH ways, so they still hold the line hidden
   through their own delay, and a line whose act has finished simply stays where
   it landed. */
.actstage__lnText{
  display:inline-block;
  transform:none; transform-origin:0 100%;
  opacity:1; will-change:transform,opacity;
}
/* THE SWEEP IS GONE, NOT RETIMED AGAIN (2026-09-25, fifth pass on this exact
   element - hairline texture removed 2026-09-21, the rule's width fixed the
   same day, the travelling bar stopped travelling the same day, its timing
   resynced to the word the same day, and Tomislav caught it again on a real
   phone after all four: a screenshot of a card showing its eyebrow, nothing
   else, and a pale line sitting where the headline should be.
   Every one of those fixes was real and measured on THIS machine. None of
   them survived a real phone, which is the exact lesson the video version of
   this component was already scrapped for (see the top of this section) -
   paint order and timing on real mobile hardware is not what a desktop
   browser measures, no matter how precisely the CSS delays are tuned. A
   decorative flourish that keeps finding a new way to be visible before the
   word it decorates is not worth a sixth attempt at retiming it.
   Reduced-motion users have never seen this bar - `.actstage__lnSweep{
   display:none }` already existed for them below, and nobody has ever called
   that reveal incomplete. So that is the treatment now, for everyone: the
   word still rises exactly as it did, nothing is drawn under it. */
.actstage__lnSweep{ display:none; }

/* THE STAGGERED WORD-RISE IS GONE TOO, FOR THE CARDS ONLY (2026-09-25,
   sixth pass, this time on a real Android phone in Firefox).
   Tomislav sent a screenshot from Firefox on Android showing the exact same
   family of fault yet again: eyebrow visible, headline nowhere, a thin line
   where it should be. The sweep removal above did not reach this one because
   it was never the sweep - it is a gap built into the numbers themselves.
   The act's own opacity fade-in (.14s delay, .22s duration - see
   .actstage__act.is-on above) finishes and the card is fully visible by
   roughly .36s. The word-rise animated here separately did not even START
   until .52s. Between those two points the CONTAINER - eyebrow included -
   is on screen and the HEADLINE is still deliberately held invisible by its
   own animation-fill-mode, waiting for its own later cue. That ~160ms gap
   exists in the CSS on every device, every engine; a slower real phone
   simply has more chance of a frame landing inside it, and this is the
   fourth device/engine combination to actually show it.
   Closing the gap rather than narrowing it again: the word no longer has a
   later cue of its own. It is part of the same element as the eyebrow now,
   so it can only ever appear in the same instant the act itself does -
   there is no window left for one to be visible without the other, on any
   device, because there is no separate timeline left to fall out of step.
   The hero headline above this section is untouched: it plays this same
   heroWord rise once, on entry, and has never produced this fault - the
   risk was always the 21 repeats down this section, not the technique
   itself. */
.actstage__lnText{ animation:none; }

@media (prefers-reduced-motion:reduce){
  .actstage__act{ transition:none; }
  .actstage__lnText{ transform:none; opacity:1; animation:none; }
}


/* ═══════════════════════════════════════════════════════════════════════
   ONE BOLD QUESTION (2026-09-15) - the second get-in-touch option
   ═══════════════════════════════════════════════════════════════════════
   Replaces "The Reveal" (a live typographic preview of a typed business
   name), removed on request. Deliberately built the opposite way this
   time: no live-preview mechanic, no bezel, no button - the premium
   feeling here comes entirely from restraint and type, not from more to
   look at. One large serif question, one open answer, nothing else.

   The textarea IS the real "message" field from the plain fields below,
   moved to the top of the same form and given real weight - not a second
   field to keep in sync with the first. */
/* PANELLED (2026-09-16). It used to float: a serif question with no frame,
   then a void where the answer goes, then a stray hairline - unrelated to the
   boxed fields directly beneath it. It is a panel now, cut from exactly the
   same material as .field: the same 1px hairline, the same 2px radius, the
   same focus language (lift, ambient shadow, warmth, label brightening). Same
   outer edges as every row below it, so the column reads as one object.

   The rule between question and answer is full-bleed, pulled out to the panel
   edges by its own padding, so the two halves are one divided object rather
   than two components sharing a margin. Padding is a variable for that reason
   - the bleed is calculated from it and cannot drift. */
.ask{
  --ask-pad:24px;
  margin-bottom:2px;
  padding:var(--ask-pad);
  border:1px solid var(--hair);
  border-radius:2px;
  transition:transform .3s var(--m-spring),
             box-shadow .25s var(--ease-out),
             background-color .25s var(--ease-out);
}
.ask:focus-within{
  transform:translateY(-1px);
  box-shadow:0 12px 28px -22px rgba(10,10,10,.5);
  background-color:rgba(242,201,76,.035);
}
.ask__eyebrow{
  margin:0 0 14px;
  font-family:var(--body); font-weight:600; font-size:11px;
  letter-spacing:.2em; text-transform:uppercase; color:var(--accent);
  transition:letter-spacing .2s var(--ease-out);
}
.ask:focus-within .ask__eyebrow{ letter-spacing:.22em; }
.ask__label{ display:block; cursor:text; }
.ask__q{
  display:block;
  font-family:var(--display); font-weight:700; letter-spacing:-.02em;
  font-size:clamp(23px,2.9vw,29px); line-height:1.2; color:var(--fg);
}
.ask__rule{
  display:block; height:1px; background:var(--hair);
  margin:20px calc(var(--ask-pad) * -1) 0;
}
.ask__input{
  display:block; width:100%; margin-top:18px; min-height:66px;
  font:inherit; font-family:var(--body); font-size:16px; font-weight:400;
  color:var(--fg); background:transparent;
  border:0; border-radius:0; padding:0;
  resize:none; overflow:hidden;
}
.ask__input::placeholder{ color:var(--muted); opacity:.7; }
.ask__input:focus{ outline:none; }
/* Caption, not panel furniture. Inside the frame it made the lower half of
   the box a block of grey and crowded the space meant for writing in. */
.ask__hint{
  margin:0; font-size:12.5px; line-height:1.5; color:var(--muted);
}

/* Phones get a tighter inset. The bleed follows it automatically. */
@media (max-width:560px){ .ask{ --ask-pad:18px; } }

@media (prefers-reduced-motion:reduce){
  /* Same split as .field: the lift is motion and goes, the shadow and the
     warmth are not motion and stay, so the panel still says where you are. */
  .ask{ transition:none; }
  .ask:focus-within{ transform:none; }
}


/* MOBILE-ONLY LINE BREAK (2026-09-15) - a reusable <br class="br--m"> for
   the rare case a heading needs a DIFFERENT wrap point on a phone than the
   one that reads best everywhere else, without splitting the mask-reveal
   into an extra line on every width. motion.js only splits [data-rv="mask"]
   headings on a PLAIN <br> (regex /<br\s*\/?>/i, no attributes) - a <br>
   carrying a class fails that match and is left embedded inside whichever
   masked line it sits in, so it can be hidden or shown per breakpoint like
   any other element while the line above it still reveals as one block. */
.br--m{ display:none; }
@media (max-width:900px){ .br--m{ display:block; } }


/* ═══════════════════════════════════════════════════════════════════════
   WHO THIS IS FOR (2026-09-16)
   ═══════════════════════════════════════════════════════════════════════
   Same hairline-grid language as .svc and .paths: 1px gaps over a --hair
   background so the cells are separated by the grid itself rather than by
   four floating bordered boxes. Nothing new invented for this section.

   Photos run in full colour (2026-09-16, Tomislav's call). They were
   desaturated at first to hold gold as the only colour on the page, but these
   are photographs of real places and the grey took the life out of them. */
.who__grid{
  list-style:none; margin:40px 0 0; padding:0;
  display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:1px;
  background:var(--hair); border:1px solid var(--hair);
}
.who__item{ background:var(--paper); display:flex; flex-direction:column; }
.band--ink .who__item{ background:var(--ink); }
/* The zoom (2026-09-16). The image is wrapped rather than scaled in place:
   without a clipping parent, a scaled photo grows over the title underneath
   it. The wrapper owns the height and hides the overflow, the image fills it,
   and only the image moves.

   Every device gets it, which takes two triggers rather than one. Pointer
   devices zoom on hover. Touch devices have no hover at all, so a tap adds
   .is-zoomed (see app.js) and a second tap, or a tap on another card, takes
   it off. Same distance and same timing in both cases, so it is one effect
   with two ways in, not two different effects. */
.who__media{
  display:block; overflow:hidden;
  height:clamp(148px,13vw,186px);
}
.who__item img{
  display:block; width:100%; height:100%;
  object-fit:cover; object-position:center;
  transform:scale(1); transform-origin:center;
  transition:transform .65s var(--ease-out);
  will-change:transform;
}
@media (hover:hover) and (pointer:fine){
  .who__item:hover img{ transform:scale(1.07); }
}
.who__item.is-zoomed img{ transform:scale(1.07); }
/* Touch cards are tappable, so say so with the cursor on anything that has a
   pointer, and keep the tap from selecting the caption text underneath. */
.who__media{ cursor:zoom-in; -webkit-tap-highlight-color:transparent; }
.who__item.is-zoomed .who__media{ cursor:zoom-out; }
.who__body{ padding:clamp(18px,1.9vw,24px); }
.who__body h3{ margin:0; font-size:17px; line-height:1.3; }
.who__body p{ margin:7px 0 0; color:var(--muted); font-size:14px; line-height:1.55; }
@media (max-width:1080px){ .who__grid{ grid-template-columns:repeat(2,minmax(0,1fr)); } }
@media (max-width:560px){ .who__grid{ grid-template-columns:1fr; } }

@media (prefers-reduced-motion:reduce){
  /* The zoom is motion and goes. Nothing is lost: the photograph was never
     carrying information the caption does not already give. */
  .who__item img{ transition:none; }
  .who__item:hover img,
  .who__item.is-zoomed img{ transform:none; }
}


/* A question linked to from another page has to land ON the answer, not
   under the fixed header, and it has to be open when you get there. The
   scroll margin clears the bar; app.js opens the row. */
.faq__row[id] { scroll-margin-top: 110px; }


/* ═══════════════ AREAS GRID (2026-09-17) ═════════════════════════════════
   Fourteen place links on the /web-design hub. .addons is the existing link-
   card list and was the obvious reuse, but it gives each row a heading, a
   description and its own hairline: fourteen of those is a page of its own,
   and these links are a directory, not fourteen offers.

   So: the same hairline-grid language as .svc and .paths, at the smallest
   size it reads at. Four across on desktop, two on a tablet, two on a phone -
   place names are short enough that one per row would waste the width, and
   every cell clears the 44px tap target at every step. */
/* Borders on the CELLS, not a hairline background behind a gapped grid.
   Fourteen items in a four-column grid leaves two empty tracks on the last
   row, and the gapped version painted those as two dead grey boxes. Drawing
   the lines per cell means the leftover space is simply the band. */
.areas{
  list-style:none; margin:36px 0 0; padding:0;
  display:grid; grid-template-columns:repeat(4,minmax(0,1fr));
  border-top:1px solid var(--hair); border-left:1px solid var(--hair);
}
.areas li{ border-right:1px solid var(--hair); border-bottom:1px solid var(--hair); }
.areas a{
  display:flex; align-items:center; justify-content:space-between; gap:12px;
  min-height:56px; padding:16px clamp(16px,1.6vw,22px);
  color:inherit; text-decoration:none;
  font-size:15.5px; letter-spacing:-.005em; line-height:1.3;
  transition:background-color .25s var(--ease-out), color .25s var(--ease-out);
}
.areas svg{ color:var(--accent); flex:none; transition:transform .3s var(--ease-fluid); }
@media (hover:hover) and (pointer:fine){
  .areas a:hover{ color:var(--accent); }
  .areas a:hover svg{ transform:translateX(4px); }
}
.areas a:focus-visible{ outline:2px solid var(--accent); outline-offset:-3px; }
@media (max-width:1080px){ .areas{ grid-template-columns:repeat(3,minmax(0,1fr)); } }
@media (max-width:720px){ .areas{ grid-template-columns:repeat(2,minmax(0,1fr)); } }
@media (prefers-reduced-motion:reduce){
  .areas a, .areas svg{ transition:none; }
}


/* The "Nearby" row on a suburb page (2026-09-17). These are navigation links
   sitting in a paragraph, and as plain inline text they measured 14px tall on
   a phone - the smallest tap target on the site, and under half the 44px we
   ship as standard and report on other people's sites. Inline-block with
   padding gives each one a real target without turning the row into buttons. */
.nearby{ display:flex; flex-wrap:wrap; gap:0 6px; align-items:center; margin-top:6px; }
.nearby a{
  display:inline-block; padding:11px 0; min-height:44px;
  line-height:22px; box-sizing:border-box;
}
.nearby span{ color:var(--muted); user-select:none; }


/* The opening line of a legal page (2026-09-17). .prose sets one body size for
   everything, which is right for an article and wrong for the sentence that
   tells you what the document is. Slightly larger and slightly darker, then
   the numbered clauses take over at the normal size. */
.prose__lede{
  font-size:clamp(18px,1.35vw,20.5px) !important; line-height:1.6 !important;
  color:rgba(10,10,10,.9) !important; max-width:58ch;
}


/* Clause headings on a legal page (2026-09-17). .prose h2 is sized for an
   article with five or six section breaks. /terms has fifteen numbered
   clauses and /privacy eleven, and at 34px a document that is mostly headings
   stops reading as a document. Down to the h3 step, with the number set in
   gold so the list structure is doing the work that size was doing. */
.prose--legal h2{
  font-size:clamp(19px,1.75vw,22px); letter-spacing:-.012em;
  margin-top:2.3em; margin-bottom:.35em;
}
.prose--legal h2 .n{ color:var(--accent); font-variant-numeric:tabular-nums; }
.prose--legal p{ font-size:16.5px; line-height:1.72; }
.prose--legal h2 + p{ margin-top:0; }


/* A second address for a section that already has one (2026-09-17). /#contact
   was the contact anchor on the site this replaces, it is in outreach going
   back months, and it draws 49 impressions. Here that section is #start, and
   renaming it would break every internal link already pointing at #start - so
   the old anchor sits alongside as an empty span. Zero-height and out of flow
   so it cannot affect layout, with the same scroll margin a band anchor gets
   so the fixed header does not land on top of the heading. */
.anchor-alias{
  display:block; height:0; width:0; overflow:hidden;
  scroll-margin-top:calc(var(--navh,85px) + var(--land-gap) - clamp(78px,10vw,140px));
}


/* Text for screen readers and crawlers only (2026-09-17).
   The three service cards link with "Read more", which Lighthouse and axe both
   flag: a link is read on its own, away from the card it sits in, so "Read
   more" tells a screen reader and a crawler nothing about where it goes. It
   cost the homepage and the services page 8 points of Lighthouse SEO.

   The visible design is not changed - Tomislav approved those cards as they
   are - so the destination is named in text that is present in the HTML and
   announced, but not painted. This is the standard technique, not cloaking:
   the words describe the link honestly and match where it actually goes. */
.u-sr{
  position:absolute; width:1px; height:1px; padding:0; margin:-1px;
  overflow:hidden; clip:rect(0 0 0 0); clip-path:inset(50%); white-space:nowrap; border:0;
}


/* ═══════ THE HERO FITS THE SCREEN, NOT JUST A TALL ONE (2026-09-17) ═══════
   The hero is sized as nav height + content, against an available height of
   100svh minus the ticker strip. On tall windows that lands exactly on the
   fold. On short ones it did not, and the strip was cut in half:

     1280x600   needed 624px, had 600   strip cut by 24px
     740x360    needed 366px, had 360   strip cut by  6px

   The cause was the bar, not the type. At 1280x600 the nav stood 176px tall,
   29% of the whole screen, because its padding and logo are sized off WIDTH
   and that window is wide but short. The same diagnosis is already recorded
   above for landscape phones on 2026-09-06; this is the tier between that one
   and a full-height desktop, which nothing covered.

   Bounded by min-height:521px so it cannot reach past the landscape-phone
   rules, which already shrink the bar further. */
@media (max-height:720px) and (min-height:521px) and (min-width:641px){
  .nav{ padding-block:12px; }
  .nav__mark img{ height:52px; }
}

/* Landscape phones were 6px over. The bar is already as small as it should
   go there, so the last of it comes out of the hero's own rhythm. */
@media (max-height:520px){
  .hero__inner .eyebrow--pill{ margin-bottom:4px; }
  .hero__h1{ margin-top:0; }
  .hero__lede{ margin:4px 0 6px; }
}


/* The dental photo is anchored to the TOP of its frame (2026-09-17).
   The card is 304x186 and the photo 1120x746, so object-fit:cover has 61px of
   the source to discard. Centred, it took 30 of those off the top and clipped
   the overhead dental light - and the light, the tooth models on the window
   ledge and the chair are the three things that say "dental practice" at a
   glance. Anchoring to the top keeps the whole light head and spends the trim
   on the floor at the bottom, which carries nothing.

   A tighter crop was tried first and rejected: it made the chair bigger but
   lost the light and the window entirely, which Tomislav caught straight away
   ("too much zoomed in, I want to see all"). This keeps the full width of the
   room and every one of those details. */
.who__item img[src*="clinics"]{ object-position:50% 0%; }

/* Below 1080px the grid drops to two columns and then one, and the card goes
   from 304x186 to roughly 348x148 - a letterbox. cover then has 270 source
   rows to discard instead of 61, and top-anchoring cut the chair off entirely
   on a phone. Measured at each width rather than assumed.
   Pulled up to 50% and then to 70% (2026-09-18, twice). Moving the frame up
   reveals further down the room, which is what brings the chair into it. The
   light is the cost, and on a card this shape something has to be. 70% is
   about as far as it goes before the window and the tooth models on the ledge
   start leaving the top of the frame. */
@media (max-width:1080px){
  .who__item img[src*="clinics"]{ object-position:50% 70%; }
}


/* ═══ THE POST-AUDIT LINK, NOT A BUTTON (2026-09-18) ═══════════════════════
   This sits directly under a report we have just handed over for nothing. As
   a gold pill it carried the same visual weight as "Run your audit" at the top
   of the page, and under a list of someone's own faults that reads as a sales
   close.

   The design rules already ask for "CTAs as typographic links with gold
   accents rather than generic pill buttons"; this one was the exception and
   there was no reason for it to be. Quiet is also the more confident position:
   the findings do the persuading, and the next step is simply there when the
   reader wants it.

   Scoped to .result__cta only. Every other .cta on the site keeps its pill. */
/* The result CTA is the same premium pill as every other button (2026-10-10). */

/* .rgroup__title became an h3 on 2026-09-18 to close a heading-order jump in
   the report (the section h2 went straight to h4). It sets its own font-size,
   weight, tracking and family, but was leaning on the h4 default for line
   height - and the shared h3 rule carries 1.24. Pinned so the change is
   semantic only. Verified by measuring the computed box before and after. */
.rgroup__title{ line-height:1.35; }


/* The cluster link above "keep reading" on a post (2026-09-18). Points at the
   hub the post belongs to, so a reader who wants the whole group gets it in
   one click and the hub collects a link from every one of its own posts. */
.post__hub{ margin:0 0 14px; font-size:14.5px; }
.post__hub a{ border-bottom:1px solid color-mix(in srgb, currentColor 32%, transparent); padding-bottom:2px; }

/* ══════════════════════════════ HOW IT WORKS, GUIDED (2026-09-19) ═══════════
 *
 * The seven steps used to stand in a column, three deep in each of the first
 * two phases, and the section ran to 4,487px on a desktop - about two fifths of
 * the whole homepage. Tomislav: compact it, keep all seven steps, keep the
 * animation, keep the scrolling mechanism.
 *
 * WHAT IT DOES NOW. Each phase lays its steps out in a row and holds that row
 * still, pinned at one place on the screen, while the reader carries on
 * scrolling down. That scrolling is spent moving the row sideways from card to
 * card. When the row runs out of cards it lets go and the page continues to the
 * next phase, which does the same. Down, right, down, right, with one gesture.
 * Measured against the section it replaces: 27% shorter on a phone, 29% at
 * 1280, 27% at 1440, 24% at 1920.
 *
 * EVERY RULE HANGS OFF .journey-guided, WHICH ONLY SCRIPT ADDS. Without
 * JavaScript there is no class and no wrappers, so the section renders as it
 * always has: seven steps in a column, all of them readable. That is the build
 * standard this site sells and it is not waived for its own homepage.
 *
 * FOUR ELEMENTS, ONE JOB EACH:
 *   .journey__phaseContent  the grid cell, given an explicit height by script.
 *                           That height IS the lock - a sticky child can only
 *                           hold while its container is still going past.
 *   .hx-stick               sticky. The thing that holds still.
 *     .hx-window            clips, and fades at its edges. Never moves.
 *       .hx-track           the cards side by side. The only thing that moves.
 *
 * They cannot be combined. Translating the element that clips takes the window
 * off the screen along with the cards. And a sticky element inside an
 * overflow:hidden ancestor sticks to that box rather than to the page, so it
 * would never move at all.
 */

/* A TRANSFORM ON AN ANCESTOR MOVES A PINNED ROW. .band carries the page's
   parallax, translate3d(0,var(--lift),0) run by motion.js, and with it the
   "locked" row slid 4.8px up the screen while its cards changed. Measured with
   it off: 0px, held at the same pixel throughout. The head lifts instead, so
   the rhythm is still visible and nothing above the row transforms. */
body.journey-guided #how{ transform:none; }
body.journey-guided #how .journey__head{ transform:translate3d(0,var(--lift,0),0); }

body.journey-guided .journey__phaseBlock{ position:relative; }
body.journey-guided .journey__phaseContent{ display:block; overflow:visible; }
body.journey-guided .hx-stick{ display:block; position:sticky; z-index:1; }
body.journey-guided .hx-window{
  display:block; overflow:hidden;
  padding-bottom:30px;              /* room for the downward pointer */
  perspective:1200px;
  touch-action:pan-y;               /* the page scrolls; this never captures it */
  /* The window fades at its edges rather than cutting a card through the middle
     of a word. Driven from script by how far the row has travelled, so the
     first card starts with a clean edge and the last finishes with one. */
  -webkit-mask-image:linear-gradient(to right,
    transparent 0, #000 var(--hx-fadeL,0px),
    #000 calc(100% - var(--hx-fadeR,0px)), transparent 100%);
  mask-image:linear-gradient(to right,
    transparent 0, #000 var(--hx-fadeL,0px),
    #000 calc(100% - var(--hx-fadeR,0px)), transparent 100%);
}
body.journey-guided .hx-track{
  display:flex; align-items:start; gap:0;
  will-change:transform;
  /* NO TRANSITION HERE ANY MORE (2026-09-19). The smoothing moved into app.js,
     because a transition meant the browser knew where the row was and the code
     did not - which is how the bar came to name a card that was still 86% off
     the screen. The frame loop holds the position, draws from it, and names the
     card from the same number. */
}

/* One card at a time with the next showing at the edge. A card cut by the edge
   of the window is the oldest signal there is that something continues that
   way, and it works without a word or an icon. */
/* THE FOURTH GREY LINE (2026-09-25). Tomislav, repeatedly, across several
   rounds: "grey lines shooting across all 7 cards", "on the rails", "on
   mobile view". Three earlier rounds each found and removed a real,
   different element - a drawn progress bar on the card (2026-09-21), then a
   7-segment progress strip pinned to the screen - and each time the line was
   still there, because this one was never touched: every card except the
   last carried `border-right:1px solid var(--hair)`, a real 1px grey line
   (rgba(244,241,234,.14) on this dark section) at the boundary of every
   adjacent pair of cards. Confirmed on the live page, not guessed: computed
   style on the first `.journey__step` was exactly that border.
   Static, it reads as a faint vertical divider. But `.hx-track` is the thing
   that moves - the whole row, border included, translates left under the
   reader on every scroll - so a vertical line sitting still in a photo
   becomes a line sweeping across the screen the moment the row is actually
   in motion, which a screenshot taken between motions cannot show and three
   rounds of screenshots kept missing.
   Removed rather than toned down, same reasoning as the other two: "next
   card showing at the edge" already tells the reader this continues -
   the comment directly below this rule says so - so a line reinforcing that
   was never carrying information of its own. */
/* THE GUTTER WAS NEVER REACHING MOBILE (2026-09-25). app.js already computes
   the right number for every width - `innerWidth > 900 ? 0 :
   Math.max(18, clientWidth * 0.06)` - and writes it to --hx-gutter on every
   card, on every screen. But the rule that actually SPENDS that variable,
   `calc(100% - var(--hx-gutter))`, only ever existed inside
   `@media (min-width:901px)` below. Mobile was stuck on a hardcoded 82%
   the whole time - an 18% sliver of the next card, about 70px on a 390px
   phone - regardless of what the script had already worked out and set.
   That sliver sits behind the window's edge-fade mask, and the row it
   belongs to is the one thing that moves on every scroll, which is a grey
   shape sliding across the screen on mobile and nowhere on desktop, because
   desktop's gutter (0 past 900px) correctly hides its neighbour completely.
   Tomislav confirmed desktop is right and asked why only mobile still shows
   it - this is why: not a new line, the SAME gutter desktop already gets,
   simply never wired up to apply below 901px. One rule now, no width gate,
   so the number the script computes is the number the layout uses,
   everywhere, the way "the card and the place it rests cannot disagree" was
   always meant to. */
body.journey-guided .journey__step{
  position:relative; flex:0 0 calc(100% - var(--hx-gutter, 24px)); min-width:0;
  padding:16px 22px 8px;
  border:none;
  /* Everything that is not the current step stands back rather than
     disappearing, so the whole phase stays readable and only the emphasis
     moves. */
  /* 34% failed Chrome's own contrast check at roughly 2.8:1 against a 4.5:1
     minimum, for everybody who has not asked for reduced motion. 48% passes and
     the card still reads as stood back, which is the whole point of it.
     Approved 2026-09-22: "if it's still visible enough to read, I'm ok with
     that." */
  opacity:.48; transform:translateY(6px);
  transition:opacity .45s var(--ease), transform .45s var(--ease);
}
/* ARRIVING IS QUICK, LEAVING IS SLOW (2026-09-19).
   Tomislav: card 5 stays darkened, it is the only one that does.
   He is right, and it is the middle card of each row - 2 and 5 - for a reason
   that is arithmetic rather than styling. Measured on the live page at 1440:
   step 1 is lit across 730px of scroll, step 3 across 600, step 7 across 1050,
   because the first card of a row is also lit while the row approaches and the
   last stays lit while it leaves. The middle card has neither. It gets 140px,
   which at an ordinary mouse wheel is about a second, and the fade-in took
   .45s of that before it even reached full strength.
   So becoming the current card is now quick and standing down stays slow. The
   asymmetry is the point: a card that is being read should arrive immediately,
   and one that is finished can take its time. */
body.journey-guided .journey__step[data-current]{
  opacity:1; transform:none;
  transition-duration:.16s;
}
body.journey-guided .journey__step[data-current] .journey__fig{ transition:filter .18s ease; }
/* Three tiers, not two: where you are, where you go next, and the rest, so the
   brightness itself slopes towards where to go. One lit step among six dark
   ones is a position, not a direction. */
body.journey-guided .journey__step[data-next]{ opacity:.72; transform:translateY(2px); }

body.journey-guided .journey__stepNum{ position:static; display:block; margin-bottom:6px; }
body.journey-guided .journey__step h3{
  margin-bottom:10px; min-height:2.3em; align-items:start;
}
body.journey-guided .journey__fig{ width:100%; max-width:min(100%,300px); margin-top:14px; }

@media (min-width:901px){
  /* Wide enough that each card in turn can come to rest at the left with the
     next one showing beside it. Much below half the window they would share a
     resting place and the row would have nothing to do on the last move. */
  /* ONE CARD ON THE SCREEN, LIKE THE PHONE (2026-09-19). At 56% of the window
     the next card stood beside the current one, lines and all, competing for
     the eye - "messy and chaotic", and he is right. The card is the window
     less the gutter it rests against, so the next card's leading edge lands
     exactly on the window edge: still there, still sliding in on cue, but not
     visible until it is the one being read. The gutter comes from the script
     that positions the row, so the width and the resting place are the same
     number rather than two that have to be kept in step. */
  /* The gutter-based width itself now lives in the base rule above and
     applies at every width - this block only carries what is genuinely
     desktop-only below. A card now spans the whole window, which left the
     number, the title and the panel huddled at the left with the rest of it
     empty. The content keeps a
     readable measure and sits in the middle of the card instead, so the space
     the next card used to occupy reads as air rather than as something missing. */
  body.journey-guided .journey__stepInner{ max-width:560px; margin-inline:auto; }
  body.journey-guided .journey__fig{ max-width:min(100%,340px); }
  /* The last phase holds one step, so it takes the full row: there is no next
     card to show beside it. */
  body.journey-guided .hx-track .journey__step:only-child{ flex:0 0 100%; }
  body.journey-guided .hx-track .journey__step:only-child .journey__fig{ max-width:min(100%,360px); }
}

/* ── the route, drawn rather than implied ────────────────────────────────────
   Several cues point the same way at once, on purpose: any one of them read on
   its own gets a reader to the next step. */

/* 1. A PATH, NOT SEVEN ISLANDS. Each step carries a segment of track along its
      top: gold behind you, lit where you are, faint ahead. */
/* NO PROGRESS BAR DRAWN ON THE CARD (2026-09-21).
   Tomislav, three times: "grey lines shooting across", "on all 7 cards", "on
   both mobile and desktop view".

   This was it, and it was a deliberate element rather than a bug, which is why
   three rounds of looking for a fault never found it:

     .journey__step::before  a 2px bar across the FULL WIDTH of every card, at
                             the top, rgba(244,241,234,.13). A faint grey line.
                             Seven of them, one per card, on both widths.
     .journey__step::after   a gold dot that RODE along that bar end to end over
                             1.1 seconds each time a card became current.

   Grey lines, on all seven cards, with something shooting across them.

   Found by diffing PIXELS between frames rather than reading the DOM. Every
   earlier measurement read opacity and rectangles, which says what the browser
   was asked to do, not what appeared on the screen. The pixel diff found it in
   one run: a 2px full-width row climbing the window at exactly scroll speed.

   Removed rather than toned down. The strip underneath already says where you
   are, in words - "3 of 7, The Decision, next: The Build" - with its own
   segments. A second progress indicator drawn on the card, with a moving part,
   was telling the reader nothing the strip does not tell them more quietly. */

/* 3. AN ARROW IN THE GAP, lit for the one to follow. At the end of a row it
      turns to face down, because that is where the next step actually is. The
      two moments a reader has to leave a row were the two with no cue at all. */
.hx-chev{
  position:absolute; top:46%; right:-9px; z-index:3;
  width:16px; height:16px; pointer-events:none;
  opacity:0; transform:translateX(-6px) scale(.8);
  transition:opacity .35s ease, transform .35s var(--ease);
}
.hx-chev::before,.hx-chev::after{
  content:""; position:absolute; left:3px; top:50%;
  width:9px; height:2px; background:var(--gold); border-radius:2px;
  transform-origin:right center;
}
.hx-chev::before{ transform:translateY(-1px) rotate(45deg); }
.hx-chev::after{ transform:translateY(-1px) rotate(-45deg); }
body.journey-guided .journey__step[data-current] .hx-chev{
  opacity:1; transform:none;
  animation:hxNudge 1.5s var(--ease) infinite;
}
@keyframes hxNudge{ 0%,100%{ transform:translateX(0); } 45%{ transform:translateX(5px); } }

.hx-chev--down{
  top:auto; bottom:-26px; right:auto; left:50%;
  width:20px; height:20px;
  transform:translateX(-50%) rotate(90deg) scale(.8);
}
body.journey-guided .journey__step[data-current] .hx-chev--down{
  opacity:1; transform:translateX(-50%) rotate(90deg);
  animation:hxNudgeDown 1.5s var(--ease) infinite;
}
@keyframes hxNudgeDown{
  0%,100%{ transform:translateX(-50%) rotate(90deg) translateX(0); }
  45%    { transform:translateX(-50%) rotate(90deg) translateX(6px); }
}

/* 4. THE DROP BETWEEN ROWS. A ride is continuous: the route used to run right
      along a row and then simply reappear at the left of the next one, which is
      a jump cut rather than a track. Each phase ends in a rail down the gap
      with the marker running down it. */
.hx-drop{
  position:absolute; left:50%; bottom:-2px; width:2px; height:34px;
  transform:translateX(-50%); pointer-events:none; z-index:2;
}
.hx-drop__rail{
  position:absolute; inset:0; background:rgba(244,241,234,.13);
  transform:scaleY(0); transform-origin:top center;
  transition:transform .5s var(--ease), background-color .4s ease;
}
.hx-drop.is-live .hx-drop__rail{
  transform:scaleY(1);
  background:linear-gradient(to bottom,var(--gold),rgba(242,201,76,.25));
}
.hx-drop__car{
  position:absolute; left:50%; top:0;
  width:8px; height:8px; margin-left:-4px; border-radius:50%;
  background:var(--gold); opacity:0; transform:translateY(-4px) scale(.5);
}
.hx-drop.is-live .hx-drop__car{ opacity:1; animation:hxDrop 1.4s var(--ease) infinite; }
@keyframes hxDrop{
  0%  { transform:translateY(-4px) scale(.55); opacity:0; }
  25% { opacity:1; }
  100%{ transform:translateY(30px) scale(1); opacity:0; }
}

/* 5. IN WORDS. A visual cue can be missed; a sentence cannot. Named steps
      rather than anonymous dots, because "3 of 7, The Decision" tells somebody
      something and a row of dots does not. */
.hx-progress{
  display:flex; position:sticky; bottom:0; z-index:5;
  align-items:center; gap:12px;
  padding:10px var(--gut,22px);
  background:linear-gradient(to top,rgba(10,10,10,.96),rgba(10,10,10,0));
  font:400 13px/1.3 var(--body); color:var(--bone);
}
/* THE SEVEN BARS ARE GONE (2026-09-22).
   Tomislav, five separate times: grey lines shooting through all seven cards.
   This was them, and it was never on the cards at all.

   .hx-progress__tick was seven 2px bars, one per card, faint bone at 20% over
   near-black, stretched across the width - 128px each at 1440 - inside a strip
   that is position:sticky at the BOTTOM of the viewport, so it is on screen the
   whole time the section is. They changed colour one after another, left to
   right, as the row advanced.

   Seven grey lines, one per card, moving as you scroll. Four earlier attempts
   went looking on the cards themselves while this sat directly underneath them,
   which is what happens when you keep measuring the thing you already suspect.

   The strip keeps its words. "3 of 7, The Decision, next: The Build" says
   everything the bars said, and says it better, because it names the card
   rather than drawing an anonymous bar for it. */
.hx-progress__ticks{ display:none; }
.hx-progress__label{ white-space:nowrap; opacity:.85; }
.hx-progress__label b{ color:var(--gold); font-weight:600; }
.hx-progress__next{
  margin-left:10px; padding-left:10px;
  border-left:1px solid rgba(244,241,234,.22); opacity:.72;
}
.hx-progress__next::before{ content:"\2192"; margin-right:6px; color:var(--gold); opacity:.9; }
/* The end has to read as arriving, not as the guidance running out. */
.hx-progress__next--end{ opacity:.9; }
.hx-progress__next--end::before{ content:"\2713"; }

/* The phase being moved INTO lights before the reader arrives, so the place the
   route is heading is visible rather than discovered. */
body.journey-guided .journey__phaseStickyName,
body.journey-guided .journey__phaseStickyNum{ transition:color .45s ease; }
body.journey-guided .journey__phaseBlock[data-phase-current] .journey__phaseStickyName{ color:var(--bone); }
body.journey-guided .journey__phaseBlock[data-phase-ahead] .journey__phaseStickyName{ color:var(--gold); }
body.journey-guided .journey__phaseBlock[data-phase-ahead] .journey__phaseStickyNum{ color:var(--gold); opacity:.85; }
/* THE DIM GOES ON THE NAME, NOT ON THE WHOLE LABEL (2026-09-19).
   Dimming the block took the phase NUMBER with it - "[02]" is already set in
   --muted, and half of that is 29% of bone against near-black, which is a
   contrast ratio of about 2.4 where 4.5 is the floor for text that size. The
   site's own accessibility gate failed it, which is what that gate is for.
   The name is large display type, so at half strength it still clears the bar
   comfortably, and the number simply stays readable. */
body.journey-guided .journey__phaseBlock:not([data-phase-current]):not([data-phase-ahead]) .journey__phaseStickyName{
  opacity:.5; transition:opacity .45s ease;
}
body.journey-guided .journey__phaseBlock[data-phase-ahead] .journey__phaseStickyName,
body.journey-guided .journey__phaseBlock[data-phase-current] .journey__phaseStickyName{ opacity:1; }

/* ── how a card arrives ──────────────────────────────────────────────────────
   Straight up from below, no rotation, staggered along the row as the phase
   reaches the screen, so the eye is given an order to read in before it has to
   choose one. Transform and opacity only: no layout is touched, so it cannot
   cost a frame or shift the page while it settles. */
body.journey-guided .journey__fig{
  opacity:0; transform:translateY(42px) scale(.97);
  transition:
    opacity .66s var(--ease) var(--open-delay,0ms),
    transform .66s var(--ease) var(--open-delay,0ms);
}
body.journey-guided .journey__step.is-open .journey__fig{ opacity:1; transform:none; }
/* The current card sits very slightly forward of the others: enough to read as
   "this one", not enough to be a bounce. */
body.journey-guided .journey__step[data-current].is-open .journey__fig{ transform:translateY(-3px); }
body.journey-guided .journey__step[data-next].is-open .journey__fig{
  transform:perspective(900px) rotateY(-3deg) translateX(-3px);
}

/* THE ANIMATION RUNS ON THE STEP BEING READ AND HOLDS ON THE OTHERS. Every
   panel keeps its picture, nothing goes blank, but only the current one moves,
   so there is one thing to watch instead of seven. */
body.journey-guided .journey__step:not([data-current]) .actstage,
body.journey-guided .journey__step:not([data-current]) .actstage *{ animation-play-state:paused; }
body.journey-guided .journey__step:not([data-current]) .journey__fig{
  filter:saturate(.55) brightness(.82); transition:filter .5s ease;
}
body.journey-guided .journey__step[data-current] .journey__fig{
  filter:none; box-shadow:0 18px 46px -22px rgba(0,0,0,.75);
}
body.journey-guided .journey__stepNum{ transition:color .4s ease, transform .4s var(--ease); transform-origin:left center; }
body.journey-guided .journey__step[data-current] .journey__stepNum{ color:var(--gold); transform:scale(1.35); }
body.journey-guided .journey__step[data-next] .journey__stepNum{ color:rgba(242,201,76,.78); }
body.journey-guided .journey__step h3{ transition:transform .45s var(--ease); }
body.journey-guided .journey__step[data-current] h3{ transform:translateY(-2px); }

@media (hover:hover) and (pointer:fine){
  body.journey-guided .journey__step:hover{ border-left:none; background:none; }
}

/* ONE STAGE ON SCREEN, NOT THREE (2026-09-21).
   Tomislav: "on all 7 cards there is a glitch on them... around it there's some
   kind of squares and rectangles glitching / flashing around".

   Measured at 1440x900, parked in the lock: the phase being read held 590px of
   the screen, and a SECOND phase's stage was showing at the same time - 58px at
   one position, 328px at another, and 140px of a third from above. Those
   neighbours are dimmed to grey by the not-current rule below, so what is on
   the screen beside the card being read is a grey rounded rectangle that slides
   in and out as the page moves. Three stages, one of them animating.

   A phone never showed this because its card fills the screen. A desktop card is
   590px in a 900px window, so there is room either side for the rows before and
   after to sit in.

   So the stage that is not being read is not painted. Faded rather than cut,
   because the rows change over while the drop marker runs between them, and a
   hard switch there would read as the flicker this removes. */
@media (min-width:901px){
  body.journey-guided .journey__phaseBlock .hx-window{
    opacity:0; transition:opacity .3s var(--ease);
  }
  body.journey-guided .journey__phaseBlock[data-phase-current] .hx-window{ opacity:1; }
}

/* Anyone who has asked for less motion gets no lock, no pinning and no ride:
   every card on, at full strength, in a row they can read straight through. The
   same information without the trip. */
@media (prefers-reduced-motion:reduce){
  body.journey-guided .journey__phaseContent{ height:auto !important; }
  body.journey-guided .hx-stick{ position:static; }
  body.journey-guided .hx-window{ overflow:auto; -webkit-mask-image:none; mask-image:none; }
  body.journey-guided .hx-track{ transition:none; }
  body.journey-guided .journey__step{ opacity:1; transform:none; transition:none; }
  body.journey-guided .journey__step[data-current] .hx-chev,
  body.journey-guided .journey__step[data-current] .hx-chev--down{ animation:none; }
  body.journey-guided .journey__step:not([data-current]) .actstage,
  body.journey-guided .journey__step:not([data-current]) .actstage *{ animation-play-state:running; }
  body.journey-guided .journey__step:not([data-current]) .journey__fig{ filter:none; }
  body.journey-guided .journey__fig{ opacity:1 !important; transform:none !important; transition:none; }
  /* Every row on, because there is no ride to follow and nothing to focus. */
  body.journey-guided .journey__phaseBlock .hx-window{ opacity:1; transition:none; }
  .hx-drop.is-live .hx-drop__car{ animation:none; opacity:.9; transform:translateY(12px); }
  .hx-drop__rail{ transition:none; }
}


/* ═════════════ THE ONE-FIELD INTERROGATION, /check (2026-09-29) ══════════
   The same real audit tool as the homepage's #check section - same form
   IDs, same app.js, same api/score.js - stripped to one question on its
   own page. No seal, no score readout sitting next to it, no other
   section competing for the same screen. The emptiness is the design:
   confidence reads as a page that isn't asking for anything else. */
.interrogation{
  min-height:100svh; display:flex; flex-direction:column;
  justify-content:center; align-items:center; text-align:center;
}
.interrogation__inner{ max-width:640px; width:100%; }
.interrogation__q{ margin:18px 0 0; }
.interrogation__form{
  display:flex; align-items:center; justify-content:center; gap:18px;
  flex-wrap:wrap; margin-top:44px;
}
.interrogation__field{
  display:flex; align-items:center; gap:10px; position:relative;
  border-bottom:2px solid var(--hair); padding-bottom:10px;
  flex:1 1 320px; max-width:420px; transition:border-color .2s var(--ease);
}
.interrogation__field:focus-within{ border-color:var(--accent); }
.interrogation__prompt{
  font-family:ui-monospace,'SF Mono','Cascadia Code','Courier New',monospace;
  font-size:clamp(20px,2.6vw,28px); color:var(--accent); font-weight:700;
}
.interrogation__field input{
  flex:1; min-width:0; background:none; border:0; outline:0;
  font-family:ui-monospace,'SF Mono','Cascadia Code','Courier New',monospace;
  font-size:clamp(18px,2.4vw,24px); color:var(--fg);
}
.interrogation__field input::placeholder{ color:var(--muted); }
/* The decorative cursor only means "type here" - once real text exists,
   the input's own native caret is the cursor, and two of them would
   argue with each other. */
.interrogation__cursor{
  width:2px; height:1em; background:var(--accent); flex:none;
  animation:logBlink 1s steps(1) infinite;
}
.interrogation__field input:focus ~ .interrogation__cursor,
.interrogation__field input:not(:placeholder-shown) ~ .interrogation__cursor{
  display:none;
}
@media (prefers-reduced-motion:reduce){
  .interrogation__cursor{ animation:none; opacity:.6; }
}
.interrogation__note{ margin-top:22px; font-size:13.5px; color:var(--muted); }
.interrogation__result{ width:100%; max-width:640px; margin-top:8px; }
.interrogation__result .result{ margin-left:auto; margin-right:auto; text-align:left; }


/* ═════════════ THE PEEL-BACK, /service-website-audits (2026-09-29) ═══════
   Third of the five design-led ideas. Same plain mockup shown three times,
   each pass with one more invisible layer drawn over it: what's actually
   there, how fast it arrives, what a search engine reads that a visitor
   never sees. Revealed on the existing scroll-reveal system (data-rv),
   not a new pinned-scroll rig - proven, reduced-motion-safe, and honest
   about what it is: illustrative, never a real business's page. */
.xray{
  display:grid; grid-template-columns:repeat(3,1fr); gap:clamp(20px,3vw,36px);
  margin-top:clamp(40px,5vw,64px);
}
@media (max-width:900px){ .xray{ grid-template-columns:1fr; max-width:420px; margin-inline:auto; } }

.xray__mock{
  position:relative; border-radius:10px; border:1px solid var(--hair);
  background:var(--paper); box-shadow:0 18px 40px -24px rgba(10,10,10,.28);
  padding:34px 14px 20px; overflow:hidden;
}
.xray__dot{
  position:absolute; top:12px; width:6px; height:6px; border-radius:50%;
  background:rgba(10,10,10,.18);
}
.xray__dot:nth-child(1){ left:12px; }
.xray__dot:nth-child(2){ left:23px; }
.xray__dot:nth-child(3){ left:34px; }
.xray__bar{
  position:absolute; top:10px; left:50px; right:12px; height:10px;
  border-radius:5px; background:rgba(10,10,10,.06);
}
.xray__block{ display:block; border-radius:4px; background:rgba(10,10,10,.08); }
.xray__block--hero{ height:38px; margin-bottom:8px; background:rgba(10,10,10,.12); }
.xray__block--a{ height:10px; width:78%; margin-bottom:6px; }
.xray__block--b{ height:10px; width:92%; margin-bottom:6px; }
.xray__block--c{ height:10px; width:56%; }

/* The tag is the whole point: one gold annotation drawn over the exact
   same mockup, naming the layer this step is about. */
.xray__tag{
  position:absolute; font-family:ui-monospace,'SF Mono','Cascadia Code','Courier New',monospace;
  font-size:11px; font-weight:700; letter-spacing:.02em; color:#0A0A0A;
  background:var(--gold); padding:3px 7px; border-radius:4px; white-space:nowrap;
}
.xray__tag--speed{ top:34px; right:10px; }
.xray__tag--seo{ top:8px; left:50px; transform:translateY(-50%); }
.xray__tag--seo2{ bottom:14px; right:14px; }
.xray__mock--speed .xray__block--hero,
.xray__mock--seo .xray__bar{ outline:2px solid var(--gold); outline-offset:2px; }

.xray__label{
  margin:16px 0 0; font-size:12px; font-weight:600; letter-spacing:.1em;
  text-transform:uppercase; color:var(--accent);
}
.xray__note{ margin:6px 0 0; font-size:13.5px; line-height:1.55; color:var(--muted); }


/* ═════════════ THE SPLIT-SCREEN REVEAL, /check (2026-09-29) ══════════════
   Fourth of the five design-led ideas. A real screenshot of the first
   screen, real measured boxes drawn only where a finding maps to a real
   element - never a box on nothing. This component only ever appears on
   the dark /check page, so it is written for that background directly
   rather than carrying a light-mode variant nothing will ever use. */
.xrayview{ margin-bottom:34px; text-align:left; }
.xrayview__label{
  margin:0 0 16px; font-size:12px; font-weight:600; letter-spacing:.1em;
  text-transform:uppercase; color:var(--accent);
}
/* ── SPLIT-SCREEN REVEAL (2026-10-10) ────────────────────────────────────
   The real screenshot on the left, each finding on the right, and a line
   from the finding to the exact spot it is about. Every coordinate comes
   from api/xray.js measuring the rendered page in real Chrome - nothing is
   placed by hand. Numbers tie a finding to its dot, so on a narrow screen
   (where the lines would cross) the panels stack and the numbers still map
   one to the other. */
.xrayview__split{
  position:relative; display:grid; grid-template-columns:1.05fr .95fr;
  gap:clamp(30px,5vw,60px); align-items:start;
}
.xrayview__shot{
  position:relative; border-radius:12px; overflow:hidden;
  border:1px solid rgba(244,241,234,.16); background:#000; z-index:2;
}
.xrayview__shot img{ display:block; width:100%; height:auto; }
.xrayview__dot{
  position:absolute; transform:translate(-50%,-50%);
  display:flex; align-items:center; justify-content:center;
  width:23px; height:23px; border-radius:50%;
  font-family:var(--body); font-size:12px; font-weight:700; color:#0A0A0A;
  border:2px solid rgba(10,10,10,.5); box-shadow:0 2px 8px rgba(0,0,0,.45);
  z-index:3;
}
.xrayview__dot--ok{ background:#4ADE80; }
.xrayview__dot--fail{ background:#F87171; }
.xrayview__dot--info,.xrayview__dot--unknown{ background:var(--gold); }
.xrayview__finds{ list-style:none; margin:0; padding:0; display:grid; gap:11px; position:relative; z-index:2; }
.xrayview__find{
  display:flex; align-items:center; gap:13px; padding:13px 15px;
  border-radius:10px; border:1px solid rgba(244,241,234,.14);
  background:rgba(244,241,234,.03);
}
.xrayview__num{
  flex:none; display:flex; align-items:center; justify-content:center;
  width:23px; height:23px; border-radius:50%;
  font-size:12px; font-weight:700; color:#0A0A0A;
}
.xrayview__find--ok .xrayview__num{ background:#4ADE80; }
.xrayview__find--fail .xrayview__num{ background:#F87171; }
.xrayview__find--info .xrayview__num,.xrayview__find--unknown .xrayview__num{ background:var(--gold); }
.xrayview__findLabel{ font-size:14px; line-height:1.4; color:var(--bone); }
.xrayview__find--fail .xrayview__findLabel{ color:#F87171; }
/* the connector lines, drawn by the render script */
.xrayview__wires{ position:absolute; inset:0; width:100%; height:100%; pointer-events:none; z-index:1; overflow:visible; }
.xrayview__wire{ stroke-width:1.5; opacity:.55; }
.xrayview__wire--ok{ stroke:#4ADE80; }
.xrayview__wire--fail{ stroke:#F87171; }
.xrayview__wireEnd{ opacity:.55; }
.xrayview__wireEnd--ok{ fill:#4ADE80; }
.xrayview__wireEnd--fail{ fill:#F87171; }
@media (max-width:860px){
  .xrayview__split{ grid-template-columns:1fr; gap:22px; }
  .xrayview__shot{ max-width:420px; margin-inline:auto; }
  .xrayview__wires{ display:none; }
}

.xrayview__invlist{ list-style:none; margin:26px 0 0; padding:0; }
.xrayview__inv{
  display:flex; justify-content:space-between; align-items:baseline; gap:12px;
  padding:10px 0; border-bottom:1px solid rgba(244,241,234,.12);
  font-size:13px;
}
.xrayview__inv b{ font-weight:600; }
.xrayview__inv span{ color:var(--muted); text-align:right; font-size:12px; max-width:28ch; }
.xrayview__inv--fail b{ color:#F87171; }
.xrayview__inv--ok b{ color:#4ADE80; }
@media (max-width:640px){
  .xrayview__inv{ flex-direction:column; gap:2px; }
  .xrayview__inv span{ text-align:left; max-width:none; }
}
}
@media (max-width:640px){
  .interrogation__form{ flex-direction:column; align-items:stretch; }
  .interrogation__field{ max-width:none; }
  .interrogation__go{ width:100%; justify-content:center; }
}

/* ═══════ HOW IT WORKS: THE CARDS SLIDE AS THEY ALWAYS DID, WITH NOTHING GREY (2026-09-30) ═══════
   Tomislav, for the fifth round: "on both desktop and mobile on the how it works
   section there's a grey line flickering across all 7 cards", then "it's only
   visible on Firefox". The sideways movement of the row is the part of this
   section he values and it is NOT changed here: app.js moves the row exactly as
   before. What is removed are the four things that draw grey while it moves,
   confirmed in Firefox frames captured during real scrolling
   (tools/check-journey-edges.mjs, BROWSER=firefox):

     1. .hx-window's edge-fade mask. Pale paper fading into near-black passes
        through grey, so it drew a grey band down the edge of a card at rest and
        a wide smear with a sharp edge across it while the row moved.
     2. filter:saturate brightness on every card that is not current, which
        turned the paper itself grey and forced an offscreen surface per card.
     3. opacity .48 (and .72 for the next card) on the whole card, the same
        thing: translucent paper over near-black is a grey slab, and the row
        slides those slabs through the window.
     4. perspective on the window plus rotateY on the next card.

   A card that is not being read is now stood back the way a reader expects it
   to be, by dimming its WORDS, and the paper stays solid white while it slides.
   Reduced-motion users keep their own rules above: every card on, no ride. */
@media (prefers-reduced-motion:no-preference){
  body.journey-guided .hx-window{
    -webkit-mask-image:none; mask-image:none; perspective:none;
  }
  body.journey-guided .journey__step,
  body.journey-guided .journey__step[data-next]{ opacity:1; }
  body.journey-guided .journey__step:not([data-current]) .journey__fig{ filter:none; }
  body.journey-guided .journey__step[data-next].is-open .journey__fig{ transform:none; }
  body.journey-guided .journey__step :is(.journey__stepNum, h3, .actstage__acts){ transition:opacity .3s ease; }
  body.journey-guided .journey__step:not([data-current]) :is(.journey__stepNum, h3, .actstage__acts){ opacity:.4; }
  body.journey-guided .journey__step[data-next] :is(.journey__stepNum, h3, .actstage__acts){ opacity:.7; }
  body.journey-guided .journey__step[data-current] :is(.journey__stepNum, h3, .actstage__acts){ opacity:1; }
}

/* HEADER SOLID OVER HOW IT WORKS (2026-09-30). Frosted glass over a pale card turns the card's top into a grey band with a hard edge
   (about 42 of 255 brighter than the header beside it, measured at eight widths in Firefox and Chrome). Over this section's own
   near-black background solid and glass look identical, so the only difference is that the band is gone. app.js adds the class only
   while the section is behind the header; every other page and section keeps the glass. */
/* !important because .nav--over.is-tight sets its own background and blur with !important, which is what the header wears once the page has
   scrolled; a plain rule here changed nothing (measured: identical numbers before and after). */
html.nav-over-how .nav{
  background:rgba(10,10,10,.985) !important;
  backdrop-filter:none !important; -webkit-backdrop-filter:none !important;
}

/* Intent fork + section next-steps (2026-10-10). The audit is for visitors who
   have a site; the fork offers the other door. Quiet, muted, with a gold inline
   link - the house voice, never a second competing pill. */
.check__alt{
  margin-top:clamp(16px,1.8vw,22px);
  font-size:clamp(13.5px,1.05vw,15px); color:var(--muted);
  letter-spacing:.01em;
}
.check__alt a{
  color:var(--bone); border-bottom:1px solid var(--gold);
  padding-bottom:1px; transition:color .25s ease, border-color .25s ease;
}
.check__alt a:hover{ color:var(--gold); }

/* ════════════════════════════════════════════════════════════════════════
   PEEL-BACK SCROLL (/check only, 2026-10-10)
   A normal-looking page pulled apart into the layers we read: structure,
   speed, hidden SEO code. All transforms live behind .peel--live, which
   peel-back.js adds only when motion is allowed and JS runs. Without it the
   layers fall back to a static, labelled exploded stack - readable, on-brand,
   no overlap. Reduced motion and no-JS both land there.
   ════════════════════════════════════════════════════════════════════════ */
.peel{ display:flex; flex-direction:column; gap:clamp(34px,5vw,64px); }
.peel__intro{ max-width:34ch; }
.peel__lede{ margin:18px 0 0; color:rgba(244,241,234,.66); font-size:clamp(15px,1.5vw,18px); line-height:1.6; }

/* ── static fallback: an exploded stack, each layer labelled ───────────── */
.peel__stage{ width:100%; }
.peel__sticky{ display:block; }
.peel__rail{ display:none; }
.peel__scene{ display:flex; flex-direction:column; gap:20px; margin:0; padding:0; list-style:none; }
.peel__layer{ position:relative; margin:0; }
.peel__panel{
  position:relative; width:100%; max-width:640px; margin-inline:auto; aspect-ratio:16/10;
  border-radius:14px; overflow:hidden; box-shadow:0 30px 60px -30px rgba(0,0,0,.7);
}
.peel__tag{
  display:inline-block; margin:0 0 10px; color:var(--gold);
  font-family:var(--body); font-weight:600; font-size:13px; letter-spacing:.12em; text-transform:uppercase;
}

/* The skin: a believable little website */
.peel__browser{ background:var(--bone); display:flex; flex-direction:column; }
.peel__chrome{ display:flex; align-items:center; gap:7px; padding:11px 14px; background:#e5e1d8; }
.peel__chrome i{ width:10px; height:10px; border-radius:50%; background:#c3bdb0; }
.peel__chrome u{ flex:1; height:12px; margin-left:10px; border-radius:6px; background:#d4cfc3; }
.peel__site{ flex:1; padding:22px 26px; display:grid; gap:13px; align-content:start; }
.peel__logo{ width:92px; height:16px; border-radius:4px; background:#2b2b2b; }
.peel__h{ width:74%; height:26px; border-radius:5px; background:#1a1a1a; }
.peel__p{ width:88%; height:9px; border-radius:4px; background:#b7b1a4; }
.peel__p--short{ width:62%; }
.peel__btn{ width:120px; height:30px; border-radius:7px; background:var(--gold); margin-top:4px; }
.peel__img{ width:100%; height:34%; border-radius:8px; background:linear-gradient(120deg,#d9d4c8,#c7c1b4); margin-top:auto; }

/* Structure: the same page as a wireframe */
.peel__wire{ background:var(--ink-2); border:1px solid rgba(244,241,234,.12); padding:24px 26px; display:grid; gap:14px; align-content:start; }
.peel__w{ display:block; border:1.5px dashed rgba(242,201,76,.5); border-radius:6px; background:rgba(242,201,76,.04); }
.peel__w--h1{ width:70%; height:30px; }
.peel__w--lead{ width:90%; height:14px; }
.peel__w--row{ display:flex; gap:12px; height:52px; border:none; background:none; }
.peel__w--row i{ flex:1; border:1.5px dashed rgba(242,201,76,.5); border-radius:6px; background:rgba(242,201,76,.04); }
.peel__w--block{ width:100%; height:64px; }
.peel__w--cta{ width:130px; height:34px; border-style:solid; border-color:rgba(242,201,76,.75); }

/* Speed: how quickly each stage of the load completes */
.peel__speed{ background:var(--ink-2); border:1px solid rgba(244,241,234,.12); padding:28px 30px; display:grid; gap:18px; align-content:center; }
.peel__meter{ display:grid; grid-template-columns:1fr; gap:7px; }
.peel__meter b{ order:-1; color:rgba(244,241,234,.6); font-family:var(--body); font-weight:500; font-size:12px; letter-spacing:.04em; }
.peel__meter i{ position:relative; height:7px; border-radius:4px; background:rgba(244,241,234,.1); overflow:hidden; }
.peel__meter i::after{ content:""; position:absolute; inset:0; width:calc(var(--f,.7) * 100%); border-radius:4px; background:linear-gradient(90deg,var(--gold-deep),var(--gold)); }

/* Hidden SEO code: what a crawler reads and a visitor never sees */
.peel__code{ background:#0d0d0d; border:1px solid rgba(244,241,234,.12); padding:24px 26px; display:flex; flex-direction:column; justify-content:center; gap:11px; font-family:ui-monospace,'SFMono-Regular',Menlo,Consolas,monospace; }
.peel__code code{ color:rgba(244,241,234,.6); font-size:clamp(11px,1.2vw,13.5px); line-height:1.5; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.peel__code code.i{ color:rgba(244,241,234,.42); padding-left:8px; }
.peel__code .k{ color:var(--gold); }

/* ── LIVE: the pinned 3D peel ──────────────────────────────────────────── */
.peel--live .peel__stage{ height:360vh; position:relative; }
.peel--live .peel__sticky{
  position:sticky; top:var(--navh,85px); height:calc(100svh - var(--navh,85px));
  display:flex; align-items:center; justify-content:center;
  perspective:1600px;
}
.peel--live .peel__scene{
  position:relative; display:block; width:min(640px,86vw); aspect-ratio:16/10;
  transform-style:preserve-3d;
}
.peel--live .peel__layer{
  position:absolute; inset:0; margin:0;
  transform-origin:center top; backface-visibility:hidden;
  will-change:transform,opacity;
}
.peel--live .peel__layer .peel__panel{ aspect-ratio:16/10; max-width:none; margin:0; height:100%; }
/* The gold layer tag rides above its panel, revealed as the layer becomes current */
.peel--live .peel__tag{
  position:absolute; left:0; top:-30px; margin:0; opacity:0; transform:translateY(8px);
  transition:opacity .4s var(--m-spring,ease), transform .4s var(--m-spring,ease);
}
.peel--live .peel__layer.is-current .peel__tag{ opacity:1; transform:none; }

/* The step rail, desktop only when live */
.peel--live .peel__rail{
  position:absolute; left:clamp(10px,4vw,46px); top:50%; transform:translateY(-50%);
  display:grid; gap:20px; margin:0; padding:0; list-style:none;
}
.peel--live .peel__rail li{
  display:flex; align-items:center; gap:12px; color:rgba(244,241,234,.4);
  font-family:var(--body); font-size:13px; letter-spacing:.02em;
  transition:color .35s ease;
}
.peel--live .peel__rail li::before{
  content:""; width:9px; height:9px; border-radius:50%; border:1.5px solid rgba(244,241,234,.3);
  transition:background .35s ease,border-color .35s ease,box-shadow .35s ease;
}
.peel--live .peel__rail li.is-on{ color:var(--bone); }
.peel--live .peel__rail li.is-on::before{ background:var(--gold); border-color:var(--gold); box-shadow:0 0 0 4px rgba(242,201,76,.16); }

@media (max-width:860px){
  .peel--live .peel__rail{ display:none; }
  .peel--live .peel__stage{ height:320vh; }
  .peel--live .peel__scene{ width:min(520px,88vw); }
}
@media (max-width:560px){
  .peel--live .peel__stage{ height:300vh; }
  .peel--live .peel__tag{ top:-26px; font-size:12px; }
}
