/* Self-hosted fonts (latin subsets of the same Google Fonts releases, Archivo and
   JetBrains Mono as variable files). The LCP element on every page is the display
   headline, i.e. text, so the render-blocking chain to fonts.googleapis.com was
   the single biggest cost on mobile. Serving from our own origin removes two
   third-party origins from the critical path and from the CSP. */
@font-face{font-family:'Archivo';font-style:normal;font-weight:400 700;font-display:swap;src:url('../assets/fonts/archivo.woff2') format('woff2');}
@font-face{font-family:'Archivo Black';font-style:normal;font-weight:400;font-display:swap;src:url('../assets/fonts/archivo-black.woff2') format('woff2');}
@font-face{font-family:'JetBrains Mono';font-style:normal;font-weight:400 700;font-display:swap;src:url('../assets/fonts/jetbrains-mono.woff2') format('woff2');}

:root{
  --khaki: oklch(0.88 0.03 95);
  --khaki-deep: oklch(0.46 0.05 95); /* darkened from 0.62: failed WCAG AA (3.28:1/2.87:1 on paper/paper-2), now 6.4:1/5.6:1 */
  --safety-on-dark: oklch(0.74 0.19 45); /* lightened variant of --safety for text on dark backgrounds (e.g. olive): base --safety measured 4.37:1 on olive, just under AA; this clears 5:1 */
  --olive: oklch(0.30 0.03 120);
  --olive-deep: oklch(0.22 0.025 120);
  --ink: oklch(0.16 0.01 100);
  --safety: oklch(0.68 0.19 45);
  --safety-deep: oklch(0.58 0.18 42);
  --safety-ink: oklch(0.46 0.16 38);
  --paper: oklch(0.965 0.01 90);
  --paper-2: oklch(0.92 0.015 90);
  --line: oklch(0.16 0.01 100 / 0.16);
  --line-soft: oklch(0.16 0.01 100 / 0.08);

  --font-black:"Archivo Black", sans-serif;
  --font-sans:"Archivo", sans-serif;
  --font-mono:"JetBrains Mono", monospace;

  --space-3:12px; --space-4:16px; --space-5:24px; --space-6:32px;
  --space-7:48px; --space-8:72px; --space-9:104px;
  --ease-out:cubic-bezier(.22,1,.36,1);
}
*{box-sizing:border-box; margin:0; padding:0;}
html{scroll-behavior:smooth;}
body{background:var(--paper); color:var(--ink); font-family:var(--font-sans); font-size:1rem; line-height:1.6; -webkit-font-smoothing:antialiased; overflow-x:hidden;}
a{color:inherit;} img{max-width:100%; display:block;}
::selection{background:var(--safety); color:var(--ink);}

/* One deliberate eyebrow label (the Problem section only, see below) rather
   than a repeated scaffold above every section heading. */
.kicker{font-family:var(--font-mono); font-size:0.75rem; color:var(--safety-ink); text-transform:uppercase; letter-spacing:0.08em; display:block; margin-bottom:var(--space-4);}
/* A plain-prose intro line, used instead of a kicker on sections where the
   heading needs a sentence of context rather than a tag. */
.lede{font-size:1rem; color:var(--olive); max-width:52ch; margin-top:var(--space-3);}

/* ---------- Staggered list entrances (fear-grid, timeline, personas, services, styles, facts) ---------- */
/* Reserved for actual lists, not a blanket fade-up on every section. */
.stagger-group > *{
  opacity:0; transform:translateY(14px);
  transition:opacity 420ms var(--ease-out), transform 420ms var(--ease-out);
  transition-delay:calc(var(--i, 0) * 55ms);
}
.stagger-group.in > *{opacity:1; transform:translateY(0);}

@media (prefers-reduced-motion: reduce){
  .stagger-group > *{opacity:1; transform:none; transition:none;}
  .hero-headline .line, .hero-subhead, .hero-foot, .hero-greeting{
    animation:none !important; opacity:1 !important; transform:none !important; clip-path:none !important;
  }
  *{transition-duration:1ms !important;}
}

/* First-run delight: the drafting-pen headline wipe is a moment worth having
   once. On a repeat visit within the same session it would just be friction
   between the visitor and the page, so skip straight to the settled state,
   using the same mechanism as the reduced-motion escape hatch above. */
html.repeat-visit .hero-headline .line,
html.repeat-visit .hero-subhead,
html.repeat-visit .hero-foot,
html.repeat-visit .hero-greeting{
  animation:none !important; opacity:1 !important; transform:none !important; clip-path:none !important;
}

/* ---------- Header strip ----------
   Full-width background bar, fixed behind the wordmark/nav/book trio so
   scrolled content passes under a solid surface instead of behind bare text
   (the wordmark previously had no background of its own). Sits below all
   three chips in stacking order (z-index 40 vs. 50/51) and is purely
   decorative, hence pointer-events:none. Height is a hand-measured ceiling
   on the tallest header item (the wordmark) plus its top offset and
   padding, with a small buffer; see the mobile override further down. */
.site-header-strip{
  position:fixed; top:0; left:0; width:100%; height:82px; z-index:40;
  background:var(--paper); border-bottom:1px solid var(--line);
  pointer-events:none;
}

/* ---------- Corner index instead of a nav bar ---------- */
/* A solid chip of its own, same treatment as .book-corner, so it never depends on
   tracking what's scrolled underneath it (sections are too fine-grained for that to stay correct).
   Anchored to the right, clearing .book-corner (measured ~182px wide at this font/padding,
   fixed text content so its width doesn't drift) with an 18px gap; both are fixed-width
   text chips, not fluid, so a hardcoded offset is safe unless either's copy changes.
   Permanent now: the header strip above gives the whole trio a solid surface
   to sit on, so there's no more scroll-depth hide/show pass to worry about. */
.index-corner{
  position:fixed; top:41px; right:calc(clamp(20px,6vw,10vw) + 220px); z-index:50;
  transform:translateY(-50%);
  padding:10px 16px; display:flex; flex-direction:row; gap:var(--space-5);
  font-family:var(--font-sans); font-size:0.85rem; font-weight:500;
  background:var(--paper); border:1px solid var(--line); border-radius:999px;
  box-shadow:0 6px 18px oklch(0.16 0.01 100 / 0.1);
}
.index-corner a{
  position:relative; opacity:0.55; text-decoration:none; color:var(--ink);
  transition:opacity 200ms var(--ease-out), color 200ms var(--ease-out);
}
.index-corner a::after{
  content:""; position:absolute; left:0; bottom:-3px; width:100%; height:1px; background:var(--safety-ink);
  transform:scaleX(0); transform-origin:left;
  transition:transform 220ms var(--ease-out);
}
.index-corner a:hover, .index-corner a:focus-visible{opacity:1; color:var(--safety-ink);}
.index-corner a:hover::after, .index-corner a.on::after{transform:scaleX(1);}
.index-corner a:focus-visible{outline:2px solid var(--safety); outline-offset:3px;}
.book-corner{
  position:fixed; top:41px; right:clamp(20px,6vw,10vw); z-index:50;
  transform:translateY(-50%);
  font-family:var(--font-sans); font-weight:600; font-size:0.88rem; border:1px solid var(--safety); border-radius:8px; padding:10px 18px;
  text-decoration:none; background:var(--paper); transition:background 200ms var(--ease-out), color 200ms var(--ease-out);
}
.book-corner:hover{background:var(--ink); color:var(--paper);}
.book-corner:focus-visible{outline:2px solid var(--safety); outline-offset:3px;}

/* ---------- Wordmark ----------
   Type only, no mark/icon: the brand name set in the same display face as every
   headline on the page, with the closing period picked out in the accent orange.
   Fixed top-left, completing the header trio with the section index (right) and
   booking CTA (right) without competing with either for space. Permanent,
   sitting on the .site-header-strip background above rather than hiding on scroll. */
.logo{
  position:fixed; top:41px; left:calc(clamp(20px,6vw,10vw) - 4px); z-index:51;
  transform:translateY(-50%);
  font-family:var(--font-black); text-transform:none; font-size:1.7rem; letter-spacing:-0.01em;
  color:var(--ink); text-decoration:none; line-height:1;
  padding:12px 4px;
}
.logo .dot{color:var(--safety);}
/* Same pairing as the footer wordmark: display-face name, mono kicker suffix.
   Inline and small, so the fixed header strip keeps its height. */
.logo .logo-suffix{
  font-family:var(--font-mono); font-weight:400; font-size:0.68rem;
  text-transform:uppercase; letter-spacing:0.06em; margin-left:10px;
}
.logo:focus-visible{outline:2px solid var(--safety); outline-offset:3px;}

.skip-link{
  position:absolute; left:-9999px; top:0; background:var(--ink); color:var(--paper); padding:12px 20px; z-index:100;
}
.skip-link:focus{left:var(--space-5); top:var(--space-5);}

/* Nav-link jumps align a target's top edge with the viewport top, which would land
   right under the header strip; push the landing position down by exactly the
   strip's height so the section sits flush against it instead of leaving a gap. */
#method, #personas, #work, #services, #faq, #contact{scroll-margin-top:82px;}

/* ---------- Sections ----------
   Removed 2026-07-10: the sticky-pin-and-recede "stack of cards" mechanic
   (position:sticky, scroll-timeline scale/dim, rounded top corners, the
   iOS/desktop internal-scrollport workarounds it required). It was a
   persistent source of scroll jank - per direct feedback, perceptible even
   after the internal scrollport was removed in an earlier pass - so the
   whole mechanic is gone now, not just tuned. Every section is a plain block
   in normal document flow; see the shared top-padding clearance rule near
   the end of this file (after every section's own padding shorthand, so it
   wins on just the top edge without disturbing the rest). */

/* ---------- Hero ---------- */
.hero{
  min-height:100vh; display:flex; flex-direction:column; justify-content:center;
  /* Plain "center" overflows equally in both directions once content is taller than
     the viewport, pushing content up past the fixed chip. "safe center" falls back to
     start-alignment instead of overflowing off the safe area; unsupported browsers
     just keep the "center" above (graceful degradation, not a hard dependency). */
  justify-content:safe center;
  /* Containing block for the absolutely-positioned .hero-grain child. */
  position:relative;
  padding:180px clamp(20px,6vw,10vw) var(--space-9);
  /* clip-path, not overflow:hidden: overflow:hidden makes an element a scroll
     container, which silently breaks the "view()" scroll-driven animation on
     its descendant (the hero-grain lift below tracks completely stuck without
     ever animating, and it's not obvious why unless you know this). */
  clip-path:inset(0);
  background:
    radial-gradient(ellipse 120% 90% at 18% -10%, var(--paper-2) 0%, transparent 60%),
    radial-gradient(ellipse 90% 70% at 100% 100%, oklch(0.68 0.19 45 / 0.07) 0%, transparent 55%),
    var(--paper);
}

/* A faint drafting-paper grain, scaled down and faded as the page scrolls
   past, like the camera lifting off the page. Doesn't intercept pointer
   events; the real interactive content (headline, links) paints above it. */
.hero-grain{
  position:absolute; inset:0; width:100%; height:100%; z-index:-1; pointer-events:none;
}
.hero-grain{
  opacity:0.05; mix-blend-mode:multiply;
  background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
  background-size:180px 180px;
}

/* The grain drifts continuously on its own "translate" channel. The scroll-tied lift
   below animates "scale"/"opacity" instead of the "transform" shorthand specifically
   so the two can run on the same element at once without one overriding the other. */
.hero-grain{ animation:grain-drift 22s steps(6) infinite; }
@keyframes grain-drift{
  0%{ translate:0 0; } 20%{ translate:-4% 3%; } 40%{ translate:3% -3%; }
  60%{ translate:-3% -4%; } 80%{ translate:4% 2%; } 100%{ translate:0 0; }
}

/* Camera lifting off the site plan as the hero itself scrolls out of view.
   "scroll()" tracks the whole document's scroll range, so at 100vh of scroll on
   an 8000px-tall page it barely moves; "view()" tracks the element's own passage
   through the viewport instead, which is what "as you scroll past the hero" means.
   animation-range:exit plays it only during the hero's own exit phase, not before.
   Unsupported browsers just skip this rule and the layers stay at their resting
   state, which already looks intentional, so no separate fallback is needed. */
@supports (animation-timeline: view()) {
  .hero-grain{
    animation:grain-drift 22s steps(6) infinite, hero-lift 1ms linear both;
    animation-timeline: auto, view(block);
    animation-range: normal, exit;
  }
}
@keyframes hero-lift{ to{ scale:1.08; opacity:0; } }

@media (prefers-reduced-motion: reduce){
  .hero-grain{ animation:none !important; }
}

/* Time-of-day greeting, set by the inline script next to the headline markup in
   index.html. :empty guards the no-JS/JS-error case so a blank line never sits
   above the headline. */
.hero-greeting{
  font-family:var(--font-mono); font-size:0.85rem; color:var(--olive);
  margin-bottom:var(--space-4);
  opacity:0; transform:translateY(8px); animation:greet-rise 480ms var(--ease-out) forwards;
}
.hero-greeting:empty{display:none;}
@keyframes greet-rise{ to{opacity:1; transform:translateY(0);} }

/* Signature entrance: the one dramatic moment on this page. A drafting-pen wipe per line, staggered. */
.hero-headline{
  font-family:var(--font-black); text-transform:uppercase;
  font-size:clamp(2.4rem, 7vw, 5.5rem); line-height:0.95; letter-spacing:-0.02em;
  text-wrap:balance;
}
.hero-headline .line{display:block; opacity:0; clip-path:inset(0 100% 0 0); animation:line-wipe 640ms var(--ease-out) forwards;}
.hero-headline .line:nth-child(1){animation-delay:60ms;}
.hero-headline .line:nth-child(2){animation-delay:190ms;}
.hero-headline .line:nth-child(3){animation-delay:320ms;}
.hero-headline .line.accent{color:var(--safety);}
@keyframes line-wipe{ to{opacity:1; clip-path:inset(0 0 0 0);} }

/* Direct, non-randomized clarity line: the headline above is deliberately
   poetic and rotates per visit, which is memorable but never states what the
   company does, who for, or where. This line answers all three in one
   sentence, so a first-time visitor never has to guess. */
.hero-subhead{
  font-family:var(--font-sans); font-weight:600; font-size:1.2rem; color:var(--ink);
  margin-top:var(--space-4); max-width:38ch; text-wrap:pretty;
  opacity:0; transform:translateY(8px); animation:foot-rise 500ms var(--ease-out) forwards; animation-delay:420ms;
}

.hero-foot{
  display:grid; grid-template-columns:1.4fr 1fr; gap:var(--space-8); margin-top:var(--space-8); align-items:end;
  opacity:0; transform:translateY(10px); animation:foot-rise 500ms var(--ease-out) forwards; animation-delay:520ms;
}
@keyframes foot-rise{ to{opacity:1; transform:translateY(0);} }
.hero-foot p{font-size:1.1rem; color:var(--olive); max-width:46ch; text-wrap:pretty;}
.hero-foot .tags{display:flex; gap:var(--space-3); flex-wrap:wrap; font-family:var(--font-mono); font-size:0.72rem;}
/* Same invert look as .style-tags below, but auto-cycled only (js/main.js) - no
   :hover here, deliberately: manual selection is disabled so the row always
   reads as one continuous automated sequence, not an interactive control. */
.hero-foot .tags span{
  border:1px solid var(--ink); border-radius:8px; padding:6px 12px;
  transition:background 200ms var(--ease-out), color 200ms var(--ease-out), transform 200ms var(--ease-out);
}
.hero-foot .tags span.is-active{
  background:var(--ink); color:var(--paper); transform:translateY(-2px);
}

/* ---------- Facts strip (real facts only, no invented stats) ---------- */
.facts{padding:var(--space-9) clamp(20px,6vw,10vw); display:grid; grid-template-columns:repeat(4,1fr); align-content:center;}
.facts div{border-right:1px solid var(--line); padding-right:var(--space-5);}
.facts div:last-child{border-right:none;}
.facts .n{font-family:var(--font-black); font-size:clamp(1.4rem,2.6vw,2rem); color:var(--safety-ink); text-transform:uppercase;}
.facts .l{font-family:var(--font-mono); font-size:0.7rem; text-transform:uppercase; letter-spacing:0.06em; color:var(--olive); margin-top:6px;}

/* ---------- The Problem ---------- */
.problem{padding:var(--space-9) clamp(20px,6vw,10vw);}
.problem-head{max-width:60ch; margin-bottom:var(--space-8);}
.problem-head h2{font-family:var(--font-black); text-transform:uppercase; font-size:clamp(1.9rem,4vw,3rem); line-height:1.02; text-wrap:balance;}
.fear-grid{display:grid; grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); gap:0;}
.fear{border-top:1px solid var(--line); padding:var(--space-5) var(--space-4) var(--space-5) 0;}
.fear:not(:nth-child(4n)){border-right:1px solid var(--line);}
.fear .x{font-family:var(--font-mono); color:var(--safety-ink); font-size:0.8rem; display:block; margin-bottom:var(--space-3);}
.fear p{font-size:0.95rem; color:var(--olive); text-wrap:pretty;}
/* The answer is the payoff, so it outranks the fear it sits under: full ink
   against the fear's olive, and heavier. The tick is deliberately ink rather
   than a green, which nothing else on this site uses. */
.fear .tick{font-family:var(--font-mono); color:var(--ink); font-size:0.8rem; display:block; margin:var(--space-4) 0 var(--space-3);}
.fear .answer{color:var(--ink); font-weight:600;}

/* ---------- The Duotone Method ---------- */
.method{padding:var(--space-9) clamp(20px,6vw,10vw); background:var(--olive); color:var(--paper);}
.method-head{display:flex; justify-content:space-between; align-items:flex-end; gap:var(--space-6); margin-bottom:var(--space-8); flex-wrap:wrap;}
.method-head h2{font-family:var(--font-black); text-transform:uppercase; font-size:clamp(1.9rem,4vw,3rem); line-height:1.02; max-width:16ch; text-wrap:balance;}
.method-head .tag{font-family:var(--font-mono); font-size:0.72rem; border:1px solid var(--paper); border-radius:8px; padding:6px 14px;}
.timeline{display:flex; flex-direction:column;}
.tl-row{display:grid; grid-template-columns:120px 1fr; gap:var(--space-6); padding:var(--space-6) 0; border-top:1px solid oklch(1 0 0 / 0.22); align-items:baseline;}
.tl-row:last-child{border-bottom:1px solid oklch(1 0 0 / 0.22);}
/* Two-Voice Rule: "timeline stamp" is the design system's own named Label
   use case, so this reads as a system readout (mono), not a statement
   (Archivo) - was set in the Body/Title face, indistinguishable from prose. */
.tl-row .stamp{font-family:var(--font-mono); font-weight:700; font-size:0.85rem; letter-spacing:0.02em; color:var(--safety-on-dark); display:inline-block; width:fit-content;}
.tl-row h3{font-size:1.3rem; font-weight:700; margin-bottom:6px; text-transform:uppercase; letter-spacing:0.015em;}
/* Paper-on-Olive: light text on a dark surface reads as lighter weight and
   needs more breathing room than the default body line-height. */
.tl-row p{opacity:0.8; max-width:62ch; line-height:1.7; letter-spacing:0.01em; text-wrap:pretty;}

/* ---------- Who we design for (personas) ---------- */
.personas{padding:var(--space-9) clamp(20px,6vw,10vw);}
.personas-head{margin-bottom:var(--space-8); max-width:60ch;}
.personas-head h2{font-family:var(--font-black); text-transform:uppercase; font-size:clamp(1.9rem,4vw,3rem); line-height:1.02; text-wrap:balance;}
.persona-grid{display:grid; grid-template-columns:repeat(auto-fit,minmax(240px,1fr)); gap:var(--space-5);}
.persona{background:var(--paper-2); padding:var(--space-6); border:1px solid var(--line); border-radius:10px;}
.persona h3{font-family:var(--font-sans); font-weight:700; text-transform:uppercase; font-size:1.15rem; letter-spacing:0.015em; margin-bottom:var(--space-3); line-height:1.15;}
.persona p{font-size:0.95rem; color:var(--olive); text-wrap:pretty;}

/* ---------- Featured Work (real completed-project covers; markup generated
   by tools/build_gallery.py between the FEATURED markers in index.html) ---------- */
.work{padding:var(--space-9) clamp(20px,6vw,10vw);}
.work-head{max-width:60ch; margin-bottom:var(--space-8);}
.work-head h2{font-family:var(--font-black); text-transform:uppercase; font-size:clamp(1.9rem,4vw,3rem); line-height:1.02; text-wrap:balance;}
.work-grid{display:grid; grid-template-columns:repeat(3,1fr); gap:var(--space-5);}
.frame{text-decoration:none; color:inherit; display:block;}
.frame-visual{
  aspect-ratio:4/3; position:relative; overflow:hidden;
  background:var(--paper-2);
  border:1px solid var(--line); border-radius:10px;
}
.frame-visual img{
  width:100%; height:100%; object-fit:cover; display:block;
  transition:scale 400ms var(--ease-out);
}
.frame:hover .frame-visual img, .frame:focus-visible .frame-visual img{scale:1.03;}
.frame:focus-visible{outline:2px solid var(--safety); outline-offset:3px; border-radius:10px;}
.frame-label{margin-top:var(--space-4); font-size:0.95rem; font-weight:600;}
.frame-sub{display:block; font-weight:400; font-size:0.95rem; color:var(--olive); margin-top:2px;}
.work-more{margin-top:var(--space-7); font-weight:600;}
.work-more a{color:var(--safety-ink); text-decoration-thickness:1px; text-underline-offset:4px;}
.work-more a:focus-visible{outline:2px solid var(--safety); outline-offset:3px;}
/* A lone "View all →" CTA (the only link in its paragraph) becomes a bordered
   ghost button: reads as a deliberate next step, quieter than .btn-primary so
   it never competes with the real CTA. The multi-link ".work-more" lines
   (blog "Read next:", project prev/next) keep the plain underlined-link style
   above — :only-child is what tells the two apart without extra markup. */
.work-more a:only-child{
  display:inline-block; font-weight:700; color:var(--ink);
  border:1px solid var(--ink); border-radius:8px; padding:14px 26px; text-decoration:none;
  transition:background 200ms var(--ease-out), color 200ms var(--ease-out), transform 200ms var(--ease-out);
}
.work-more a:only-child:hover{background:var(--ink); color:var(--paper); transform:translateY(-2px);}
.work-more a:only-child:active{transform:scale(0.98); transition-duration:100ms;}

/* ---------- Gallery page (projects.html; sections generated by
   tools/build_gallery.py). Fixed 4:3 tiles prevent layout shift; the
   lightbox (desktop, fine pointers) shows the uncropped photo. ---------- */
.gallery-page .g-intro{
  padding:180px clamp(20px,6vw,10vw) var(--space-8);
  display:grid; grid-template-columns:1.3fr 1fr; gap:var(--space-8); align-items:end;
}
.gallery-page .g-intro h1{
  font-family:var(--font-black); text-transform:uppercase;
  font-size:clamp(2.4rem, 7vw, 5.5rem); line-height:0.95; letter-spacing:-0.02em;
  text-wrap:balance;
}
.gallery-page .g-intro h1 .accent{color:var(--safety);}
.g-project{padding:var(--space-8) clamp(20px,6vw,10vw); scroll-margin-top:90px;}
/* Twelve near-identical project sections in a row read as monotonous on a
   long scroll with no other rhythm device (no color-block alternation like
   the homepage has). A faint paper/paper-2 alternation gives the same
   "beat" cheaply without touching the photo grid or gallery markup. */
.g-project:nth-of-type(even){background:var(--paper-2);}
.g-head{display:flex; align-items:baseline; gap:var(--space-5); flex-wrap:wrap; margin-bottom:var(--space-5); border-top:1px solid var(--line); padding-top:var(--space-6);}
.g-head h2{font-family:var(--font-black); text-transform:uppercase; font-size:clamp(1.3rem,2.4vw,1.8rem); line-height:1.05;}
.g-sub{color:var(--olive); font-size:0.95rem;}
.g-perma{
  margin-left:auto; font-size:0.88rem; font-weight:600; color:var(--safety-ink);
  text-decoration-thickness:1px; text-underline-offset:4px; white-space:nowrap;
}
.g-perma:focus-visible{outline:2px solid var(--safety); outline-offset:3px;}
.g-grid{display:grid; grid-template-columns:repeat(3,1fr); gap:var(--space-4);}
.g-item{
  display:block; border-radius:10px; overflow:hidden; border:1px solid var(--line);
  aspect-ratio:4/3; background:var(--paper-2);
}
.g-item img{width:100%; height:100%; object-fit:cover; display:block; transition:scale 400ms var(--ease-out);}
.g-item:hover img{scale:1.03;}
.g-item:focus-visible{outline:2px solid var(--safety); outline-offset:3px;}

/* Completed-projects overview (projects.html): one featured photo per
   project; the whole card links to the project page with the full gallery. */
.p-grid{
  display:grid; grid-template-columns:repeat(3,1fr); gap:var(--space-5);
  padding:var(--space-6) clamp(20px,6vw,10vw) var(--space-8);
}
.p-card{
  display:block; border-radius:10px; overflow:hidden; border:1px solid var(--line);
  background:var(--paper-2); text-decoration:none; color:inherit; scroll-margin-top:90px;
}
.p-card picture{display:block; aspect-ratio:4/3; overflow:hidden;}
.p-card img{width:100%; height:100%; object-fit:cover; display:block; transition:scale 400ms var(--ease-out);}
.p-card:hover img{scale:1.03;}
.p-card:focus-visible{outline:2px solid var(--safety); outline-offset:3px;}
.p-card-text{padding:var(--space-4) var(--space-4) var(--space-5); display:grid; gap:6px;}
.p-card h2{font-family:var(--font-black); text-transform:uppercase; font-size:1.05rem; line-height:1.1;}
.p-card-sub{color:var(--olive); font-size:0.95rem;}

/* Lightbox: native <dialog> on the individual project pages, desktop
   enhancement (js/gallery.js only wires it up for fine pointers at >=900px;
   elsewhere tiles are inert). */
.lb{
  border:none; padding:0; background:transparent; max-width:none; max-height:none;
  width:100vw; height:100vh; display:none;
  align-items:center; justify-content:center; gap:var(--space-4);
}
.lb[open]{display:flex;}
.lb::backdrop{background:oklch(0.16 0.01 100 / 0.92);}
.lb-figure{margin:0; max-width:calc(100vw - 160px); max-height:92vh; display:flex; flex-direction:column; gap:var(--space-3);}
.lb-figure img{max-width:100%; max-height:86vh; width:auto; height:auto; border-radius:8px;}
.lb-figure figcaption{color:var(--paper); font-size:0.92rem; opacity:0.85; text-align:center;}
.lb-close, .lb-nav{
  background:transparent; color:var(--paper); border:1px solid oklch(1 0 0 / 0.4);
  border-radius:8px; font-family:var(--font-sans); font-size:1.1rem; cursor:pointer;
  padding:12px 16px; transition:background 200ms var(--ease-out);
}
.lb-close{position:fixed; top:var(--space-5); right:var(--space-5);}
.lb-close:hover, .lb-nav:hover{background:oklch(1 0 0 / 0.15);}
.lb-close:focus-visible, .lb-nav:focus-visible{outline:2px solid var(--safety); outline-offset:3px;}

@media (prefers-reduced-motion: reduce){
  .g-item img, .p-card img, .frame-visual img{transition:none;}
}
@media (max-width:900px){
  .g-grid, .p-grid{grid-template-columns:repeat(2,1fr);}
  .gallery-page .g-intro{padding-top:120px; grid-template-columns:1fr;}
}
@media (max-width:560px){
  .g-grid, .p-grid{grid-template-columns:1fr;}
}

/* ---------- Services ---------- */
.services{padding:var(--space-9) clamp(20px,6vw,10vw); background:var(--ink); color:var(--paper);}
.services-head{margin-bottom:var(--space-8); max-width:60ch;}
.services-head h2{font-family:var(--font-black); text-transform:uppercase; font-size:clamp(1.9rem,4vw,3rem); line-height:1.02; text-wrap:balance;}
.services-head .lede{color:var(--paper); opacity:0.75;}
.service-row{border-top:1px solid oklch(1 0 0 / 0.2);}
.service{display:grid; grid-template-columns:56px 1fr 1.2fr; gap:var(--space-6); padding:var(--space-6) 0; border-bottom:1px solid oklch(1 0 0 / 0.2); align-items:baseline;}
/* Chunks the flat 6-item list into two groups of 3 (plan/design vs. build/deliver)
   instead of one undifferentiated column, per the cognitive-load chunking guideline
   used everywhere else on this page (every other list on it caps at 4-5 items). */
.service-group-label{
  padding:var(--space-6) 0 var(--space-3);
  font-family:var(--font-mono); font-size:0.72rem; text-transform:uppercase; letter-spacing:0.08em;
  color:var(--safety); opacity:0.85;
}
.service-group-label:first-child{padding-top:0;}
.service .n{font-family:var(--font-mono); color:var(--safety); font-size:0.85rem;}
.service h3{font-size:1.15rem; font-weight:700; text-transform:uppercase; letter-spacing:0.015em;}
.service h3 a{
  color:inherit; text-decoration:none;
  border-bottom:1px solid oklch(1 0 0 / 0.35);
  transition:color 200ms var(--ease-out), border-color 200ms var(--ease-out);
}
.service h3 a:hover{color:var(--safety); border-color:var(--safety);}
.service h3 a:focus-visible{outline:2px solid var(--safety); outline-offset:3px;}
/* Paper-on-Ink: same light-on-dark compensation as .tl-row p above. */
.service p{opacity:0.75; font-size:0.95rem; max-width:60ch; line-height:1.7; letter-spacing:0.01em; text-wrap:pretty;}

/* ---------- Design styles ---------- */
.styles{padding:var(--space-9) clamp(20px,6vw,10vw);}
.styles-head{margin-bottom:var(--space-7); max-width:60ch;}
.styles-head h2{font-family:var(--font-black); text-transform:uppercase; font-size:clamp(1.9rem,4vw,3rem); line-height:1.02; text-wrap:balance;}
.style-tags{display:flex; flex-wrap:wrap; gap:var(--space-4);}
.style-tags span{
  display:inline-block; font-family:var(--font-mono); font-size:0.85rem; border:1px solid var(--ink); border-radius:8px; padding:12px 22px;
  transition:background 200ms var(--ease-out), color 200ms var(--ease-out), transform 200ms var(--ease-out);
}
/* .is-active is the auto-cycle state (js/main.js). Same rule as .hero-foot
   .tags now: no :hover invert, so the row reads as one continuous automated
   sequence rather than a set of clickable filters (the chips are spans and
   never led anywhere; the hover affordance was a false promise). The cycle
   still pauses under a pointer via JS, as decorative-motion courtesy, it
   just doesn't restyle the chip. */
.style-tags span.is-active{background:var(--ink); color:var(--paper); transform:translateY(-2px);}

/* ---------- Philosophy quote (sourced from brand manifesto, not an invented testimonial) ---------- */
.quote{padding:var(--space-9) clamp(20px,6vw,10vw);}
blockquote{font-family:var(--font-black); text-transform:uppercase; font-size:clamp(1.6rem,3.6vw,3rem); line-height:1.05; max-width:26ch; text-wrap:balance;}
cite{display:block; font-family:var(--font-mono); font-style:normal; font-size:0.85rem; color:var(--olive); margin-top:var(--space-5);}

/* ---------- Trust (real, verifiable credentials only - see PRODUCT.md;
   no invented testimonials, review counts, client logos, or awards) ---------- */
.trust{padding:var(--space-9) clamp(20px,6vw,10vw); background:var(--paper-2);}
.trust-head{max-width:60ch; margin-bottom:var(--space-8);}
.trust-head h2{font-family:var(--font-black); text-transform:uppercase; font-size:clamp(1.9rem,4vw,3rem); line-height:1.02; text-wrap:balance;}
.trust-grid{display:grid; grid-template-columns:repeat(auto-fit,minmax(240px,1fr)); gap:var(--space-5);}
.trust-item{background:var(--paper); padding:var(--space-6); border:1px solid var(--line); border-radius:10px;}
.trust-item h3{font-family:var(--font-sans); font-weight:700; text-transform:uppercase; font-size:1.15rem; letter-spacing:0.015em; margin-bottom:var(--space-3); line-height:1.15;}
.trust-item p{font-size:0.95rem; color:var(--olive); text-wrap:pretty;}

/* ---------- FAQ (objection handling directly before the ask; progressive
   disclosure via native <details>, so it works with zero JS and inherits
   keyboard/screen-reader semantics for free). Questions are sentence-case
   Archivo 700, deliberately not the uppercase Title role: they're
   conversational prose a visitor is meant to read, not card labels. ---------- */
.faq{
  padding:var(--space-9) clamp(20px,6vw,10vw);
  display:grid; grid-template-columns:1fr 1.4fr; gap:var(--space-8); align-items:start;
}
.faq-head{max-width:60ch;}
.faq-head h2{font-family:var(--font-black); text-transform:uppercase; font-size:clamp(1.9rem,4vw,3rem); line-height:1.02; text-wrap:balance;}
.faq-list{max-width:75ch; border-top:1px solid var(--line);}
.faq-item{border-bottom:1px solid var(--line);}
.faq-item summary{
  display:flex; justify-content:space-between; align-items:baseline; gap:var(--space-5);
  padding:var(--space-5) 0; cursor:pointer; list-style:none;
  font-weight:700; font-size:1.05rem; line-height:1.35;
  transition:color 200ms var(--ease-out);
}
.faq-item summary::-webkit-details-marker{display:none;}
.faq-item summary::after{
  content:"+"; font-family:var(--font-mono); font-size:1.15rem; color:var(--safety-ink);
  flex:none; line-height:1; align-self:center;
  transition:rotate 220ms var(--ease-out);
}
.faq-item[open] summary::after{rotate:45deg;}
.faq-item summary:hover{color:var(--safety-ink);}
.faq-item summary:focus-visible{outline:2px solid var(--safety); outline-offset:3px;}
.faq-item p{color:var(--olive); max-width:65ch; padding:0 0 var(--space-5); line-height:1.7; text-wrap:pretty;}
/* "Go deeper" prompt in the FAQ head's left column: gives the blog link a
   reason and fills the whitespace beside the accordion instead of trailing
   under the whole section as a bare link. */
.faq-more{margin-top:var(--space-7); padding-top:var(--space-6); border-top:1px solid var(--line);}
.faq-more p{color:var(--olive); line-height:1.7; margin-bottom:var(--space-4); text-wrap:pretty;}
/* Same bordered ghost button as the "View all" CTA, for one coherent
   "next step" treatment across the site. */
.faq-more a{
  display:inline-block; font-weight:700; color:var(--ink);
  border:1px solid var(--ink); border-radius:8px; padding:14px 26px; text-decoration:none;
  transition:background 200ms var(--ease-out), color 200ms var(--ease-out), transform 200ms var(--ease-out);
}
.faq-more a:hover{background:var(--ink); color:var(--paper); transform:translateY(-2px);}
.faq-more a:active{transform:scale(0.98); transition-duration:100ms;}
.faq-more a:focus-visible{outline:2px solid var(--safety); outline-offset:3px;}

/* ---------- CTA ---------- */
.cta{
  padding:var(--space-9) clamp(20px,6vw,10vw);
  display:flex; flex-direction:row; justify-content:space-between; align-items:center; gap:var(--space-6); flex-wrap:wrap;
}

/* Every section needs the same top clearance the hero already has, or its
   heading collides with the fixed top-right "Book a Consultation" button,
   which stays visible for the whole page (it only hides near the footer).
   One rule here, after every section's own padding shorthand in source
   order, wins for just the top edge without disturbing each section's own
   left/right/bottom spacing. */
.hero, .facts, .problem, .method, .personas, .work, .services, .styles, .quote, .trust, .faq, .cta{
  padding-top:140px;
}
@media (max-width:900px){
  /* No fixed top-right button at this width (it docks to the bottom instead),
     so sections only need their own normal top padding, plus enough bottom
     padding to clear the bottom-docked book button (~80px) without stacking
     a full desktop-sized void between adjacent sections. */
  .hero, .facts, .problem, .method, .personas, .work, .services, .styles, .quote, .trust, .faq, .cta{
    padding-top:var(--space-8);
    padding-bottom:var(--space-9);
  }
}
.cta h2{font-family:var(--font-black); text-transform:uppercase; font-size:clamp(1.9rem,4vw,3rem); max-width:14ch; line-height:1.02; text-wrap:balance;}
.cta h2 .accent{color:var(--safety);}
.cta p{font-size:1rem; color:var(--olive); max-width:40ch; margin-top:var(--space-4);}
.cta-right{display:flex; flex-direction:column; align-items:flex-end; gap:var(--space-3);}
.cta-note{font-size:0.85rem; color:var(--olive);}
.btn-primary{
  background:var(--safety); color:var(--ink); font-weight:700; font-family:var(--font-sans);
  padding:18px 32px; border-radius:8px; text-decoration:none; font-size:1.05rem;
  transition:transform 200ms var(--ease-out), background 200ms var(--ease-out); display:inline-block;
}
.btn-primary:hover{transform:translateY(-2px); background:var(--safety-deep);}
.btn-primary:active{transform:scale(0.97); transition-duration:100ms;}
.btn-primary:focus-visible{outline:2px solid var(--ink); outline-offset:3px;}

/* Two-Voice Rule: the footer's nav links and prose (address, tagline) are
   content to be read, not a system readout, so the base face is Archivo, not
   mono - previously the whole block (including real navigation) inherited
   JetBrains Mono, which the design system explicitly reserves for
   labels/counters/stamps, never body copy or navigation. Column kickers
   (h3) and the legal/registration row stay mono deliberately: both are
   genuine metadata, the doc's actual Label use case. */
footer{padding:var(--space-7) clamp(20px,6vw,10vw); border-top:1px solid var(--line); font-family:var(--font-sans); font-size:0.85rem; color:var(--olive);}
.footer-grid{display:grid; grid-template-columns:1.2fr 0.8fr 1fr 1fr; gap:var(--space-6); margin-bottom:var(--space-6);}
.footer-grid.cols-5{grid-template-columns:1.2fr 0.9fr 0.9fr 1fr 0.9fr;}
.footer-col h3{font-family:var(--font-mono); text-transform:uppercase; letter-spacing:0.06em; color:var(--ink); margin-bottom:var(--space-3); font-size:0.75rem;}
.footer-col h3 .logo-mark{font-family:var(--font-black); text-transform:none; letter-spacing:-0.01em; font-size:1rem;}
.footer-col h3 .logo-mark .dot{color:var(--safety);}
.footer-col a{display:block; text-decoration:none; opacity:0.8; margin-bottom:8px; transition:opacity 200ms var(--ease-out);}
.footer-col a:hover{opacity:1; color:var(--safety-ink);}
.footer-col p.addr{opacity:0.85; max-width:26ch;}
.footer-bottom{font-family:var(--font-mono); border-top:1px solid var(--line); padding-top:var(--space-5); display:flex; justify-content:space-between; flex-wrap:wrap; gap:var(--space-3); font-size:0.74rem;}
.footer-bottom .legal{max-width:70ch; opacity:0.85;}

@media (max-width:1100px){
  .footer-grid, .footer-grid.cols-5{grid-template-columns:1fr 1fr;}
}

@media (max-width:900px){
  .faq{grid-template-columns:1fr;}
  .faq-head{margin-bottom:var(--space-8);}
  .hero-foot{grid-template-columns:1fr;}
  .facts{grid-template-columns:repeat(2,1fr); row-gap:var(--space-5);}
  .facts div:nth-child(2n){border-right:none;}
  .fear-grid{grid-template-columns:repeat(2,1fr);}
  .fear:not(:nth-child(2n)){border-right:1px solid var(--line);}
  .fear:nth-child(2n){border-right:none;}
  .tl-row, .service{grid-template-columns:1fr;}
  .persona-grid{grid-template-columns:repeat(2,1fr);}
  .work-grid{grid-template-columns:repeat(2,1fr);}
  .trust-grid{grid-template-columns:repeat(2,1fr);}
  .footer-grid{grid-template-columns:1fr 1fr;}
  /* Full text labels don't fit alongside the wordmark and the bottom-docked
     book button at this width, and a numbers-only fallback reads as bare,
     unlabeled chrome; a footer already carries the same section links for
     mobile wayfinding, so the corner nav simply steps aside here. */
  .index-corner{ display:none; }
  .logo{
    left:calc(clamp(20px,6vw,10vw) - 2px); right:auto; top:30px; transform:translateY(-50%);
    font-size:1.3rem; padding:8px 2px;
  }
  /* Only the wordmark occupies the top strip at this width (nav is hidden,
     Book docks to the bottom), so the strip can be shallower. */
  .site-header-strip{ height:60px; }
  /* A fixed top-right button overlaps whatever heading scrolls under it at this width;
     anchor it to the bottom instead, where it only ever nicks trailing whitespace. */
  .book-corner{
    top:auto; right:auto; bottom:var(--space-4); left:50%; transform:translateX(-50%);
    box-shadow:0 6px 18px oklch(0.16 0.01 100 / 0.14);
    transition:opacity 200ms var(--ease-out), transform 200ms var(--ease-out), background 200ms var(--ease-out);
  }
  .book-corner.is-hidden{opacity:0; transform:translateX(-50%) translateY(12px); pointer-events:none;}
  main{padding-bottom:64px;}

  /* The hero-lift scroll-driven animation scales .hero-grain up to 1.08x as
     the hero exits the viewport. Measured directly: at 430px wide that's a
     real ~464px-wide box, genuinely overflowing the page horizontally
     (confirmed via documentElement.scrollWidth going 430 -> 448 once scrolled
     past the hero) - not an elastic-scroll illusion, an actual scrollable
     region a user can pan into on the left or right. clip-path on .hero only
     clips paint, not scrollable overflow, and explicit overflow:hidden/clip
     on .hero didn't suppress it either when tested; scroll-linked transforms
     seem to make browsers keep the animated element's max extent in the
     scrollable-area calculation regardless of an ancestor's overflow
     property, at least in this engine. Disable the scroll-timeline outright
     on mobile, where the animation was already a minor flourish. */
  .hero-grain{
    animation:none !important;
    animation-timeline:none;
    animation-range:normal;
  }
}
@media (max-width:560px){
  .fear-grid, .persona-grid, .work-grid, .trust-grid{grid-template-columns:1fr;}
  .fear{border-right:none !important;}
  .footer-grid, .footer-grid.cols-5{grid-template-columns:1fr;}
}

/* ---------- Service & project detail pages ----------
   Thin additive layer over the gallery-page shell: same g-intro headline
   treatment, plus a readable long-form prose block the homepage never
   needed. Everything else (photo grids, FAQ, CTA, footer) reuses the
   existing components unchanged. */
.svc-crumb{
  font-family:var(--font-mono); font-size:0.75rem; text-transform:uppercase;
  letter-spacing:0.08em; color:var(--olive); margin-bottom:var(--space-4);
}
.svc-crumb a{color:inherit; text-decoration:none;}
.svc-crumb a:hover, .svc-crumb a:focus-visible{color:var(--safety-ink);}
.svc-crumb a:focus-visible{outline:2px solid var(--safety); outline-offset:3px;}
.svc-body{padding:var(--space-8) clamp(20px,6vw,10vw);}
.svc-body .svc-prose{max-width:70ch;}
.svc-body h2{
  font-family:var(--font-black); text-transform:uppercase;
  font-size:clamp(1.3rem,2.4vw,1.8rem); line-height:1.05;
  margin:var(--space-7) 0 var(--space-4); text-wrap:balance;
}
.svc-body h2:first-child{margin-top:0;}
.svc-body p{color:var(--olive); max-width:65ch; margin-bottom:var(--space-4); line-height:1.7; text-wrap:pretty;}
.svc-body ul{list-style:none; margin:var(--space-5) 0; max-width:65ch;}
.svc-body ul li{
  position:relative; padding:var(--space-3) 0 var(--space-3) 28px;
  border-top:1px solid var(--line-soft); color:var(--olive); line-height:1.6;
}
.svc-body ul li::before{
  content:"+"; position:absolute; left:0; top:var(--space-3);
  font-family:var(--font-mono); color:var(--safety-ink);
}
.svc-body ul li strong{color:var(--ink);}
.svc-gallery{padding:var(--space-8) clamp(20px,6vw,10vw); background:var(--paper-2);}
.svc-gallery .g-head{border-top:none; padding-top:0;}
.svc-gallery .work-more{margin-top:var(--space-6);}

/* ---------- Legal documents (privacy.html, terms.html) ----------
   Reuses the .svc-body prose shell wholesale; only three things are new. */

/* Inline links inside legal prose. Elsewhere on the site, links in prose are
   rare enough to be styled per-component; here they're frequent (opt-out
   tools, the Commissioner, our own contact details), so they need one
   consistent, visible treatment. The underline stays - in a legal document
   colour alone is not a sufficient affordance. */
.svc-body p a, .svc-body li a{
  color:var(--safety-ink); text-decoration-thickness:1px; text-underline-offset:3px;
}
.svc-body p a:hover, .svc-body li a:hover{color:var(--safety-deep);}
.svc-body p a:focus-visible, .svc-body li a:focus-visible{outline:2px solid var(--safety); outline-offset:3px;}
/* A button placed inside prose (the WhatsApp CTA on /contact) is a button, not a
   link: the underlined safety-ink treatment above outranks .btn-primary on
   specificity and was repainting the button's text orange. */
.svc-body p a.btn-primary{color:var(--ink); text-decoration:none;}
.svc-body p a.btn-primary:hover{color:var(--ink);}

/* Brochure callout in service-page prose: cover thumbnail beside the download
   button, echoing the cards on /brochures. */
.brochure-callout{display:flex; gap:var(--space-5); align-items:flex-start; margin:var(--space-4) 0 var(--space-5); max-width:65ch;}
.brochure-callout__cover{flex:0 0 auto; display:block; width:160px; max-width:38%;}
.brochure-callout__cover img{width:100%; height:auto; display:block; border:1px solid var(--line); border-radius:8px; transition:border-color 200ms var(--ease-out);}
.brochure-callout__cover:hover img{border-color:var(--ink);}
.brochure-callout__body{flex:1 1 auto;}
.brochure-callout__body p:first-child{margin-top:0;}
@media (max-width:600px){ .brochure-callout{flex-direction:column; gap:var(--space-4);} .brochure-callout__cover{width:180px; max-width:60%;} }

/* The "which act, last revised when" line under each document's title. Mono,
   because it is metadata about the document, not part of it - the Label role. */
.doc-stamp{
  font-family:var(--font-mono); font-size:0.75rem; text-transform:uppercase; letter-spacing:0.06em;
  color:var(--olive); margin-bottom:var(--space-7); padding-bottom:var(--space-4);
  border-bottom:1px solid var(--line);
}

/* The Bahasa Malaysia half of the privacy notice (PDPA s.7(3) requires both
   languages). Same paper/paper-2 alternation the gallery uses to mark a new
   block, rather than a tab or a language switcher: both versions are legally
   operative, so both should be readable without an interaction, and printing
   or Ctrl-F should reach either one. */
.doc-alt{background:var(--paper-2);}

/* Footer legal row: reg number, then the two document links, then copyright.
   Links are inline here (not the stacked .footer-col treatment). */
.footer-legal-links{display:flex; gap:var(--space-5);}
.footer-legal-links a{text-decoration:none; opacity:0.85; transition:opacity 200ms var(--ease-out), color 200ms var(--ease-out);}
.footer-legal-links a:hover{opacity:1; color:var(--safety-ink);}
.footer-legal-links a:focus-visible{outline:2px solid var(--safety); outline-offset:3px;}

/* ---------- Guides / blog (blog.html + blog/*.html, generated by
   tools/build_blog.mjs) ----------
   Another thin layer over the same shell. Articles are .svc-body prose - the
   service pages already proved that block reads well at length - so the only
   new work here is the four things long-form needs that a service page never
   did: a third heading level, numbered steps, figures with captions, and a
   table that can hold a cost range.

   The index reuses .p-grid / .p-card from the projects overview. The cards
   carry no photo: a guide's subject is its sentence, and a decorative image
   above every one of them would be the stock-photo habit DESIGN.md exists to
   prevent. Emptiness where a photo would go is the point. */
.blog-index{grid-template-columns:repeat(3,1fr); align-items:stretch;}
.blog-index .p-card{
  display:flex; background:var(--paper-2);
  transition:border-color 200ms var(--ease-out), background 200ms var(--ease-out);
}
.blog-index .p-card:hover{border-color:var(--ink); background:var(--paper);}
.blog-index .p-card-text{padding:var(--space-5); gap:var(--space-3); align-content:start;}
.blog-index .p-card h2{font-size:1.15rem; line-height:1.15; text-wrap:balance;}
.blog-index .p-card .p-card-sub{font-size:0.92rem; line-height:1.55;}

/* Label role: the kicker on a card and, on the article itself, the .doc-stamp
   line the legal pages already use. Both are metadata about the document. */
.post-kicker{
  font-family:var(--font-mono); font-size:0.68rem; text-transform:uppercase;
  letter-spacing:0.08em; color:var(--safety-ink);
}

/* Article header. The gallery .g-intro bottom-aligns two columns, which works
   when both are short. An article headline runs three display lines and the
   lede runs five, so `align-items:end` pins the lede to the headline's baseline
   and leaves a hole above it. Articles top-align instead, and the lede gets the
   weight DESIGN.md always specified for the role (1.2rem/600) rather than the
   bare body size it inherits elsewhere. Header only; the gallery is untouched. */
.gallery-page .g-intro.article-intro{align-items:start; grid-template-columns:1.25fr 1fr;}
.gallery-page .g-intro.article-intro .lede{
  font-size:1.2rem; font-weight:600; line-height:1.5; color:var(--ink); max-width:46ch;
}

/* Article measure. Prose stays at the 65ch reading column, but figures, tables
   and the money note run wider. That step out and back is the only rhythm a
   1,500-word page gets, and without it the whole article is one grey column of
   the same width from top to bottom. Legal pages keep the old single measure:
   this targets <article class="svc-prose">, they use a <div>. */
.svc-body article.svc-prose{max-width:84ch;}
.svc-body article.svc-prose > p,
.svc-body article.svc-prose > h2,
.svc-body article.svc-prose > h3,
.svc-body article.svc-prose > ul,
.svc-body article.svc-prose > ol,
.svc-body article.svc-prose > blockquote{max-width:65ch;}

/* Third level. 1.15rem is the Title role: at 1.05rem it sat a hair above body
   text and read as a bold paragraph rather than a level. */
.svc-body h3{
  font-family:var(--font-sans); font-weight:700; text-transform:uppercase;
  font-size:1.15rem; letter-spacing:0.015em; line-height:1.2;
  margin:var(--space-6) 0 var(--space-3);
}

/* Numbered steps. Same mono marker as the "+" bullet above, so a list of
   stages and a list of items read as one family. */
.svc-body ol{list-style:none; counter-reset:step; margin:var(--space-5) 0; max-width:65ch;}
.svc-body ol li{
  position:relative; counter-increment:step;
  padding:var(--space-3) 0 var(--space-3) var(--space-6);
  border-top:1px solid var(--line-soft); color:var(--olive); line-height:1.6;
}
.svc-body ol li::before{
  content:counter(step, decimal-leading-zero); position:absolute; left:0; top:var(--space-3);
  font-family:var(--font-mono); font-size:0.8rem; color:var(--safety-ink);
}
.svc-body ol li strong{color:var(--ink);}

/* Pull-quote inside prose. A rule, not quote marks: the site has no decorative
   punctuation anywhere else and italics would be a fourth voice. */
.svc-body blockquote{
  margin:var(--space-6) 0; padding:var(--space-3) 0 var(--space-3) var(--space-5);
  border-left:2px solid var(--safety); max-width:62ch;
}
.svc-body blockquote p{
  margin:0; color:var(--ink); font-weight:600; font-size:1.15rem; line-height:1.5;
}

/* Figures. Real project photography where it exists; stock or generated imagery
   is permitted in /blog only (DESIGN.md), and the caption always says which. */
.post-figure{margin:var(--space-6) 0;}
.post-figure picture{display:block; border-radius:10px; overflow:hidden; border:1px solid var(--line);}
.post-figure img{width:100%; height:auto; display:block;}
.post-figure svg{width:100%; height:auto; display:block; border-radius:10px; border:1px solid var(--line); background:var(--paper-2);}
.post-figure figcaption{
  font-family:var(--font-mono); font-size:0.7rem; text-transform:uppercase;
  letter-spacing:0.06em; color:var(--olive); margin-top:var(--space-3);
}
.post-figure figcaption a{color:var(--safety-ink);}

/* Charts. One component, one set of metrics, real text. The bars are DOM, not
   SVG: they reflow with the column, the labels stay on the page's own type
   ramp, and a chart can never be clipped by a viewBox somebody sized by hand.
   Label / bar / value is a three-column grid, so every row in every chart on
   the site lines up on the same two vertical edges. */
.post-chart .chart{
  display:grid; gap:var(--space-3);
  padding:var(--space-5); border:1px solid var(--line); border-radius:10px;
  background:var(--paper-2);
}
.chart-row{
  display:grid; grid-template-columns:minmax(9ch,15ch) 1fr auto;
  align-items:center; gap:var(--space-4);
}
.chart-label{
  font-family:var(--font-mono); font-size:0.68rem; text-transform:uppercase;
  letter-spacing:0.06em; color:var(--ink); line-height:1.4;
}
.chart-track{display:block; height:14px; border-radius:999px; background:var(--line-soft);}
.chart-bar{display:block; height:100%; border-radius:999px; background:var(--olive);}
.chart-row.is-accent .chart-bar{background:var(--safety);}

/* Gantt: same three-column grid, but the bar carries a left offset, so a phase
   that starts in week nine starts nine weeks along the track. The track is the
   whole project; the bar is this phase's slice of it. The empty run to the left
   of each bar is the point, it is what shows the phases are sequential and not
   simultaneous, so the track stays a visible rail rather than a filled row. */
.chart.is-gantt .chart-track{
  background:transparent; border-radius:0;
  border-top:1px solid var(--line-soft); border-bottom:1px solid var(--line-soft);
  height:20px; display:flex; align-items:center;
}
.chart.is-gantt .chart-bar{height:12px;}
.chart.is-gantt .chart-row{gap:var(--space-4);}
.chart-value{
  font-family:var(--font-mono); font-size:0.68rem; letter-spacing:0.04em;
  color:var(--olive); white-space:nowrap; font-variant-numeric:tabular-nums;
}
@media (max-width:560px){
  .chart-row{grid-template-columns:1fr auto; gap:var(--space-3) var(--space-4);}
  .chart-label{grid-column:1 / -1;}
}

/* Cost tables. Wide content scrolls inside its own box rather than pushing the
   page sideways on a phone. */
.post-table{margin:var(--space-6) 0; overflow-x:auto; border:1px solid var(--line); border-radius:10px;}
.post-table table{border-collapse:collapse; width:100%; min-width:520px; background:var(--paper-2);}
.post-table th{
  font-family:var(--font-mono); font-size:0.68rem; text-transform:uppercase; letter-spacing:0.06em;
  text-align:left; color:var(--ink); background:var(--paper);
  padding:var(--space-3) var(--space-4); border-bottom:1px solid var(--line);
}
.post-table td{
  padding:var(--space-3) var(--space-4); color:var(--olive);
  font-size:0.95rem; line-height:1.5; border-bottom:1px solid var(--line-soft);
  font-variant-numeric:tabular-nums;
}
.post-table tr:last-child td{border-bottom:none;}
.post-table td strong{color:var(--ink);}

/* The "these are indicative ranges, not a quote" note that has to sit next to
   every number we publish. Safety rule on the left because it is the one thing
   on the page a reader must not skim past. */
.post-note{
  margin:var(--space-6) 0; padding:var(--space-4) var(--space-5);
  background:var(--paper-2); border:1px solid var(--line); border-left:2px solid var(--safety);
  border-radius:0 10px 10px 0;
}
.post-note p{margin:0; font-size:0.95rem; max-width:none;}

@media (max-width:900px){
  .blog-index{grid-template-columns:repeat(2,1fr);}
  /* The shared .g-intro collapse lives higher up the file, so this rule, being
     later, would otherwise re-impose two columns on a phone and crush the lede
     into a 40px gutter. It has to restate the collapse, not rely on it. */
  .gallery-page .g-intro.article-intro{grid-template-columns:1fr; gap:var(--space-5);}
  .gallery-page .g-intro.article-intro .lede{max-width:65ch;}
}
@media (max-width:560px){
  .blog-index{grid-template-columns:1fr;}
}