/* ============================================================================
   app.css — layout for the hub. Reads everything from tokens.css.
   ============================================================================ */
*,*::before,*::after{ box-sizing:border-box; margin:0; padding:0 }
html{ -webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility }
/* `min-height:100dvh` + a flex column is what keeps the decorative motif from stopping
   dead above the fold on a SHORT page (2026-09-15). `.spine` is `inset:0` on this box, so
   on /work/ and /perspectives/ — where the content ran out around 1100px — the body box
   ended there too, the art was clipped on a hard horizontal line, and the rest of the
   window was bare white. Nothing decorative renders below the footer now because the
   footer IS the last thing on the page: `<main>` takes the slack (`flex:1`) and pushes it
   down. `dvh` rather than `vh` so a mobile browser's collapsing toolbar doesn't leave a
   strip of white at the bottom of the first screen. */
body{ background:var(--ground); color:var(--ink); font-family:var(--sans);
      overflow-x:hidden; position:relative;
      min-height:100dvh; display:flex; flex-direction:column }
body > main{ flex:1 0 auto; min-width:0 }

/* Skip to content — the first focusable element on every page, off-screen until focused.
   Four nav links sit between the top of the document and the reading copy, and a keyboard
   visitor was tabbing through all four on every page. `transform` rather than
   `display:none` or `visibility:hidden`, because both of those take it out of the tab
   order, which is the one thing it exists to be in. */
.skip{ position:fixed; top:0; left:0; z-index:20; transform:translateY(-110%);
       background:var(--ink); color:var(--bg); text-decoration:none;
       padding:12px 18px; font-size:var(--micro); letter-spacing:var(--track-label);
       text-transform:uppercase; font-weight:var(--w-med) }
.skip:focus{ transform:none }

/* Visually hidden, still announced. For a heading a section needs structurally but does
   not print — About's bio block, whose left band is the photo. NOT `display:none`, which
   removes it from the accessibility tree along with everything else. */
.vh{ font:inherit; text-transform:uppercase; letter-spacing:var(--track-label); position:absolute; width:1px; height:1px; margin:-1px; padding:0; border:0;
     overflow:hidden; clip-path:inset(50%); white-space:nowrap }

/* The river — full-bleed background, top-anchored, clipped at the bottom.
   Swap assets/brand/river.svg to change the form.

   THIS IS A DIV WITH A BACKGROUND, NOT AN <img> — deliberately, after both ways of sizing an
   img here failed (2026-08-13). An img is a REPLACED element with an intrinsic ratio, and:
     · `height:auto` → CSS takes the INTRINSIC height and ignores `bottom:0`. The motifs are
       portrait (river-2/-3 are 1910×2660), so at 1440px the img resolved to 2005px tall and
       PROPPED THE DOCUMENT OPEN — Perspectives held 989px of content in a 2005px scroll,
       ~955px of it empty below the footer; About, 258px.
     · `height:100%` with `width:auto` → the ratio then drove the WIDTH instead: 1254px of art
       in a 1440px viewport, anchored left, bare ground down the right-hand side. That is the
       "cut off on the right" Kurt reported, and it shipped for one deploy.
   A div has no intrinsic ratio to fight: `inset:0` gives it exactly the body box, and
   `background-size:100% auto` pins the art to the FULL WIDTH at natural proportions, drawn
   from the top and clipped wherever the page ends. Sides can never crop. And a background
   contributes nothing to layout, so the dead scroll cannot return.
   Don't turn this back into an <img>, and don't switch to `cover` — cover crops the SIDES
   whenever the page is taller than the art's 0.718 ratio, which is most short pages. */
/* `repeat-y`, added 2026-08-13: at `background-size:100% auto` the art is only as tall as its
   own ratio allows — 522px on a 375px phone — so on a 2800px mobile page it ran out near the
   top and left the rest bare ground. Tiling down fixes exactly that case and is a NO-OP
   everywhere it already covers: at 1440px the art draws 2005px against a 1747px page, so the
   second tile never appears. Do not "tidy" this back to no-repeat. */
.spine{ position:absolute; top:0; right:0; bottom:0; left:0; display:block;
        background-repeat:repeat-y; background-position:50% 0; background-size:100% auto;
        z-index:0; pointer-events:none }
.content{ position:relative; z-index:1 }

/* Primitives — tracking kept tight (Satoshi's own metrics, not spread out) */
.label{ font-size:var(--micro); letter-spacing:var(--track-label); text-transform:uppercase; color:var(--ink); font-weight:var(--w-med) }
.rule{ border:0; border-top:1px solid var(--rule) }
.pad{ padding-left:var(--margin); padding-right:var(--margin) }

/* Nav — ONE tier (2026-08-08): About · Work · Perspective. The old contextual second row
   (.subbar/.subnav) is gone with the per-project and per-POV pages it navigated. */
nav{ display:flex; gap:24px }
nav a{ font-size:var(--label); text-transform:uppercase; color:var(--ink); text-decoration:none; font-weight:var(--w-med); letter-spacing:var(--track-label) }
nav a[aria-current]{ font-weight:var(--w-black) }   /* the current page lights up bold */

/* ============================================================================
   FOCUS — one ring, every control (2026-09-15).

   `:focus-visible`, never `:focus`, so a mouse user never sees a ring they didn't
   ask for and a keyboard user always does. This is the fix for it, NOT the old
   trick of suppressing the outline and hoping a colour change reads as focus: the
   form inputs carried `outline:0` with only a 1.2px→2.4px border change behind it,
   which is not a focus state anyone can see. Scattered per-component
   `:focus-visible` rules further down (.pill, .proj > summary, .disclose > summary,
   .roster > summary, .vcell) predate this and now just repeat it with a different
   offset; they're left alone because each was tuned to its own shape.

   2px of --ink on white is ~18:1 — far past the 3:1 non-text contrast floor.
   ========================================================================== */
:is(a, button, summary, input, textarea, select, [tabindex]):focus-visible{
  outline:2px solid var(--ink); outline-offset:2px;
}

/* Hero */
.hero{ position:relative; padding-top:clamp(2.6rem,6vw,5rem); padding-bottom:clamp(1.8rem,3.5vw,3rem) }
.hero .eyebrow{ margin-bottom:clamp(1.2rem,2.4vw,2rem) }
/* SENTENCE CASE, sitewide at title tier (2026-09-15, Kurt). Caps came off the page
   headers, the project rows and the case-study titles in one pass; the small tracked
   LABELS — eyebrows, tags, meta, nav, stamps, the GATE chip — are untouched and stay
   uppercase, which is what the Visual language section means by "uppercase labels".
   **Two caps-tuned values have to travel with every transform that comes off**, and
   getting this wrong is what made the home hero look broken before it was fixed:
     · **Leading.** `.9` is safe for caps because caps have NO DESCENDERS. In lowercase
       the `g` and `p` run into the x-height of the line beneath. → 1.06 here.
     · **Tracking.** `-.035em` is right for caps, which carry more sidebearing than
       lowercase and need some taken back at display size. The same value JAMS lowercase.
       Lowercase wants roughly half the negative. → -.015em here.
   The markdown already carries the right casing, so nothing in content/ changed: titles
   that are sentence case set as sentence case, and a real title like "Faster Alone,
   Further Together" keeps its title case instead of being flattened to caps. */
.hero h1{ font-size:var(--display); line-height:1.06; font-weight:var(--w-black);
          letter-spacing:var(--track-tight) }   /* wraps to fit — never clipped */
/* HOME ONLY, and now it is one property. The home hero is a full SENTENCE where the
   other three are short phrases, and ~115 characters at `--display` runs about eight
   lines. `--display-sm` takes ~30% off and holds it to four at 1440 and five at 390.
   (It was first cut for a different reason — the hero was Kurt's NAME and the bar said
   it twice — which stopped being true when the headline became a claim.)
   The transform, leading and tracking used to sit here too; they are on `.hero h1` now,
   because every hero is sentence case. */
body.home .hero h1{ font-size:var(--display-sm) }
.hero h1 .em{ font-weight:var(--w-thin) }   /* emphasis by weight contrast, not colour */
.hero .dek{ margin-top:clamp(1.3rem,2.6vw,2rem); max-width:54ch; font-family:var(--sans);
            font-weight:var(--w-thin); font-size:var(--dek); line-height:1.4; color:var(--ink) }

/* Work index */
.index{ padding-top:clamp(2rem,4vw,3.2rem); padding-bottom:clamp(2rem,4vw,3.2rem) }
.index .head{ display:flex; justify-content:space-between; align-items:baseline; margin-bottom:1.4rem }
.index .head .label{ font-size:var(--label); font-weight:var(--w-black) }   /* match About/Contact + nav weight */

/* Footer. It sits OUTSIDE `<main>` now (shell() emits it, 2026-09-15), so it no longer
   inherits `.content`'s max-width or its `z-index:1` — and without that z-index the
   `.spine`, which is a positioned element at `z-index:0`, paints over the stamps. */
.foot{ max-width:var(--maxw); margin-inline:auto; width:100%;
       position:relative; z-index:1 }
footer{ display:flex; justify-content:space-between; align-items:center; height:80px }
footer .stamp{ font-size:var(--micro); text-transform:uppercase; color:var(--ink); font-weight:var(--w-med); letter-spacing:var(--track-label) }

/* ============================================================================
   GRID + SHELL — every page sits on this. The river stays full-bleed behind.
   ============================================================================ */
.content{ max-width:var(--maxw); margin-inline:auto }

/* Sticky nav — slim bar, greige ground so the river doesn't bleed through */
.topbar{ position:sticky; top:0; z-index:5; background:var(--ground);
         border-bottom:1px solid var(--rule) }
.topbar .bar{ display:flex; align-items:center; justify-content:space-between;
              height:60px; max-width:var(--maxw); margin-inline:auto }
/* `nowrap` because a wordmark that breaks across two lines reads as a bug, and the bar is a
   fixed 60px — so the second line simply overflowed it. Four nav links squeezed the flex row
   enough to trigger that on EVERY phone width from 320 to 430px (found 2026-08-13; the
   earlier "no horizontal overflow" check missed it precisely because the wrap was absorbing
   the pressure instead of pushing the page wide). */
.topbar .name{ font-size:var(--label); font-weight:var(--w-black); letter-spacing:var(--track-label); text-transform:uppercase;
               text-decoration:none; color:var(--ink); white-space:nowrap;
               transition:opacity .22s ease, visibility .22s }
/* The wordmark steps aside while the home hero — which is the SAME NAME at display size —
   is on screen, and comes back the moment it scrolls away (src/motion.js toggles
   `.is-bare` from an IntersectionObserver on `body.home .hero h1`).
   Three things make this safe rather than clever:
   · it is gated on `html.js`, so with JS off the name is simply always there;
   · `visibility:hidden` rides along with the opacity, so the faded wordmark is not a
     focusable, clickable invisible target sitting over the bar;
   · nothing about the bar's LAYOUT changes — the link keeps its box, so the nav does
     not shift sideways as it comes and goes. */
html.js .topbar.is-bare .name{ opacity:0; visibility:hidden }
/* FOUR nav items don't fit a phone at the desktop gap. Adding Values (2026-08-12) pushed the
   bar 50px past a 375px viewport and 11px past 414px, which gave the whole SITE a horizontal
   scroll — a one-tier bar has no room to grow for free, and three items happened to fit
   exactly. Tightening the gap and the link size buys ~62px and keeps ONE row: letting the bar
   wrap would trade a sideways scroll for extra height on every page, which is backwards.
   The wordmark is deliberately untouched. Re-check this the next time a tab is added. */
@media (max-width:520px){
  nav{ gap:12px }
  nav a{ font-size:var(--micro) }
}
/* Below ~430px the bar has to give up more, because `white-space:nowrap` above means the
   wordmark can no longer absorb the squeeze by wrapping. Everything here buys horizontal
   room: a tighter bar gutter (the bar only — `--margin` still governs the content columns),
   a smaller wordmark, and closer links.
   **THE LINK SIZE IS NO LONGER ONE OF THE THINGS THAT GIVES (2026-09-15.)** It used to
   step 13 → 11.5 → 10.5 → 9.5px down this ladder, which is how the nav ended up as the
   smallest type on the site on the smallest screens — exactly backwards. It holds at
   `--micro` (12px) from 430px down, and the gap and the gutter absorb the squeeze instead.
   Measured: one line, zero page overflow at 360 / 375 / 390 / 414 / 430. */
@media (max-width:430px){
  .topbar .bar{ padding-left:14px; padding-right:14px }
  .topbar .name{ font-size:var(--micro) }
  nav{ gap:8px }
}
/* The 320px floor (original iPhone SE). With the links held at 12px there is no squeeze
   left below ~350px: the gap is already at 6px and the gutter at 10px, and the bar is
   still ~20px over at 320. So the WORDMARK gives instead, and it is the right thing to
   give — it is a link to the home page, and the "About" tab two inches to its right goes
   to the same URL. Nothing becomes unreachable; the bar loses a duplicate.
   `.name` is only hidden here, never removed, so it returns the moment there is room. */
@media (max-width:374px){
  .topbar .bar{ padding-left:10px; padding-right:10px }
  .topbar .name{ display:none }
  nav{ gap:6px; width:100%; justify-content:space-between }
}

/* The strict 12-column grid — every narrative section rides it.
   Label in cols 1–3 (margin band), body in cols 4–12. */
/* vertical padding only (longhand) — must NOT reset the horizontal padding that
   the paired .pad supplies, or the label band bleeds to the viewport edge */
.sec{ padding-top:clamp(1.9rem,4vw,3.2rem); padding-bottom:clamp(1.9rem,4vw,3.2rem) }
.g12{ display:grid; grid-template-columns:repeat(var(--cols),minmax(0,1fr));
      column-gap:var(--gutter); align-items:start }
.col-label{ grid-column:1 / 4; position:sticky; top:76px; font-size:var(--label); font-weight:var(--w-black) }
.col-body{ grid-column:4 / 13; min-width:0 }
.col-body > * + *{ margin-top:1rem }
.col-body p{ font-family:var(--sans); font-weight:var(--w-light); font-size:var(--body);
             line-height:1.42; color:var(--ink); max-width:var(--measure) }
.col-body p strong{ font-weight:var(--w-black); color:var(--ink) }
/* Prose that fills the full body width (cols 4–12) as two columns — keeps a readable
   measure while the right edge lands on the grid's col-12 line (not floating at ~col 9). */
.col-body.cols2{ column-count:2; column-gap:var(--gutter) }
/* No break-inside:avoid here (removed 2026-08-09). With it, the browser can only pack WHOLE
   paragraphs into columns; the About runs six paragraphs from ~48px to ~265px tall, and no
   whole-paragraph split balances them — it left col 2 ~145px longer than col 1. Letting a
   paragraph flow across the column break is what makes column-fill:balance work, and it is
   the idiomatic setting for editorial multi-column text. orphans/widows stop 1-line stragglers. */
.col-body.cols2 p{ max-width:none; margin:0 0 .95rem; orphans:2; widows:2 }
.col-body.cols2 > * + *{ margin-top:0 }
@media (max-width:760px){ .col-body.cols2{ column-count:1 } }

/* About — TWO SECTIONS, TWO COLUMNS (2026-08-11). Not `.cols2`: that is column-count,
   which flows ONE pool of text down col 1 and into col 2. Canon now holds the About as two
   distinct claims — `## The pitch` (the argument) and `## Bio paragraph` (the person) — and
   each has to stay whole in its own track, so this is a 2-track grid instead.
   The second column carries a hairline rule to mark it as a different KIND of claim: the
   design system allows thin lines and rectangular blocks, and rules are how this site
   organises a page. Deliberately NOT italic — italic body copy over ~150 words is a
   readability cost, and this column runs to four paragraphs. */
.col-body.about-two{ display:grid; grid-template-columns:1fr 1fr; column-gap:var(--gutter);
                     align-items:start }
.col-body.about-two > * + *{ margin-top:0 }
.col-body.about-two p{ max-width:none; orphans:2; widows:2 }
.col-body.about-two p + p{ margin-top:.95rem }
/* col 2: the person. Hairline box, ink-on-white, square corners (no rounded cards). */
.about-cb{ border:1px solid var(--line); padding:clamp(1rem,1.6vw,1.35rem) }
.about-cb p{ font-size:var(--body); line-height:1.44 }
@media (max-width:760px){
  .col-body.about-two{ grid-template-columns:1fr; row-gap:1.6rem }
}

/* Inline diagram inside an article row. Sits at the reading measure like the prose it
   argues with, not full-bleed — it is a figure in an essay, not a hero. */
.dgm{ margin:1.9rem 0 .4rem; max-width:var(--measure) }
.dgm svg{ display:block; width:100%; height:auto; color:var(--ink) }

/* About — profile photo placeholder in the left band (cols 1–3), under the label */
.about-col{ position:static }
.about-col .label{ font-size:var(--label); font-weight:var(--w-black) }   /* match the other left-rail headers */
/* The frame matches the PLATE, never the other way round (Kurt, 2026-08-08) — the portrait is a
   drawn plate carrying its own ground and keyline, so any mask that isn't its own ratio cuts the
   artwork's frame off. It was square at 700x700, tall at 704x1486 for the hedcut, and is square
   again since 2026-08-09 (Kurt's photographic portrait, full frame, uncropped).
   Swap the plate → set --plate-ar to its ratio. */
/* The plate is 342px (2026-08-09, +80%), which does NOT fit the standard 3-col label band —
   that band tops out at 274px even at --maxw. So About, and only About, runs a 4-col band;
   its body drops to 8 cols and still sets two comfortable columns. Widen the plate further
   and this split has to move with it, or max-width silently stops doing anything. */
/* THE PORTRAIT IS ITS OWN ROW (Kurt, 2026-09-15: "its own line, so no text to the right").
   It used to sit under the section's heading in the left band, beside the prose. It is now
   a second `.g12` row inside the same section — below the copy, above the rule that opens
   Core Competencies — so nothing sets alongside it.
   It keeps the left band's columns (1/5) and its 342px cap, so the plate does not move
   horizontally or change size; only the row it lives on is new. Below the grid breakpoint
   the columns collapse and it simply stacks, as it did before. */
.about-plate{ margin-bottom:clamp(1.6rem,3.2vw,2.6rem) }
.about-plate .about-photo{ grid-column:1 / 5 }
@media (max-width:760px){ .about-plate .about-photo{ grid-column:1 / -1 } }
#about .col-label{ grid-column:1 / 5 }
/* NOT ASKED FOR, and easy to revert: Kurt moved Core Competencies and Contact onto the
   margin, which left the bio as the only block on the page still indented into columns
   5–13 — one block out of line reads as an oversight rather than a choice. It spans the
   full row now and takes `--measure` instead, so the reading width is set by the copy
   rather than by a column it no longer shares with anything. */
#about .col-body{ grid-column:1 / -1; max-width:var(--measure) }
/* 6px, and that number is the whole argument (Kurt, 2026-09-15, radius left to me).
   The shape vocabulary is thin lines, rectangular blocks and circles, so a radius here is
   a deliberate exception and the size is what keeps it from becoming a second rule: at 6px
   on a 342px plate it reads as a CROP — the corner is eased, the block is still a block.
   Anything from ~12px up starts reading as a rounded card, which is the SaaS shape
   CLAUDE.md rules out, and it would then want to be repeated on the work-card thumbnails
   and the slide stage. It isn't; the photo is the only rounded thing on the site. */
.about-photo{ display:block; aspect-ratio:var(--plate-ar, 1/1); background:transparent;
              margin-top:0; width:100%; max-width:228px; overflow:hidden; position:relative;
              border-radius:var(--radius) }
.about-photo img{ position:absolute; inset:0; width:100%; height:100%; object-fit:cover; display:block }
.about-photo .ph{ position:absolute; inset:0; display:grid; place-items:center; font-size:var(--micro);
                  letter-spacing:var(--track-label); text-transform:uppercase; color:#fff; opacity:.38 }
@media (max-width:760px){
  .g12{ grid-template-columns:1fr; row-gap:1rem }
  .col-label{ grid-column:1; position:static }
  .col-body{ grid-column:1 }
}

/* Hero CTA */
/* White text on a solid midnight rectangle — the subtitle tag (host eyebrow) */
.tag{ display:inline-block; background:var(--ink); color:#fff; font-size:var(--micro);
      text-transform:uppercase; font-weight:var(--w-med); padding:5px 10px; letter-spacing:var(--track-label) }

/* How-I-lead "moves" + values list — a grid; .two-col splits to two columns on wide screens */
.moves{ list-style:none; display:grid; row-gap:1.3rem }
.moves.two-col{ grid-template-columns:repeat(2,minmax(0,1fr)); column-gap:var(--gutter);
                row-gap:clamp(1.5rem,2.4vw,2.1rem) }
@media (max-width:760px){ .moves.two-col{ grid-template-columns:1fr; row-gap:1.3rem } }
.moves .mv-t{ display:block; font-family:var(--sans); font-weight:var(--w-black); font-size:var(--body); color:var(--ink); margin-bottom:.28rem }
.moves .mv-b{ font-family:var(--sans); font-weight:var(--w-light); font-size:var(--body); line-height:1.42; color:var(--ink) }
/* AI-first operating model — 16:9 slide placeholder (Kurt drops his own diagram here) */
.aiom-slide{ aspect-ratio:16/9; background:var(--ink); position:relative; margin:.4rem 0 2.4rem }
.aiom-slide .ph{ position:absolute; inset:0; display:grid; place-items:center; font-size:var(--micro);
                 letter-spacing:var(--track-label); text-transform:uppercase; color:#fff; opacity:.38 }

/* "The model" — numbered list spanning the full body width (number · title · body per row) */
.model-list{ list-style:none }
.model-list li{ display:grid; grid-template-columns:auto minmax(10ch,0.8fr) minmax(0,1.25fr);
                gap:clamp(1rem,2.4vw,2.4rem); align-items:baseline;
                padding:clamp(1.2rem,2.2vw,1.7rem) 0; border-top:1px solid var(--line) }
.model-n{ font-weight:var(--w-black); font-size:var(--head); line-height:1;
          letter-spacing:var(--track-tight); font-variant-numeric:tabular-nums; color:var(--ink) }
.model-t{ display:block; font-weight:var(--w-black); font-size:var(--body);
          line-height:1.22; color:var(--ink) }
.model-b{ display:block; font-weight:var(--w-light); font-size:var(--body); line-height:1.45; color:var(--ink) }
@media (max-width:760px){
  .model-list li{ grid-template-columns:auto 1fr; row-gap:.5rem }
  .model-b{ grid-column:1 / -1 } }

/* Contact — email + LinkedIn + CV with monochrome icons */
.contact-list{ list-style:none; display:grid; gap:1rem }
.col-body a.contact-link{ display:inline-flex; align-items:center; gap:.65em; width:fit-content;
  font-size:var(--body); font-weight:var(--w-light); color:var(--ink); text-decoration:none }
.contact-link .ic{ width:22px; height:22px; flex:0 0 auto }

/* A single dark "title" block on an otherwise-light page (AI-First Operating Model).
   Redefines the leaf tokens within the section so eyebrow/headline/dek go white.
   Full-bleed: the dark band spans the whole viewport (breaks out of the centered
   .content column) while its text stays aligned to the page grid via the padding calc. */
.hero-invert{ background:#0F1124; color:#FFFFFF;
  --ink:#FFFFFF; --bg:#0F1124; --muted:#FFFFFF; --accent:#FFFFFF; --line:rgba(255,255,255,.20);
  width:100vw; margin-left:calc(50% - 50vw);
  padding-left:max(var(--margin), calc((100vw - var(--maxw)) / 2 + var(--margin)));
  padding-right:max(var(--margin), calc((100vw - var(--maxw)) / 2 + var(--margin)));
  padding-bottom:clamp(2.4rem,4.5vw,3.6rem) }

/* CV page — same grid, denser typographic blocks */
.cv-contact{ margin-top:1rem; font-size:var(--label); letter-spacing:var(--track-label); text-transform:uppercase;
             font-weight:var(--w-med); color:var(--ink) }
.cv-body > * + *{ margin-top:1rem }
.cv-role{ font-size:var(--title); font-weight:var(--w-black); letter-spacing:var(--track-tight); margin-top:2.2rem }
.cv-body > .cv-role:first-child{ margin-top:0 }
.cv-roleline{ margin-top:.35rem !important; font-weight:var(--w-med); font-size:var(--body); color:var(--ink) }
.cv-roleline strong{ font-weight:var(--w-black) }
.cv-sub{ margin-top:1.4rem; font-size:var(--micro); letter-spacing:var(--track-label); text-transform:uppercase;
         font-weight:var(--w-med); color:var(--ink) }
.cv-list{ list-style:none; margin-top:.7rem }
.cv-list li{ position:relative; padding-left:1.2em; margin-top:.5rem; font-weight:var(--w-light);
             font-size:var(--body); line-height:1.45; max-width:var(--measure) }
.cv-list li::before{ content:"—"; position:absolute; left:0; color:var(--ink) }
.cv-list li strong{ font-weight:var(--w-bold) }
/* Inline emphasis in CV running text: Bold, matching the printable CV (cv/print.html).
   Scoped to .cv-body so other pages keep their existing Black emphasis. */
.cv-body p strong{ font-weight:var(--w-bold) }
.values .mv-t{ font-size:var(--body) }
/* Inline content links (e.g. the "Leading AI Transformation" → AI Operating Model link) */
.col-body a, .host .ctx a{ color:var(--ink); text-decoration:underline; text-decoration-thickness:1px;
                           text-underline-offset:3px; text-decoration-color:var(--line) }
/* Hover affordance — clickable TEXT gets a black-highlight / white-type marker.
   The box-shadow gives the highlight breathing room without shifting layout. On
   inverted pages --ink/--bg swap, so it flips (white highlight / ink text). */
nav a:hover, .topbar .name:hover,
.col-body a:hover, .host .ctx a:hover,
.work-card:hover .card-ttl{
  background:var(--ink); color:var(--bg); text-decoration:none;
  box-shadow:.14em 0 0 var(--ink), -.14em 0 0 var(--ink);
}

/* Anchors clear the sticky bar (one 60px bar + its rule) */
.sec, .index, .hero, .host, .proj{ scroll-margin-top:70px }

/* ----- Capability filters (the Work page) -----
   SQUARED chips, not stadium pills. The system's shape list is thin lines, rectangular
   blocks and circles (tokens.css), and `.tag` already fixes what a small tracked
   ink-on-white capability label looks like here — an off pill is that same block with the
   fill swapped for a 1px rule, an ON pill IS a `.tag`. One line makes them round if that
   trade is ever re-made: `border-radius:999px` on `.pill`.

   The whole bar is JS-GATED. `html.js` is set inline in <head> (see shell()), so there is
   no flash of an unusable control; with JS off the page is simply the full list. Nothing
   here should ever become the only way to reach a project. */
.filters{ display:none }
html.js .filters{ display:block;
  padding-top:clamp(1.1rem,2.2vw,1.7rem); padding-bottom:clamp(1.1rem,2.2vw,1.7rem) }
.filterbar{ display:flex; flex-wrap:wrap; align-items:center; gap:.5rem }
.filter-lede{ margin-right:.45rem }
.pill{ font-family:inherit; font-size:var(--micro); text-transform:uppercase;
       font-weight:var(--w-med); line-height:1.35; color:var(--ink); background:transparent;
       border:1px solid var(--ink); padding:5px 10px; cursor:pointer;
       display:inline-flex; align-items:center; gap:.5em; letter-spacing:var(--track-label) }
.pill:hover, .pill[aria-pressed="true"]{ background:var(--ink); color:var(--bg) }
.pill:focus-visible, .filter-clear:focus-visible{ outline:2px solid var(--ink); outline-offset:3px }
/* the ✕ is the "unset this one" affordance and only exists on a pill that IS set —
   it ships in every pill so turning one on doesn't reflow the row's width */
.pill .x{ display:none; flex:0 0 auto }
.pill[aria-pressed="true"] .x{ display:block }
.filter-clear{ font-family:inherit; font-size:var(--micro); text-transform:uppercase;
       font-weight:var(--w-med); color:var(--ink); background:transparent; border:0;
       padding:5px 2px; margin-left:.2rem; cursor:pointer; text-decoration:underline;
       text-underline-offset:3px; display:inline-flex; align-items:center; gap:.45em; letter-spacing:var(--track-label) }
.filter-clear[hidden]{ display:none }
.filter-count{ font-size:var(--micro); text-transform:uppercase;
       font-weight:var(--w-med); color:var(--ink); opacity:.55; margin-left:auto; letter-spacing:var(--track-label) }
@media (max-width:560px){ .filter-count{ margin-left:0; flex-basis:100% } }
/* A filtered-out project takes its trailing hairline with it — otherwise the page stacks
   two rules with nothing between them. `.proj{display:block}` outranks the UA's
   [hidden]{display:none}, so both need saying explicitly. */
.proj[hidden], .rule[hidden]{ display:none }

/* ----- Project accordion (the Work page) -----
   Each case study is a <details>; the <summary> IS the closed row — tag · claim
   headline · meta · arrow — so a page nobody expands still reads as four claims.
   One opens at a time via `name="work"`; src/accordion.js is the fallback. */
.proj{ display:block }
.proj > summary{ list-style:none; cursor:pointer; display:flex; align-items:center;
                 gap:var(--gutter);
                 padding-top:clamp(1.6rem,3.2vw,2.6rem); padding-bottom:clamp(1.6rem,3.2vw,2.6rem) }
.proj > summary::-webkit-details-marker{ display:none }   /* Safari's default triangle */
.proj > summary::marker{ content:"" }
.proj > summary:focus-visible{ outline:2px solid var(--ink); outline-offset:4px }
/* The row card — one frame of the project's own deck, to the LEFT of the title
   (2026-08-19). It replaced a capability roundel from earlier the same day; Kurt asked for
   the image to come from the work rather than from a new icon.
   A 16:9 RECTANGLE, not the circle it replaced: rectangular blocks are in the shape
   vocabulary, and a circle crop throws away most of a slide — being able to see what the
   work looks like is the entire point. The hairline matches `.viewer-stage`, so a card and
   the deck it opens into are bounded the same way.
   `flex` here, NOT the 12-col band: on the band the card sat in columns 1–4 and pushed
   every headline a quarter of the page right. Kurt asked for the titles closer to the
   images, so the card takes only its own width and the words start one gutter after it. */
/* ABOVE the title, on the same left margin (Kurt, 2026-09-15). It has now been in three
   places in one day and this is the reasoning that settled it: leading the row, it pushed
   every headline a card-width off the grid; trailing the row, it left the titles correct
   but put the image in the gutter with the chevron. Stacked above, the card and the claim
   share one left edge, and that edge is the same one the hero and every section label sit
   on. `.proj-main` is the wrapper that makes the stack possible while the chevron stays a
   flex sibling on the right. */
/* Rounded to match the About photo and the panels. NOT asked for explicitly — Kurt asked
   for the radius on the panels and the portrait, then grouped "photo and project images"
   in the next breath, and a square thumbnail beside a 20px-rounded portrait reads as an
   oversight rather than a decision. One line to revert. */
.proj-card{ display:block; margin-bottom:clamp(.9rem,1.8vw,1.4rem); border-radius:var(--radius);
            width:clamp(104px,15vw,240px); aspect-ratio:16/9;
            overflow:hidden; border:1px solid var(--line); background:var(--bg) }
.proj-card img{ display:block; width:100%; height:100%; object-fit:cover }
/* Below 640px the card comes off. It is not a scale problem — at 375px a 16:9 card wide
   enough to read anything leaves under 200px for an all-caps display headline, and the
   headline is what the row is for. */
@media (max-width:640px){ .proj-card{ display:none } }
.proj-main{ display:block; min-width:0; flex:1 1 auto }
.proj-text{ display:block; min-width:0 }
/* The project title. Sentence case since 2026-09-15 — see the note on `.hero h1` for
   why the leading and tracking move with the transform. `20ch` is unchanged and holds
   MORE now: `ch` is a fixed width, and lowercase fits more characters into it. */
/* The tag leads the row (Kurt, 2026-09-15) — tag → thumbnail → title → meta, all on the
   page margin. It sits OUTSIDE `.proj-text` for that, so it needs its own gap to the
   image; inside the text block it was getting one from the stack. */
.proj-main > .tag{ margin-bottom:clamp(.7rem,1.4vw,1rem) }
.proj-text h2{ font-size:var(--head); font-weight:var(--w-black); letter-spacing:var(--track-tight);
               line-height:1.1; max-width:20ch; margin-top:.85rem }
.proj-text h2 .em{ font-weight:var(--w-thin) }
.proj-text .meta{ display:block; margin-top:.85rem; font-size:var(--micro);
                  text-transform:uppercase; color:var(--ink); font-weight:var(--w-med); letter-spacing:var(--track-label) }
/* the sanctioned circle, as on the work cards. Chevron, not a glyph: `>` in Satoshi is a
   maths sign — too light and sitting off the optical centre. It rotates to ∨ when open.
   RIGHT-hand placement is deliberate: every headline then starts on the same left margin
   as the page hero, and a rule-bounded index line terminating at the right edge is
   already this system's idiom (.card-foot, the footer stamps). To move it left, swap the
   two children in project_summary() and drop the `justify-content` below. */
.proj-arrow{ flex:0 0 auto; width:42px; height:42px; border:1px solid var(--ink); border-radius:50%;
             display:grid; place-items:center; color:var(--ink) }
.proj-arrow .chev{ display:block }
.proj > summary:hover .proj-arrow{ background:var(--ink); color:var(--bg) }
.proj[open] > summary .proj-arrow{ transform:rotate(90deg) }
/* the summary already carried the header, so the body opens straight into the copy */
.proj-body > .host{ padding-top:0 }
/* CLOSED, the summary's bottom padding is half the gutter between two rows in a list, and
   the generous clamp above is right. OPEN, that same padding is separating a title from its
   OWN body — 61px of it at 1440px, which read as a hole and pushed the deck under the fold
   (Kurt, 2026-08-12). Tightened to the value `.proj-body .sec` already uses, so an open row
   has one internal rhythm rather than two. The closed-row spacing is deliberately unchanged:
   only `[open]` is touched. */
.proj[open] > summary{ padding-bottom:clamp(1rem,1.9vw,1.5rem) }
/* …and the first block of body copy then does NOT also need its own top margin — stacking
   both is what made the gap read as doubled. Later paragraphs keep theirs (that is the
   reading rhythm); only the first loses it. Covers both body shapes: an essay opens on
   `.essay-dek`, an article on `.ctx`. */
.proj-body .host > :is(.ctx, .essay-dek):first-child{ margin-top:0 }
/* images landing inside an expanded row must not drag the scroll position with them */
/* THE READING PANEL (2026-09-15, Kurt). An expanded case study or essay is the one place
   on the site with a screen or more of running text, and the background motif was drifting
   straight through it — a grey arc crossing a paragraph mid-sentence.
   `.proj-body` is the whole expanded body on BOTH accordion pages (Work and Perspectives
   build the same `<details class="proj"> → <summary class="proj-head"> + <div
   class="proj-body">` shape), so one declaration covers every case study and every essay,
   and nothing else — the collapsed rows, the heroes, About's `.fold-body` disclosures and
   /values/ keep the motif behind them exactly as before.
   · **`position:relative` + `z-index:1` is the load-bearing half.** `.spine` is a
     POSITIONED element at `z-index:0`; a plain `background` on an unpositioned div paints
     underneath it, so the panel would be invisible. This lifts it into the same layer
     `.content` sits in.
   · **No padding here, deliberately.** Every section inside already carries `.pad`
     (`--margin`: 56px at 1440, ~16px at 390), so the white already runs that far past the
     text column — adding more would move the column and break its alignment with the
     closed rows above it.
   · **Nothing about the open/close animation changes.** accordion.js animates this
     element's inline `height` with `overflow:hidden`; a background on the same box grows
     and shrinks with it, so the panel arrives and leaves with the row. No new element, no
     new class on the animated node. */
.proj-body{ overflow-anchor:none; position:relative; z-index:1; background:var(--bg) }
/* DENSER inside a row. The stock .sec padding was sized to leave air either side of a
   hairline; with the rules removed from inside a row (2026-08-08) that same padding reads
   as a hole between sections. Roughly halved — the uppercase eyebrow carries the break
   now, so it doesn't need a gap to announce itself as well. Only scoped here: the
   Perspective page and the CV still have rules between their sections and still need it. */
.proj-body .sec{ padding-top:clamp(1rem,1.9vw,1.5rem); padding-bottom:clamp(1rem,1.9vw,1.5rem) }
.proj-body > .host{ padding-bottom:clamp(.9rem,1.7vw,1.3rem) }
.proj-body .essay-h{ margin-bottom:1rem }
.proj-body .essay .col-body > .essay-h{ margin-top:.3rem }
@media (max-width:560px){
  .proj-arrow{ width:34px; height:34px; font-size:var(--body) }
}

/* ----- Host header (the copy under an open row; also the CV/essay header) -----
   Was a standalone per-project page until 2026-08-08. */
.host{ padding-top:clamp(2rem,4vw,3.4rem); padding-bottom:clamp(1.8rem,3.5vw,2.8rem) }
.host .back{ font-size:var(--micro); letter-spacing:var(--track-label); text-transform:uppercase; color:var(--ink);
             text-decoration:none; font-weight:var(--w-med) }
.host .back:hover{ text-decoration:underline; text-underline-offset:4px }
.host h1, .host h2{ font-size:var(--head); font-weight:var(--w-black); letter-spacing:var(--track-tight);
          line-height:1.1; max-width:20ch; margin-top:1.3rem }
.host .meta{ margin-top:.9rem; font-size:var(--micro); text-transform:uppercase;
             color:var(--ink); font-weight:var(--w-med); letter-spacing:var(--track-label) }
.host .ctx{ margin-top:1.2rem; max-width:var(--measure); font-family:var(--sans);
            font-weight:var(--w-light); font-size:var(--body); line-height:1.42; color:var(--ink) }

/* ----- Perspective blocks (How I lead · Values · AI-First Operating Model on one page) -----
   The left band carries the POV's name; the body opens with its headline and dek. */
.pov-h{ font-size:var(--head); font-weight:var(--w-black); letter-spacing:var(--track-tight);
        line-height:1.02; max-width:22ch }
.col-body > .pov-h{ margin-top:0 }              /* beats .col-body > * + * */
.pov-dek{ max-width:52ch; font-family:var(--sans); font-weight:var(--w-thin);
          font-size:var(--dek); line-height:1.3; }
.col-body > .pov-dek + *{ margin-top:1.7rem }

/* ----- Values page — one principle per row (2026-08-13) -----
   Replaced the slide viewer, which was five 16:9 frames of outlined vector text: at 375px
   each rendered ~211px tall with a whole paragraph inside it. The words are real type now,
   and the deck's art rides the left band as a ROUNDEL — a filled circle in the frame's own
   colour with the frame's icon knocked out of it. Colour here is imagery (the deck's), not
   chrome: it enters ONLY through --vc, which build.py reads off the slide, and every piece
   of type on the page stays ink. Filled circles are in the allowed shape vocabulary. */
.val-mark-col{ grid-column:1 / 4 }
.val-mark{ display:grid; place-items:center; width:clamp(88px,10vw,132px); aspect-ratio:1;
           border-radius:50%; background:var(--vc,var(--ink)); color:#fff }
.val-mark svg{ display:block; width:56%; height:auto }
.val-t{ font-size:var(--head); font-weight:var(--w-black); letter-spacing:var(--track-tight);
        line-height:1.04; max-width:20ch }
.col-body > .val-t{ margin-top:0 }              /* beats .col-body > * + * */
.val-b{ max-width:var(--measure) }
.col-body > .val-t + .val-b{ margin-top:.9rem }
/* Stacked: the roundel sits ABOVE its claim and shrinks, so a principle still opens on a
   phone with its colour visible but the words starting near the top of the row. */
@media (max-width:760px){
  .val-mark-col{ grid-column:1 }
  .val-mark{ width:72px }
  .val .g12{ row-gap:1.15rem }
}

/* ----- Essay layout (long-form work piece: `layout: essay` in content/work/<slug>.md) -----
   Same 12-col grid as every other page. Weight + size only; hairlines organise it. */
.host :is(h1,h2) .em{ font-weight:var(--w-thin) }   /* weight-contrast accent word, as in the hero */
.host .essay-dek{ margin-top:1.2rem; max-width:44ch; font-weight:var(--w-thin);
                  font-size:var(--dek); line-height:1.28; }
/* One left-justified column: eyebrow directly above the heading, both on the page's
   left margin — no left label band (the .g12 stays so the right edge lands on a grid line). */
.essay .col-body{ grid-column:1 / 11 }
.essay .essay-label{ display:block; margin-top:0 }
.essay-h{ font-size:var(--head); font-weight:var(--w-black); letter-spacing:var(--track-tight);
          line-height:1.02; max-width:24ch; margin-bottom:1.35rem }
.essay .col-body > .essay-h{ margin-top:.5rem }   /* beats .col-body > * + * */
.essay-sub{ margin-top:2.2rem; font-size:var(--body); font-weight:var(--w-black); }
.col-body .essay-sub + *{ margin-top:.6rem }
.essay-q{ margin-top:1.4rem; padding-left:1.1rem; border-left:2px solid var(--ink);
          max-width:52ch; font-weight:var(--w-thin); font-size:var(--title); line-height:1.34 }
/* Essay lists RUN IN — the bold lead-in and its body are one sentence here
   ("**Design the lens set for the room**, not from a textbook acronym"), so breaking
   after the title reads as a dropped line. Stacked stays right for How I lead / Values /
   AI-OM, where each item is a bold sentence followed by a separate one. */
.essay .moves{ row-gap:1.05rem }
.essay .moves .mv-t{ display:inline; margin-bottom:0 }
.essay .moves .mv-t::after{ content:" " }

/* ----- Roster (`roster: yes`) — a reference list that shouldn't cost a screen -----
   CLOSED: a dense grid of just the names, so the inventory still reads at a glance.
   OPEN: the annotated two-column list, names grid withdrawn so nothing repeats. */
/* ⚠ UNRENDERED since 2026-09-15 — `roster_ul()` no longer emits a <details>, so nothing
   below matches. Kept, not deleted: restoring the disclosure is putting the markup back in
   that one function. See its docstring for why it went. */
.roster > summary{ list-style:none; cursor:pointer; display:block }
.roster > summary::-webkit-details-marker{ display:none }
.roster > summary::marker{ content:"" }
.roster > summary:focus-visible{ outline:2px solid var(--ink); outline-offset:4px }
.roster-names{ display:grid; grid-template-columns:repeat(auto-fill,minmax(200px,1fr));
               column-gap:var(--gutter); row-gap:.4rem; margin-bottom:1.1rem }
.rn{ font-family:var(--sans); font-weight:var(--w-med); font-size:var(--body); color:var(--ink) }
.roster-cta{ display:inline-flex; align-items:center; gap:.75em; font-size:var(--micro); text-transform:uppercase; font-weight:var(--w-med); letter-spacing:var(--track-label) }
.roster-arrow{ flex:0 0 auto; width:30px; height:30px; border:1px solid var(--ink);
               border-radius:50%; display:grid; place-items:center }
.roster > summary:hover .roster-arrow{ background:var(--ink); color:var(--bg) }
.roster[open] .roster-names{ display:none }
.roster[open] .roster-arrow{ transform:rotate(90deg) }
.say-open{ display:none }
.roster[open] .say-shut{ display:none }
.roster[open] .say-open{ display:inline }
/* The 12 skills and the 4 instruments are panelled too (Kurt, 2026-09-15) — same grey,
   same radius, same padding as the anatomy and step blocks above them, so the case study
   reads as four figures rather than two figures and two lists. This rule is what the
   panel's `margin` has to beat, so it carries the same value rather than its old 1.1rem;
   leave them in step or the roster sits tighter than the blocks it matches. */
.col-body .roster-full{ margin-top:2rem }

/* ----- Foldable row (`<details class="disclose g12">`) — Core Competencies, Career History -----
   Label, body and chevron share ONE grid row, top-aligned (Kurt, 2026-08-20): the label
   reads on the same line as the first item of its content and as the circle, so a foldable
   row is indistinguishable from the fixed rows above it. The summary spans all 12 columns
   (heading on the page's left margin, circle at the right edge — .proj-arrow's reasoning,
   which every index row in this system follows); the body takes 4–12, one column short of
   the summary's right end so it clears the circle instead of running under it.
   The two overlap in row 1 by design, so `pointer-events` comes OFF the summary box and
   back ON for the label and the circle — otherwise the invisible strip of summary behind
   the body would eat clicks and text selection.
   Core Competencies ships OPEN and Career History ships FOLDED (`open_` in disclose()) —
   the fold is a reader's affordance, not a gate, but the history is context you ask for.
   The chevron is the entire control (no CTA text); its accessible name is the summary's
   aria-label. Native <details>: accordion.js drives `details.proj` only.
   To move the circle back to the left, swap the two children in disclose() and drop the
   `justify-content` here — the same escape hatch .proj-arrow documents. */
.disclose{ --fold-h:36px }
/* NO LEFT LABEL BAND (Kurt, 2026-09-15: "like the work page"). About's two sections used
   to be a 12-column band — heading in cols 1–4, chevron in 12–13, and the body PULLED UP
   by exactly the summary's height so its first line sat on the heading's top edge. That
   negative `margin-top` was the cleverest rule in this file and it is gone: the heading
   now sits on the page margin with its content directly beneath, which is the shape the
   Work rows already use and the reason the whole page shares one left edge.
   The summary keeps the flex row so the chevron stays at the right edge, exactly as
   `.proj > summary` does. */
.disclose > summary{ list-style:none; cursor:pointer; pointer-events:none;
                     display:flex; align-items:center; justify-content:space-between;
                     gap:var(--gutter) }
.disclose > summary::-webkit-details-marker{ display:none }
.disclose > summary::marker{ content:"" }
.disclose > summary:focus-visible{ outline:2px solid var(--ink); outline-offset:4px }
.fold-head > *{ pointer-events:auto }
.fold-head .label{ line-height:1 }
.fold-arrow{ flex:0 0 auto;
             width:var(--fold-h); height:var(--fold-h); border:1px solid var(--ink);
             border-radius:50%; display:grid; place-items:center; color:var(--ink);
             transition:transform .18s ease }
.fold-arrow .chev{ display:block }
.disclose > summary:hover .fold-arrow{ background:var(--ink); color:var(--bg) }
.disclose[open] > summary .fold-arrow{ transform:rotate(90deg) }
.fold-body{ margin-top:clamp(1rem,2vw,1.6rem) }
.fold-body .moves{ margin-top:0 }
/* A section heading that sits ON the margin with its content under it, rather than beside
   it in a band. Bigger and heavier than the `.label` it is built from, because here the
   label IS the heading — there is no display headline under it to carry the weight. */
.sec-h{ display:block; font-size:var(--label); font-weight:var(--w-black) }
#contact .sec-h{ margin-bottom:clamp(1rem,2vw,1.6rem) }
/* Core Competencies, in three labelled groups. The bucket label is the plain 12px `.label`,
   deliberately a step under the section's own 14px/900 heading — it is a sub-heading inside
   Core Competencies, not a second section. */
/* HAIRLINES, NOT BOXES (2026-09-15). Two reasons, and the second is the one that decided it:
   · The Visual language section says hairline rules ORGANISE the page, and it rules out
     rounded-card layouts. The grey `--panel` exists, but it was a documented exception for
     the two translated diagrams — "this block is one figure". Three grey boxes here would
     stretch that to "this block is a grouped list", and they would be the only grey on the
     About page, which puts the most weight on the page's supporting material.
   · `--line` already MEANS "divider inside a component" here — it is what separates the
     rows of `.fl` and `.steps-list` — while `--rule` at 40% means "next section". Using the
     lighter one says these are three groups of one thing, not three sections, which is
     exactly the relationship. A box would have said it louder and less precisely.
   Swapping to panels is two properties if it reads too quiet: `background:var(--panel);
   border-radius:var(--radius); padding:…` in place of the border-top. */
/* Core Competencies, in three groups. The label is a `.tag` — white type on a solid
   midnight block, which the Visual language section names as the sanctioned way to give a
   label presence, and the only place white type appears. It replaced two earlier tries the
   same day: a plain hairline (too quiet to separate three groups) and coloured kicker bars
   (Kurt: "aren't working" — and they put colour in the chrome, which the same section rules
   out). The block carries the separation now, so there is no rule as well; add
   `border-top:var(--hair)` here if you want both. Spacing between groups went up with it,
   because a strong label needs air above it or it reads as attached to the list before. */
.cbucket + .cbucket{ margin-top:clamp(2.4rem,4.8vw,3.6rem) }
.cbucket-h{ margin-bottom:clamp(1rem,2vw,1.4rem) }
.cbucket .moves{ margin-top:0 }

/* The two value chips at the foot of an article (2026-09-15). Roundel + name, linking
   down to that principle's block at the page foot. Same roundel as `.val-mark`, at chip
   scale — colour still arrives as `--vc` from the deck frame, so a re-export moves both. */
.vchips{ display:flex; flex-wrap:wrap; align-items:center; gap:.75rem 1.1rem;
         margin-top:clamp(1.6rem,3.2vw,2.4rem); padding-top:clamp(1rem,2vw,1.5rem);
         border-top:1px solid var(--line) }
.vchips-lede{ flex:0 0 100%; margin-bottom:-.2rem }
.vchip{ display:inline-flex; align-items:center; gap:.55em; text-decoration:none;
        color:var(--ink); font-size:var(--micro); letter-spacing:var(--track-label);
        text-transform:uppercase; font-weight:var(--w-med) }
.vchip-mark{ width:30px; height:30px; flex:0 0 auto; border-radius:50%;
             background:var(--vc,var(--ink)); color:#fff; display:grid; place-items:center }
.vchip-mark svg{ width:17px; height:17px; display:block }
.vchip:hover .vchip-t{ background:var(--ink); color:var(--bg);
                       box-shadow:.18em 0 0 var(--ink), -.18em 0 0 var(--ink) }
.vals-lede{ margin-top:.6rem }

/* A case-study section as a foldable row. The eyebrow and heading stack on the left of the
   summary; the chevron keeps the right edge, as on every other row on the site. */
.fold-ttl{ display:block; min-width:0 }
.fold-ttl .essay-label{ display:block; margin-bottom:.35rem }
.fold-ttl .essay-h{ margin:0 }
.fold-sec{ margin:0 }
/* A nested section. Subordinate by TYPE, not by indent — the whole page was just pulled
   onto one left edge, and indenting these two would be the only thing on it that steps
   right. The heading drops a step to `--title`, the chevron drops to 30px, and the top
   margin is what separates it from the parent's own body copy. */
.fold-sub{ --fold-h:30px; margin-top:clamp(1.6rem,3.2vw,2.4rem) }
.fold-sub > .fold-head .essay-h{ font-size:var(--title) }
/* `.col-body > * + *` is what gives every block in an essay its 1rem of air, and the fold
   broke it: the paragraphs are children of `.fold-body` now, not of `.col-body`, so the
   selector stopped matching and the whole section set solid. This restores it one level
   down, with the same value, so a folded section and an unfolded one have identical
   rhythm. `:first-child` keeps its own top gap off — `.fold-body` already supplies it. */
.fold-body > * + *{ margin-top:1rem }
.fold-sec > .fold-body > :first-child{ margin-top:clamp(.6rem,1.2vw,1rem) }
.sec.fold{ padding-top:clamp(1.15rem,2.2vw,1.75rem); padding-bottom:clamp(1.15rem,2.2vw,1.75rem) }

/* `code` spans — filenames and skill names inside running text (2026-09-15).
   NOT a monospace face. The site has ONE typeface and treatments are weight + size, so a
   second family here would be a louder change than the filenames deserve; Satoshi at
   --w-med against --w-light body copy is the same weight contrast every label on the page
   uses. The tabular figures matter for `2x2` and version numbers, and the slight negative
   tracking keeps a long filename from sprawling mid-sentence. */
.code{ font-family:inherit; font-weight:var(--w-med);
       font-variant-numeric:tabular-nums; white-space:nowrap }

/* anatomy rows — tier · note · files.
   ON A PANEL since 2026-09-15 (Kurt: "enclose the content that used to be images into
   light grey boxes with rounded corners"). This block and `.steps-list` below are the two
   Figma diagrams that became real type earlier the same day, and the panel does the job
   the image's own edges used to: it says "this is one figure", so the rows read as a
   diagram rather than as more of the running text they sit in. Nothing else on the site
   gets a panel — these two are the only translated images.
   The outer hairlines come off (the panel's edge replaces them) and the last row drops
   its divider, or it draws a line against the padding with nothing under it. */
.fl, .steps-list, .roster-full, .panel{ margin:2rem 0; background:var(--panel);
                  border-radius:var(--radius);
                  padding:clamp(.9rem,2vw,1.5rem) clamp(1.3rem,2.6vw,2.1rem) }
/* `.panel` is retained but unused: it was the wrapper that put a section's heading inside
   its grey box, which the section fold superseded the same day (the heading is the
   disclosure control now, so it lives in the `<summary>`). Kept because it is the piece a
   "grey card with its own title" would be built from. */
.panel > .roster-full{ margin:0; background:none; border-radius:0; padding:0 }
.panel > .panel-h{ margin:0 0 clamp(1rem,2vw,1.5rem) }
.fl-row:last-child, .steps-list li:last-child{ border-bottom:0 }
.fl-row{ display:grid; grid-template-columns:minmax(0,1fr) minmax(0,2.1fr); gap:var(--gutter);
         padding:.85rem 0; border-bottom:var(--hair) }
.fl-k{ font-weight:var(--w-black); font-size:var(--body); line-height:1.2 }
.fl-k span{ display:block; margin-top:.3rem; font-size:var(--micro); letter-spacing:var(--track-label);
            text-transform:uppercase; font-weight:var(--w-med) }
.fl-v{ font-weight:var(--w-light); font-size:var(--body); line-height:1.5 }
@media (max-width:640px){ .fl-row{ grid-template-columns:1fr; row-gap:.45rem } }

/* the eight steps — panelled with `.fl` above */
.steps-list{ list-style:none }
.steps-list li{ display:grid; grid-template-columns:2.9rem minmax(0,1fr); column-gap:var(--gutter);
                padding:.9rem 0; border-bottom:var(--hair) }
.st-n{ font-weight:var(--w-black); font-size:var(--body); font-variant-numeric:tabular-nums }
.st-t{ display:block; font-weight:var(--w-black); font-size:var(--body); }
.st-gate{ display:inline-block; margin-left:.6em; padding:1px 6px; border:1px solid var(--ink);
          font-size:var(--micro); letter-spacing:var(--track-label); text-transform:uppercase; font-weight:var(--w-med);
          vertical-align:2px }
.st-b{ grid-column:2; display:block; margin-top:.3rem; font-weight:var(--w-light);
       font-size:var(--body); line-height:1.5 }

/* run figures */
.stats{ display:flex; flex-wrap:wrap; border-top:var(--hair); border-bottom:var(--hair) }
.stats .stat{ flex:1 1 118px; padding:.95rem 1.1rem .95rem 0 }
.stats .stat + .stat{ padding-left:1.1rem; border-left:var(--hair) }
.stats .stat b{ display:block; font-size:var(--head); font-weight:var(--w-black);
                line-height:1; letter-spacing:var(--track-tight); font-variant-numeric:tabular-nums }
.stats .stat span{ display:block; margin-top:.45rem; font-size:var(--micro); letter-spacing:var(--track-label);
                   text-transform:uppercase; font-weight:var(--w-med) }

/* the 2x2 */
.mx{ margin-top:1.6rem }
.mx-ax{ font-size:var(--micro); text-transform:uppercase; font-weight:var(--w-med); letter-spacing:var(--track-label) }
.mx-top, .mx-bottom{ text-align:center; padding:.5rem 0 }
.mx-mid{ display:grid; grid-template-columns:1.5rem minmax(0,1fr) 1.5rem; align-items:stretch }
.mx-side{ display:flex; align-items:center; justify-content:center }
.mx-side span{ writing-mode:vertical-rl; transform:rotate(180deg) }
.mx-mid > .mx-side:last-child span{ transform:none }
.mx-grid{ display:grid; grid-template-columns:1fr 1fr; border-top:var(--hair); border-left:var(--hair) }
.mx-cell{ border-right:var(--hair); border-bottom:var(--hair); padding:1.15rem 1.2rem;
          display:flex; flex-direction:column; gap:.35rem; min-height:160px }
.mx-coord{ font-size:var(--micro); text-transform:uppercase; font-weight:var(--w-med); letter-spacing:var(--track-label) }
.mx-name{ font-weight:var(--w-black); font-size:var(--title); letter-spacing:var(--track-tight);
          line-height:1.05 }
.mx-gloss{ font-weight:var(--w-light); font-size:var(--body); line-height:1.45 }
@media (max-width:640px){
  .mx-mid{ grid-template-columns:1fr }
  .mx-side{ display:none }
  .mx-grid{ grid-template-columns:1fr } }

/* inline diagrams — `:::figure` in an essay page.
   A wide diagram is emitted at natural width in a scroll container: shrinking a 2040px
   process diagram to the text measure would render its own labels at ~5px. */
.dgm{ margin:0 }
.dgm-scroll{ overflow-x:auto; overscroll-behavior-x:contain }
.dgm img{ display:block; width:100%; height:auto; max-width:none }
.dgm figcaption{ margin-top:.7rem; font-size:var(--micro); letter-spacing:var(--track-label);
                 text-transform:uppercase; color:var(--ink); font-weight:var(--w-med) }
/* `full` widens the figure across the two grid columns the essay column doesn't use,
   so a diagram gets the whole page width without leaving the grid. Deliberately NOT the
   `margin-left:calc(50% - 50vw)` viewport breakout used by .hero-invert — that one only
   lands correctly when its container is centred in the viewport, and the essay column
   (cols 1–10 of 12) is not, so it dragged the diagram off-canvas and clipped the caption. */
.dgm.full{ width:calc(100% + ((100% + var(--gutter)) / 10 * 2)) }
@media (max-width:820px){ .dgm.full{ width:100% } }

/* the evaluation table */
.scores-wrap{ overflow-x:auto; border-top:var(--hair); margin-top:1.4rem }
.scores{ border-collapse:collapse; width:100%; min-width:520px; font-variant-numeric:tabular-nums }
.scores th, .scores td{ padding:.5rem .6rem; text-align:center; border-bottom:var(--hair);
                        font-size:var(--body); font-weight:var(--w-light) }
.scores th{ font-size:var(--micro); letter-spacing:var(--track-label); text-transform:uppercase;
            font-weight:var(--w-med); vertical-align:bottom }
.scores th:first-child, .scores td:first-child{ text-align:left; min-width:11rem; }
.scores .sc-total td{ font-weight:var(--w-black); border-bottom:0; border-top:1px solid var(--rule) }

/* ----- Selected Work — image card; title + kind + round arrow BELOW the image (not overlaid) ----- */
.work-grid{ display:grid; grid-template-columns:repeat(2,minmax(0,1fr));
            gap:clamp(34px,4vw,56px) var(--gutter); margin-top:1.4rem }
@media (max-width:760px){ .work-grid{ grid-template-columns:1fr } }
.work-card{ display:block; text-decoration:none; color:var(--ink) }
.card-img{ display:block; aspect-ratio:16/9; background:var(--ink); position:relative; overflow:hidden }
.card-img .ph{ position:absolute; inset:0; display:grid; place-items:center; font-size:var(--micro);
               letter-spacing:var(--track-label); text-transform:uppercase; color:#fff; opacity:.38 }
.card-img img{ position:absolute; inset:0; width:100%; height:100%; object-fit:cover; display:block }
.card-foot{ display:flex; align-items:flex-start; justify-content:space-between; gap:18px; padding-top:15px }
.card-text{ min-width:0 }
.card-ttl{ display:block; font-weight:var(--w-black); font-size:var(--title);
           line-height:1.04; letter-spacing:var(--track-tight); color:var(--ink) }
.card-kind{ display:block; margin-top:7px; font-size:var(--micro); letter-spacing:var(--track-label);
            text-transform:uppercase; color:var(--ink); font-weight:var(--w-med) }
.card-arrow{ flex:0 0 auto; width:42px; height:42px; border:1px solid var(--ink); border-radius:50%;
             display:grid; place-items:center; font-size:var(--body); color:var(--ink);
             transition:background .15s, color .15s }
.work-card:hover .card-arrow{ background:var(--ink); color:#fff }

/* ----- Slide viewer — a 16:9 stage with controls OVERLAID on the slide; the
   little expand box opens a full-screen takeover (same overlaid controls, larger). ----- */
.viewer{ margin-top:1.8rem; outline:none; position:relative }
/* --slide-ar is set inline per deck from its first frame (build.py deck_ratio), so a
   portrait 8.5x11 deck sizes to its own page instead of letterboxing into 16:9. */
/* WHITE ground (Kurt, 2026-08-19). It was --ink, which was invisible for a Figma deck —
   a 16:9 frame in a 16:9 stage covers the ground completely — and actively wrong for a
   book of white pages photographed on white, where the letterbox bars read as a frame
   the artifact does not have. White needs the hairline that the dark ground supplied for
   free, so the border the portrait decks already carried is now on every stage. */
.viewer-stage{ position:relative; aspect-ratio:var(--slide-ar, 16/9); background:#fff;
               border:1px solid var(--line); overflow:hidden }
.viewer.portrait{ max-width:620px; margin-inline:auto }
.viewer.portrait .vslide img{ object-fit:contain }
.vslide{ position:absolute; inset:0; display:flex; flex-direction:column; justify-content:flex-end;
         padding:clamp(20px,3vw,40px); opacity:0; pointer-events:none }
.vslide.is-on{ opacity:1; pointer-events:auto; z-index:1 }   /* crossfade between slides */
.vslide img{ position:absolute; inset:0; width:100%; height:100%; object-fit:contain }   /* real Figma SVG drops in */
.vslide .vslide-ph{ position:absolute; inset:0; display:grid; place-items:center; font-size:var(--micro);
                    letter-spacing:var(--track-label); text-transform:uppercase; color:var(--ink); opacity:.45 }
.vslide .vslide-n{ position:absolute; top:clamp(16px,2.4vw,30px); left:clamp(20px,3vw,40px);
                   font-size:var(--micro); color:var(--ink); font-weight:var(--w-med) }
.vslide .vslide-l{ position:relative; font-weight:var(--w-black); font-size:var(--head);
                   line-height:1.1; letter-spacing:var(--track-tight); color:var(--ink) }

/* Overlaid controls. These sit ON the slide, and the slide can be anything — a black
   Figma frame or a white book page — so they are a translucent WHITE pill with an ink
   glyph: light enough to separate from a dark slide, bordered enough to separate from a
   white one. (They were white-on-translucent-dark until 2026-08-19, which disappeared
   the moment the stage went white.) */
.viewer-stage .vbtn{ position:absolute; z-index:2; display:grid; place-items:center; cursor:pointer;
       border:1px solid rgba(15,17,36,.22); background:rgba(255,255,255,.82); color:var(--ink);
       backdrop-filter:saturate(1.2) blur(3px); border-radius:50%; font-family:inherit }
.viewer-stage .vbtn:hover{ background:var(--ink); color:#fff; border-color:var(--ink) }
.viewer-stage .vbtn:disabled{ opacity:0; pointer-events:none }   /* hide the dead arrow at either end */
.vprev, .vnext{ top:50%; transform:translateY(-50%); width:46px; height:46px; font-size:var(--body) }
.vprev{ left:14px } .vnext{ right:14px }
.vexpand{ top:14px; right:14px; width:38px; height:38px }
.vexpand svg{ display:block }
.viewer-hud{ position:absolute; left:0; right:0; bottom:0; z-index:2; display:flex; align-items:center;
             gap:14px; padding:16px clamp(16px,2.4vw,26px) }
.vticks{ display:flex; gap:6px; margin-left:auto }
.vtick{ width:22px; height:3px; background:var(--accent,var(--ink)); opacity:.3 }
.vtick.is-on{ opacity:1 }

/* ----- the contact sheet — every page at once, for a deck too long to page through.
   Rendered only when import_slides.py wrote a thumbs/ folder (see build.py slide_viewer).
   It sits INSIDE the stage, so opening it reflows nothing: the accordion row keeps its
   height and the reader keeps their place on the page. ----- */
.vindex{ top:14px; right:60px; width:38px; height:38px }
.vindex svg{ display:block }
.vgrid{ position:absolute; inset:0; z-index:3; overflow-y:auto; overscroll-behavior:contain;
        background:#fff; -webkit-overflow-scrolling:touch }
.vgrid[hidden]{ display:none }
.vgrid-bar{ position:sticky; top:0; z-index:1; display:flex; align-items:center; gap:14px;
            padding:14px clamp(14px,2vw,24px); background:#fff;
            box-shadow:0 1px 0 var(--line) }
.vgrid-ttl{ font-size:var(--micro); letter-spacing:var(--track-label); text-transform:uppercase; color:var(--ink);
            font-weight:var(--w-med) }
.vgrid-x{ position:static; margin-left:auto; width:32px; height:32px; flex:none }
.vgrid-inner{ display:grid; gap:clamp(7px,.9vw,13px); padding:0 clamp(14px,2vw,24px) clamp(16px,2vw,26px);
              grid-template-columns:repeat(auto-fill,minmax(clamp(84px,11vw,150px),1fr)) }
.vcell{ position:relative; display:block; padding:0; line-height:0; cursor:pointer;
        background:none; font-family:inherit; border:1px solid var(--line) }
.vcell img{ display:block; width:100%; height:auto }
.vcell:hover{ border-color:var(--ink) }
.vcell:focus-visible{ outline:2px solid var(--ink); outline-offset:2px }
.vcell.is-cur{ border-color:var(--accent,var(--ink));
               box-shadow:0 0 0 2px var(--accent,var(--ink)) }
.vcell .vn{ position:absolute; left:0; bottom:0; padding:2px 6px; line-height:1.4;
            font-size:var(--micro); color:var(--ink);
            background:rgba(255,255,255,.86) }
/* the sheet owns the stage while it is open — arrows and the progress line underneath
   would page a slide nobody can see */
.viewer.is-grid .vprev, .viewer.is-grid .vnext, .viewer.is-grid .viewer-hud{ display:none }
.viewer.is-grid .vindex{ background:var(--ink); color:#fff; border-color:var(--ink); z-index:4 }

/* a long deck's HUD — 98 ticks would be a 2,700px bar the stage just clips */
.vcount{ font-size:var(--micro); color:var(--ink); font-weight:var(--w-med);
         font-variant-numeric:tabular-nums; flex:none }
.vbar{ position:relative; flex:1; height:3px; background:rgba(15,17,36,.16) }
.vfill{ position:absolute; left:0; top:0; bottom:0; width:0; background:var(--accent,var(--ink)) }

/* On a phone the stage is only ~250px tall — a scrollable grid inside it is a peephole.
   There the sheet breaks out to the whole window instead; it is the same markup, and
   `position:fixed` means the page underneath still does not move. */
@media (max-width:720px){
  .vgrid{ position:fixed; z-index:60 }
  .vgrid-bar{ padding-top:calc(14px + env(safe-area-inset-top,0px)) }
}

/* full-screen takeover — slide centered on a white ground, controls scaled up */
.viewer:fullscreen{ background:#fff; display:grid; place-items:center; padding:clamp(12px,3vh,46px) }
.viewer:fullscreen .viewer-stage{ height:100%; width:auto; max-width:100%;
  aspect-ratio:var(--slide-ar, 16/9); border:1px solid var(--line) }
/* the inline portrait cap must not constrain the full-screen takeover — a portrait deck
   fills the window's HEIGHT there and derives its width from --slide-ar (Kurt, 2026-08-08) */
.viewer.portrait:fullscreen{ max-width:none; margin-inline:0 }
.viewer:fullscreen .vprev, .viewer:fullscreen .vnext{ width:56px; height:56px; font-size:var(--body) }
.viewer:fullscreen .vprev{ left:clamp(14px,3vw,40px) } .viewer:fullscreen .vnext{ right:clamp(14px,3vw,40px) }
.viewer:fullscreen .vexpand{ width:44px; height:44px; top:clamp(14px,3vh,28px); right:clamp(14px,3vw,28px) }
.viewer:fullscreen .vindex{ width:44px; height:44px; top:clamp(14px,3vh,28px);
                            right:calc(clamp(14px,3vw,28px) + 54px) }
.viewer:fullscreen .viewer-hud{ padding:clamp(18px,3vh,32px) clamp(20px,3vw,40px) }

/* Figma Slides — live embed (edits in Figma update here automatically). No frame.
   Our own Fullscreen button drives it (reliable cross-browser, unlike the in-player one). */
.figma-embed{ margin:1.8rem 0 0; display:flex; flex-direction:column }
.figma-stage{ aspect-ratio:16/9; overflow:hidden; background:#f4f4f6 }
.figma-stage iframe{ display:block; width:100%; height:100%; border:0 }
.figma-stage:fullscreen{ aspect-ratio:auto; width:100%; height:100%; background:#0a0c1a }
.figma-fs{ align-self:flex-end; margin-top:10px; display:inline-flex; align-items:center; gap:7px;
           font-size:var(--micro); letter-spacing:var(--track-label); text-transform:uppercase; font-weight:var(--w-med);
           color:var(--ink); background:none; border:0; cursor:pointer; padding:4px 2px; font-family:inherit }
.figma-fs svg{ display:block }
.figma-fs:hover span{ text-decoration:underline; text-underline-offset:3px }

/* ============================================================================
   MOTION — restrained scroll-reveal + hover micro-interactions. The hidden
   start-state applies ONLY when JS is present (html.js) AND the user allows
   motion, so reduced-motion / no-JS visitors get the full page instantly.
   ============================================================================ */
@media (prefers-reduced-motion: no-preference){
  /* in-page nav anchors glide; page-to-page navigations cross-fade */
  html{ scroll-behavior:smooth }
  html.js body{ animation:pagein .35s ease }
  html.is-leaving body{ opacity:0; transition:opacity .24s ease }

  html.js :is(.sec, .index .head, .work-card, .host, .viewer){
    opacity:0; transform:translateY(13px);
    transition:opacity .6s ease, transform .6s cubic-bezier(.22,.7,.2,1);
  }
  html.js :is(.sec, .index .head, .work-card, .host, .viewer).is-in{
    opacity:1; transform:none;
  }
  /* THE FIRST SCREEN IS NEVER ANIMATED (2026-09-15, Kurt: "a few seconds of blankness").
     Everything above hid itself at `opacity:0` and waited for motion.js to observe it —
     which meant the top of the page was blank for as long as the JS took to arrive.
     Measured locally on a warm cache: first contentful paint at 704ms against a DOM that
     was interactive at 15ms. On a cold connection the gap is however long src/motion.js
     takes, and that is the "few seconds".
     Two rules fix it, and the split matters:
     · **The hero is exempt in CSS, unconditionally.** It is above the fold on every page
       at every width by construction, so no measurement is needed and nothing has to run
       before it can paint. This is the rule that makes the first screen independent of
       JavaScript entirely.
     · **`.no-reveal` is the measured half** — motion.js marks any OTHER target already
       inside the first viewport, before it starts observing (see the comment there).
     Everything below the fold keeps the reveal exactly as it was. */
  html.js :is(.sec, .index .head, .work-card, .host, .viewer).no-reveal{
    opacity:1; transform:none; transition:none;
  }
  /* The hero's children used to lead in one after another (eyebrow → headline → dek) off
     `.hero.is-in`. That staircase is GONE rather than re-timed: it was the thing holding
     the name, the role line and the positioning copy at opacity 0 until JavaScript said
     otherwise, and a 0.6s transition plus a 0.2s delay on top of that wait is most of
     what the blankness was. The hero now simply IS there. `body`'s 0.35s `pagein` is the
     only entrance left on the first screen, and it is the other half of the cross-fade
     between pages, not a reveal. */
  /* An opening row cascades instead of arriving all at once: the body height glides in
     accordion.js while these two ride in just behind it — prose first, deck a beat later.
     Delays only, so a row revealed by scrolling is unaffected. */
  html.js .proj-body .host{ transition-delay:.06s }
  html.js .proj-body .viewer{ transition-delay:.16s }
  /* gentle stagger as a card grid scrolls in */
  html.js .work-card:nth-child(2){ transition-delay:.06s }
  html.js .work-card:nth-child(3){ transition-delay:.12s }
  html.js .work-card:nth-child(4){ transition-delay:.18s }
  html.js .work-card:nth-child(5){ transition-delay:.24s }
  /* hover micro-interactions */
  .card-arrow{ transition:background .25s ease, color .25s ease, transform .25s ease }
  .work-card:hover .card-arrow{ transform:translateX(3px) }
  .proj-arrow{ transition:background .2s ease, color .2s ease, transform .28s ease }
  nav a, .col-body a, .host .ctx a, .work-card .card-ttl{
    transition:background .12s ease, color .12s ease, box-shadow .12s ease;
  }
  /* slide viewer — crossfade between slides + eased controls */
  .vslide{ transition:opacity .4s ease }
  .viewer-stage .vbtn{ transition:background .2s ease, color .2s ease, border-color .2s ease }
  .vtick{ transition:opacity .3s ease }
}
@keyframes pagein{ from{ opacity:0 } to{ opacity:1 } }

/* ============================================================================
   Contact forms — the message form + the CV request (added 2026-09-09, when the
   site came out from behind its password).

   Everything here is the existing vocabulary, not a new one: hairline rules,
   uppercase tracked labels in --w-med, ink on white, and the one sanctioned
   button treatment (white type on a solid midnight rectangle, inverting to an
   outline on hover). No radius, no fill, no card — an input is a hairline with
   type sitting on it, which is the same rule the .model-list rows follow.
   ========================================================================== */
.cform{ margin:1.6rem 0 2.2rem; max-width:34rem }
.cform[hidden]{ display:none }

.cform-lede{ font-weight:var(--w-thin); font-size:var(--body); line-height:1.45;
             max-width:var(--measure); margin-bottom:1.4rem }

.cfield{ margin-bottom:1.35rem }
.cfield label{ display:block; font-size:var(--micro); font-weight:var(--w-med);
               text-transform:uppercase; letter-spacing:var(--track-label); opacity:.7;
               margin-bottom:.45rem }
.cfield input,
.cfield textarea{
  width:100%; font:inherit; font-size:var(--body); font-weight:var(--w-light);
  color:var(--ink); background:transparent;
  border:0; border-bottom:1.2px solid var(--ink); border-radius:0;
  padding:.5rem 0; -webkit-appearance:none; appearance:none }
.cfield textarea{ resize:vertical; min-height:6.5rem; line-height:1.45 }
/* The thicker rule stays as the quiet "this field is live" cue on any focus. What came
   OFF is `outline:0` (2026-09-15): 1.2px → 2.4px is not a focus indicator anyone can
   actually see, and suppressing the ring on top of that left the form with no visible
   keyboard focus at all. The global `:focus-visible` rule near the top of this file now
   draws the 2px ink ring here like everywhere else. */
.cfield input:focus,
.cfield textarea:focus{ border-bottom-width:2.4px }

/* The honeypot. Off-screen rather than display:none — a bot that skips
   display:none fields still fills this one, which is the entire point. It must
   stay focus-proof for anyone using a keyboard: tabindex="-1" is on the input,
   and nothing here can be reached by scrolling to it. */
.cf-hp{ position:absolute; left:-9999px; width:1px; height:1px; overflow:hidden }

.cf-turnstile{ margin:0 0 1.3rem }

/* The one sanctioned button treatment — see CLAUDE.md § Visual language. */
.cbtn{ font:inherit; font-weight:var(--w-black); font-size:var(--body); cursor:pointer;
       background:var(--ink); color:var(--bg);
       border:1.2px solid var(--ink); border-radius:0;
       padding:.6rem 1.5rem }
/* Hover inverts the block; focus inverts it AND keeps the ring (the `outline:0` that used
   to be here made keyboard focus and hover indistinguishable). */
.cbtn:hover,
.cbtn:focus-visible{ background:var(--bg); color:var(--ink) }
.cbtn:disabled{ opacity:.45; cursor:default }
.cbtn:disabled:hover{ background:var(--ink); color:var(--bg) }

.cform-status{ margin-top:1rem; font-size:var(--body); font-weight:var(--w-light);
               line-height:1.45; min-height:1.2em }
.cform-status:empty{ margin-top:0; min-height:0 }
/* Status colour is weight, not hue — the palette is monochrome and an error is
   not an exception to that. Failure reads as heavier type, success as lighter. */
.cform-status.is-err{ font-weight:var(--w-med) }
.cform-status.is-ok{ font-weight:var(--w-light) }
.cform-status a{ color:var(--ink) }
.cform-fine{ opacity:.55 }

/* The CV toggle is a <button> sitting in a list of <a>s — it has to inherit the
   contact-link treatment exactly, or it reads as a different kind of control. */
.col-body button.contact-link{
  display:inline-flex; align-items:center; gap:.65em; width:fit-content;
  font:inherit; font-size:var(--body); font-weight:var(--w-light); color:var(--ink);
  background:none; border:0; border-radius:0; padding:0; cursor:pointer;
  text-align:left }
