/* =====================================================================
   LAYOUT FAMILIES — loaded after v2.css, which holds the tokens.

   The design skill bans reusing a layout family across sections and caps
   eyebrow labels at one per three sections. Six families rotate here:

     hero-split · marquee · split · bento · cards · band

   `split` is also the fix for the dead whitespace the first build had: a
   62ch prose column inside a 1280px container left the right half of
   every section empty. The heading now occupies that column and sticks
   while the body scrolls past it. That uses the width AND removes the
   need for an eyebrow label above every section.
   ===================================================================== */

/* ---- split: sticky heading beside body ---------------------------- */
.split { display: grid; gap: clamp(28px, 4vw, 72px); }
@media (min-width: 900px) {
  .split { grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.4fr); }
  .split-head { position: sticky; top: calc(var(--bar-h) + var(--bar-gap)); align-self: start; }
}
.split-head .h-section { max-width: 16ch; }
.split-aside { margin: 18px 0 0; font: 400 12px/1.7 var(--mono); color: var(--dimmer); max-width: 34ch; }
/* Inside a split the body already has a sane measure, so the prose cap
   would only narrow it further and re-create the empty gutter. */
.split-body .prose { max-width: none; }
.split-body > * + * { margin-top: 34px; }

.section.tint { background: var(--bg-2); }

.row-head {
  display: flex; flex-wrap: wrap; align-items: baseline;
  justify-content: space-between; gap: 18px; margin-bottom: 22px;
}

/* ---- hero-split --------------------------------------------------- */
.hero { padding-block: clamp(48px, 9vh, 96px) clamp(56px, 8vh, 104px); }

/*  On a phone the bar is two rows tall and the wordmark sits above it,
    so the hero's own 9vh of head-room stacked on top of all that left
    roughly 90px of nothing between the navigation and the first line
    of text. Vertical space is the scarcest thing on a 800px screen. */
@media (max-width: 720px) { .hero { padding-block: 30px 44px; } }
.hero-in { display: grid; gap: clamp(32px, 5vw, 72px); align-items: end; }
@media (min-width: 980px) { .hero-in { grid-template-columns: minmax(0, 1.55fr) minmax(0, 0.7fr); } }

/*  THE TABLET HERO, 800-979px. Designed, not inherited.

    Measured at 820px before this rule: the hero column is 739px wide and
    the longest headline line was 364px. The headline filled 49% of its
    own column and the right half of the screen was empty.

    The cause is that .h-display's 4.6vw term is tuned for the DESKTOP
    grid, where the headline sits in a 1.55fr column of roughly 790px. In
    this band the hero is a single column, so the headline gets the full
    width while the font size is still being computed as though it did
    not. A viewport-derived size stops being correct the moment the
    column stops being a fixed fraction of the viewport.

    .h-sub moves with it. At 820px the two were 37.7px and 31.2px, a
    ratio of 1.21, against 1.53 on desktop: the sub-line was competing
    with the headline rather than sitting under it. These values hold
    the desktop ratio across the band. */
@media (min-width: 800px) and (max-width: 979px) {
  .hero .h-display { font-size: clamp(2.8rem, 7.4vw, 3.9rem); }
  .hero .h-sub     { font-size: clamp(1.8rem, 4.8vw, 2.5rem); }
}

.hero-eyebrow {
  /*  FLUID, and the clamp has to sit here rather than in the phone
      media query above. `font:` is a shorthand and resets font-size, so
      an override placed EARLIER in the file loses to it: media queries
      carry no specificity of their own. That is exactly how a first
      attempt at this silently did nothing.

      40 characters at 11px with 0.14em of tracking measures 342px
      against 319px of usable width on a 351px screen, so "INDIA" wrapped
      to a line of its own and left the status dot stranded between the
      two. Measured, this fits on one line from 320px up. */
  font: 500 clamp(8.5px, 2.7vw, 11px)/1 var(--mono);
  letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--dim); margin: 0 0 30px;
  display: inline-flex; align-items: center; gap: 9px;
}
/* The only status dot on the site, and scoped to the HOME hero alone.
   The same eyebrow class labels inner pages ("About", "Method"), where a
   green dot would be decoration attached to nothing. A status indicator
   that indicates no status is exactly the tell the design skill bans. */
.hero .hero-eyebrow::before {
  content: ""; width: 7px; height: 7px; background: var(--sev-low);
  border-radius: 50%; flex: none;
}
.hero .h-display { margin-bottom: 26px; }
.hero .h-display span { display: block; }
/*  The orange line sits HALFWAY between the headline and the paragraph
    (Ram, 21 Sep 2026), not tucked under the headline: the same gap above
    it as below it. --hero-gap is the one number that sets both. */
.hero { --hero-gap: clamp(20px, 3vw, 30px); }
/*  0.8 because the paragraph's taller line-height already adds space
    above its first line; measured, this makes the visible gaps match. */
.hero .h-display { margin-bottom: calc(var(--hero-gap) * 0.8); }
.hero .h-sub { margin-top: var(--hero-gap); }
.h-sub {
  display: block; color: var(--dim); font-weight: 500;
  letter-spacing: -0.02em; line-height: 1.06; margin-top: 0.16em;
  font-size: clamp(1.4rem, 3.8vw, 2.7rem);
}
.hero-cta { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 34px; }

/* The rail. This is what stops the right half of the hero being empty. */
.hero-rail { display: grid; gap: 1px; background: var(--rule-soft); border: 1px solid var(--rule-soft); }
@media (max-width: 979px) { .hero-rail { grid-template-columns: repeat(2, 1fr); } }
.hero-rail > div { background: var(--bg); padding: 16px 18px; }
.hero-rail dt {
  font: 500 10px/1 var(--mono); letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--dimmer); margin: 0 0 8px;
}
.hero-rail dd { margin: 0; font: 700 26px/1 var(--sans); letter-spacing: -0.03em; }

/* ---- page-head: inner pages -------------------------------------- */
.page-head {
  padding-block: clamp(40px, 7vh, 80px) clamp(32px, 5vh, 56px);
  border-bottom: 1px solid var(--rule-soft);
}
.h-page {
  font-size: clamp(2rem, 5.6vw, 4.2rem); line-height: 1.02;
  letter-spacing: -0.035em; max-width: 18ch; margin: 0 0 24px;
}
.page-head .lede { margin: 0; }

/* ---- bento -------------------------------------------------------- */
.bento { display: grid; gap: 1px; background: var(--rule-soft); border: 1px solid var(--rule-soft); }
@media (min-width: 620px)  { .bento { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1040px) {
  /* Six items, three columns, two rows, exactly. The span rules that
     used to sit here made two cells double-width, which needed eight
     tracks across three rows of three and left a visible empty tile.
     Variation now comes from the tint in ai.css, not from resizing. */
  .bento { grid-template-columns: repeat(3, 1fr); }
}
.bento-cell { background: var(--bg); padding: 24px 22px; transition: background var(--fast) var(--ease); }
.bento-cell:hover { background: var(--bg-3); }
.bento-cell h3 {
  font: 500 11px/1 var(--mono); letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--signal); margin-bottom: 13px;
}
.bento-cell p { margin: 0; font-size: 0.89rem; line-height: 1.62; color: var(--dim); }

/* ---- cards: case studies with real report thumbnails -------------- */
.cases { display: grid; gap: 1px; background: var(--rule-soft); border: 1px solid var(--rule-soft); }
@media (min-width: 860px) { .cases { grid-template-columns: repeat(2, 1fr); } }
/*  ABOVE 1040px nothing in this sheet used to respond at all: the whole
    site froze at two columns while .wrap kept growing to --maxw. On a
    1920 screen that is eight case studies in a 1280px column with 320px
    of empty gutter each side and a lot of scrolling.

    --maxw stays at 1280. Widening the container would widen .split-body,
    whose prose already runs to about 90 characters, and a longer measure
    is a worse read, not a denser one. Only the CARD GRID gains a column,
    because cards are the one block here that is genuinely list-shaped. */
@media (min-width: 1360px) { .cases { grid-template-columns: repeat(3, 1fr); } }

/*  NO EMPTY CELLS. The hairline grid paints its gaps in --rule-soft, so a
    short last row shows as a grey block where a card should be. Ram asked
    for none anywhere (21 Sep 2026). Cards come from data and their number
    varies per page, so this is solved here for any count rather than per
    page: the last card of a short row stretches to close it.

    Three across is done on a six-track grid, each card spanning two. That
    lets a row of two split the width evenly (span 3 each) instead of
    leaving a third of it bare. Four goes two by two, not three and one. */
@media (min-width: 860px) and (max-width: 1359px) {
  .cases > :last-child:nth-child(odd) { grid-column: 1 / -1; }
  /*  A stretched card keeps a readable line length inside it. */
  .cases > :last-child:nth-child(odd) .case-body { max-width: 68ch; }
}
/*  Exactly three (the proof row on most money pages): three across as
    soon as a card gets about 350px, instead of two and a stretched one. */
@media (min-width: 1100px) and (max-width: 1359px) {
  .cases:has(> :nth-child(3):last-child) { grid-template-columns: repeat(3, 1fr); }
  .cases:has(> :nth-child(3):last-child) > * { grid-column: auto; }
}
@media (min-width: 1360px) {
  .cases { grid-template-columns: repeat(6, 1fr); }
  .cases > * { grid-column: span 2; }
  .cases > :nth-child(3n+1):nth-last-child(2),
  .cases > :nth-child(3n+2):last-child { grid-column: span 3; }
  .cases > :nth-child(3n+1):last-child { grid-column: 1 / -1; }
  .cases:has(> :nth-child(4):last-child) > * { grid-column: span 3; }
}

.case {
  background: var(--bg); display: grid; gap: 20px;
  padding: clamp(20px, 2.6vw, 30px); position: relative;
  transition: background var(--fast) var(--ease);
}
.case:hover { background: var(--bg-2); }
.case::before {
  content: ""; position: absolute; inset: 0 0 auto 0; height: 2px;
  background: var(--case); opacity: 0.9;
}


.case-body { display: flex; flex-direction: column; gap: 13px; min-width: 0; }
.case-top { display: flex; justify-content: space-between; gap: 14px; align-items: baseline; }
.case-sector {
  font: 500 10.5px/1.4 var(--mono); letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--case);
}
.case-period { font: 400 10.5px/1.4 var(--mono); color: var(--dimmer); white-space: nowrap; }
.case h3 { font-size: clamp(1.05rem, 1.7vw, 1.35rem); line-height: 1.18; }
.case h3 a { transition: color var(--fast) var(--ease); }
.case h3 a:hover { color: var(--case); }
.case-engagement { margin: 0; font-size: 0.87rem; color: var(--dim); line-height: 1.55; }

.case-meta {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(84px, 1fr));
  gap: 1px; background: var(--rule-soft); border: 1px solid var(--rule-soft); margin: 0;
}
.case-meta.wide { max-width: 520px; margin-block: 28px; }
.case-meta > div { background: var(--bg); padding: 10px 12px; }
.case-meta dt {
  font: 500 9.5px/1 var(--mono); letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--dimmer); margin: 0 0 6px;
}
.case-meta dd { font: 600 15px/1.1 var(--mono); margin: 0; }
.case-meta dd.sm { font-size: 11px; line-height: 1.4; }

.sev { display: flex; flex-wrap: wrap; gap: 5px; }
.sev span {
  font: 500 10px/1 var(--mono); letter-spacing: 0.06em; padding: 6px 8px;
  border: 1px solid currentColor; display: inline-flex; gap: 6px; text-transform: uppercase;
}
.s-critical { color: var(--sev-critical); }
.s-high     { color: var(--sev-high); }
.s-medium   { color: var(--sev-medium); }
.s-low      { color: var(--sev-low); }
/*  One engagement was rated on its own report's scale, critical /
    warning / info, rather than C/H/M/L. Those two were dropped
    silently until 20 Sep 2026. Warning sits with high and info with
    low, which is their sense in that report. */
.s-warning { color: var(--sev-high); }
.s-info    { color: var(--sev-low); }

/* The withdrawn-findings badge. The most important element on the page,
   so it gets a border and its own colour rather than being a footnote. */
.withdrawn { border: 1px solid var(--rule); padding: 14px 16px; display: flex; gap: 14px; align-items: flex-start; }
.withdrawn + .withdrawn { margin-top: 12px; }
.withdrawn b { font: 700 19px/1 var(--mono); color: var(--sev-high); flex: none; }
.withdrawn p { margin: 0; font-size: 0.86rem; color: var(--dim); line-height: 1.55; }

.case-status {
  font: 400 10.5px/1.5 var(--mono); color: var(--dimmer);
  border-top: 1px solid var(--rule-soft); padding-top: 12px; margin: auto 0 0;
}
.case-links { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 26px; }
.chip {
  font: 500 10.5px/1 var(--mono); letter-spacing: 0.07em; text-transform: uppercase;
  border: 1px solid var(--rule); padding: 9px 12px; color: var(--dim);
  transition: color var(--fast) var(--ease), border-color var(--fast) var(--ease);
}
.chip:hover { color: var(--fg); border-color: var(--fg); }

.case-head { display: grid; gap: clamp(28px, 4vw, 60px); align-items: start; }
@media (min-width: 900px) { .case-head { grid-template-columns: minmax(0, 1.35fr) minmax(0, 0.65fr); } }

/*  The contact form's honeypot. Moved off-screen rather than hidden
    with display:none: a bot that skips display:none fields would sail
    past the trap and fill in the real ones. Nothing real reaches it,
    because it is aria-hidden and out of the tab order. */
.hp {
  position: absolute;
  left: -9999px;
  width: 1px; height: 1px;
  overflow: hidden;
}

/* ---- money pages: breadcrumb, byline, contents, questions ---------
   Added with the service pages (Part 3 of the 20 Sep SEO brief). All
   four are quiet on purpose: they are wayfinding and trust signals, and
   none of them should compete with the headline for attention.       */

.crumbs ol {
  list-style: none; margin: 0 0 22px; padding: 0;
  display: flex; flex-wrap: wrap; gap: 6px;
  font: 500 10.5px/1.4 var(--mono); letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--dimmer);
}
.crumbs li + li::before { content: "/"; margin-right: 6px; color: var(--rule); }
.crumbs a { color: var(--dim); }
.crumbs a:hover { color: var(--signal); }
.crumbs [aria-current="page"] { color: var(--fg); }

/*  The "Updated" date is one of the two page features the brief's
    citation research found held up with ranking held constant. It is
    set in services.js by hand, so what this displays is real. */
.byline {
  margin: 20px 0 0;
  font: 400 12px/1.6 var(--mono); color: var(--dimmer);
}
.byline a { color: var(--dim); text-decoration: underline; text-underline-offset: 3px; }
.byline a:hover { color: var(--signal); }

.toc-label {
  margin: 0 0 14px;
  font: 500 10.5px/1 var(--mono); letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--signal);
}
.toc { list-style: none; margin: 0; padding: 0; counter-reset: toc; }
.toc li { counter-increment: toc; border-top: 1px solid var(--rule-soft); }
.toc li:last-child { border-bottom: 1px solid var(--rule-soft); }
.toc a {
  display: flex; gap: 12px; padding: 11px 0;
  font-size: 0.9rem; line-height: 1.4; color: var(--dim);
}
.toc a::before {
  content: counter(toc, decimal-leading-zero);
  font: 500 11px/1.6 var(--mono); color: var(--dimmer); flex: none;
}
.toc a:hover { color: var(--fg); }

.faq-item + .faq-item { margin-top: 30px; padding-top: 30px; border-top: 1px solid var(--rule-soft); }
.faq-item h3 { margin: 0 0 12px; font-size: 1.08rem; line-height: 1.35; }
.faq-item .prose p { margin: 0 0 12px; }

/*  Section anchors land below the sticky bar rather than under it. */
[id] { scroll-margin-top: calc(var(--bar-h) + var(--bar-gap)); }

/* ---- onward: the read-next row ------------------------------------
   Sits directly above the band, and has to look like a different thing
   from it: the band is one loud orange block with a single button, this
   is a quiet hairline row of two or three choices. Loud CTA under a
   quiet set of options is a normal way to end a page; two loud blocks
   stacked is not.

   Hairline grid, same construction as .bento and .kv: 1px gap over a
   rule-coloured background, each cell repainting --bg over the top.
   The cells are links here, so they get a hover state and an arrow,
   which is what keeps it from reading as another static panel.        */
.onward { border-top: 1px solid var(--rule-soft); }
.onward-in {
  display: grid; gap: 1px;
  background: var(--rule-soft);
  border-inline: 1px solid var(--rule-soft);
  border-bottom: 1px solid var(--rule-soft);
}
/*  Six tracks, two per item, so a short last row closes up instead of
    leaving a bare cell. Same rule as .cases above. */
@media (min-width: 760px) {
  .onward-in { grid-template-columns: repeat(6, 1fr); }
  .onward-in > * { grid-column: span 2; }
  .onward-in > :nth-child(3n+1):nth-last-child(2),
  .onward-in > :nth-child(3n+2):last-child { grid-column: span 3; }
  .onward-in > :nth-child(3n+1):last-child { grid-column: 1 / -1; }
  .onward-in:has(> :nth-child(4):last-child) > * { grid-column: span 3; }
}
/*  Two items, two columns. Three across a phone would give each about
    100px, which is not enough for a sentence. */
@media (max-width: 759px) { .onward-in { grid-template-columns: 1fr; } }

/*  THE READ-NEXT BAR RUNS THE FULL WIDTH OF THE SCREEN (21 Sep 2026).
    Its markup is class="wrap onward-in", so it sat inside the 1280px
    content column, and the grid's divider colour filled .wrap's side
    padding: a grey strip at each edge, so the bar looked cut off. On
    screens wider than the column it also stopped short of both sides.
    Scoped to .onward on purpose: the services hub reuses .onward-in for
    grids INSIDE its page content, and those should stay in the column.
    The gutter moves onto the cards, so the first card's text still lines
    up with the page content above it wherever the column is full width. */
.onward .onward-in {
  max-width: none;
  padding-inline: 0;
  border-inline: 0;
}
.onward .onward-item { padding-inline: var(--gutter); }

.onward-item {
  background: var(--bg);
  display: block;
  padding: 22px 22px 24px;
  transition: background var(--fast) var(--ease);
}
.onward-item:hover { background: var(--bg-3); }

.onward-k {
  display: block;
  font: 500 10.5px/1 var(--mono);
  letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--signal);
  margin-bottom: 11px;
}
/*  The arrow is a pseudo-element rather than a character in the text,
    so it never reaches a crawler's extraction of the anchor. */
.onward-v {
  display: block;
  font-size: 0.95rem; line-height: 1.5;
  color: var(--fg);
  max-width: 34ch;
}
.onward-v::after {
  content: ""; display: inline-block;
  width: 0.62em; height: 0.62em;
  margin-left: 0.5em;
  border-top: 1.5px solid var(--dimmer);
  border-right: 1.5px solid var(--dimmer);
  transform: translateY(-1px) rotate(45deg);
  transition: transform var(--fast) var(--ease), border-color var(--fast) var(--ease);
}
.onward-item:hover .onward-v::after {
  border-color: var(--signal);
  transform: translateY(-1px) rotate(45deg) translate(2px, -2px);
}

/* ---- band: full-width CTA -----------------------------------------
   The one place on the site that inverts. It used to be near-white;
   it is now the accent, which makes the four CTAs the loudest thing
   on their pages by a wide margin.

   CONTRAST, measured against #FF4D2E:
     #09090B  near-black  6.02  passes AA for body and large
     #3A3A42  the old ink 3.41  FAILS for the 0.95rem paragraph
     #F2F2F4  near-white  2.96  fails
   So the ink is var(--bg) throughout, including the paragraph, which
   previously used a mid-grey that was only legible on near-white.

   The paragraph keeps a hierarchy below the heading by fading rather
   than by changing hue: 0.82 over orange composites to #351511, which
   is 5.02:1. Still AA. Any lower and it stops being.                  */
.band { background: var(--signal); color: var(--bg); padding-block: clamp(40px, 6vh, 68px); }
.band-in { display: flex; flex-wrap: wrap; gap: 26px; align-items: center; justify-content: space-between; }
.band-in p { margin: 10px 0 0; max-width: 52ch; color: var(--bg); opacity: 0.82; font-size: 0.95rem; }
.h-band { font-size: clamp(1.4rem, 3vw, 2.1rem); line-height: 1.1; max-width: 22ch; }
.band .btn { background: var(--bg); color: var(--fg); border-color: var(--bg); }
.band .btn:hover { background: transparent; color: var(--bg); }

/* ---- lists -------------------------------------------------------- */
.kv { display: grid; gap: 1px; background: var(--rule-soft); border: 1px solid var(--rule-soft); }
@media (min-width: 760px) { .kv { grid-template-columns: 1fr 1fr; } }
/*  An odd last panel takes the full row rather than leaving a bare cell. */
@media (min-width: 760px) { .kv > :last-child:nth-child(odd) { grid-column: 1 / -1; } }
.kv > div { background: var(--bg); padding: 19px 20px; }
.kv dt {
  font: 500 10.5px/1 var(--mono); letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--dimmer); margin: 0 0 9px;
}
.kv dd { margin: 0; font-size: 0.91rem; color: var(--fg); line-height: 1.55; }

.findings { margin: 0; padding-left: 20px; color: var(--dim); max-width: 68ch; }
.findings li { margin-bottom: 14px; line-height: 1.6; }
.findings li::marker { color: var(--signal); }

/* Findings-by-category. A count plus a hairline row, no filled
   background track: the skill bans dashboard-style track+fill on a
   marketing page, and the count alone carries the comparison. */
.bars { margin-top: 34px; display: grid; gap: 10px; }
.bar-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 14px; align-items: baseline; }
.bar-row span { font-size: 0.87rem; color: var(--dim); }
.bar-row b { font: 600 13px/1 var(--mono); color: var(--fg); }

.steps-list {
  list-style: none; counter-reset: s; margin: 0; padding: 0; display: grid; gap: 1px;
  background: var(--rule-soft); border: 1px solid var(--rule-soft);
}
.steps-list li { counter-increment: s; background: var(--bg); padding: 18px 20px; display: grid; gap: 6px; }
.steps-list b::before {
  content: "0" counter(s); font: 500 10px/1 var(--mono); color: var(--signal);
  letter-spacing: 0.1em; margin-right: 10px;
}
.steps-list b { font-size: 0.95rem; }
.steps-list span { font-size: 0.87rem; color: var(--dim); line-height: 1.55; }

.phases {
  list-style: none; margin: 0; padding: 0; display: grid; gap: 1px;
  background: var(--rule-soft); border: 1px solid var(--rule-soft);
}
@media (min-width: 700px)  { .phases { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1040px) { .phases { grid-template-columns: repeat(3, 1fr); } }
.phases li { background: var(--bg); padding: 26px 24px; display: grid; gap: 14px; }
.phases b { font: 700 28px/1 var(--mono); color: var(--signal); }
.phases h3 { font-size: 1.02rem; margin-bottom: 7px; }
.phases p { margin: 0; font-size: 0.87rem; color: var(--dim); line-height: 1.58; }

.proof-list { display: grid; gap: 1px; background: var(--rule-soft); border: 1px solid var(--rule-soft); margin-top: 26px; }
.proof-item {
  background: var(--bg); padding: 16px 18px; display: flex;
  justify-content: space-between; align-items: baseline; gap: 14px;
  transition: background var(--fast) var(--ease);
}
.proof-item:hover { background: var(--bg-2); }
.proof-item span { font-size: 0.91rem; }
.proof-item em {
  font: 400 10.5px/1 var(--mono); color: var(--dimmer); font-style: normal;
  text-transform: uppercase; letter-spacing: 0.08em; white-space: nowrap;
}

.empty-state { border: 1px dashed var(--rule); padding: clamp(28px, 5vw, 52px); text-align: center; }
.empty-state p {
  font: 500 11px/1 var(--mono); letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--dimmer); margin: 0;
}

/* ---- about -------------------------------------------------------- */
.about-grid { display: grid; gap: clamp(28px, 4vw, 60px); align-items: start; }
@media (min-width: 900px) { .about-grid { grid-template-columns: minmax(0, 1.25fr) minmax(0, 0.75fr); } }
.about-grid .prose { max-width: none; }
.about-grid .kv { margin-top: 32px; }
/*  With no portrait above it, the list starts level with the text. */
.about-grid .proof-list:first-child { margin-top: 0; }
.portrait {
  border: 1px solid var(--rule); aspect-ratio: 4 / 5; background: var(--bg-2);
  display: grid; place-items: center; text-align: center; padding: 24px; margin: 0;
}
.portrait img { width: 100%; height: 100%; object-fit: cover; }
.portrait-empty {
  font: 500 10.5px/1.7 var(--mono); letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--dimmer);
}
.portrait-cap { font: 400 11px/1.6 var(--mono); color: var(--dimmer); margin-top: 11px; }

/* ---- contact ------------------------------------------------------ */
.contact-alt { margin-top: 40px; border-top: 1px solid var(--rule-soft); padding-top: 26px; }
.contact-alt > p:first-child {
  font: 500 10.5px/1 var(--mono); letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--dimmer); margin: 0 0 14px;
}
.contact-mail {
  font-size: clamp(1rem, 2.4vw, 1.5rem); font-weight: 700; letter-spacing: -0.02em;
  border-bottom: 2px solid var(--rule); display: inline-block; padding-bottom: 4px;
  word-break: break-word; transition: border-color var(--fast) var(--ease);
}
.contact-mail:hover { border-color: var(--signal); }
.response {
  font: 400 11px/1 var(--mono); color: var(--dimmer);
  letter-spacing: 0.08em; text-transform: uppercase; margin-top: 16px;
}

/* ---- footer ------------------------------------------------------- */
.foot { border-top: 1px solid var(--rule-soft); padding-block: 44px 52px; }
.foot-in { display: grid; gap: 32px; }
@media (min-width: 820px) { .foot-in { grid-template-columns: 1.2fr 1fr 1.2fr; } }
.foot p { margin: 0 0 6px; font-size: 0.86rem; color: var(--dim); }
.foot-name { font-weight: 700; color: var(--fg); font-size: 0.95rem; }
.foot-mail { border-bottom: 1px solid var(--rule); }
.foot-mail:hover { border-color: var(--fg); }
.foot-nav { display: grid; gap: 9px; align-content: start; }
.foot-nav a {
  font: 500 11px/1 var(--mono); letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--dim); transition: color var(--fast) var(--ease);
}
.foot-nav a:hover { color: var(--fg); }
.foot-legal p { font: 400 11px/1.7 var(--mono); color: var(--dimmer); }

/* ---- ticker (one marquee per page, home only) --------------------- */
/* ---- strip: the findings ticker, on the accent --------------------
   The homepage's SECOND orange section, and deliberately a different
   shape from the band: a thin horizontal rule of moving text near the
   top against a tall static block near the bottom. Two of the same
   family stacked on one page would read as a template.

   The site's rule, so it stays a signal rather than wallpaper: one
   orange section per page, two on the homepage. Nothing else.

   Ink is --bg at 6.02:1 on the accent, which clears AA for the 12px
   mono. The dot before each item was --sev-high, an orange, which is
   invisible on orange.                                               */
.strip { background: var(--signal); }
.strip .ticker { border-block: 0; }
.strip .ticker-item { color: var(--bg); }
.strip .ticker-item::before { background: var(--bg); }

.ticker {
  border-block: 1px solid var(--rule-soft); padding-block: 15px; overflow: hidden;
  /*  The fade was a flat 7%, which is 100px at 1440 and reads correctly,
      but only 27px on a 390px phone. At 12px mono that is about three
      characters, so the marquee did not fade at the edge, it was cut:
      "...template placeholder text · D2C foo" with a hard vertical
      boundary through the middle of a word. A percentage fade on a
      full-bleed element is always wrong at one end of the range.

      Sized against the TYPE, not the container. The item text is 12px
      mono, so one character is ~7.2px wide; a fade has to cover five or
      six characters to read as a fade rather than a chop. 56px is eight
      characters, and it is a floor rather than the value, so wide
      screens still get the roomier 7%. A first pass used 30px, which was
      three pixels more than the 27px it replaced and changed nothing. */
  --ticker-fade: clamp(56px, 7%, 96px);
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 var(--ticker-fade),
                        #000 calc(100% - var(--ticker-fade)), transparent 100%);
          mask-image: linear-gradient(90deg, transparent 0, #000 var(--ticker-fade),
                        #000 calc(100% - var(--ticker-fade)), transparent 100%);
}
.ticker-track { display: flex; width: max-content; animation: ticker 64s linear infinite; }
.ticker:hover .ticker-track { animation-play-state: paused; }
.ticker-item {
  font: 400 12px/1 var(--mono); color: var(--dim); white-space: nowrap;
  padding-inline: 26px; display: inline-flex; align-items: center; gap: 11px;
}
.ticker-item::before { content: ""; width: 5px; height: 5px; flex: none; background: var(--sev-high); transform: rotate(45deg); }
@keyframes ticker { to { transform: translate3d(-50%, 0, 0); } }

/* ---- buttons ------------------------------------------------------ */
.btn {
  font: 600 12px/1 var(--mono); letter-spacing: 0.08em; text-transform: uppercase;
  padding: 15px 24px; border: 1px solid var(--fg); background: var(--fg); color: var(--bg);
  display: inline-block; cursor: pointer;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}
.btn:hover { background: transparent; color: var(--fg); }
.btn:active { transform: translateY(1px); }
.btn-ghost { background: transparent; color: var(--fg); border-color: var(--rule); }
.btn-ghost:hover { background: var(--fg); color: var(--bg); border-color: var(--fg); }

/* ---- form --------------------------------------------------------- */
.audit-form { display: grid; gap: 18px; border: 1px solid var(--rule); padding: clamp(20px, 3vw, 32px); }
@media (min-width: 680px) { .audit-form { grid-template-columns: 1fr 1fr; } }
.field { display: flex; flex-direction: column; gap: 8px; }
.field-wide { grid-column: 1 / -1; }
.field label {
  font: 500 10.5px/1.4 var(--mono); letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--dimmer);
}
.field input, .field textarea {
  font: 400 15px/1.5 var(--sans); background: var(--bg-2); color: var(--fg);
  border: 1px solid var(--rule); border-radius: 0; padding: 13px 14px; width: 100%;
  transition: border-color var(--fast) var(--ease);
}
.field input::placeholder, .field textarea::placeholder { color: var(--dimmer); }
.field input:focus, .field textarea:focus { outline: none; border-color: var(--signal); }
.field textarea { resize: vertical; min-height: 84px; }
/* Only after the field has been touched. Marking an untouched empty
   required field as an error is hostile. */
.field input:user-invalid { border-color: var(--sev-critical); }
.form-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 16px; }
.form-note { margin: 0; font: 400 11px/1.6 var(--mono); color: var(--dimmer); flex: 1 1 200px; }
.form-status { grid-column: 1 / -1; margin: 0; font: 500 12px/1.5 var(--mono); color: var(--sev-low); }
.form-status:empty { display: none; }
.form-status[data-error] { color: var(--sev-critical); }

/* =====================================================================
   HUMAN / MACHINE toggle. Bottom-right, not bottom-centre as on
   basement.studio: theirs floats over a 3D scene, this site is a prose
   column and a centred bar sat on top of body text.
   ===================================================================== */
.hm {
  position: fixed; z-index: 95;
  right: max(16px, env(safe-area-inset-right));
  bottom: max(16px, env(safe-area-inset-bottom));
  display: flex; border: 1px solid var(--rule);
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: blur(10px);
}
.hm a {
  font: 500 10px/1 var(--mono); letter-spacing: 0.14em; text-transform: uppercase;
  padding: 9px 15px; color: var(--dimmer); transition: color var(--fast) var(--ease);
}
.hm a:hover { color: var(--fg); }
.hm a[aria-current="true"] { color: var(--signal); }

/* =====================================================================
   THE POINTER  —  breeder.studio's, rebuilt.

   Theirs, measured off the live site: a 123px circle, rounded-full,
   pointer-events-none, mix-blend-mode: difference, positioned with
   `translate(Xpx, Ypx) translate(-50%, -50%)`, containing a starburst
   at 123x123. Only opacity transitions; the transform was identical
   across five sampled frames, so there is NO easing on position. It
   sits exactly on the pointer.

   WHICH WAY ROUND IT GOES, because the first build of this had it
   backwards. Sampling their starburst.gif pixel by pixel:

     outside the circle   transparent
     the disc body        rgb(229,231,228)   off-white
     the star             rgb(22,28,34)      near-black

   So the FILLED part is the disc and the star is the hole. Off-white
   differenced against any colour returns near-enough that colour's
   inverse, so the whole 123px disc shows the negative of what it
   crosses: near-black page becomes near-white, the orange wordmark
   becomes cyan. Near-black differenced against anything is close to a
   no-op, so the star reads as a window back onto the real page.

   The first version had it inside out, a white star on an empty disc,
   which inverts eight thin spokes instead of a circle. Same parts,
   nothing like the same effect.

   Three deliberate differences from theirs:

   1. The disc is the ACCENT, not their off-white. A grey disc on a
      site built around one signal colour read as a borrowed asset.
   2. The starburst is drawn here, not copied. Theirs is an asset on
      their server and not ours to take, and an SVG costs nothing,
      stays sharp and can be stopped for reduced motion.
   3. Desktop only, matching their `lg:block` at 1024px. Below that
      there is no cursor to replace and hiding the native one would
      strand people.

   No separate aiming dot, unlike the version this replaces. That one
   lagged, which made a dot necessary to aim with. This one does not
   lag, so the starburst's own centre IS the pointer position.
   ===================================================================== */

.cursor {
  position: fixed; top: 0; left: 0; z-index: 150;
  width: 123px; height: 123px;
  pointer-events: none;
  mix-blend-mode: difference;
  opacity: 0;
  transition: opacity 300ms var(--ease);
  will-change: transform;
}
.cursor.is-on { opacity: 1; }

/*  SHRINKS OVER ANYTHING CLICKABLE (Ram, 21 Sep 2026: the full disc sat
    on top of the very button being clicked). Over a link or button it
    drops to a ~30px dot, so the target stays visible and the aim point
    is obvious; pressing takes it a little smaller again. */
.cursor-disc {
  width: 100%; height: 100%;
  border-radius: 50%;
  overflow: hidden;
  transition: transform 250ms cubic-bezier(0.4, 0, 0.2, 1);
  /*  The disc is filled. This is the half that makes the blend read as
      a circle rather than as a star.

      Theirs is off-white, #E5E7E4, and that is what this was. Ours is
      the accent instead: a grey disc on a site with one strong signal
      colour just looked like a borrowed asset. See the note under the
      keyframes about what this does over the band. */
  background: var(--signal);
}
body.pointer-link .cursor-disc { transform: scale(0.24); }
body.pointer-down .cursor-disc { transform: scale(0.18); }
body.pointer-link.pointer-down .cursor-disc { transform: scale(0.16); }
/*  Over a text field: no disc at all, and the normal I-beam is back. */
body.pointer-field .cursor { opacity: 0; }
body.has-pointer.pointer-field input,
body.has-pointer.pointer-field textarea,
body.has-pointer.pointer-field select { cursor: text; }
body.has-pointer.pointer-field select { cursor: pointer; }

/*  Near-black, so it barely shifts what is under it: the star is a hole
    in the disc, not a shape drawn on top of it. */
.cursor svg { width: 100%; height: 100%; display: block; fill: #161C22; }

/*  THE TWINKLE. Their GIF does not rotate; watched frame by frame it
    swaps between star shapes, sparse to dense. So four stars are drawn
    into the SVG and exactly one is shown at a time.

    1.8s over four frames is 450ms each: slow enough to register as a
    shape rather than a flicker, fast enough to be moving whenever the
    pointer is still. Opacity flips inside 0.1% of the cycle, which is
    16ms, so it reads as a hard cut and not a cross-fade.

    The first frame also carries a static opacity, so the star is still
    there if the animation never runs. */
.cursor svg path { opacity: 0; }
.cursor svg path:first-child { opacity: 1; }

@media (prefers-reduced-motion: no-preference) {
  .cursor svg path { animation: cursor-burst 1.8s linear infinite; }
  .cursor svg path:nth-child(2) { animation-delay: 0.45s; }
  .cursor svg path:nth-child(3) { animation-delay: 0.90s; }
  .cursor svg path:nth-child(4) { animation-delay: 1.35s; }
}
@keyframes cursor-burst {
  0%, 24.9% { opacity: 1; }
  25%, 100% { opacity: 0; }
}

/*  WHAT THE ACCENT DISC DOES OVER THE ORANGE BAND, because it is worth
    knowing before it surprises someone.

    Difference blend subtracts, so a colour crossing ITSELF returns
    black. Over the near-black page the disc reads as the accent, which
    is the intent. Over the four orange bands it inverts to a black disc
    with an orange star, because the disc and the band are the same
    colour. Still legible, still deliberate-looking, but it is the one
    place on the site where the pointer is not orange.

    Two ways out if that ever reads wrong: drop mix-blend-mode, which
    makes the disc orange everywhere and loses the inversion that is the
    whole character of the effect; or give .band a scoped override that
    swaps the disc to --fg. Neither is done here because the inversion
    over the accent looks intentional. */

/* ---- the hover label ----------------------------------------------
   Theirs swaps the starburst for a word when the pointer is over a
   link: a full-circle off-white bubble, black uppercase, scaling from
   0 to 1 over 300ms. Same curve as the wordmark handoff, so the whole
   site moves on one clock.

   It sits inside the difference blend like everything else here, so
   what you actually see is the bubble's inverse with the word punched
   through it. That is what theirs does too.

   18px, not their 20px: theirs is a narrow face, ours is a monospace,
   and every word has to fit one line inside 91px of usable width.
   Measured at 18px the widest (VISIT, EMAIL, SHEET) is 59px, so there
   is room, but not room for a sixth letter. */
/*  Since 21 Sep 2026 the word is a small tag BESIDE the dot, not a
    full bubble over the link: the bubble covered the thing being
    clicked. It grows out from the dot's corner. */
.cursor-label {
  position: absolute;
  left: calc(50% + 20px); top: calc(50% + 14px);
  padding: 4px 8px;
  border-radius: 999px;
  background: var(--signal);
  color: #161C22;
  font: 700 11px/1.2 var(--mono);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  white-space: nowrap;
  transform: scale(0);
  transform-origin: 0 0;
  transition: transform 250ms cubic-bezier(0.4, 0, 0.2, 1);
}
body.pointer-link .cursor-label { transform: scale(1); }

/*  Below 1024px the element is never mounted (see v2.js), so the native
    cursor is untouched. This is belt and braces for a desktop window
    dragged narrow after load. */
@media (max-width: 1023px) { .cursor { display: none; } }

/*  Every selector the hover detection in v2.js matches has to appear
    here too, or the native cursor comes back over the elements the
    replacement cares most about. It listed `a` and `button` only, so
    the I-beam was still showing over the contact form's fields. */
body.has-pointer,
body.has-pointer a,
body.has-pointer button,
body.has-pointer [role="button"],
body.has-pointer summary,
body.has-pointer label,
body.has-pointer input,
body.has-pointer textarea { cursor: none; }

/* =====================================================================
   Reveals. Scoped to .js so a visitor whose JavaScript failed still sees
   the whole page.
   ===================================================================== */
.js [data-reveal],
.js [data-reveal-child] {
  opacity: 0; transform: translate3d(0, 16px, 0);
  transition: opacity var(--slow) var(--ease), transform var(--slow) var(--ease);
  transition-delay: var(--delay, 0ms);
}
.js [data-reveal].is-in,
.js [data-reveal-child].is-in { opacity: 1; transform: none; }

/*  [data-reveal-child] was in the markup and in the JS since the rebuild,
    but it had no rule here, so the stagger set --delay and .is-in on
    eight case cards and nothing read either. Every block faded as one
    slab. The selector above is the whole fix.

    .is-group is a [data-reveal] that CONTAINS children. It is a grouping
    element, not an animating one: its children carry the motion, so it
    must never start at opacity 0. Without this, a 3,938px .cases wrapper
    gates all eight cards behind a single intersection test on itself,
    which is exactly the bug that left /work blank on arrival.

    .is-instant is anything already on screen at load. It appears with no
    transition at all. Content above the fold should be there when the
    page is there, not arrive afterwards. */
.js [data-reveal].is-group { opacity: 1; transform: none; transition: none; }
.js [data-reveal].is-instant,
.js [data-reveal-child].is-instant { transition: none; }

/* =====================================================================
   Machine view
   ===================================================================== */
.machine { background: #000; color: var(--signal); }
.machine a { color: var(--signal); }
.machine .wrap { max-width: 1040px; padding-block: 34px 90px; }
.machine pre, .machine div, .machine p, .machine li {
  font-family: var(--mono); font-size: clamp(10px, 1.55vw, 12.5px); line-height: 1.75;
}
.ascii {
  font-family: var(--mono); white-space: pre; overflow-x: auto;
  font-size: clamp(6px, 1.75vw, 13px); line-height: 1.02;
  margin: 0 0 22px; color: var(--signal);
}
.m-nav { display: flex; flex-wrap: wrap; gap: 14px; margin-bottom: 26px; }
.m-nav a { text-transform: uppercase; letter-spacing: 0.06em; }
.m-nav a:hover { background: var(--signal); color: #000; }
.m-rule { color: color-mix(in srgb, var(--signal) 55%, #000); margin-block: 22px; white-space: pre; overflow: hidden; }
.m-row { display: grid; grid-template-columns: minmax(96px, 168px) 1fr; gap: 10px; }
.m-row + .m-row { margin-top: 3px; }
.m-k { color: color-mix(in srgb, var(--signal) 68%, #000); white-space: pre; }
.machine p { margin: 0 0 13px; max-width: 92ch; }
.m-list { list-style: none; padding: 0; margin: 0; }
.m-list li { margin-bottom: 4px; }
.m-list li::before { content: "* "; }

/* =====================================================================
   Reduced motion. Everything that moves stops. The ticker becomes a
   static wrapping list rather than disappearing, because its content is
   real information.
   ===================================================================== */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important; animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important; scroll-behavior: auto !important;
  }
  .js [data-reveal], .js [data-reveal-child] { opacity: 1; transform: none; }
  .ticker { -webkit-mask-image: none; mask-image: none; }
  .ticker-track { animation: none; flex-wrap: wrap; width: 100%; }
  .ticker-item { padding: 4px 18px 4px 0; white-space: normal; }
  .ticker-dup { display: none; }
  /*  Named .cursor-ring/.cursor-dot until now, which were the classes of
      a cursor implementation two versions ago. Neither element has
      existed for a while, so this rule was quietly doing nothing and a
      123px spinning disc was still following people who asked for less
      motion. Unspinning it is not enough; it goes. */
  .cursor { display: none; }
  .split-head { position: static; }
}

@media print {
  .topbar, .hm, .ticker, #progress, .cursor, .band { display: none !important; }
  body { background: #fff; color: #000; }
  .js [data-reveal], .js [data-reveal-child] { opacity: 1 !important; transform: none !important; }
}

/* ---- mobile corrections (found in the Browser pane, 20 Sep 2026) --- */

/*  The tab strip scrolls horizontally on narrow screens, but without
    nowrap the labels broke across two lines ("AI / SEARCH") and the last
    item was clipped mid-word. */
.tab { white-space: nowrap; }

/*  Below the two-column card breakpoint the 116px thumbnail column
    becomes full width, so a 774x1095 portrait render filled most of the
    screen before any text. Crop it to a banner instead: still a real
    artefact, no longer the whole viewport. */
/*  The card was a two-column grid because it carried a rendered PDF
    thumbnail. The thumbnail is gone, so the column must go with it:
    leaving it in reserved 116px of nothing beside every case and
    squeezed the text into a ribbon. Single column now. */

/*  Same problem on a case page, where the report render sits beside the
    title at desktop and above it on mobile. */

/* ------------------------------------------------------------ teardowns
   Added 21 Sep 2026 for /teardowns/<slug>. Built only from existing
   tokens so a restyle of v2.css carries through. The blog loads this
   file by absolute URL (design coupling), so these are additions only:
   nothing existing was renamed. */

/* A verbatim quote from Google's documentation, with its source. */
.src-quote {
  margin: 1.2em 0; padding: 14px 0 14px 18px;
  border-left: 2px solid var(--signal);
}
.src-quote p { color: var(--fg); margin: 0 0 8px; }
.src-quote cite {
  display: block; font: 400 11px/1.5 var(--mono); font-style: normal;
  color: var(--dimmer); letter-spacing: 0.02em;
}
.src-quote cite a { color: var(--dimmer); text-decoration: underline; text-underline-offset: 3px; }
.src-quote cite a:hover { color: var(--signal); }

/* Numbered steps a reader follows, and questions they carry to a developer. */
.steps, .asks { max-width: var(--prose); color: var(--dim); margin: 0; padding-left: 1.3em; }
.steps li, .asks li { margin: 0 0 0.9em; padding-left: 0.3em; }
.steps li:last-child, .asks li:last-child { margin-bottom: 0; }
.steps li::marker { font: 500 12px/1 var(--mono); color: var(--signal); }
.asks li::marker { color: var(--signal); }

.inline-link { color: var(--fg); text-decoration: underline; text-underline-offset: 3px; }
.inline-link:hover { color: var(--signal); }

.td-note { margin: 18px 0 0; font: 400 12px/1.7 var(--mono); color: var(--dimmer); }
