/* ============================================================
   Quintessentia — LIGHT THEME (proof)
   Loaded AFTER style.css. Overrides tokens and the dark-only
   components. Structure, spacing and markup are unchanged.

   Synthesised from three references the owner selected:
     · SynthAI  — white page, near-black type, black buttons,
                  medium-weight headings, one iridescent object
     · Lumen    — pure white, black pills, pastel mesh gradient
     · Mosa AI  — restraint: small light headings, mono eyebrows

   The common rule across all three: no colour accent anywhere,
   except ONE pearlescent moment. Everything else is black,
   white and grey.
   ============================================================ */

:root {
  --bg: #ffffff;
  --bg-2: #fafafa;
  --surface: #ffffff;
  --surface-2: #f6f4f1;   /* warmed with --ground; #f5f5f6 was cool (B highest) */
  --border: rgba(0, 0, 0, 0.09);
  --border-strong: rgba(0, 0, 0, 0.16);
  --text: #1a1a1c;
  --muted: #6b7280;

  /* The cyan brand accent does not survive on white and none of the
     three references use a colour accent. Interactive = near-black. */
  --accent: #1a1a1c;
  --accent-deep: #000000;
  --accent-soft: rgba(0, 0, 0, 0.05);
  --grad-accent: linear-gradient(135deg, #1a1a1c 0%, #2f2f33 100%);

  /* The single iridescent moment, borrowed from Lumen/SynthAI. */
  /* Was a SIX-STOP rainbow: lavender, blue, mint, peach, pink, lavender. That
     spectrum sweep is the single most recognisable "AI-generated" visual signature,
     and it is the named anti-pattern in the design guidance this pass followed. A
     premium mark reads as ONE material catching light, not as a colour wheel. Now a
     single-family sheen: warm stone into the brand indigo and back, so the highlight
     travels without the hue ever leaving the palette. */
  --irid: linear-gradient(115deg,
            #e8e4dd 0%, #cdc8e4 26%, #8f8fc4 50%,
            #585ece 66%, #9a97cf 82%, #e6e2db 100%);

  /* 🔴 THREE elevation steps, and the earlier note here claiming two was wrong.
     It asserted --shadow-md was "used exactly ONCE, a one-off rather than a step"
     and aliased it to --shadow-sm. It is used TWICE, and both uses are ROLES, not
     one-offs: .card:hover and .offer.featured. Aliasing it meant a hovered card and
     the featured pricing card each rendered the SAME shadow as their resting state,
     so both box-shadow declarations were silently no-ops - the featured card was
     supposed to read as lifted above its siblings and did not. Rest / hover /
     overlay are three distinct jobs, so three steps is a scale, not drift.

     All three are now ZERO-CHROMA. They were rgba(16,24,40,...), a navy tint, which
     is the same defect that made an earlier design read hazy: tinted greys against
     neutral ones. And --shadow-sm carried alpha 0.04, roughly a third of the 0.12
     the measured reference holds, which is why 8 of 9 white cards on white sections
     were separated by a hairline alone. */
  --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.10), 0 1px 2px rgba(0, 0, 0, 0.06);
  --shadow-md: 0 4px 10px rgba(0, 0, 0, 0.10), 0 2px 4px rgba(0, 0, 0, 0.06);
  --shadow-lg: 0 18px 48px rgba(0, 0, 0, 0.12);
}

body { background: var(--bg); color: var(--text); }
::selection { background: rgba(0, 0, 0, 0.10); }

/* ---------- typography: medium weight, smaller, per the references ---------- */
/* 500 -> 600. The reference sets every heading at 600 and it is a large part of why it
   reads as designed rather than defaulted. Figtree at 600 is a real cut, not a synthesised
   bold, so nothing is faux-bolded. */
h1, h2, h3, h4 { font-weight: 600; color: var(--text); }
h1 { font-size: var(--fs-d1); letter-spacing: -0.025em; }
/* 🔴 line-height belongs here, not only on .sec-h2. style.css sets h2 to 1.08,
   which was tuned for its larger --fs-d2; on the light theme h2 renders at
   --fs-d3 (36.8px), so a bare h2 and a .sec-h2 h2 were the SAME SIZE with
   different leading - 39.7px against 44.9px. Two leadings at one size is the
   drift .sec-h2 exists to end, so the class must not be the only place the
   value lives. .statement keeps its own tighter 1.04 below, which is correct:
   it is a genuinely larger display size. */
h2 { font-size: var(--fs-d3); line-height: 1.22; letter-spacing: -0.02em; }
p  { color: var(--muted); }

/* Two-tone heading (Mosa's signature): second clause drops to grey. */
/* 🔴 was #b0b4bb — a dark-theme grey stranded on a light page, measuring 2.08:1
   on white, the worst contrast on the site. var(--muted) makes it adapt, including
   inside the footer where the token is re-scoped below. */
.muted { color: var(--muted); }

/* Mosa's two-tone heading: first clause near-black, second clause grey.
   NOT the iridescent gradient - measured 1.32:1 to 1.81:1 against white,
   which fails WCAG AA (3.0:1 for large text) and would contradict the
   commitment on /trust. The iridescence lives in the hero bloom and the
   logo card instead, exactly as SynthAI and Lumen do it. */
.accent-text {
  background: none;
  -webkit-background-clip: border-box;
  background-clip: border-box;
  color: #6b7280;          /* 4.83:1 on white - passes AA */
}

.eyebrow { color: var(--muted); font-weight: 600; }
.eyebrow::before { background: var(--muted); }

/* ---------- topbar ---------- */
.topbar-inner {
  background: rgba(255, 255, 255, 0.82);
  border-color: var(--border);
  box-shadow: var(--shadow-sm);
}
.nav-links a { color: var(--muted); }
.nav-links a:hover, .nav-links a.active { color: var(--text); background: var(--surface-2); }
.nav-links a.btn-accent,
.nav-links a.btn-accent:hover {
  color: #ffffff;
  background: #1a1a1c;
  box-shadow: none;
}
.nav-menu {
  background: #ffffff;
  border-color: var(--border);
  box-shadow: var(--shadow-lg);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
.nav-menu a:hover b, .nav-menu a:focus-visible b { color: #000000; }
.nav-menu a:hover { background: var(--surface-2); }
.brand small { color: var(--muted); }

/* ---------- buttons: black, per all three references ---------- */
.btn { border-radius: var(--r-sm); }
.btn-accent, .btn-accent:hover {
  background: #1a1a1c;
  color: #ffffff;
  box-shadow: none;
}
/* 🔴 This was `background: #000000` against a resting `#1a1a1c` - a luminance
   delta of about 0.4%, which is to say no hover at all. Combined with the cyan
   glow being neutralised at light.css:107 and the lift cancelled on touch, the
   primary call to action responded to the pointer with a 2px nudge and nothing
   else.

   Steps to the brand indigo instead, which is a change you can actually see, and
   pairs a real shadow with it. White on #585ece measures 5.38:1, so the label
   stays past AA through the whole transition.

   NOT the reference's four-part travelling-box choreography: that needs a span
   wrapper on every CTA across 30 pages, and its own risk note flags that the
   `overflow: hidden` it depends on would clip the 2px focus outline added
   earlier today. Decorative gain, real regression - not a trade worth making. */
.btn-accent:hover {
  background: var(--indigo);
  box-shadow: var(--shadow-lg);
  transition: background var(--dur-2) var(--ease-out),
              box-shadow var(--dur-2) var(--ease-out),
              transform var(--dur-2) var(--ease-out);
}
.btn-ghost {
  background: #ffffff;
  color: var(--text);
  border: 1px solid var(--border-strong);
}
.btn-ghost:hover { background: var(--surface-2); border-color: #1a1a1c; }

/* ---------- hero: replace the dark atmosphere with one pastel wash ---------- */
.hero-bg, .hero-aurora, .hero-vignette, .hero-grain,
.hero-trails, .float-field, .hero-grid-lines { display: none !important; }

.hero, .page-hero { background: #ffffff; position: relative; overflow: hidden; }

/* 🔴 This bloom was THE remaining AI-slop source, and it is on all 28 pages, not
   just the homepage - .page-hero carries it too. Three pastels stacked: lavender
   rgba(186,164,236,0.62), mint rgba(150,224,206,0.52), pink rgba(246,182,214,0.52).
   Measured on rendered pixels across every page, it put chroma 46 to 97 into the
   top-right of pages whose ground is otherwise neutral - law-firms peaked at 97.

   Replacing the homepage mesh alone was not enough precisely because this sits on a
   PSEUDO-ELEMENT: a DOM walk of .page-hero's children finds nothing, and reports the
   page clean. Only sampling rendered pixels caught it. Two layers now, one family. */
.hero::before, .page-hero::before {
  content: "";
  position: absolute;
  top: -18%; right: -8%;
  width: 62vw; height: 62vw;
  max-width: 900px; max-height: 900px;
  background:
    radial-gradient(closest-side, rgba(88, 94, 206, 0.20), transparent 70%),
    radial-gradient(closest-side, rgba(120, 110, 96, 0.16), transparent 70%);
  background-position: 34% 30%, 66% 58%;
  background-size: 74% 74%, 66% 66%;
  background-repeat: no-repeat;
  filter: blur(28px);
  pointer-events: none;
  z-index: 0;
}
.page-hero::before { top: -30%; width: 48vw; height: 48vw; opacity: 0.75; }
/* NOT .hero .container - .hero-meta is position:absolute and anchors to
   the nearest positioned ancestor. Positioning .container re-breaks the
   hero-meta/button overlap. .hero-inner already has position:relative;z-index:1. */
.page-hero .container { position: relative; z-index: 1; }
.hero-inner, .hero-meta { z-index: 1; }

/* a hairline under the hero, so sections read as distinct bands */
.hero::after, .page-hero::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0;
  height: 1px; background: var(--border);
}

/* ---------- cards: white, bordered, softly shadowed ---------- */
.card, .card.glass, .offer, .cta-band, .callout {
  background: #ffffff;
  border: 1px solid var(--border);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
/* .flagship was measured with NO shadow and a white ground on a white section, so
   the single most important card on the homepage had the least separation of any
   card on it - a 1px hairline doing 100% of the work. */
.card, .card.glass, .offer, .flagship { box-shadow: var(--shadow-sm); }
.card:hover, .offer:hover, .flagship:hover { box-shadow: var(--shadow-md); border-color: var(--border-strong); }

/* 🔴 `background` here is not redundant. style.css:1314 still carries the dark-theme
   .offer.featured - a cyan border and a rgba(13,20,26,0.6) near-black ground - at
   specificity (0,2,0). The only reason it does not paint is that `.card.glass` in the
   light layer TIES it at (0,2,0) and wins on source order. Every one of these cards is
   markup'd `card glass offer featured`; delete `glass` from any of the 8 and the card
   turns near-black on a white pricing page. Measured before writing this: the cards
   currently render rgb(255,255,255) with background-image none, so this is closing a
   trap, not fixing a live defect. */
.offer.featured { border-color: #1a1a1c; background: #ffffff; box-shadow: var(--shadow-md); }
.offer .price { color: var(--text); }
.offer .price small { color: var(--muted); }
.offer-sub { color: var(--muted); }
.offer-list li { color: var(--text); }
.offer-list.excl li { color: var(--muted); }

.callout { background: var(--surface-2); border-left: 3px solid #1a1a1c; }
.callout p { color: var(--text); }

.cta-band { background: var(--surface-2); }

.icon-tile {
  background: var(--surface-2);
  border: 1px solid var(--border);
  color: var(--text);
}

/* ---------- structural components ---------- */
.spec, .spec-row { border-color: var(--border); }
.spec-row dt { color: var(--text); }
.spec-row dd { color: var(--muted); }

.practice-rows, .practice-row { border-color: var(--border); }
.practice-name { color: var(--text); }
a.practice-row:hover .practice-name { color: #000000; }
.practice-desc { color: var(--muted); }
/* 🔴 #b0b4bb measured 2.08:1 on white — the worst contrast on the site, and the
   second place that dark-theme grey survived. These ordinals are visible text, so AA
   applies; any value that clears 4.5:1 on white is already about as light as
   var(--muted), so there is no subtler option that also keeps the promise. */
.practice-index { color: var(--muted); }
.practice-flag {
  color: var(--text);
  background: var(--surface-2);
  border-color: var(--border-strong);
}
.practice-flag.neutral { color: var(--muted); }
.practice-arrow { border-color: var(--border); color: var(--text); }

.cmp th { color: var(--muted); border-color: var(--border); }
.cmp td { color: var(--muted); border-color: var(--border); }
.cmp td:first-child { color: var(--text); }
.cmp .us { color: var(--text); font-weight: 500; }

.post, .post-list { border-color: var(--border); }
.post-title { color: var(--text); }
.post-excerpt, .post-date, .post-read { color: var(--muted); }
.post:hover .post-title { color: #000000; }

.statement { color: var(--text); }
.prose p, .prose li { color: var(--muted); }
.prose h2, .prose h3, .prose strong { color: var(--text); }
.prose blockquote { border-color: #1a1a1c; background: var(--surface-2); }
.prose table th { color: var(--text); border-color: var(--border); }
.prose table td { color: var(--muted); border-color: var(--border); }
/* 🔴 `:not(.btn)` is load-bearing. `.prose a` is specificity (0,1,1) and BEATS
   `.btn-accent` (0,1,0), so every .btn-accent inside an article rendered #1a1a1c text
   on its own #1a1a1c background — an invisible CTA on all four insights articles,
   measured at 1.00:1. Third instance of the same inversion as the .reveal blackout and
   the black-on-black footer headings; excluding buttons removes the race instead of
   winning it. See data/vault/Bugs/01-Reveal-Blackout.md. */
.prose a:not(.btn) { color: #1a1a1c; text-decoration: underline; text-underline-offset: 3px; }

/* 🔴 This re-declaration sits OUTSIDE any media query, and light.css loads after
   style.css, so it silently overrode style.css's
   `@media (prefers-reduced-motion: reduce) { .reveal.visible { animation: none } }`
   — same specificity (0,2,0), later sheet wins. Every visitor who had asked their OS
   for reduced motion still got the full staggered fade on 18 blocks. The guard existed
   and was already defeated. Re-stated below so the promise actually holds. */
.reveal.visible { animation: reveal-rise var(--dur-3) var(--ease-out-soft) backwards; }
@media (prefers-reduced-motion: reduce) {
  .reveal.visible { animation: none; }
}

/* ---------- forms ---------- */
input, select, textarea {
  background: #ffffff;
  border: 1px solid var(--border-strong);
  color: var(--text);
}
/* #9aa0a8 measured 2.64:1 on white. Placeholders are still text. */
input::placeholder, textarea::placeholder { color: var(--muted); }
label { color: var(--text); }

/* ---------- footer ---------- */
footer {
  background: var(--surface-2);
  border-top: 1px solid var(--border);
  color: var(--muted);
}
footer h2 { color: var(--text); }
footer a { color: var(--muted); }
footer a:hover { color: var(--text); }
.footer-bottom { border-color: var(--border); color: var(--muted); }
.footer-blurb { color: var(--muted); }

/* ---------- fit check ---------- */
.opt { background: #ffffff; border: 1px solid var(--border-strong); }
.opt:hover { border-color: #1a1a1c; background: var(--surface-2); }
.opt b { color: var(--text); }
.opt span { color: var(--muted); }
.progress i { background: var(--border); }
.progress i.done { background: #1a1a1c; }
.qnum { color: var(--muted); }
.nodata { background: var(--surface-2); border-color: var(--border); color: var(--muted); }
.nodata svg { color: #1a1a1c; }
.v-good { background: #1a1a1c; color: #ffffff; }
.v-maybe { background: var(--surface-2); color: var(--text); border-color: var(--border-strong); }
.v-no { background: transparent; color: var(--muted); border-color: var(--border-strong); }
.math dt { color: var(--muted); }
.math dd { color: var(--text); }

/* ---------- mobile nav ---------- */
@media (max-width: 760px) {
  .nav-links { background: #ffffff; box-shadow: var(--shadow-lg); }
  .nav-menu { box-shadow: none; border-left: 1px solid var(--border); }
}

/* ---------- light-theme hero figure (replaces the dark orb field) ---------- */
.hero-inner { max-width: 72ch; }

.hero-figure-light {
  position: absolute;
  top: 46%; right: 4%;
  transform: translateY(-50%);
  width: 260px; height: 260px;
  display: grid; place-items: center;
  z-index: 0;
  pointer-events: none;
}
.hero-figure-light .hf-glow {
  position: absolute; inset: 0;
  border-radius: 42%;
  background: var(--irid);
  filter: blur(34px);
  opacity: 0.85;
  animation: hf-drift 18s ease-in-out infinite;
}
.hero-figure-light img {
  position: relative;
  border-radius: var(--r-xl);
  box-shadow: var(--shadow-lg);
}
@keyframes hf-drift {
  0%, 100% { transform: scale(1) rotate(0deg); border-radius: 42%; }
  50%      { transform: scale(1.06) rotate(8deg); border-radius: 48% 38% 46% 40%; }
}
@media (prefers-reduced-motion: reduce) {
  .hero-figure-light .hf-glow { animation: none; }
}
@media (max-width: 900px) {
  .hero-figure-light { display: none; }
}

/* ============================================================
   ANIMATED IRIDESCENT MESH
   Pure CSS — no JS, no video, ~2KB, no network cost.
   Four blurred radial gradients drifting on independent cycles.
   The overlap is what reads as a mesh; each blob alone is flat.
   Chosen over a video background: the references all use an
   animated object, a 3MB hero video would undercut a 400ms site,
   and text over video fails the WCAG AA commitment on /trust.
   ============================================================ */
.mesh { position: absolute; inset: 0; overflow: hidden; pointer-events: none; z-index: 0; }
.mesh span {
  position: absolute;
  display: block;
  border-radius: 50%;
  filter: blur(60px);
  opacity: 0.55;
  will-change: transform;
}
/* FOUR pastel blobs - lavender, mint, pink, peach - became TWO. Four hues at 0.55
   opacity is a colour wheel sitting behind the headline, and that is what reads as
   "generated". Two layers in ONE family read as depth instead. m1 carries a faint
   indigo, the brand accent and the only hue left on the page; m2 is warm stone with
   no hue at all. m3 and m4 keep their elements but take no paint, so the markup on
   18 pages does not have to change. */
.mesh .m1 { width: 52vw; height: 52vw; max-width: 700px; max-height: 700px;
  top: -16%; right: -2%; background: radial-gradient(circle, rgba(88,94,206,0.30) 0%, rgba(88,94,206,0) 68%);
  animation: mesh-a 34s ease-in-out infinite; }
.mesh .m2 { width: 44vw; height: 44vw; max-width: 600px; max-height: 600px;
  top: 12%;  right: 24%; background: radial-gradient(circle, rgba(120,110,96,0.20) 0%, rgba(120,110,96,0) 70%);
  animation: mesh-b 42s ease-in-out infinite; }
.mesh .m3, .mesh .m4 { display: none; }

@keyframes mesh-a { 0%,100%{transform:translate3d(0,0,0) scale(1)}   50%{transform:translate3d(-6%,7%,0) scale(1.14)} }
@keyframes mesh-b { 0%,100%{transform:translate3d(0,0,0) scale(1.06)} 50%{transform:translate3d(9%,-5%,0) scale(0.94)} }
@keyframes mesh-c { 0%,100%{transform:translate3d(0,0,0) scale(0.96)} 50%{transform:translate3d(-8%,-9%,0) scale(1.1)} }
@keyframes mesh-d { 0%,100%{transform:translate3d(0,0,0) scale(1)}   50%{transform:translate3d(7%,10%,0) scale(1.08)} }

@media (prefers-reduced-motion: reduce) {
  .mesh span { animation: none !important; }
}
/* The static bloom is replaced by the mesh wherever .mesh is present. */
.hero:has(.mesh)::before, .page-hero:has(.mesh)::before { display: none; }

/* ---------- proof strip: our differentiators, NOT fake client logos ---------- */
.proof-strip {
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
  background: var(--surface-2);
}
.proof-strip .container {
  display: grid; grid-template-columns: repeat(4, 1fr);
  gap: 1px; background: var(--border);
}
.proof-item { background: var(--surface-2); padding: var(--sp-4) var(--sp-3); }
.proof-item b {
  display: block; font-family: var(--font-display);
  font-size: var(--fs-2xl); font-weight: 500; color: var(--text);
  letter-spacing: -0.02em; margin-bottom: 0.3rem;
}
.proof-item span { display: block; font-size: var(--fs-sm); color: var(--muted); line-height: var(--lh-dense); }
@media (max-width: 900px) { .proof-strip .container { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 520px) { .proof-strip .container { grid-template-columns: 1fr; } }

/* ---------- numbered process blocks (SynthAI / Mosa pattern) ---------- */
.steps { display: grid; gap: 0; border-top: 1px solid var(--border); }
.step {
  display: grid; grid-template-columns: 7rem 1fr;
  gap: var(--gap-8); padding: var(--sp-4) 0;
  border-bottom: 1px solid var(--border);
  align-items: start;
}
.step-n {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--fs-xs); letter-spacing: var(--track-label-md); text-transform: uppercase;
  color: var(--muted); padding-top: 0.35rem;
}
.step h3 { font-size: var(--fs-lg); font-weight: 500; margin-bottom: 0.5rem; color: var(--text); }
.step p { color: var(--muted); font-size: var(--fs-lg); max-width: 62ch; }
@media (max-width: 700px) { .step { grid-template-columns: 1fr; gap: var(--gap-2); } }

/* ---------- showcase: a real product visual beside its claim ---------- */
.showcase { display: grid; grid-template-columns: 1.05fr 1fr; gap: var(--gap-14); align-items: center; }
.showcase + .showcase { margin-top: var(--sp-8); }
.showcase.flip > :first-child { order: 2; }
.showcase-media {
  border: 1px solid var(--border); border-radius: var(--r-lg);
  overflow: hidden; background: #fff; box-shadow: var(--shadow-lg);
}
.showcase-media img { display: block; width: 100%; height: auto; }
.showcase-label {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--fs-2xs); letter-spacing: var(--track-label-sm); text-transform: uppercase;
  color: var(--muted); display: block; margin-bottom: 0.9rem;
}
@media (max-width: 860px) {
  .showcase { grid-template-columns: 1fr; gap: var(--gap-8); }
  .showcase.flip > :first-child { order: 0; }
}

/* ============================================================================
   SECTION-CARD SHELL
   Measured from synthai.demos.tailgrids.com (2026-08-16), not eyeballed:
     page ground  oklch(0.97 0 0)      every <section>  bg-white
     section      radius 12px, margin-bottom 12px, padding 100px 12px
     headline     60px / weight 500 / line-height 1.0
     buttons      radius 8px, padding 12px 20px, Space Mono 14px
     tint panels  #edf4ea mint  #ebebfc lavender  #f7eff7 pink, radius 12px
     footer       #000, radius 22px
   Structure only. None of their code, imagery or copy is reproduced.
   ========================================================================= */

:root {
  --shell-gap: var(--gap-3);
  --shell-radius: 12px;
  /* warmed from #f2f2f3. That was a COOL grey (B >= R), and the measured reference
     runs a warm canvas as its dominant surface. Lightness is held: measured L* 95.52
     -> 95.22, which moves text on the ground from 15.53:1 to 15.41:1 against --text.
     A 0.12 drop against a 4.5 threshold, so nothing that sits on the ground changes
     contrast ratio - and in practice almost nothing does, because the ground is
     only visible in the gaps BETWEEN the white section cards. */
  --ground: #f3f1ee;
  /* Three unrelated hues became three weights of ONE. Mint, lavender and pink on
     sibling cards is the colour-wheel tell again; a premium set varies TONE, not hue. */
  --tint-mint: #f2f0ec;   /* warm stone, lightest */
  --tint-lav:  #ecebf6;   /* faint indigo, the accent family */
  --tint-pink: #f4f2ee;   /* warm stone, one step down */
}

body { background: var(--ground); }

main { padding: 0 var(--shell-gap); }

main > section {
  background: var(--surface);
  border-radius: var(--shell-radius);
  margin-bottom: var(--shell-gap);
  /* 96px -> 120px. The measured reference runs 120px of section padding against our
     96, and the comment here previously said "100px" while the token resolved to 96 -
     a stale number describing the value before the spacing scale was collapsed. */
  padding: var(--sp-15) 0.75rem;
  overflow: hidden;                   /* clip the mesh to the card corners */
  position: relative;
}
main > section:last-child { margin-bottom: 0; }

/* sections that carry their own ground keep it, but adopt the card geometry */
main > section.hero { background: transparent; }

/* the hero card: iridescent wash, taller, corners clipped */
main > section.hero,
main > section.page-hero {
  background:
    radial-gradient(120% 90% at 84% 14%, rgba(88,94,206,0.13), transparent 58%),
    radial-gradient(100% 85% at 96% 66%, rgba(120,110,96,0.10), transparent 62%),
    linear-gradient(180deg, #fbfaf8 0%, #ffffff 74%);
}
main > section.hero { min-height: 80vh; padding-top: var(--sp-20); padding-bottom: var(--sp-12); }

/* 🔴 REMOVED: three `main > section.cta-band` rule blocks - a background wash,
   a padding/text-align block and a .container max-width. They matched NOTHING.
   `cta-band` is a <div> inside a <section> on all 19 pages that carry it, there
   is no `<section class="cta-band">` anywhere in the 28 files, and no JS adds
   the class. The live styling is the `.cta-band` block further down, which was
   already annotated as the element that actually exists - so the dead rules had
   been identified and left in place, which is how a reader ends up editing the
   half that does nothing. Verified before deleting: 0 section matches, 0 JS
   references. That check is the difference between this and the 65 "unused"
   classes documented in style.css, which were NOT removed because the scan
   behind them was wrong about at least one entry. */

/* the proof strip is a band, not a card - it sits flush and tight */
/* 🔴 This rule is (0,1,2) and `.proof-strip` is (0,1,0), so the background set on
   `.proof-strip` itself - var(--surface-2) - never applied. The section painted white
   while only the grid CELLS inside .container painted --surface-2, which rendered as a
   tinted band flanked by two white gutters where the centred container ran out. Same
   defect family as the blackout: the losing rule read like the live one.
   The band is meant to be a full-bleed tone break, so the tint belongs on the winner. */
main > section.proof-strip {
  padding: 0;
  background: var(--surface-2);
}

/* ---------- tinted feature panels ---------- */
.tint { border-radius: var(--shell-radius); padding: var(--sp-5); }
.tint-a { background: var(--tint-mint); }
.tint-b { background: var(--tint-lav); }
.tint-c { background: var(--tint-pink); }
.tint .eyebrow { color: #4a5a46; }
.tint-b .eyebrow { color: #4a4a72; }
.tint-c .eyebrow { color: #6a4a64; }

/* ---------- buttons: 8px radius, mono label ---------- */
/* 🔴 These switch to Space Mono, which ships ONLY 400 and 700. Anything asking
   for 500 or 600 here gets a browser-SYNTHESISED weight - faux bold, which smears
   the stems and reads muddier than a real cut. 25 elements were doing exactly that:
   .btn at 600, .eyebrow at 600, and <b> at 500 and 600. Each is now snapped to a
   weight the family actually has. If you add a mono rule, its weight must be 400
   or 700 - there is nothing in between to render. */
.btn, .btn-accent, .btn-ghost {
  border-radius: var(--r-sm);
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: var(--fs-xs);
  letter-spacing: var(--track-btn);
  text-transform: uppercase;
  padding: 0.78rem 1.25rem;
}

/* ---------- footer: black card ---------- */
footer {
  background: #000;
  border: 0;
  border-radius: var(--r-xl);
  color: rgba(255,255,255,0.62);
  margin: var(--shell-gap);
  padding: var(--sp-7) 0.75rem var(--sp-4);
}
footer h2, footer .footer-brand, footer strong { color: #fff; }
footer a { color: rgba(255,255,255,0.62); }
footer a:hover { color: #fff; }
footer .footer-bottom,
footer hr { border-color: rgba(255,255,255,0.14); }

/* ---------- responsive ---------- */
@media (max-width: 860px) {
  main > section { padding: var(--sp-8) 0.75rem; }
  main > section.hero { padding-top: var(--sp-12); padding-bottom: var(--sp-8); min-height: 0; }
  .tint { padding: var(--sp-3); }
}

/* ---------- type: Figtree body+display, Space Mono for labels (as measured) ---------- */
:root {
  --font-display: "Figtree", system-ui, sans-serif;
  --font-body:    "Figtree", system-ui, sans-serif;
  --font-mono:    "Space Mono", ui-monospace, Consolas, monospace;
}
.eyebrow, .work-kicker, .showcase-label, .step-n, .hero-meta, .proof-item b {
  font-family: var(--font-mono);
  /* 400, not 600: these are small uppercase labels with wide tracking, where Space
     Mono's regular cut is already emphatic enough and 700 would shout. */
  font-weight: 400;
}
.proof-item b, .work-metric b, .case-stat b { font-weight: 700; }
h1, .statement { line-height: 1.04; letter-spacing: -0.02em; }

/* ---------- shell fixes ----------
   .hero-meta is position:absolute with left:0/right:0. Its containing block is
   the .hero section, so inside the card shell it bled to the card edge instead
   of lining up with the .container gutter. Re-anchor it to container geometry
   (same math as .container: width min(var(--container), 100% - 3rem)). */
.hero-meta {
  left: 50%; right: auto;
  transform: translateX(-50%);
  width: min(var(--container), calc(100% - 3rem));
}
@media (max-width: 760px) {
  .hero-meta { left: auto; transform: none; width: auto; }
}

/* cap the hero so a tall viewport doesn't stretch the card into dead space */
main > section.hero { min-height: min(80vh, 780px); }

/* mono labels are wider than the sentence-case originals - keep the hero row
   on one line at desktop and let it wrap cleanly below */
/* 🔴 Size and padding removed. This rule was the third of three competing button
   presentations: the baseline at light.css:530, `.nav-links a` in style.css at
   (0,1,1) which outranks it for the nav CTA, and this one at (0,2,0) for the
   hero. Ten buttons on one page rendered as three different components - three
   font sizes, two radii, two heights - from two classes. Only the tracking token
   stays here; the rest now comes from the single baseline. */
.hero-actions .btn { letter-spacing: var(--track-btn); }

/* Beats style.css `.nav-links a` (0,1,1) at (0,3,1), so the nav CTA stops being
   a pill in a different size from every other button on the site. */
.topbar .nav-links a.btn,
.topbar .nav-links a.btn-accent,
.topbar .nav-links a.btn-ghost {
  font-size: var(--fs-xs);
  border-radius: var(--r-sm);
  padding: 0.78rem 1.25rem;
}

/* ---------- footer card: beat the light-theme selectors on specificity ----------
   style.css `.footer-grid h3` is (0,1,1) and outranks a bare `footer h3`
   (0,0,2), so the headings kept --text (#1a1a1c) on a black card. Match or
   exceed the class-level specificity rather than reaching for !important.
   Measured on #000: rgba(255,255,255,0.62) -> 7.8:1, white -> 21:1. Both AA. */
footer .footer-grid h2        { color: #ffffff; }
footer .footer-grid ul a      { color: rgba(255,255,255,0.62); }
footer .footer-grid ul a:hover{ color: #ffffff; }
footer .footer-blurb          { color: rgba(255,255,255,0.62); }
footer .footer-bottom         { color: rgba(255,255,255,0.62); border-color: rgba(255,255,255,0.14); }
footer .footer-bottom a       { color: rgba(255,255,255,0.62); }
footer .footer-bottom a:hover { color: #ffffff; }
footer .brand span            { color: #ffffff; }
footer .brand small           { color: rgba(255,255,255,0.55); }

/* ---------- CTA band ----------
   .cta-band is a <div> inside a <section>, so `main > section.cta-band` never
   matched it. Style the element that actually exists. */
.cta-band {
  border-radius: var(--shell-radius);
  padding: var(--sp-8) var(--sp-4);
  text-align: center;
  background:
    radial-gradient(80% 150% at 12% 50%, rgba(88,94,206,0.14), transparent 62%),
    radial-gradient(80% 150% at 88% 50%, rgba(120,110,96,0.11), transparent 62%),
    radial-gradient(70% 130% at 50% 0%,  rgba(255,217,192,0.28), transparent 60%),
    linear-gradient(180deg, #fbfbfe, #ffffff);
  border: 1px solid var(--border);
}
.cta-band p { max-width: 56ch; margin-inline: auto; }

/* ---------- tinted work cards ----------
   SynthAI rotates mint / lavender / pink across its feature panels. Same idea,
   applied via nth-child so no page markup changes. style.css:1361 sets a dark
   rgba background on .work-card - override at equal-or-higher specificity. */
.work-stack .work-card { border: 1px solid transparent; }
.work-stack .work-card:nth-child(3n+1) { background: var(--tint-mint); }
.work-stack .work-card:nth-child(3n+2) { background: var(--tint-lav); }
.work-stack .work-card:nth-child(3n+3) { background: var(--tint-pink); }
.work-stack .work-card:hover { border-color: var(--border-strong); }
.work-card .work-kicker { color: #5a5a68; }
.work-card .work-metric { border-top-color: rgba(0,0,0,0.10); }

/* ============================================================================
   SYNTHAI SECTION TYPES — pass 2
   The card shell gave us their page *frame*. These are the section *types*
   inside it, measured from synthai.demos.tailgrids.com 2026-08-16:
     centred header w/ inline icon chips · connector process · integration
     cloud · FAQ accordion · full-width tinted feature rows.
   Structure rebuilt in our own CSS; none of their code or assets reproduced.
   ========================================================================= */

/* ---------- centred section header ---------- */
.sec-head-c { max-width: 860px; margin: 0 auto 3.5rem; text-align: center; }
.sec-head-c .eyebrow {
  display: inline-flex; align-items: center; gap: var(--gap-2);
  justify-content: center; margin-bottom: 1.1rem;
}
.sec-head-c h2 {
  font-size: var(--fs-d3);
  line-height: 1.22; letter-spacing: -0.02em; font-weight: 500;
}
/* SynthAI greys the connective words and keeps the nouns black */
.sec-head-c h2 .soft { color: var(--muted); }
.sec-head-c > p { max-width: 62ch; margin: 1.1rem auto 0; color: var(--muted); }

/* inline icon chip sitting inside a headline */
.h-chip {
  display: inline-flex; align-items: center; justify-content: center;
  width: 1.5em; height: 1.15em; border-radius: var(--r-sm);
  vertical-align: -0.18em; margin: 0 0.12em;
  font-size: 0.78em; line-height: 1;
}
.h-chip.a { background: var(--tint-mint); }
.h-chip.b { background: var(--tint-lav); }
.h-chip.c { background: var(--tint-pink); }
.h-chip svg { width: 1em; height: 1em; opacity: 0.75; }

/* ---------- process with a vertical connector ---------- */
.proc { display: grid; grid-template-columns: 1fr 1.05fr; gap: var(--gap-14); align-items: start; }
.proc-rail { display: flex; flex-direction: column; gap: var(--gap-4); position: relative; }
.proc-step {
  display: grid; grid-template-columns: 2.6rem 1fr; gap: var(--gap-4);
  align-items: stretch; position: relative;
}
.proc-n {
  position: relative;
  display: flex; align-items: flex-start; justify-content: center;
  font-family: var(--font-mono); font-size: var(--fs-2xs); color: var(--muted);
  padding-top: 1.15rem;
}
/* the connector line runs between the numbers, not past the ends */
.proc-step:not(:last-child) .proc-n::after {
  content: ""; position: absolute; top: 2.4rem; bottom: -1.6rem;
  left: 50%; width: 1px; background: var(--border-strong); opacity: 0.55;
}
.proc-card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--shell-radius); padding: 1.1rem 1.25rem;
}
.proc-card h3 { font-size: var(--fs-lg); font-weight: 500; margin-bottom: 0.3rem; }
.proc-card p { font-size: var(--fs-sm); color: var(--muted); line-height: var(--lh-body); margin: 0; }
.proc-ico {
  width: 30px; height: 30px; border-radius: var(--r-sm); margin-bottom: 0.75rem;
  display: flex; align-items: center; justify-content: center;
}
.proc-ico.a { background: var(--tint-mint); }
.proc-ico.b { background: var(--tint-lav); }
.proc-ico.c { background: var(--tint-pink); }
.proc-ico.d { background: #eef2f7; }
.proc-ico svg { width: 15px; height: 15px; opacity: 0.7; }

/* ---------- integration / stack cloud ---------- */
.cloud { position: relative; padding: 1rem 0; }
.cloud-row { display: flex; justify-content: center; flex-wrap: wrap; gap: var(--gap-10); }
.cloud-row.spread { justify-content: space-between; }
.cloud-item { display: flex; flex-direction: column; align-items: center; gap: var(--gap-2); width: 84px; }
.cloud-badge {
  width: 54px; height: 54px; border-radius: var(--r-lg);
  background: var(--surface); border: 1px solid var(--border);
  box-shadow: var(--shadow-sm);
  display: flex; align-items: center; justify-content: center;
  font-family: var(--font-mono); font-size: var(--fs-2xs); font-weight: 700;
  color: #3d4250;
}
.cloud-item span {
  font-family: var(--font-mono); font-size: var(--fs-2xs); letter-spacing: var(--track-label-sm);
  color: var(--muted); text-align: center; white-space: nowrap;
}
.cloud-mid { text-align: center; padding: var(--sp-4) 0; }

/* ---------- FAQ accordion ---------- */
.faq { display: grid; grid-template-columns: 0.8fr 1.2fr; gap: var(--gap-14); align-items: start; }
.faq-list { border-top: 1px solid var(--border); }
.faq-item { border-bottom: 1px solid var(--border); }
.faq-q {
  width: 100%; background: none; border: 0; cursor: pointer;
  display: flex; align-items: flex-start; gap: var(--gap-3);
  padding: 1.15rem 0; text-align: left;
  font-family: var(--font-body); font-size: var(--fs-lg); font-weight: 500;
  color: var(--text); line-height: var(--lh-dense);
}
.faq-mark {
  flex-shrink: 0; width: 20px; height: 20px; border-radius: var(--r-sm);
  border: 1px solid var(--border-strong);
  display: flex; align-items: center; justify-content: center;
  margin-top: 0.1rem; transition: background var(--dur-2, .2s) ease, border-color .2s ease;
}
.faq-mark svg { width: 11px; height: 11px; transition: transform .22s ease; }
.faq-item[open] .faq-mark { background: #1a1a1c; border-color: #1a1a1c; color: #fff; }
.faq-item[open] .faq-mark svg { transform: rotate(180deg); }
.faq-a { padding: 0 0 1.25rem var(--sp-6); }
.faq-a p { font-size: var(--fs-sm); color: var(--muted); line-height: var(--lh-body); margin: 0; }
.faq-q::-webkit-details-marker { display: none; }

/* ---------- full-width tinted feature row ---------- */
.feat {
  display: grid; grid-template-columns: 1fr 1.15fr; gap: var(--gap-12);
  align-items: center; border-radius: var(--shell-radius);
  padding: var(--sp-5); margin-bottom: var(--shell-gap);
}
.feat:last-child { margin-bottom: 0; }
.feat.a { background: var(--tint-mint); }
.feat.b { background: var(--tint-lav); }
.feat.c { background: var(--tint-pink); }
.feat h3 { font-size: var(--fs-d4); font-weight: 500; line-height: 1.25; margin-bottom: 0.7rem; }
.feat p { color: #5a5f6a; margin: 0; }
.feat .eyebrow { margin-bottom: 0.9rem; display: inline-block; }
.feat-media {
  border-radius: var(--r-sm); overflow: hidden;
  background: rgba(255,255,255,0.6); border: 1px solid rgba(255,255,255,0.9);
  box-shadow: var(--shadow-sm);
}
.feat-media img { display: block; width: 100%; height: auto; }

/* ---------- responsive ---------- */
@media (max-width: 900px) {
  .proc, .faq, .feat { grid-template-columns: 1fr; gap: var(--gap-8); }
  .feat { padding: var(--sp-3); }
  .cloud-row { gap: var(--gap-6); }
  .cloud-row.spread { justify-content: center; }
  .faq-a { padding-left: 0; }
}

/* ---------- architecture diagram: the last dark element on the site ----------
   style.css:1049+ styles these nodes as dark glossy chips (fill url(#nodeGrad),
   labels #f3f7fa) for the original dark theme. On a white card that reads as a
   foreign object. Repainted light; the SVG gradient defs stay in the markup but
   are overridden here, so nothing in the HTML needs touching.
   Contrast on white: label #1a1a1c = 16.1:1, sub #6b7280 = 4.8:1 — both AA. */
.arch-panel {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--shell-radius);
}
.arch-svg .node {
  fill: #ffffff;
  stroke: rgba(0,0,0,0.13);
  stroke-width: 1;
}
.arch-svg .node.hub {
  fill: var(--tint-lav);
  stroke: rgba(90,90,140,0.35);
  stroke-width: 1.2;
}
.arch-svg .gloss { display: none; }
.arch-svg .node-label { fill: #1a1a1c; font-weight: 500; }
.arch-svg .node-sub   { fill: #6b7280; }
.arch-svg .flow       { stroke: rgba(0,0,0,0.22); stroke-width: 1.6; }
.arch-svg .pulse      { fill: #8b8fa8; }
.arch-svg .spark      { fill: #b8a8e0; }

/* ============================================================================
   PALETTE CORRECTION — measured off synthai.demos.tailgrids.com 2026-08-17
     headings  oklch(0.205 0 0) = #171717   PURE NEUTRAL, zero chroma
     body      oklch(0.556 0 0) = #737373   PURE NEUTRAL
     footer    oklch(0.708 0 0) = #a1a1a1
     accent    #585ece on #f4f5ff           a real, saturated indigo
     nav links #000000
   Two faults this fixes:
   1. Our greys were BLUE-TINTED (#6b7280 = R107 G114 B128). Against SynthAI's
      zero-chroma greys ours read hazy and washed out.
   2. We had NO accent colour at all — every accent was the same grey as body
      text, which is most of why the site read as "basic".
   Contrast on white: #585ece = 5.37:1, #737373 = 4.74:1 — both pass AA body.
   ========================================================================= */

:root {
  /* 🔴 #737373 measured 4.24:1 on the hero ground (#f2f2f3), 4.35:1 on #f5f5f6 and
     4.02:1 on the lavender work-card tint — all under the 4.5:1 AA floor /trust.html
     publicly commits to. #686868 clears 4.5:1 on every light ground we actually use
     (worst case 4.73:1 on #ebebfc) while staying zero-chroma. Re-measure on the
     RENDERED page if you change it; the tints are what bind, not white. */
  --muted: #686868;
  --text: #171717;
  --indigo: #585ece;
  --indigo-deep: #3f45b0;    /* hover / darker pairing */
  --indigo-wash: #f4f5ff;
  --border: rgba(0,0,0,0.10);
  --border-strong: rgba(0,0,0,0.17);
}

/* the accent now actually reads as an accent */
.accent-text { background: none; color: var(--indigo); }
a:not(.btn):not(.brand):not(.work-card):not(.nav-link) { color: var(--indigo); }
a:not(.btn):not(.brand):not(.work-card):not(.nav-link):hover { color: var(--indigo-deep); }

/* nav links: black, as measured */
/* 🔴 These were `.nav-link, .topbar nav a` — BOTH phantom. There is no
   `class="nav-link"` anywhere (0 occurrences; the real token is the plural
   `nav-links` on the UL) and no `<nav>` element inside `.topbar`. So the rule
   styled nothing, AND the matching `:not(.nav-link)` exemption on the global
   anchor rule above exempted nothing — which meant that rule, at specificity
   (0,4,1), beat `.nav-links a` (0,1,1) and painted the ENTIRE main navigation
   indigo. Measured: every topbar link computed rgb(88,94,206).

   A phantom selector is not merely dead weight here; the dead half of a
   `:not()` pair silently hands the cascade to something else. The specificity
   below is (0,4,1) to tie the global rule and win on order. */
.topbar .nav-links a:not(.btn-accent):not(.brand) { color: var(--text); }
.topbar .nav-links a:not(.btn-accent):not(.brand):hover { color: var(--indigo); }

/* eyebrows pick up the accent rather than dissolving into the body grey */
.eyebrow { color: var(--indigo); }
.section-head .eyebrow, .sec-head-c .eyebrow { color: var(--indigo); }

/* category pill — their exact treatment: mono, 4px radius, indigo on wash */
.pill-cat {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  letter-spacing: var(--track-label-sm);
  text-transform: uppercase;
  color: var(--indigo);
  background: var(--indigo-wash);
  border-radius: var(--r-sm);
  padding: 0.18rem 0.42rem;
}

/* the hub node and step markers gain the accent too */
.arch-svg .node.hub { fill: var(--indigo-wash); stroke: rgba(88,94,206,0.42); }
.proc-ico.b { background: var(--indigo-wash); }
.faq-item[open] .faq-mark { background: var(--indigo); border-color: var(--indigo); }
.work-card .work-kicker { color: var(--indigo); }
.cloud-badge { color: #44485c; }

/* ============================================================================
   POST CARDS with generated art
   SynthAI's blog cards lead with a large abstract 3D render. Their images are
   licensed TailGrids assets, so the art here is generated from layered CSS
   gradients — original, weightless, and themable. Card anatomy is theirs:
   art -> category pill -> 2-line title -> avatar + mono name + dot + mono date.
   ========================================================================= */

.post-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: var(--gap-6);
}
.post-card {
  display: flex; flex-direction: column; gap: var(--gap-3);
  text-decoration: none; color: inherit;
}
.post-art {
  position: relative;
  aspect-ratio: 16 / 10;
  border-radius: var(--r-sm);
  overflow: hidden;
  background: #eceef5;
  isolation: isolate;
}
.post-art::before,
.post-art::after {
  content: ""; position: absolute; inset: -25%;
  border-radius: 50%;
  filter: blur(34px);
}
/* six deterministic variants so a grid never repeats itself */
.post-art.v1 { background: linear-gradient(140deg,#eef0fb,#dfe3f7); }
.post-art.v1::before { background: radial-gradient(circle,#8f96ea,transparent 62%); transform: translate(-14%,-16%) scale(.85); }
.post-art.v1::after  { background: radial-gradient(circle,#f3c9dd,transparent 62%); transform: translate(28%,26%) scale(.75); }

.post-art.v2 { background: linear-gradient(140deg,#e9f5f0,#d8ece4); }
.post-art.v2::before { background: radial-gradient(circle,#79ccb2,transparent 62%); transform: translate(22%,-20%) scale(.8); }
.post-art.v2::after  { background: radial-gradient(circle,#a9b6f0,transparent 62%); transform: translate(-20%,30%) scale(.7); }

.post-art.v3 { background: linear-gradient(140deg,#fdeee4,#f7ded0); }
.post-art.v3::before { background: radial-gradient(circle,#f0a97e,transparent 62%); transform: translate(-18%,22%) scale(.82); }
.post-art.v3::after  { background: radial-gradient(circle,#c3a9ee,transparent 62%); transform: translate(26%,-22%) scale(.72); }

.post-art.v4 { background: linear-gradient(140deg,#e7effa,#d5e3f6); }
.post-art.v4::before { background: radial-gradient(circle,#6f9fe0,transparent 62%); transform: translate(20%,20%) scale(.85); }
.post-art.v4::after  { background: radial-gradient(circle,#9ae0d2,transparent 62%); transform: translate(-24%,-18%) scale(.7); }

.post-art.v5 { background: linear-gradient(140deg,#f3ecfb,#e6dbf7); }
.post-art.v5::before { background: radial-gradient(circle,#a98be6,transparent 62%); transform: translate(-16%,-22%) scale(.8); }
.post-art.v5::after  { background: radial-gradient(circle,#f4cdb6,transparent 62%); transform: translate(24%,24%) scale(.76); }

.post-art.v6 { background: linear-gradient(140deg,#eaf2f3,#dae9ec); }
.post-art.v6::before { background: radial-gradient(circle,#77b7c4,transparent 62%); transform: translate(18%,-24%) scale(.84); }
.post-art.v6::after  { background: radial-gradient(circle,#bfb2ef,transparent 62%); transform: translate(-22%,26%) scale(.72); }

/* a faint grain/sheen so the blobs read as a rendered surface, not a blur */
.post-art > .sheen {
  position: absolute; inset: 0; z-index: 2;
  background:
    linear-gradient(115deg, rgba(255,255,255,0.42) 0%, rgba(255,255,255,0) 42%),
    linear-gradient(300deg, rgba(255,255,255,0.28) 0%, rgba(255,255,255,0) 38%);
}
.post-card:hover .post-art::before { transform: translate(4%,4%) scale(.95); }
.post-art::before, .post-art::after { transition: transform 700ms cubic-bezier(.22,.61,.36,1); }

.post-card h3 {
  font-size: var(--fs-lg); font-weight: 500; line-height: 1.35;
  color: var(--text); margin: 0;
}
.post-card:hover h3 { color: var(--indigo); }
.post-meta {
  display: flex; align-items: center; gap: var(--gap-2);
  font-family: var(--font-mono); font-size: var(--fs-2xs);
  letter-spacing: var(--track-label-sm); color: var(--muted); text-transform: uppercase;
}
/* initials, not a stock headshot — we are not inventing a person */
.post-avatar {
  width: 26px; height: 26px; border-radius: 50%;
  background: var(--indigo-wash); color: var(--indigo);
  display: inline-flex; align-items: center; justify-content: center;
  font-family: var(--font-mono); font-size: var(--fs-2xs); font-weight: 700;
  letter-spacing: 0; flex-shrink: 0;
}
.post-dot { width: 3px; height: 3px; border-radius: 50%; background: var(--border-strong); }

@media (prefers-reduced-motion: reduce) {
  .post-art::before, .post-art::after { transition: none; }
}

/* ---------- post-card fixes ----------
   .post-card is a column flex container, so children stretch to full width by
   default — the category pill was rendering as a full-width bar instead of
   hugging its label. inline-block does not defeat a flex parent; align-self does. */
.post-card .pill-cat { align-self: flex-start; }

/* the meta row was wrapping the name onto two lines and orphaning the date */
.post-meta {
  flex-wrap: wrap;
  row-gap: var(--gap-1);
  font-size: var(--fs-2xs);
  letter-spacing: var(--track-label-sm);
}
.post-meta .post-avatar { width: 22px; height: 22px; font-size: var(--fs-2xs); }

/* four posts in a 3-up grid leaves a lone orphan. Two up, with bigger art. */
@media (min-width: 900px) {
  .post-grid { grid-template-columns: repeat(2, 1fr); gap: var(--gap-8) var(--gap-8); }
  .post-card h3 { font-size: var(--fs-xl); }
}

/* The HERO GLASS OBJECT block lived here. Superseded 2026-08-23 by
   HERO BRAND REVEAL (the logo video) - the built mark did not read as a Q.
   assets/img/glass-mark.svg retired to assets/img/_retired/. */

/* ---------- lean harder into the mesh ----------
   Was 4 blobs at opacity 0.55 / blur 60px — present but timid. Bigger, more
   saturated, more of them, and a slow hue drift so it never sits still. */
.mesh span { filter: blur(72px); opacity: 0.72; }
.mesh .m1 { width: 54vw; height: 54vw; max-width: 760px; max-height: 760px; top: -18%; right: -2%; }
.mesh .m2 { width: 48vw; height: 48vw; max-width: 660px; max-height: 660px; top: 4%;   right: 20%; }
.mesh .m3 { opacity: 0.62; }
.mesh .m4 { opacity: 0.58; }

/* two more blobs, added to the left so the wash crosses the whole card */
.mesh .m5, .mesh .m6 {
  position: absolute; display: block; border-radius: 50%;
  filter: blur(78px); will-change: transform;
}
.mesh .m5 {
  width: 40vw; height: 40vw; max-width: 520px; max-height: 520px;
  top: 28%; left: -8%; opacity: 0.42;
  /* was #f0a9c8 pink. Measured on the rendered hero, this was putting rgb(247,233,238)
     - chroma 14 - across the mid-left of the headline. Now warm stone, no hue. */
  background: radial-gradient(circle, rgba(120,110,96,0.22) 0%, rgba(120,110,96,0) 70%);
  animation: mesh-a 38s ease-in-out infinite reverse;
}
.mesh .m6 {
  width: 34vw; height: 34vw; max-width: 460px; max-height: 460px;
  bottom: -14%; left: 22%; opacity: 0.38;
  /* was #9fd8f2 cyan, measuring rgb(240,247,247) along the bottom. Now the brand
     indigo at low alpha - the one hue the page is allowed. */
  background: radial-gradient(circle, rgba(88,94,206,0.18) 0%, rgba(88,94,206,0) 70%);
  animation: mesh-b 44s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
  .mesh .m5, .mesh .m6 { animation: none !important; }
}

/* ============================================================================
   HERO BRAND REVEAL — the 3D logo video
   Replaces assets/img/glass-mark.svg. The owner's note was that the built mark
   did not read as a Q; the rendered logo in the footage plainly does.

   The footage is dark navy + gold and this page is light. Per
   [[Operating-Rules/Frontend-CSS-Lessons]] §4, a dark asset spread across a
   light page reads as a pasted foreign object — so it is CONTAINED in a framed
   panel, which makes the darkness deliberate rather than accidental. Same move
   as the black footer card.
   ========================================================================= */

.hero-brand {
  position: absolute;
  top: 50%; right: 4%;
  transform: translateY(-50%);
  width: clamp(320px, 38vw, 560px);
  z-index: 1;
  pointer-events: none;
  perspective: 1400px;
}

/* the frame: dark card, deep shadow, slight 3D tilt so it sits IN space */
.hero-brand-frame {
  position: relative;
  border-radius: var(--r-lg);
  overflow: hidden;
  background: #0a1018;
  transform: rotateY(-7deg) rotateX(2deg);
  transform-style: preserve-3d;
  box-shadow: var(--shadow-lg);
  /* a hairline of the footage's own gold, so the frame belongs to the artwork */
  outline: 1px solid rgba(198,164,92,0.28);
  outline-offset: -1px;
}
.hero-brand-video {
  display: block;
  width: 100%; height: auto;
  /* The source shipped with baked-in pillarbox: cropdetect found the real
     content at 1434x1080 offset 224,0 — 224px of pure #000 down the left.
     Cropped at encode time, so the file is now 1080x814 (4:3) edge-to-edge
     artwork. Match that ratio here; forcing 16/10 would re-introduce a crop. */
  aspect-ratio: 1080 / 814;
  object-fit: cover;
}

/* One slow specular pass across the glass. This originally existed because the
   reveal ENDED on a held still and needed something to keep it alive; the mark
   now keeps turning on its own, so the sheen is no longer load-bearing — it is
   kept because it reads as light on glass in front of the artwork, not as motion
   standing in for motion. */
.hero-brand-sheen {
  position: absolute; inset: 0;
  background: linear-gradient(115deg,
    rgba(255,255,255,0) 38%,
    rgba(255,255,255,0.10) 48%,
    rgba(255,255,255,0) 58%);
  pointer-events: none;
}

/* warm bloom bleeding out of the panel — ties the gold into the light page
   instead of letting the dark rectangle sit there unexplained */
.hero-brand-glow {
  position: absolute;
  inset: -14% -10% -18% -10%;
  z-index: -1;
  border-radius: 50%;
  background:
    radial-gradient(60% 55% at 50% 55%, rgba(198,164,92,0.20), transparent 70%),
    radial-gradient(70% 60% at 45% 45%, rgba(88,94,206,0.14), transparent 72%);
  filter: blur(38px);
}

@media (prefers-reduced-motion: no-preference) {
  .hero-brand { animation: brand-float 16s ease-in-out infinite; }
  .hero-brand-sheen { animation: brand-sheen 9s ease-in-out 1.2s infinite; }
}
@keyframes brand-float {
  0%, 100% { transform: translateY(-50%) }
  50%      { transform: translateY(calc(-50% - 12px)) }
}
@keyframes brand-sheen {
  0%, 62%, 100% { transform: translateX(-120%); opacity: 0 }
  70%           { opacity: 1 }
  86%           { transform: translateX(120%); opacity: 0 }
}

/* 🔴 Reduced motion: a hero video is motion whether or not CSS animates it.
   Hide the video element entirely and show the poster, which is the final
   resolved frame — so the mark is still presented, just static. */
@media (prefers-reduced-motion: reduce) {
  .hero-brand, .hero-brand-sheen { animation: none !important; }
  .hero-brand-frame { transform: none; }
  .hero-brand-video { display: none; }
  .hero-brand-frame {
    background: #0a1018 url("../video/logo-poster.webp") center / cover no-repeat;
    aspect-ratio: 1080 / 814;
  }
}

/* 🔴 Below ~1180px the absolutely-positioned panel runs UNDER the headline —
   at 900px it sat on top of "in Canada". Pulling it right or shrinking it only
   moves the collision. The mark already appears in the topbar, so drop the
   panel rather than let it overlap the one thing the hero has to say. */
@media (max-width: 1180px) {
  .hero-brand { display: none; }
}

/* 🔴 Collision band. Measured the HEADLINE TEXT extent (Range.getClientRects,
   not the h1's block box — a block spans the container and gives a false
   overlap) against the panel's left edge:
       1258px  gap  -4px   <- actual overlap
       1344px  gap   2px
       1418px  gap   8px
       1578px  gap  61px   <- fine
   So between ~1180 and ~1500 the panel crowds or touches the headline.
   Narrow it there instead of hiding it; it stays prominent and the gap is real. */
@media (min-width: 1181px) and (max-width: 1500px) {
  .hero-brand { width: clamp(300px, 30vw, 430px); right: 3%; }
}

/* ---------- hero constellation field ----------
   Sits above the mesh and below every piece of hero content. Purely
   decorative: if assets/js/horizon.js never runs, this is an empty canvas and
   the hero is unchanged. Never give it a background — an unpainted canvas
   would then show as a block. */
.hero-field {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 0;          /* mesh is 0 too, but the canvas comes later in the DOM */
  pointer-events: none;
  display: block;
}
/* Content must stay above it. .hero-inner and .hero-brand are already z-index:1;
   this guarantees the container is too, without touching .hero-meta's
   positioning (see the note at .page-hero .container above). */
.hero .container { position: relative; z-index: 1; }

@media (max-width: 760px) {
  .hero-field { display: none; }
}

/* 🔴 .hero-meta is position:absolute at bottom:2.4rem; the CTA row is in normal
   flow. Measured overlap of -5px at 760/860/900/1000px viewport height — it is
   CONSTANT, not viewport-dependent, so it was never a "short screen" bug.
   A margin on .hero-actions cannot fix it: margins move following flow content,
   and an absolutely-positioned element is not in flow. Pad the content column
   instead, which is what actually reserves the strip. Do NOT reposition
   .hero-meta — see the note at `.page-hero .container` above for why. */
@media (min-width: 761px) {
  .hero .hero-inner { padding-bottom: var(--sp-8); }
}


/* ---------- footer: the tokens invert on a black ground -------------------
   The footer is a black card on a light page, so the light-ground values fail
   there instead: --muted #686868 drops to 3.77:1 and --indigo #585ece to
   3.91:1, both under 4.5:1.

   Re-scoping the TOKENS rather than the rules is deliberate. `a:not(.btn)
   :not(.brand):not(.work-card):not(.nav-link)` is specificity (0,4,1) and
   beats both `footer a` (0,0,2) and `footer .footer-grid ul a` (0,3,3), so
   those white-link rules never actually apply and every footer link renders
   indigo. Fixing the value both rules resolve to works whichever one wins,
   which is the same reason `.reveal:not(.visible)` was chosen over
   out-specifying. See data/vault/Bugs/01-Reveal-Blackout.md. */
footer {
  --muted: #a3a3a3;   /* 8.3:1 on black */
  --indigo: #8b90e8;  /* 7.3:1 on black */
}

/* ---------- dark-theme leftovers stranded on the light page ----------------
   .step-num was built for the dark theme: `color: transparent` painted only by a
   1px `-webkit-text-stroke` in the old cyan accent, rgba(34,211,238,0.6). That
   composites to roughly 1.46:1 over white, so "01 / 02 / 03" on the homepage were
   effectively invisible — the numerals were being drawn, just not in anything a
   reader could see. light.css never overrode the rule, so the dark-theme styling
   simply survived the theme change.

   Solid indigo instead of a lighter stroke: a 1px outline cannot be measured or
   guaranteed at AA the way a filled glyph can, and #585ece on white is 5.38:1.
   Same call as .practice-index above. */
.step-num {
  color: var(--indigo);
  -webkit-text-stroke: 0;
}

/* Two more dark-theme accents that light.css never overrode. Both still RENDER on
   the light page — confirmed by reading computed styles, not by reading the source,
   because most of the other 31 cyan references in style.css turned out to be dead.

   .scroll-progress kept a cyan glow around a bar whose fill light.css had already
   re-themed to dark, so the bar and its own glow disagreed. .flagship washed a teal
   radial across a panel on a light page — the pasted-foreign-object failure in
   Operating-Rules/Frontend-CSS-Lessons section 4. Both are now the real indigo. */
.scroll-progress { box-shadow: 0 0 10px rgba(88, 94, 206, 0.45); }
.flagship {
  background:
    radial-gradient(ellipse 55% 70% at 85% 15%, rgba(88, 94, 206, 0.10), transparent 60%),
    var(--surface);
}

/* ---------- form status: dark-theme pastels on a light wash --------------------
   .form-status.ok was #86efac and .err was #fca5a5 — pastel greens and pinks chosen
   for a dark background, measuring 1.27:1 and 1.63:1 over their own 12%-alpha washes
   on white. The message telling someone their enquiry failed to send was effectively
   unreadable. Darkened to the same hues at light-theme luminance. */
.form-status.ok  { color: #15803d; border-color: rgba(21, 128, 61, 0.45); }
.form-status.err { color: #b91c1c; border-color: rgba(185, 28, 28, 0.45); }

/* ---------- hero mark: reveal, then a live loop ---------------------------
   Two videos in the same frame. The reveal stays in flow and defines the
   frame's height; the loop is absolutely positioned over it and starts at
   opacity 0. main.js crossfades between them ~0.55s BEFORE the reveal ends,
   so the two clips overlap and the cut never becomes a cut.

   Fading rather than swapping `display` matters: the two clips were graded
   slightly differently (the reveal resolves to a paler, more silver gold than
   the loop's richer one) and are one frame apart in rotation phase. A hard cut
   shows both. A half-second crossfade shows neither, which is why the geometry
   was matched at encode time AND crossfaded here rather than either alone.

   If main.js never runs, `.is-looping` is never set, the loop stays at opacity
   0 and never downloads, and the hero behaves exactly as it did before. */
/* Named `-intro`, NOT `-reveal`. The scroll-animation convention on this site is
   the bare `.reveal` class, and this repo guards it with grep. `hero-brand-reveal`
   matched `reveal` because a hyphen is a word boundary, so a routine
   "do all reveals resolve?" sweep reported a phantom unresolved block on the live
   site. CSS was never affected — class tokens match exactly — but the guard was,
   and a guard that cries wolf gets ignored. */
.hero-brand-loop {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  transition: opacity 520ms linear;
  will-change: opacity;
}
.hero-brand-frame.is-looping .hero-brand-loop   { opacity: 1; }
.hero-brand-frame.is-looping .hero-brand-intro { opacity: 0; transition: opacity 520ms linear; }

@media (prefers-reduced-motion: reduce) {
  /* main.js does not hand over at all under reduced motion, but if the class
     ever lands some other way, do not animate the swap. */
  .hero-brand-loop, .hero-brand-frame.is-looping .hero-brand-intro { transition: none; }
}

/* ---------- architecture diagram on a phone --------------------------------
   The SVG carries `viewBox="0 0 960 300"` and `width:100%`, so its 13px and
   10.5px labels are viewBox UNITS that scale with the rendered width. At a 375px
   viewport the container leaves about 257px of SVG — a scale factor of 0.267 —
   which renders those labels at 3.5px and 2.8px. Nothing errors; the diagram just
   becomes a field of smudges, and no rule anywhere touched it.

   A 960-unit diagram does not fit a 375px phone, so it scrolls rather than
   shrinks. This is the same `overflow-x: auto` + `min-width` pattern style.css
   already uses for .cmp-wrap tables; reusing it beats inventing a second one.
   `.arch-panel` normally sets `overflow: hidden` for its rounded corners, so the
   axes are split here: x scrolls, y stays clipped. */
@media (max-width: 760px) {
  .arch-panel {
    overflow-x: auto;
    overflow-y: hidden;
    padding-left: 1.1rem;
    padding-right: 1.1rem;
    -webkit-overflow-scrolling: touch;
  }
  .arch-svg { min-width: 860px; }
  /* 860/960 = 0.896, so these land at ~11.6px and ~10.3px rendered. */
  .arch-svg .node-sub { font-size: 11.5px; }
}

/* .node-sub kept a blue-cast literal while the rest of the palette is zero-chroma. */
.arch-svg .node-sub { fill: var(--muted); }

/* 🔴 .page-hero is `padding: 11rem 0 5rem` — 176px of dead space above the
   headline — with no responsive override anywhere in either stylesheet. On a
   375x667 phone that is more than a quarter of the viewport spent on nothing
   before the visitor sees a single word, on all 26 inner pages. The homepage
   hero has its own responsive rules; this one was simply never given any. */
@media (max-width: 760px) {
  .page-hero { padding: var(--sp-12) 0 var(--sp-6); min-height: 0; }
}
@media (max-width: 480px) {
  .page-hero { padding: var(--sp-12) 0 var(--sp-5); }
}

/* 🔴 .post-meta is `display:flex` + `flex-wrap:wrap`, so every BARE TEXT RUN
   inside it became its own anonymous flex item. The separator dots could then
   wrap away from the text they separate, leaving an orphaned dot at the start
   or end of a line on all four article cards. Same defect class as the pricing
   line that scrambled into "a held- you help / out set" — see
   Operating-Rules/Frontend-CSS-Lessons section 2. Each dot now travels with the
   text it introduces, as one unbreakable item. */
.post-seg { display: inline-flex; align-items: center; gap: var(--gap-2); white-space: nowrap; }

/* ============================================================
   STACKED SCROLL — the production-systems cards deal themselves into a pile
   ============================================================
   Cards park under the header one after another, each 16px lower than the last,
   so the previous card's top edge stays visible as the spine of a deck.

   Measured from the reference rather than eyeballed: 3 sticky cards, top
   96/112/128px, z-index 10/20/30, 24px apart, radius 12px, and tints
   rgb(237,244,234) / rgb(235,235,252) / rgb(247,239,247) — which are already
   this site's --tint-mint / --tint-lav / --tint-pink, to the byte. The offsets
   here are 92/108/124 instead, because this header is 68px, not 72px, and the
   number that matters is the 24px of clearance beneath it.

   🔴 The wrapper's SECTION sets `overflow: hidden`, which silently disables
   position:sticky — a sticky element resolves against its nearest scroll
   container, so inside a clipped box it simply never sticks and there is no
   error to notice. The :has() override below is load-bearing, not tidy-up.
   ============================================================ */
section:has(.work-stack) { overflow: visible; }

.work-stack {
  display: block;          /* NOT grid: sticky needs normal flow to travel in */
  position: relative;
  margin-top: var(--sp-6);
}
.work-stack .work-card {
  position: sticky;
  display: grid;
  grid-template-columns: 1fr 1.15fr;
  align-items: center;
  gap: var(--gap-10);
  min-height: 360px;
  padding: var(--sp-4) var(--sp-5);
  margin-bottom: 1.5rem;
  border-radius: var(--r-xl);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
}
.work-stack .work-card:last-child { margin-bottom: 0; }

/* The stagger. Four cards, four stops, each 16px below the one before.

   🔴 THIS LIST IS KEYED TO THE CARD COUNT. A card with no rule here gets no
   `top` and no `z-index`: `position:sticky` with `top:auto` never sticks, so it
   scrolls away while its siblings pin, and its auto z-index loses to the explicit
   10/20/30, so it slides UNDERNEATH them. Neither failure throws, and both are
   invisible in a static screenshot - the card looks correct until you scroll.
   Found when Quinndatum was added as a fourth card. Add a stop with every card. */
.work-stack .work-card:nth-child(1) { top: 92px;  z-index: 10; }
.work-stack .work-card:nth-child(2) { top: 108px; z-index: 20; }
.work-stack .work-card:nth-child(3) { top: 124px; z-index: 30; }
.work-stack .work-card:nth-child(4) { top: 140px; z-index: 40; }

/* Text left, artwork right — the card was column-stacked as a grid tile. */
.work-stack .work-body {
  order: -1;
  padding: 0;
  gap: var(--gap-3);
  max-width: 46ch;
}
.work-stack .work-shot {
  aspect-ratio: 16 / 10;
  border-bottom: 0;
  border-radius: var(--r-lg);
  background: rgba(255,255,255,0.55);
  box-shadow: var(--shadow-lg);
}
.work-stack .work-card h3 { font-size: var(--fs-d3); letter-spacing: -0.02em; }
.work-stack .work-card p  { font-size: var(--fs-lg); }
/* The lift-on-hover from the grid layout fights a card that is pinned in place. */
/* 🔴 An INSET press, not an outer bloom. These cards are position:sticky and
   deliberately do not lift (a pinned card cannot rise), so the hover was
   expressed as the outer shadow jumping from 1px to 48px - which on a card
   already pinned under the header reads as it growing a halo rather than
   responding to the pointer. Pressing inward is the response a stuck element
   can actually make.

   No background change, deliberately: the cards cycle --tint-mint /
   --tint-lav / --tint-pink via :nth-child above, so any single hover background
   would be right for one card and wrong for two. The border and the inset carry
   it instead. Properties are named explicitly rather than using
   `transition: all`. */
.work-stack .work-card:hover {
  transform: none;
  box-shadow: inset 0 2px 7px rgba(16, 24, 40, 0.13);
  border-color: var(--border-strong);
  transition: box-shadow var(--dur-2) var(--ease-out),
              border-color var(--dur-2) var(--ease-out);
}

@media (max-width: 900px) {
  /* Below this the two columns are too narrow to read, and a pinned 360px card
     eats most of a phone viewport. Falls back to plain stacked cards. */
  .work-stack .work-card {
    position: static;
    grid-template-columns: 1fr;
    min-height: 0;
    padding: 1.4rem 1.4rem var(--sp-3);
    gap: var(--gap-4);
  }
  .work-stack .work-body { order: 0; max-width: none; }
}

@media (prefers-reduced-motion: reduce) {
  /* Pinning is motion the visitor did not ask for: the page scrolls and the
     content does not move with it. Honour the setting and just stack them. */
  .work-stack .work-card { position: static; }
}

/* Remaining Space Mono contexts that still asked for a weight the family does not
   ship. .hero-meta is mono, so its <b> at 600 was synthesised; the nav CTA inherits
   a 600 from the base sheet's .nav-links a.btn-accent. Both snapped to 700. */
.hero-meta b { font-weight: 700; }
.nav-links a.btn-accent, .nav-links a.btn-accent:hover { font-weight: 700; }

/* ---------- tech marquee ----------------------------------------------------
   The stack strip used to be two static rows. It now scrolls, which is the device
   the reference templates use for a CLIENT LOGO wall - a thing this site cannot
   honestly show, having no clients yet and saying so on /trust and /projects.
   Running real tooling through it borrows the motion without borrowing the claim.

   Mechanics, measured from the reference: `linear infinite`, ~9-18s, with the item
   list DUPLICATED so a second copy slides in as the first leaves. Translating -50%
   of a track holding exactly two copies lands the second copy precisely where the
   first began, which is what makes the seam invisible. If you add an item, add it
   to BOTH copies or the loop will jump. */
.cloud-marquee {
  overflow: hidden;
  padding: 0.4rem 0;
  /* fade the ends so items enter and leave instead of being chopped off */
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent);
}
.cloud-track {
  display: flex;
  gap: var(--gap-10);
  width: max-content;
  /* 🔴 The trailing padding is load-bearing, not spacing. A flex row of 10 items
     has NINE gaps, so the track measured 1186px while the repeating unit - five
     items plus five gaps - is 612px. translateX(-50%) moves 593px, and the loop
     would jump 19px every cycle: a visible stutter, roughly once every 17s, of
     exactly the kind nobody can describe but everybody notices. One trailing gap
     makes the track exactly two repeating units wide, so -50% lands the second
     copy where the first began. Keep this equal to `gap`. */
  padding-right: var(--sp-5);
  animation: cloud-scroll 34s linear infinite;
}
.cloud-track.reverse { animation-direction: reverse; animation-duration: 42s; }
.cloud-marquee:hover .cloud-track,
.cloud-marquee:focus-within .cloud-track { animation-play-state: paused; }
.cloud-track .cloud-item { flex: 0 0 auto; }

@keyframes cloud-scroll {
  from { transform: translate3d(0, 0, 0); }
  /* -50% because the track holds exactly TWO copies of the list. */
  to   { transform: translate3d(-50%, 0, 0); }
}

@media (prefers-reduced-motion: reduce) {
  /* Perpetual horizontal motion is the clearest possible case for honouring this.
     Falls back to the static centred rows the strip had before. */
  .cloud-track { animation: none; width: auto; padding-right: 0; flex-wrap: wrap; justify-content: center; }
  .cloud-marquee { -webkit-mask-image: none; mask-image: none; }
  /* 🔴 Hide the duplicate copy. It exists ONLY to make the loop seamless; with the
     animation off it is just every technology listed twice, which is worse than the
     static strip this replaced. Stopping the motion is not the whole job - the
     scaffolding the motion needed has to go with it. */
  .cloud-track .cloud-item[aria-hidden="true"] { display: none; }
}

@media (max-width: 760px) {
  .cloud-track { gap: var(--gap-6); animation-duration: 26s; }
  .cloud-track.reverse { animation-duration: 32s; }
}

/* ---------- services grid: hover reveals what the service does ---------------
   Measured from productized-agency-template-acetern.vercel.app: an overlay at
   `absolute inset-0`, 50% black with `backdrop-blur-md` (12px), radius 24px,
   padding 24/32px, `flex-col justify-between`, ~400ms ease-out.

   🔴 One deliberate DEPARTURE from the reference. There, the tile underneath is a
   screenshot that says nothing on its own, so the hover only ever ADDS meaning.
   Here the thing being covered is a paragraph a visitor may actually want to
   read, so a literal copy would hide real content behind a pointer - lost on
   touch, lost to a crawler, and lost to anyone navigating by keyboard. The
   description therefore stays in the DOM, appears on focus-within as well as
   hover, and is shown UNCONDITIONALLY wherever hover does not exist. Copy the
   interaction, not the information architecture. */
/* ============================================================================
   SERVICE CARDS - preview graphic, price, name, description, link.

   Replaces a hover-reveal card that hid the description until you pointed at it.
   Every card now leads with a diagram of the thing it sells, and the description
   is always readable - including on touch, which previously needed a whole
   `@media (hover: none)` branch to undo the hiding.

   The six graphics are deliberately NOT one template with swapped icons. Each is
   drawn for its own service: a document with extracted fields feeding a queue, a
   Read/Decide/Act chain with a human approval gate, an answer citing back to
   permissioned sources, three editors reaching three systems through one server,
   a perimeter with traffic that stops at it, and spend-per-run either side of an
   audit marker. A card set where the picture is interchangeable is a card set
   where the picture means nothing.

   No invented data. The reference this layout is taken from puts scorecards in
   its previews (92 / 88 / 81 / 14); we have no such numbers, so ours carry
   structure instead of figures. Nothing here asserts a measurement.
   ============================================================================ */
.svc-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  /* 🔴 --gap-5 DOES NOT EXIST. The gap scale is 4px-based and deliberately skips 5:
     1,2,3,4,6,8,10,12,14. An undefined custom property with no fallback makes the whole
     declaration invalid at computed-value time, so `gap` fell back to `normal` and the
     six cards rendered FLUSH - measured columnGap 0px, rowGap 0px. It looked fine in a
     screenshot because adjacent white cards with 1px borders read as a divided grid:
     the "gaps" were two borders touching. This is why an undefined-token check now runs
     in tools/. Restores var(--gap-6), 24px, which is what this grid used before. */
  gap: var(--gap-6);
  margin-top: var(--sp-5);
}
.svc-card {
  display: flex;
  flex-direction: column;
  background: #ffffff;
  border: 1px solid var(--border);
  border-radius: var(--r-xl);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
  text-decoration: none;
  color: var(--text);
  transition: box-shadow var(--dur-2) var(--ease-out),
              border-color var(--dur-2) var(--ease-out);
}
.svc-card:hover { box-shadow: var(--shadow-md); border-color: var(--border-strong); }

/* the preview plate */
.svc-art {
  display: block;
  background:
    radial-gradient(120% 100% at 20% 0%, rgba(88,94,206,0.07), transparent 62%),
    radial-gradient(110% 100% at 90% 100%, rgba(120,110,96,0.06), transparent 60%),
    var(--surface-2);
  border-bottom: 1px solid var(--border);
  padding: var(--sp-3) var(--sp-3) calc(var(--sp-3) - 4px);
}
.svc-art .gx { display: block; width: 100%; height: auto; }

.svc-body {
  display: flex;
  flex-direction: column;
  gap: var(--gap-2);
  padding: var(--sp-4);
  flex: 1;
}
.svc-flag {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  font-weight: 700;
  letter-spacing: var(--track-label-md);
  text-transform: uppercase;
  color: var(--indigo);
}
.svc-name {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--gap-2);
  font-family: var(--font-display);
  font-size: var(--fs-xl);
  line-height: var(--lh-display);
  color: var(--text);
}
.svc-go { width: 15px; height: 15px; flex-shrink: 0; margin-top: 0.35em; color: var(--muted); transition: color var(--dur-1) var(--ease-out); }
.svc-card:hover .svc-go { color: var(--indigo); }
.svc-desc {
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  color: var(--muted);
  max-width: 46ch;
  flex: 1;
}
.svc-cta {
  display: inline-flex; align-items: center; gap: var(--gap-2);
  margin-top: var(--gap-2);
  font-family: var(--font-mono); font-weight: 700;
  font-size: var(--fs-2xs); letter-spacing: var(--track-label-sm); text-transform: uppercase;
  color: var(--text);
}
.svc-card:hover .svc-cta { color: var(--indigo); }

/* ---- graphic palette ----
   Styled from the stylesheet rather than with inline fills, so the diagrams take
   the same tokens as the rest of the page and cannot drift from it. */
.gx-plate   { fill: #ffffff; stroke: rgba(0,0,0,0.12); stroke-width: 1; }
.gx-mute    { fill: rgba(0,0,0,0.16); }
.gx-ink     { fill: #1a1a1c; }
.gx-ink-c   { fill: #1a1a1c; }
.gx-ink-s   { fill: none; stroke: #1a1a1c; stroke-width: 1.6; }
.gx-line    { fill: none; stroke: rgba(0,0,0,0.30); stroke-width: 1.4; stroke-linecap: round; stroke-linejoin: round; }
.gx-dash    { fill: none; stroke: rgba(0,0,0,0.26); stroke-width: 1.2; stroke-dasharray: 3 3.5; stroke-linecap: round; }
.gx-field   { fill: rgba(88,94,206,0.10); stroke: var(--indigo); stroke-width: 1.1; }
.gx-hub     { fill: #1a1a1c; }
.gx-act     { fill: #1a1a1c; }
/* 🔴 These MUST out-specify .gx-label / .gx-okmark rather than merely precede them.
   As plain single classes they tie at (0,1,0) and lose on source order, which put
   black label text on the black Act, MCP and model nodes - invisible, and invisible
   in a way no validator reports. Compound selectors are order-independent. */
.gx-label.gx-on   { fill: #ffffff; }
.gx-okmark.gx-onmark { stroke: #ffffff; }
.gx-ok      { fill: rgba(46,125,90,0.14); }
.gx-okmark  { fill: none; stroke: #2e7d5a; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.gx-except  { fill: rgba(176,110,30,0.07); stroke: rgba(176,110,30,0.35); stroke-width: 1; }
.gx-flag    { fill: rgba(176,110,30,0.16); }
.gx-flagmark{ fill: none; stroke: #8a5510; stroke-width: 1.8; stroke-linecap: round; }
.gx-cite    { fill: rgba(88,94,206,0.12); stroke: var(--indigo); stroke-width: 1; }
.gx-cnum    { fill: var(--indigo); font-family: var(--font-mono); font-size: 9px; font-weight: 700; text-anchor: middle; }
.gx-lock    { fill: none; stroke: #1a1a1c; stroke-width: 1.3; }
.gx-lockarc { fill: none; stroke: #1a1a1c; stroke-width: 1.3; }
.gx-bound   { fill: rgba(0,0,0,0.02); stroke: rgba(0,0,0,0.30); stroke-width: 1.3; stroke-dasharray: 5 4; }
.gx-blocked { fill: none; stroke: rgba(0,0,0,0.22); stroke-width: 1.4; stroke-dasharray: 4 4; }
.gx-stop    { fill: rgba(176,110,30,0.12); stroke: rgba(176,110,30,0.45); stroke-width: 1.2; }
.gx-stopmark{ fill: none; stroke: #8a5510; stroke-width: 1.8; stroke-linecap: round; }
.gx-gate    { fill: #ffffff; stroke: rgba(0,0,0,0.20); stroke-width: 1; }
.gx-label   { fill: #1a1a1c; font-family: var(--font-display); font-size: 13px; font-weight: 500; text-anchor: middle; }
.gx-mini    { fill: #1a1a1c; font-family: var(--font-body); font-size: 10.5px; text-anchor: middle; }
.gx-tag     { fill: #57534e; font-family: var(--font-mono); font-size: 8.5px; letter-spacing: 0.06em; text-transform: uppercase; text-anchor: middle; }
.gx-tagl    { text-anchor: start; }

@media (max-width: 1024px) { .svc-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 680px)  { .svc-grid { grid-template-columns: 1fr; } }

/* ---------- focus ring colour: ground-aware ----------------------------------
   A single fixed ring colour cannot serve both grounds on this site. --accent is
   #1a1a1c and the footer is #000000, so the obvious choice measures 1.21:1
   there - an invisible focus indicator on 28 links, which fails WCAG 2.4.7 and
   is worse than the UA ring it would replace.

   Scoping the TOKEN rather than the rule is the same technique used for --muted
   and --indigo in the footer: whichever selector ends up winning, the value it
   resolves is correct for the ground it sits on.
     #1a1a1c on #ffffff = 18.9:1   |   #ffffff on #000000 = 21:1
   Both clear the 3:1 floor for non-text UI by a wide margin. */
:root { --focus-ring: #1a1a1c; }
/* 🔴 ONLY the footer, and it was checked by sampling rendered pixels rather than
   reading CSS. The first version of this line also listed .cta-band, on the
   assumption that a "CTA band" is dark - it is not. Its background-color computes
   to transparent and its darkness was imagined; the real fill is a
   background-IMAGE, `radial-gradient(... rgba(201,184,240,0.4) ...)`, which
   samples at rgb(235,229,249): pale lavender. A white ring there would have been
   about 1:1 - the exact invisible-indicator bug this token exists to prevent,
   reintroduced by guessing at a class name instead of measuring.
   footer samples rgb(0,0,0). White on it is 21:1. */
footer, [data-ground="dark"] { --focus-ring: #ffffff; }

/* ---------- section heading classes -----------------------------------------
   🔴 76 of the 282 h2 elements carried INLINE styles, in 12 distinct strings, of
   which the top four are near-duplicates that differ only in margin or in the
   whitespace after a semicolon. The measured consequence: five section headings
   on the homepage, all rendering at the same 36.8px, arrived in FOUR different
   combinations of line-height and margin - two different leadings at one size.

   Inline styles also sit above every class in the cascade, so each of those 76
   was a small permanent veto over the design system. Two classes replace them.
   If a heading needs something these do not give it, that is a signal the scale
   is wrong, not that the element needs an exception. */
/* Measured off productized-agency-template-acetern.vercel.app at 1440x900: their section
   headings render 48px / weight 600 / line-height 1.0 / letter-spacing -1.2px. Ours were
   36.8px / 500 / 1.22 - a full step smaller and a weight lighter, which is most of why
   theirs reads editorial and ours read like body copy that happened to be bold.
   --fs-d2 tops out at 48px, so this matches at desktop and still scales down on a phone. */
.sec-h2 {
  font-size: var(--fs-d2);
  line-height: 1.06;
  font-weight: 600;
  letter-spacing: -0.025em;
  margin: 0.9rem 0 1rem;
}
.sub-h2 {
  font-size: var(--fs-d4);
  line-height: 1.25;
  font-weight: 600;
  margin: 0 0 0.7rem;
}

/* ---------- recessed well ----------
   The audit called this a surface ROLE we lack. Measured, we half-had it: .arch-panel
   already renders on --surface-2 rather than white, so it separates by tone. What was
   missing is the inset that makes it read as recessed rather than merely tinted. No
   outer shadow here on purpose - an element cannot sit both above and below the page. */
.arch-panel {
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.05), inset 0 0 0 1px rgba(0, 0, 0, 0.02);
}


/* ---------- leading ramp: display text out of body-sized line boxes ----------
   Measured on index: .step-num 33.6px and .proof-item b 27.2px both ran line-height
   1.65, so "01" occupied a 55.4px line box and a one-word label a 44.9px one. Both are
   single-line display elements that had simply inherited body leading. .hero-sub is
   real prose at 30.4px, so it takes the dense step rather than the display one - 1.65
   on 30px reads as two separate lines rather than a paragraph. */
.step-num { line-height: var(--lh-display); }
.proof-item b { line-height: var(--lh-display); }
.hero-sub { line-height: var(--lh-dense); }


/* ---------- comparison table: card shell, verdict row, mobile stack ----------
   Was a bare <table>: no ground, no border, no radius, and .cmp { min-width: 640px }
   inside an overflow-x:auto wrapper, so below 640px it became a horizontal scroller.

   The audit asked for an accordion at mobile. Deliberately NOT an accordion: this is
   the build-vs-buy table, and /trust.html publicly commits to "the build-vs-buy call".
   An accordion hides that behind a tap. Stacked cards show every row without one. */
.cmp-wrap {
  background: #ffffff;
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm);
  overflow: hidden;              /* clip rows to the shell corners */
  overflow-x: auto;
}
.cmp { min-width: 640px; }
.cmp thead th {
  background: var(--surface-2);
  border-bottom: 1px solid var(--border);
}
/* the verdict row - the one the table exists to make */
.cmp tbody tr:last-child { background: var(--tint-lav); }
.cmp tbody tr:last-child td { border-bottom: none; }
.cmp .us { color: var(--text); font-weight: 600; }

@media (max-width: 700px) {
  /* table semantics stay in the a11y tree; only the box model changes */
  .cmp-wrap { overflow-x: visible; background: transparent; border: 0; box-shadow: none; }
  .cmp, .cmp tbody, .cmp tr, .cmp td { display: block; width: 100%; }
  .cmp { min-width: 0; }
  .cmp thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
  .cmp tbody tr {
    background: #ffffff;
    border: 1px solid var(--border);
    border-radius: var(--r-lg);
    box-shadow: var(--shadow-sm);
    padding: var(--sp-3);
    margin-bottom: var(--gap-3);
  }
  .cmp tbody tr:last-child { background: var(--tint-lav); }
  .cmp td { border-bottom: none; padding: 0 0 var(--gap-2) 0; white-space: normal; }
  .cmp td:last-child { padding-bottom: 0; }
  /* the column header each cell lost when the table stopped being a grid */
  .cmp td[data-label]::before {
    content: attr(data-label);
    display: block;
    font-family: var(--font-display);
    font-size: var(--fs-2xs);
    font-weight: 600;
    letter-spacing: var(--track-label-md);
    text-transform: uppercase;
    color: var(--muted);
    margin-bottom: 0.25rem;
  }
  /* the option name leads the card, so it needs no label above it */
  .cmp td:first-child::before { content: none; }
  .cmp td:first-child { font-size: var(--fs-lg); font-weight: 600; color: var(--text); }
}


/* ---------- FAQ: the intro column follows the list ----------
   Measured on index: the left column is 224px tall against a 571px list, leaving
   347px of dead space beside the questions. Rather than invent filler content for
   18 pages, the intro tracks the list as it scrolls.

   🔴 The wrapping SECTION sets `overflow: hidden`, which silently disables
   position:sticky - a sticky element resolves against its nearest scroll container,
   so inside a clipped box it never sticks and throws no error. Same load-bearing
   :has() override the work-stack needs. Nothing in this section reaches the rounded
   corners, so releasing the clip has no visual cost.

   top: 92px matches the first work-card stop - 71px of fixed nav plus clearance -
   so the two sticky systems on the site share one offset instead of drifting apart.

   Above the 900px breakpoint only: below it .faq is a single column, where a sticky
   heading would pin over its own list. */
section:has(.faq) { overflow: visible; }

@media (min-width: 901px) {
  .faq > :first-child { position: sticky; top: 92px; }
}


/* ---------- offer card: two planes ----------
   The price and the deliverables shared one flat ground, so the number - the thing the
   card exists to communicate - had no more visual weight than a bullet. The price now
   sits on a recessed plate and the deliverables stay on the card ground.

   Done entirely on .price, with no wrapper element, because these cards appear on 9
   pages and a markup change would have to land identically on all of them. */
.offer .price {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: var(--sp-3);
  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.04);
}


/* ---------- measure caps exposed by the 1140 -> 1376 band ----------
   19 blocks on this site already cap their measure in `ch`, so they held a readable
   line length when the band widened. These two had no cap and went to 94ch and 92ch,
   well past the 45-75ch a reader can track without losing the line. Capped in `ch`
   rather than px so the cap follows the font size instead of drifting from it.

   Found by measuring AFTER the width change, not predicted before it - the blocks that
   would break were not the ones I would have guessed. */
.faq-a p { max-width: 70ch; }
.callout p { max-width: 70ch; }


/* ---------- grain ----------
   86% of the homepage measured as white or near-white, which reads sterile on a large
   screen. A single fixed noise layer breaks that up without touching any colour value.

   Sits at --z-grain (45): above page content, BELOW --z-nav (50), so the nav remains the
   topmost layer rather than being filmed over. `pointer-events: none` so it can never
   intercept a click, and `position: fixed` so it does not scroll with content and give
   the texture a parallax it should not have.

   The tile is an inline feTurbulence SVG, so there is no network request and nothing to
   404. Note it contains NO XML comment: a `--` inside one makes an SVG unparseable and
   the browser reports nothing at all.

   Dropped entirely under reduced-motion? No - it does not move. But it IS dropped for
   forced-colors, where an overlay would fight the user's chosen palette. */
body::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: var(--z-grain);
  pointer-events: none;
  /* Opacity chosen by MEASUREMENT, not taste. The tile has a per-pixel standard
     deviation of 20.6 levels; at 0.035 that composites to an effective sd of 0.72 of
     255, which is below 8-bit quantisation - the layer would have been present in the
     DOM, decoding correctly, and doing visually nothing. 0.08 gives ~1.65 levels:
     enough to break a flat expanse, well under the threshold where it reads as dirt. */
  opacity: 0.08;
  background-image: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='140'%20height='140'%3E%3Cfilter%20id='qn'%3E%3CfeTurbulence%20type='fractalNoise'%20baseFrequency='0.82'%20numOctaves='3'%20stitchTiles='stitch'/%3E%3C/filter%3E%3Crect%20width='140'%20height='140'%20filter='url%28%23qn%29'/%3E%3C/svg%3E");
  background-repeat: repeat;
  background-size: 140px 140px;
}
@media (forced-colors: active) {
  body::after { display: none; }
}


/* ---------- founder's desk ----------
   Reuses .card, so it inherits the ground, border, radius and resting elevation the
   rest of the site's cards already share - no new surface role invented for one block. */
.founder { padding: var(--sp-5); max-width: 900px; margin-inline: auto; }
.founder p { max-width: 68ch; }
.founder p + p { margin-top: var(--gap-3); }
.founder-sign {
  margin-top: var(--gap-4);
  font-family: var(--font-display);
  font-size: var(--fs-sm);
  letter-spacing: var(--track-label-sm);
  text-transform: uppercase;
  color: var(--text);
}


/* ---------- footer: one internal surface level ----------
   Measured, the footer was rgb(0,0,0) with an EMPTY set of internal backgrounds - a
   single flat slab, which is what the audit flagged. The legal bar is a different KIND
   of content from the link columns above it, so it gets its own ground rather than
   relying on a hairline alone.

   3% white over black is ~#080808. Deliberately tiny: the footer text is
   rgba(255,255,255,0.62), so a heavier plate would start eating the contrast that was
   measured to AA across all 28 pages. Re-run the contrast gate after touching this. */
footer .footer-bottom {
  background: rgba(255, 255, 255, 0.03);
  border-radius: var(--r-md);
  border-top: 0;            /* the plate separates it now; a hairline too is doubling up */
  padding: var(--sp-3);
  margin-top: var(--gap-3);
}


/* ---------- service-page hero diagram ----------
   The larger sibling of the card previews on automation.html. Same plate treatment and
   the same .gx-* palette, so the thumbnail on the index and the full diagram on the
   detail page read as one drawing at two sizes.

   Width is capped rather than filling the 1376px band. A 560x200 viewBox stretched the
   full width would render ~490px tall and thin its 1.2-1.4px strokes out to nothing -
   the diagram would get bigger and less legible at the same time. */
.svc-hero-art {
  margin-top: var(--sp-5);
  max-width: 760px;
  background:
    radial-gradient(120% 100% at 20% 0%, rgba(88,94,206,0.07), transparent 62%),
    radial-gradient(110% 100% at 90% 100%, rgba(120,110,96,0.06), transparent 60%),
    var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-xl);
  box-shadow: var(--shadow-sm);
  padding: var(--sp-4);
}
.svc-hero-art .gx-hero { display: block; width: 100%; height: auto; }

/* 🔴 On a phone the diagram SCROLLS rather than shrinking, for exactly the reason
   already documented for the architecture diagram further up this file: the 13px, 10.5px
   and 8.5px labels are viewBox UNITS, so they scale with rendered width. Measured at a
   390px viewport the plate leaves about 268px of SVG - a scale factor of 0.48 - which
   renders the tags at roughly 4px. Nothing errors; the diagram just becomes a smudge.

   Same `overflow-x: auto` + `min-width` pattern .cmp-wrap and .arch-panel already use;
   reusing it beats inventing a third. The axes are split because the plate has rounded
   corners to keep: x scrolls, y stays clipped. */
@media (max-width: 700px) {
  .svc-hero-art {
    padding: var(--sp-3);
    overflow-x: auto;
    overflow-y: hidden;
    -webkit-overflow-scrolling: touch;
  }
  .svc-hero-art .gx-hero { min-width: 460px; }
}

/* 🔴 REMOVED: .gx-bar, .gx-barflag, .gx-barok, .gx-base and .gx-marker - the bar,
   axis and marker classes of the diagram palette. Verified unused first: 0 occurrences
   as a class token across all 28 pages and both JS files, checked as whole tokens rather
   than substrings, because a previous "65 unused classes" scan on this codebase was wrong
   about at least one entry.

   They are removed for a reason beyond tidiness. These five are exactly what made an
   invented chart easy to draw, and it happened TWICE: a "sorted by return" bar ranking in
   the llm-audit hero, and the same before/after chart left live on its card for two more
   sprints. A palette that ships bars and an axis invites a diagram to report data it has
   no source for. This site publishes real prices and refuses fabricated statistics, so the
   drawing kit should not hand you a chart by default.

   If a future diagram genuinely needs to plot sourced data, add these back deliberately,
   and cite the source in the same commit. */

/* ---------- measure floor for prose dropped straight into a container ----------
   Every other prose context on this site caps its measure in `ch` - .hero-inner 72,
   .step p 62, .cta-band p 56, .sec-head-c > p 62, .faq-a p 70, .callout p 70,
   .founder p 68. A bare <p> placed directly in .container had NO cap, so it took the
   full band: measured 126ch on services/agent-automation.html and 83ch on the
   industries pages. Widening --container from 1140 to 1376 made it worse, not new -
   at 1140 that same paragraph ran about 104ch, already past readable.

   🔴 `:where()` is load-bearing here, not decoration. It contributes ZERO to
   specificity, so this rule lands at (0,1,0) and every existing explicit cap - all of
   which are (0,1,1) - still wins. Written as a plain `.container > p` it would tie
   .cta-band p and beat it on source order, silently widening that centred block from
   56ch to 70ch. A default has to lose to the thing it is a default for. */
.container > :where(p) { max-width: 70ch; }

/* ---------- inline-style consolidation ----------------------------------------
   458 inline style attributes were measured across the 28 pages. These six patterns
   accounted for 157 of them and were byte-identical every single time, so they were
   not one-offs - they were a class the codebase never got around to writing.

   This matters beyond tidiness: an inline style outranks EVERY selector, so each of
   those 157 was a permanent veto over the design system. Change a token and they do
   not follow. That is the same argument that collapsed 76 inline heading styles into
   .sec-h2 earlier.

   Only ONE new class is invented (.sec-lead). The rest move into selectors that
   already exist - .muted, .orb-cyan, .orb-violet - or key off a data attribute the
   markup already carries. Verified before writing: all 34 muted spans and all 51
   lead paragraphs are BARE elements with no other class, so nothing can now
   out-specify what the inline style used to guarantee. A full before/after computed
   -style diff of every element on all 28 pages backs that up. */
/* 🔴 `p.sec-lead`, not `.sec-lead`. The before/after diff caught this: as a bare
   class it is (0,1,0) and LOSES to `.feat p { margin: 0 }` at (0,1,1), which zeroed the
   top margin on 48 of the 51 paragraphs - sections shrank by 16 to 48px on 13 pages. The
   inline style it replaced outranked everything, so the regression only appears once the
   inline is gone. Qualifying with the element type makes this (0,1,1), which TIES the
   component reset and wins on source order from the end of this file.

   Deliberately not `!important` and not a deeper selector: a utility should beat a
   component's blanket reset by one step, not by force. */
p.sec-lead { font-size: var(--fs-lg); margin-top: 1rem; }

.float-wrap[data-depth="0.7"] { top: 23%; right: 9%; }
.float-wrap[data-depth="1.3"] { bottom: 20%; right: 27%; }

/* ---------- second inline-consolidation pass ----------------------------------
   All four selectors are ELEMENT-QUALIFIED on purpose, landing at (0,1,1). The
   competing rules are component resets like `.feat p`, `.step p` and `.callout p`,
   all (0,1,1), so these tie and win on source order from the end of this file. A bare
   class at (0,1,0) would LOSE - that is what silently zeroed 48 margins last pass.

   Checked before writing: none of these 52 elements sits inside `.prose`, whose
   `.prose p` is (0,2,2) and would beat a qualified class outright.

   🔴 NOT converted, and this is a decision rather than an omission: 26 inline
   link colours (15 `color:var(--accent)`, 11 `color:inherit`) stay inline. The rule
   `a:not(.btn):not(.brand):not(.work-card):not(.nav-link)` is (0,4,1), so any readable
   class on a bare <a> loses to it. Beating it would need a deliberately uglier selector
   than the inline it replaced. Sometimes the inline IS the simplest correct expression. */
p.muted-lead   { color: var(--muted); margin-top: 0.9rem; }
p.muted-narrow { color: var(--muted); max-width: 42ch; }

/* deliberate reductions of the 120px section rhythm, previously inline on 20 sections */
section.pt-tight { padding-top: var(--sp-6); }
section.pt-flush { padding-top: 1rem; }

/* ---------- cinematic hero entrance ----------
   🔴 The first version of this block targeted `section.hero .reveal.visible`.
   MEASURED: revealsInHero = 0. There is not a single .reveal element inside the hero -
   the hero copy has no entrance at all and never did. That rule was a phantom, styling
   nothing, which is the exact defect class this stylesheet has been cleaned of twice.
   I had counted the string "reveal" in the hero markup and got 7; those were
   hero-brand-reveal and comment text, not the class.

   🔴 And the obvious fix - give the hero copy a fade-in - is the WRONG fix here.
   An opacity animation with `backwards` fill renders the element at opacity 0 until the
   animation advances, and a surface that does not advance animations (a crawler, a
   headless render) sees a blank hero. This site has already shipped a blank page once
   for a closely related reason, and the h1 is its most important text.

   So the entrance is TRANSFORM ONLY. Opacity is never touched, so the copy is legible at
   every frame including frame zero and in any renderer that never paints frame one. The
   motion still reads as cinematic because --ease-cinematic has a long tail: the block
   settles rather than stops. Gated behind no-preference, so reduced-motion gets nothing. */
@media (prefers-reduced-motion: no-preference) {
  /* Both hero kinds. .hero is the homepage; .page-hero is the other 25 pages, which
     had no entrance at all. Same transform-only rule, so the same guarantee holds
     everywhere: opacity is never animated, so no renderer can show an empty hero. */
  .hero .container > *,
  .page-hero .container > * {
    animation: hero-rise var(--dur-4) var(--ease-cinematic) backwards;
  }
  .hero .container > :nth-child(1), .page-hero .container > :nth-child(1) { animation-delay: 40ms; }
  .hero .container > :nth-child(2), .page-hero .container > :nth-child(2) { animation-delay: 130ms; }
  .hero .container > :nth-child(3), .page-hero .container > :nth-child(3) { animation-delay: 210ms; }
  .hero .container > :nth-child(4), .page-hero .container > :nth-child(4) { animation-delay: 280ms; }
  .hero .container > :nth-child(5), .page-hero .container > :nth-child(5) { animation-delay: 340ms; }
  @keyframes hero-rise {
    from { transform: translate3d(0, 22px, 0); }
    to   { transform: translate3d(0, 0, 0); }
  }

  /* the atmospheric layers resolve first, so the ground is already there when the words
     arrive. These ARE safe to fade - they are decorative and aria-hidden, and a renderer
     that never advances them simply shows a clean ground. */
  .hero .mesh span { animation-name: mesh-a, hero-settle; }
  .hero .mesh .m2 { animation-name: mesh-b, hero-settle; }
  @keyframes hero-settle { from { opacity: 0; } to { opacity: 0.72; } }
}

/* ============================================================================
   BENTO — measured off productized-agency-template-acetern.vercel.app at 1440x900.
   Their grid: 2 columns, 4 rows of 131px, 12px gap, 16px radius, white cards on the
   stone ground, with children spanning 3 / 1 / 2 rows and one spanning both columns.
   Structure only. None of their code, imagery or copy is reproduced.

   🔴 WHAT THEIR CARDS HOLD, AND WHY OURS HOLD SOMETHING ELSE.
   Theirs: "100+ Companies served", a Microsoft/Google/Adobe logo marquee, and a named
   client testimonial. All three are social proof. This firm has none of it and says so
   on two pages — projects.html: "These are our own products — not client engagements",
   trust.html: not SOC 2, not ISO 27001. Copying those three cards would mean inventing
   a client count, borrowing logos we have no relationship with, and attributing a quote
   to a person who does not exist.

   So the LAYOUT is copied exactly and every cell is filled with something already
   published on this site. The one card that transfers verbatim is "Technologies we use",
   because that is a fact about us, not about customers we do not have.
   ============================================================================ */
.bento {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-auto-rows: 148px;
  gap: var(--gap-3);
  margin-top: var(--sp-5);
}
.bento-card {
  background: #ffffff;
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm);
  padding: var(--sp-4);
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  overflow: hidden;
  transition: box-shadow var(--dur-2) var(--ease-out),
              border-color var(--dur-2) var(--ease-out);
}
.bento-card:hover { box-shadow: var(--shadow-md); border-color: var(--border-strong); }

.bento-tall  { grid-row: span 2; }
.bento-wide  { grid-column: span 2; }
.bento-full  { grid-column: 1 / -1; }

.bento-eyebrow {
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  font-weight: 700;
  letter-spacing: var(--track-label-md);
  text-transform: uppercase;
  color: var(--muted);
}
/* the big figure. Their equivalent reads "100+"; ours reads "2", and the caption is
   what makes 2 the point rather than a shortfall. */
.bento-figure {
  font-family: var(--font-display);
  font-size: var(--fs-d1);
  font-weight: 600;
  line-height: var(--lh-display);
  letter-spacing: -0.03em;
  color: var(--text);
  font-variant-numeric: tabular-nums lining-nums;
}
.bento-card h3 {
  font-family: var(--font-display);
  font-size: var(--fs-xl);
  font-weight: 600;
  line-height: var(--lh-display);
  color: var(--text);
  margin: 0;
}
.bento-card p { font-size: var(--fs-sm); line-height: var(--lh-body); color: var(--muted); margin: 0; max-width: 46ch; }
.bento-card p + p { margin-top: var(--gap-2); }

/* 🔴 Technology chips are TEXT, not brand marks. The design guidance is explicit that a
   guessed logo path is worse than no logo, and we have no licence to display third-party
   marks as if they endorsed us. A named chip states the same fact and cannot be wrong. */
.bento-chips { display: flex; flex-wrap: wrap; gap: var(--gap-2); }
.bento-chip {
  display: inline-flex; align-items: center;
  padding: 0.34rem 0.68rem;
  border: 1px solid var(--border);
  border-radius: var(--r-pill, 999px);
  background: var(--surface-2);
  font-family: var(--font-mono);
  font-size: var(--fs-2xs);
  letter-spacing: var(--track-label-sm);
  color: var(--text);
  white-space: nowrap;
}
.bento-quote {
  font-size: var(--fs-lg);
  line-height: var(--lh-dense);
  color: var(--text);
  margin: 0;
  max-width: 40ch;
}
.bento-attrib { font-family: var(--font-mono); font-size: var(--fs-2xs); letter-spacing: var(--track-label-sm); text-transform: uppercase; color: var(--muted); }

@media (max-width: 1024px) {
  .bento { grid-template-columns: repeat(2, 1fr); }
  .bento-wide { grid-column: span 2; }
}
@media (max-width: 680px) {
  .bento { grid-template-columns: 1fr; grid-auto-rows: auto; }
  .bento-tall, .bento-wide { grid-row: auto; grid-column: auto; }
  .bento-card { min-height: 148px; }
}

/* ---------- residency, rebuilt as a bento -------------------------------------
   Was four equal cells in a row: every option weighted the same, no hierarchy, and
   the longest card (the Canadian one, which carries PIPEDA and PHIPA) squeezed into
   the same width as the shortest. Now the reference's asymmetric rhythm - spans that
   follow content length - on the same 12px gap and 16px radius as .bento.

   The fifth cell is not filler. Four cells in a three-column grid leaves a hole; the
   contract line is a real commitment already published in the FAQ, and it is the one
   that makes the other four mean anything. */
.residency-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--gap-3);
  margin-top: var(--sp-4);
}
.res-item {
  background: #ffffff;
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm);
  padding: var(--sp-4);
  transition: box-shadow var(--dur-2) var(--ease-out),
              border-color var(--dur-2) var(--ease-out);
}

/* 🔴 THE HOVER WAS STILL CYAN. style.css:1433 sets
   `.res-item:hover { border-color: rgba(34,211,238,0.4) }` - the DARK theme accent -
   and light.css contained zero occurrences of `res-item`, so it applied.

   A colour that only exists in a hover state is invisible to every check this project
   runs: it is not in the static palette sweep, and pixel-sampling a screenshot cannot
   see it because nothing is hovered. Two sibling rules had the same problem,
   `a.card:hover` at (0,2,1) and `.stack-chip:hover`, both also unoverridden.

   (The CSSOM would have shown this directly, but stylesheet rules are cross-origin
   blocked on file:// - all three sheets report BLOCKED - so this was found by reading
   the source and confirming no override exists.) */
.res-item:hover,
a.card:hover,
.stack-chip:hover {
  border-color: var(--border-strong);
  box-shadow: var(--shadow-md);
}

.res-item b {
  display: block;
  font-family: var(--font-display);
  font-size: var(--fs-lg);
  font-weight: 600;
  line-height: var(--lh-display);
  color: var(--text);
  margin-bottom: var(--gap-2);
}
.res-item span { font-size: var(--fs-sm); color: var(--muted); line-height: var(--lh-body); }
.res-wide { grid-column: span 2; }

@media (max-width: 900px) {
  .residency-grid { grid-template-columns: repeat(2, 1fr); }
  .res-wide { grid-column: span 2; }
}
@media (max-width: 620px) {
  .residency-grid { grid-template-columns: 1fr; }
  .res-wide { grid-column: auto; }
}

/* ============================================================================
   HONEST-GRID - the "when you should not buy this" block, on 8 pages.

   Measured, not guessed. The section-richness probe found .callout is the most
   repeated plain block on this site: 29 instances across 17 pages. Classifying
   them by internal structure showed 8 are not prose at all - EVERY paragraph is
   `<strong>a condition.</strong> followed by its explanation`. That is a card
   grid written as a wall of text. On industries/law-firms.html it renders 839px
   tall with zero cards, zero grids and zero media, so a prospect has to read all
   of it to learn whether one of the five disqualifiers applies to them. This is
   the single most valuable page on the site to be able to SCAN, because it is the
   one that tells people not to buy.

   The other 21 callouts are genuine prose and are deliberately left alone.

   NO COPY IS CHANGED BY THIS. The only edit per page is the container's class
   attribute; the text, the <strong> semantics and the reading order are
   byte-identical before and after.

   A 6-column track so every card count lands on a full row with no orphan:
     3 conditions -> 3 x span-2  (one row of three)
     4            -> 4 x span-3  (two rows of two)
     5            -> 3 x span-2, then 2 x span-3
   ============================================================================ */
.honest-grid {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: var(--gap-3);
  margin-top: var(--sp-4);
}
.honest-grid > p {
  grid-column: span 2;
  margin: 0;
  background: #ffffff;
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm);
  padding: var(--sp-4);
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  color: var(--muted);
  max-width: none;
  transition: box-shadow var(--dur-2) var(--ease-out),
              border-color var(--dur-2) var(--ease-out);
}
.honest-grid > p:hover {
  box-shadow: var(--shadow-md);
  border-color: var(--border-strong);
}

/* style.css:1479 `.callout p + p` painted paragraphs 2..n --muted while leaving the
   first --text. That is right for a stacked callout whose opening line is a lede.
   It is wrong for a grid, where N siblings of equal standing must read as equals -
   as a callout, the first disqualifier looked weightier than the other four for no
   reason anyone chose. Every card is now identical. */
.honest-grid > p > strong {
  display: block;
  margin-bottom: var(--gap-2);
  font-family: var(--font-display);
  font-size: var(--fs-lg);
  font-weight: 600;
  line-height: var(--lh-display);
  letter-spacing: -0.015em;
  color: var(--text);
}

/* Order matters: these tie .honest-grid > p at (0,1,1) and must follow it. */
.hg-4 > p { grid-column: span 3; }
.hg-5 > p:nth-child(n+4) { grid-column: span 3; }

@media (max-width: 900px) {
  .honest-grid { grid-template-columns: repeat(2, 1fr); }
  .honest-grid > p,
  .hg-4 > p,
  .hg-5 > p:nth-child(n+4) { grid-column: span 1; }
}
@media (max-width: 620px) {
  .honest-grid { grid-template-columns: 1fr; }
}

/* The honest-grid sits in the right-hand cell of a .feat, which is 1fr 1.15fr -
   a ~663px column. Three cards across that measured 213px wide, about 26 characters
   per line, well under the 45-75 the type guidance asks for. The heading in the left
   cell is two short lines, so the space belongs to the cards.

   Explicit class rather than `.feat:has(.honest-grid)`: :has() is fine in current
   browsers, but where it is not supported the whole rule is dropped and the cards
   silently go back to 213px. A class cannot fail that way.

   The mobile override below is NOT redundant. `@media (max-width:900px){.feat{...}}`
   at light.css:843 is (0,1,0) and so is `.feat-honest`; media queries add no
   specificity, so this rule - appended later in the file - would beat it and keep two
   columns on a phone. Same-specificity-later-source-wins is the bug that has cost the
   most time in this codebase, so it gets an explicit override rather than a hope. */
.feat-honest { grid-template-columns: 1fr 2.3fr; align-items: start; }
.feat-honest .honest-grid { margin-top: 0; }

@media (max-width: 900px) {
  .feat-honest { grid-template-columns: 1fr; }
}
