/* ============================================================
   Vantage Advisory, site candidate v2.
   Brand: brand/brand-spec.md (v2) + brand/design-language.md (v3).
   WHITE GROUND THROUGHOUT (2026-08-09, Rokas). Oak ramp, Instrument Sans,
   NO ACCENT COLOUR. No price anywhere (decision 2026-08-08).

   Structure, four blocks and a contact panel:
     A  anchor      hero, WebGL field dissolving into the ground below it
     B  the console one product object: routine rail drives the board and the trace
     C  functions   one at a time, each with the artefact it leaves behind
     D  terms       what you are paying for, as a term sheet
     E  contact     the diagnostic call, in the closing box

   The dark ground is gone. What made the product read as an object was the dark;
   on white that job is done by ONE soft shadow on the app surface, which is the
   split design-language.md principle 3 already draws: editorial flat, app lifted.
   ============================================================ */
:root{
  --ground:#FFFFFF; --warm:#FBFAF8; --warm-2:#F7F4EF;
  --ink:#2C2015; --ink-2:#3D2F20; --ink-soft:#726550; --ink-faint:#7F7058;
  --line:#E9E4DB; --line-soft:#F1EDE6;
  /* Three tokens that exist so the palette can INVERT rather than just darken.
     --paper is the app window's own surface, which is not the same idea as the page
     ground even though both are white here: on white the window is separated by the
     lift shadow, and a shadow does nothing on a dark ground, so there --paper has to
     carry the separation itself by sitting lighter than the page.
     --on-ink is text sitting on an --ink fill. Hardcoding #fff there would have put
     white text on a near-white button the moment --ink inverted.
     --fade is the hero dissolve and the nav veil as an rgb triplet, so the hero can
     run out into whichever ground is underneath it. */
  --paper:#FFFFFF; --on-ink:#FFFFFF; --fade:255,255,255; --oak:114,101,80;
  /* ---- atmosphere ----
     The hero used to own all the material and the page went flat underneath it, which is
     the exact place a dark page starts reading as a generic dark theme. These three carry
     the oak out of the hero and across every section.
     --grain the film grain the shader puts in the field, given to the DOM on top of it so
             text and texture share one tooth instead of reading as two layers.
     The per-section washes that used to live here are gone. The field is the material now,
     and a tint of the same hue sitting beside real stone only ever looked like a shortcut. */
  --grain:.030;
  /* --plate and --panel are both gone. Nothing carries a baked still any more. The material
     is the live field, in exactly two places.
     --slab is --warm as an rgb triplet, and it exists for one reason: the closing box runs
     the field out into its OWN surface rather than into the page ground. Fading to --fade
     inside a box leaves the bottom of that box the same colour as the page outside it, so
     the only thing left holding the shape down there is a hairline. */
  --slab:251,250,248;
  --font:'Instrument Sans',-apple-system,BlinkMacSystemFont,'SF Pro Text',system-ui,sans-serif;
  --s1:4px; --s2:8px; --s3:16px; --s4:24px; --s5:40px; --s6:64px; --s7:96px; --s8:128px; --s9:184px;
  /* The one lift. Offset plus soft blur, oak-tinted rather than grey, so a raised
     surface still belongs to the warm world. */
  --lift:0 1px 2px rgba(44,32,21,.05), 0 2px 6px -2px rgba(44,32,21,.06),
         0 30px 64px -28px rgba(44,32,21,.22);
}

/* ---------- dark ----------
   Not a neutral black. The ground is the oak ramp taken past its dark end, so the
   dark page belongs to the same warm world as the white one and nothing reads as a
   generic dark theme dropped on top. The ink ramp inverts to warm bone rather than
   pure white, because #FFF on near-black glares at body sizes.
   The one relationship that flips deliberately: on white, --warm sits DARKER than the
   page and reads as inset. On dark it sits LIGHTER, because on a dark ground lifted
   is lighter, and the rail, trace and contact panel all want to read as raised. */
:root[data-theme="dark"]{
  --ground:#12100C; --warm:#191510; --warm-2:#171310;
  --ink:#F4F0E9; --ink-2:#DCD4C7; --ink-soft:#AC9F8A; --ink-faint:#948773;
  --line:#302921; --line-soft:#241F19;
  --paper:#1B1712; --on-ink:#12100C; --fade:18,16,12; --oak:172,159,138; --slab:25,21,16;
  /* All three run hotter on dark, and they have to. A wash that reads as a lit corner on
     white disappears entirely against #12100C, and grain is what stops a near-black ground
     from looking like a flat fill. */
  --grain:.042;
  /* Ambient occlusion does the separating, plus a hairline lip of warmth on top. */
  --lift:0 1px 2px rgba(0,0,0,.44), 0 2px 8px -2px rgba(0,0,0,.40),
         0 34px 70px -30px rgba(0,0,0,.72), inset 0 1px 0 rgba(244,240,233,.05);
}
/* The rail's selected item is the one thing that would have inverted WRONG: it fills
   with --paper, which on dark is lighter than the rail and correct, but the trace
   summary and the board rows need the same treatment to stay a plane above. */
:root[data-theme="dark"] .rt-item[aria-current="true"]{box-shadow:0 1px 2px rgba(0,0,0,.5)}
/* The closing box draws its own hairline in both themes (see E. CONTACT), so nothing extra
   is needed here: --line inverts with the palette and the edge inverts with it. */
*{box-sizing:border-box}
/* scroll-padding-top, added 2026-08-12. The nav is position:fixed, so before this every
   in-page jump (#inside and #how are linked from the footer of all four guides) landed with
   the target heading UNDERNEATH it. Measured: nav is 74px here and 58px under 820px, so the
   value tracks the same breakpoint the nav's own padding does, plus a little air so the
   heading does not sit flush against the nav's bottom edge. */
html{scroll-behavior:smooth; scroll-padding-top:calc(74px + var(--s4))}
body{margin:0; background:var(--ground); color:var(--ink); font-family:var(--font);
     font-size:17px; line-height:1.55; -webkit-font-smoothing:antialiased}

/* ---------- the grain ----------
   The hero shader dithers its own output (`hash(...) * 0.016`). Nothing else on the page
   did, so the hero read as one material and everything below it as another. This gives the
   whole page the same tooth.
   Fixed rather than scrolling, because grain belongs to the screen and not to the document;
   it also means zero repaint cost while scrolling. Sits under the nav and the moon and over
   everything else, and it is deliberately NOT blended: mix-blend-mode here would force the
   entire page into a blended compositor layer on every frame, next to two live WebGL
   canvases, for a difference you cannot see at 4%. */
body::after{content:""; position:fixed; inset:0; z-index:55; pointer-events:none;
  opacity:var(--grain); background-size:200px 200px;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23n)'/%3E%3C/svg%3E")}

.wrap{max-width:1240px; margin:0 auto; padding:0 var(--s6)}
@media(max-width:760px){ .wrap{padding:0 var(--s4)} }

h1{margin:0 0 var(--s4); font-weight:600; font-size:clamp(40px,6.6vw,88px);
   line-height:1.0; letter-spacing:-.045em; max-width:15ch}
h1 em{font-style:italic}
h2{margin:0 0 var(--s4); font-weight:600; font-size:clamp(30px,4.2vw,54px);
   line-height:1.06; letter-spacing:-.035em; max-width:none; text-wrap:balance}
h3{margin:0 0 var(--s3); font-weight:600; font-size:clamp(21px,2.2vw,27px);
   line-height:1.2; letter-spacing:-.03em}
.lede{font-size:21px; color:var(--ink-2); max-width:48ch; margin:0 0 var(--s5)}
.sub{font-size:clamp(17px,1.7vw,20px); color:var(--ink-soft); max-width:52ch; margin:0; line-height:1.5}
/* Back on the text floor. It was moved one step up the ramp while the field ran the whole
   page, because --ink-faint at 13px only has about 0.4 of margin on flat white and the
   material took it to 4.27. Both captions sit on flat ground again, so the reason is gone
   and so is the exception. Measures 4.77 there. */
.illus{font-size:13px; color:var(--ink-faint); margin:var(--s3) 0 0}
/* The note under the expansion block ran the full 1112px while the paragraph directly above it
   stops at the grid's first column (558px at 1440), so it read as a line that had escaped its
   column (Rokas, 2026-08-11, screenshot). Matched to `.con-lead`'s first column using the grid's
   OWN arithmetic rather than a magic number: the columns are 1fr and .82fr with a --s7 gap, so
   column one is (100% - gap) / 1.82. If those ratios change, this follows them.
   Scoped to #context deliberately. The other two .illus lines sit under full-width components
   (the console, the wiring diagrams) where running the full width is correct. */
#context .illus{max-width:calc((100% - var(--s7)) / 1.82)}
/* .con-lead collapses to one column here, so the constraint has to come off with it. */
@media(max-width:960px){ #context .illus{max-width:none} }

section{padding:var(--s8) 0; position:relative}
@media(max-width:760px){ section{padding:var(--s7) 0} }

/* ============================================================
   THE ATMOSPHERE
   Sections paint no ground of their own. The body carries the colour and the field carries
   the material, so a section is a box of text sitting on a continuous surface rather than a
   panel with its own fill.

   Everything that used to live here is gone: six radial washes anchored per section, a
   plate cropped into the console, and a hairline seam drawn between every pair of sections.
   Together they were the thing that made the page read as assembled. The washes repeated one
   gesture six times, the crops repeated one image three times, and the seams drew a literal
   line under the problem.

   Section separation is done by LIGHT, and by only two moments of it: the field's envelope
   brings the stone up over the hero and over the close, and everything between them is flat
   ground. The trough IS the boundary. There is nothing to draw and nothing to align.
   Envelope and amplitudes live in site.js, measured off those two elements. */

/* ---------- nav ---------- */
nav{position:fixed; top:0; left:0; right:0; z-index:60; transition:background .3s, box-shadow .3s}
nav[data-scrolled="1"]{background:rgba(var(--fade),.86);
  backdrop-filter:saturate(180%) blur(14px); box-shadow:inset 0 -1px 0 var(--line)}
.nav-in{max-width:1240px; margin:0 auto; padding:var(--s4) var(--s6);
        display:flex; align-items:center; justify-content:space-between; gap:var(--s4)}
.nav-mark{display:flex; align-items:center; gap:10px; text-decoration:none; color:var(--ink)}
.nav-mark span{font-size:17px; font-weight:600; letter-spacing:-.03em; white-space:nowrap}
.nav-links{display:flex; gap:var(--s5); align-items:center}
.nav-links a{font-size:13px; color:var(--ink-soft); text-decoration:none; white-space:nowrap}
.nav-links a:hover{color:var(--ink)}
/* padding-block + equal negative margin-block EXPANDS THE TAP TARGET WITHOUT MOVING ANYTHING.
   Measured 2026-08-12 in the pre-deploy mobile pass: this link was 64x20, under the 24px
   WCAG 2.5.8 floor, and below 820px `.nav-links a:not(.cta){display:none}` makes it the ONLY
   nav element AND the primary conversion action on the device most likely to open the site.
   20 + 24 = 44px, which clears the AAA target too.
   VERTICAL ONLY, deliberately. Negative horizontal margin would pull the desktop nav items
   closer together, since .nav-links spaces them with `gap` and margins are outside it. */
.nav-links a.cta{color:var(--ink); font-weight:500; padding-block:12px; margin-block:-12px}

/* The moon, parked bottom-right and floating over the page rather than living in the nav.
   It is a body-level element on purpose: the nav takes a backdrop-filter once scrolled,
   and a filtered ancestor becomes the containing block for position:fixed descendants,
   so a toggle left inside it would have started travelling with the nav.
   Given its own surface here because it now sits over whatever section happens to be
   behind it, and a bare glyph on the console or a diagram panel would disappear. */
.theme-btn{position:fixed; right:var(--s5); bottom:var(--s5); z-index:70;
  display:grid; place-items:center; width:44px; height:44px; padding:0;
  border:1px solid var(--line); border-radius:50%; background:var(--paper);
  box-shadow:var(--lift); color:var(--ink-soft); cursor:pointer;
  -webkit-tap-highlight-color:transparent;
  transition:color .15s, transform .25s cubic-bezier(.16,1,.3,1), border-color .15s}
.theme-btn:hover{color:var(--ink); border-color:var(--ink-soft); transform:translateY(-2px)}
.theme-btn:focus-visible{outline:2px solid var(--ink); outline-offset:3px}
.theme-btn .ic{grid-area:1 / 1; width:17px; height:17px;
  transition:opacity .28s ease, transform .42s cubic-bezier(.4,0,.2,1)}
.theme-btn .ic-sun{opacity:0; transform:rotate(-80deg) scale(.6)}
:root[data-theme="dark"] .theme-btn .ic-moon{opacity:0; transform:rotate(80deg) scale(.6)}
:root[data-theme="dark"] .theme-btn .ic-sun{opacity:1; transform:none}
@media(prefers-reduced-motion: reduce){ .theme-btn .ic{transition:opacity .01s}
  .theme-btn:hover{transform:none} }
@media(max-width:760px){ .theme-btn{right:var(--s4); bottom:var(--s4)} }

.theming, .theming *, .theming *::before, .theming *::after{
  transition:background-color .34s ease, color .34s ease, border-color .34s ease,
             box-shadow .34s ease, fill .34s ease, stroke .34s ease !important}
@media(max-width:820px){
  html{scroll-padding-top:calc(58px + var(--s3))}
  .nav-in{padding:var(--s3) var(--s4)}
  .nav-links{gap:var(--s3)}
  .nav-links a:not(.cta){display:none}
}

/* ============================================================
   A. ANCHOR
   The hero has no field of its own any more. It is simply the part of the document where
   the field's envelope is at full strength, which is why it no longer needs to dissolve
   into anything: there is nothing below it to dissolve INTO, only the same surface at a
   lower amplitude.
   ============================================================ */
.hero{min-height:100svh; display:flex; flex-direction:column; padding:var(--s8) 0 var(--s7)}
/* FIXED, and behind the whole document rather than inside the hero. Everything on the page
   paints over it: sections are transparent, and the surfaces that need a clean ground
   (the console window, the cards, the FAQ items) bring their own opaque fill.
   overflow:hidden came off .hero at the same time, because a fixed descendant of a clipping
   ancestor is clipped to it, which cropped the field to the first screen. */
#field{position:fixed; inset:0; width:100%; height:100%; display:block; z-index:0;
  pointer-events:none}
/* The left wedge that protected the headline is gone from here and lives in the shader now.
   As a DOM layer it was bounded by .hero, so it ended at the hero's bottom edge and drew a
   step across the left half of the page at the one join that must not have one. In the field
   it is bounded by the hero's POOL, so it fades on the same curve as the material.
   See "the hero's left wedge" in site.js. */
/* width:100% is load-bearing and was deliberately absent before. .wrap's auto SIDE
   margins cancel a flex item's cross-axis stretch, so without a width this column
   shrink-wraps to its text and self-centres, which is what put the headline ~110px
   right of the nav. With it, .wrap's own max-width + auto margins land the headline on
   the SAME 1240px gutter as the nav and every section below. One gutter for the page.
   Horizontal padding stays on .wrap; a shorthand here would zero the mobile gutter.
   margin-block:auto centres the column in the space ABOVE the terms line, and
   collapses to 0 when the content is tall, which is why the two can never collide. */
.hero .inner{position:relative; z-index:2; width:100%; padding-top:56px; margin-block:auto}
/* In flow, not absolute. Absolute took it out of the hero's layout entirely, so at any
   viewport where the column outgrew 100svh the buttons landed on top of it. */
.hero .terms{position:relative; z-index:2; padding-top:var(--s5)}
.hero .terms .wrap{color:var(--ink-soft); font-size:13px; text-align:center}
@media(max-width:760px){ .hero .terms{padding-top:var(--s4)} }

/* ---------- CENTRED HERO (Rokas, 2026-08-12) ----------
   Replaces the left-anchored hero, and it CLOSES a question that had been logged open rather
   than reopening a settled one: CONTINUE.md TASK 2 listed "hero is left-anchored, sections
   below are centred" as an unresolved alignment inconsistency. The page now agrees with
   itself. ("Left-anchored hero" also sat on that file's do-not-re-litigate list; Rokas put it
   there and Rokas took it off, which is the only way that list is allowed to change.)

   THE THING THAT MAKES THIS MORE THAN A text-align, and it is measured rather than assumed.
   The shader carries a LEFT WEDGE whose only job is to flatten the marble under the headline
   so text keeps its contrast (`float wedge = 1.0 - smoothstep(0.02, 0.78, uv.x)` in site.js).
   It is at full strength on the far left and gone by 78% across. The home page's amplitudes
   were tuned around "a hero that has no body text on its right", and on the guides index text
   sitting at 83% across, outside the wedge, measured 2.47:1 and failed. Centring moves every
   line of this hero rightward into weaker wedge.
   So every hero text colour was re-measured on rendered pixels after this change rather than
   reasoned about. The 13px --ink-soft terms line is the thin one and is the first thing that
   will break if the amplitudes, the pool geometry or the wedge falloff are ever retuned.
   Re-run site/verify/hero-contrast.py if you touch any of them. */
.hero .inner{text-align:center}
.hero h1,.hero .lede{margin-inline:auto}
.hero .actions{justify-content:center}



/* ---------- buttons ---------- */
.actions{display:flex; flex-wrap:wrap; gap:var(--s3); align-items:center}
/* Any button row that follows copy gets the same air above it, wherever it appears. The
   guides CTA had none at all because only two sections had ever set it locally. */
.con-head .actions{margin-top:var(--s5)}
.btn{font:inherit; font-size:15px; font-weight:500; padding:13px 24px; border-radius:4px;
     border:1px solid transparent; cursor:pointer; text-decoration:none; display:inline-block;
     transition:background .15s, color .15s, border-color .15s}
.btn-primary{background:var(--ink); color:var(--on-ink)}
.btn-primary:hover{background:var(--ink-2)}
.btn-quiet{border-color:var(--line); color:var(--ink)}
.btn-quiet:hover{border-color:var(--ink)}
a:focus-visible,button:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--ink);
  outline-offset:3px; border-radius:2px}

/* ============================================================
   B. THE CONSOLE
   One object. The rail is the control, the board is what accumulates, the trace is
   what is running right now. Six routines all writing into the SAME board is the
   literal shape of "one function, unpacked in full", which is why the six separate
   cards that used to live below could be deleted rather than restyled.
   ============================================================ */
.con-head{max-width:none}
.con-head h2{max-width:none}
/* align-items was `end` until 2026-08-11 (Rokas: "not very aligned", with a screenshot of the
   expansion section). Bottom-aligning the bullets against a much taller paragraph pushed their
   first line 111px down in #context and 119px in #inside, measured, which left a hole in the top
   right of both blocks and no shared edge anywhere. `start` gives the two columns one top edge.
   The 960px breakpoint below was ALREADY `start`, so desktop was the odd one out. */
.con-lead{display:grid; grid-template-columns:minmax(0,1fr) minmax(0,.82fr); gap:var(--s7);
          align-items:start}
.con-points{list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:12px}
.con-points li{font-size:15px; color:var(--ink-soft); line-height:1.5;
               padding-left:var(--s4); position:relative}
.con-points li::before{content:""; position:absolute; left:0; top:9px; width:5px; height:5px;
                       border-radius:50%; background:var(--ink-faint)}
.con-points b{color:var(--ink); font-weight:600}
@media(max-width:960px){ .con-lead{grid-template-columns:1fr; gap:var(--s5); align-items:start} }

/* ---- the app window ---- */
.shell{color:var(--ink); background:var(--warm); border-radius:10px; overflow:hidden;
  margin-top:var(--s6); box-shadow:var(--lift), inset 0 0 0 1px var(--line)}
.shell-top{display:flex; align-items:center; gap:12px; padding:11px 14px;
  border-bottom:1px solid var(--line); background:var(--paper)}
.shell-top .mk{display:flex; align-items:center; gap:8px; font-size:13px;
  font-weight:600; color:var(--ink)}
.shell-top .mk svg{stroke:currentColor}
.shell-top .fn{font-size:12.5px; color:var(--ink-faint); padding-left:12px;
  border-left:1px solid var(--line)}
.shell-top .rt{margin-left:auto; display:flex; align-items:center; gap:8px;
  font-size:12.5px; color:var(--ink-faint)}
.shell-body{display:grid; grid-template-columns:224px minmax(0,1fr) 316px; background:var(--paper)}
@media(max-width:1100px){ .shell-body{grid-template-columns:206px minmax(0,1fr)} .shell-trace{display:none} }
@media(max-width:760px){ .shell-body{grid-template-columns:1fr} }

.shell .live-dot{width:6px; height:6px; border-radius:50%; background:var(--ink);
  flex:none; position:relative}
.shell .live-dot::after{content:""; position:absolute; inset:-3px; border-radius:50%;
  border:1px solid var(--ink); opacity:0; animation:shPulse 2.4s ease-out infinite}
@keyframes shPulse{0%{opacity:.55; transform:scale(.6)}70%{opacity:0; transform:scale(1.5)}100%{opacity:0}}

/* ---- the rail: the control, and the selected-in-a-list pattern ----
   Active is full ink, the rest sit at the text floor. No colour involved, which is
   the whole point of the no-accent constraint. */
.shell-rail{border-right:1px solid var(--line); padding:10px 9px; background:var(--warm)}
.shell-rail .grp{font-size:11px; letter-spacing:.06em; text-transform:uppercase;
  color:var(--ink-faint); padding:6px 8px 8px; margin:0}
.rt-item{display:flex; align-items:flex-start; gap:9px; padding:9px; border-radius:4px;
  font-size:13px; color:var(--ink-faint); line-height:1.3; width:100%; text-align:left;
  background:none; border:0; font-family:inherit; cursor:pointer;
  transition:color .25s ease, background-color .25s ease}
.rt-item .live-dot{opacity:.22; margin-top:4px; transition:opacity .25s ease}
.rt-item:hover{color:var(--ink-soft)}
.rt-item[aria-current="true"]{background:var(--paper); color:var(--ink); font-weight:600;
  box-shadow:0 1px 2px rgba(44,32,21,.07)}
.rt-item[aria-current="true"] .live-dot{opacity:1}
.rt-item .cad{display:block; font-size:11.5px; font-weight:400; color:var(--ink-faint);
  margin-top:2px}
@media(max-width:760px){
  /* Horizontal scroller at phone width: a 224px sidebar and a table cannot share 390px. */
  .shell-rail{border-right:0; border-bottom:1px solid var(--line); padding:8px;
    display:flex; gap:6px; overflow-x:auto; -webkit-overflow-scrolling:touch}
  .shell-rail .grp{display:none}
  .rt-item{width:auto; flex:none; white-space:nowrap}
  .rt-item .cad{display:none}
}

/* Column, so the footer can be pushed to the bottom edge: the trace beside it is
   taller, and without this the board left bare panel under its footer. */
.shell-main{min-width:0; border-right:1px solid var(--line); display:flex; flex-direction:column}
@media(max-width:1100px){ .shell-main{border-right:0} }
.shell-main .bd-foot{margin-top:auto}
.shell-head{display:flex; align-items:center; justify-content:space-between; gap:12px;
  padding:11px 14px; border-bottom:1px solid var(--line)}
.shell-head .t{display:flex; align-items:center; gap:8px; font-size:13px;
  font-weight:600; color:var(--ink)}
.shell-head .s{font-size:12.5px; color:var(--ink-faint); text-align:right}
.shell-head .s b{color:var(--ink); font-weight:600; font-feature-settings:"tnum"}

/* justify-self, because a grid item stretches to its column by default and a 136px
   chip around the word "New" reads as an empty input field, not as a status. */
.chip{display:inline-flex; align-items:center; justify-self:start; font-size:12px;
  font-weight:600; border-radius:4px; padding:4px 9px; border:1px solid var(--line);
  color:var(--ink-soft); background:transparent; white-space:nowrap;
  transition:background-color .4s ease, color .4s ease, border-color .4s ease}
.chip.idle{color:var(--ink-faint)}
.chip.mid{border-color:var(--ink-soft); color:var(--ink)}
.chip.done{background:var(--ink); color:var(--on-ink); border-color:var(--ink)}

/* The status column is sized to the LONGEST chip ("Booked · Fri 11am"), not to a
   round number: at 112px it clipped and every chip sat flush against the column edge. */
.bd-r{display:grid; grid-template-columns:1.35fr 1fr .95fr .8fr 136px; gap:10px;
  padding:10px 14px; align-items:center; border-bottom:1px solid var(--line-soft); font-size:13px}
.bd-r.h{background:var(--warm); font-size:11px; color:var(--ink-faint); font-weight:600;
  letter-spacing:.04em; text-transform:uppercase; border-bottom-color:var(--line)}
.bd-body .bd-r{opacity:0; transform:translateY(-6px)}
.bd-body .bd-r.show{opacity:1; transform:none;
  transition:opacity .4s ease, transform .5s cubic-bezier(.16,1,.3,1)}
.bd-r .nm{display:flex; align-items:center; gap:8px; min-width:0;
  font-weight:600; color:var(--ink)}
.bd-r .nm em{font-style:normal; overflow:hidden; text-overflow:ellipsis; white-space:nowrap}
.av{width:22px; height:22px; border-radius:50%; background:var(--warm);
  border:1px solid var(--line); flex:none; display:grid; place-items:center;
  font-size:10.5px; font-weight:600; color:var(--ink-soft)}
.bd-r .cell{color:var(--ink-soft); overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  transition:color .4s ease}
.bd-r .cell.blank{color:var(--line)}
.bd-foot{display:flex; align-items:center; justify-content:space-between; gap:12px;
  padding:10px 14px; background:var(--warm); font-size:12.5px; color:var(--ink-faint);
  border-top:1px solid var(--line)}
.bd-foot b{color:var(--ink); font-weight:600; font-feature-settings:"tnum"}
.bd-foot .fl{transition:opacity .3s ease}
@media(max-width:620px){
  .bd-r{grid-template-columns:1.2fr .9fr 128px}
  .bd-r .c-fund, .bd-r .c-move{display:none}
}

.shell-trace{min-width:0; background:var(--warm); display:flex; flex-direction:column}
.shell-trace .shell-head{border-bottom:1px solid var(--line)}
.tr{padding:12px 15px}
.tr-intro{margin:0 0 6px; font-size:12.5px; color:var(--ink-faint); line-height:1.45}
.tr-step{display:flex; align-items:flex-start; gap:11px; padding:7px 0;
  opacity:.3; transition:opacity .4s ease}
.tr-step.on{opacity:1}
.tr-step .mk2{width:14px; height:14px; border-radius:50%; border:1px solid var(--ink-soft);
  flex:none; margin-top:2px; display:grid; place-items:center; background:var(--warm);
  transition:background-color .35s ease, border-color .35s ease}
.tr-step.on .mk2{background:var(--ink); border-color:var(--ink)}
.tr-step .mk2 svg{opacity:0; transition:opacity .3s ease .12s; stroke:var(--on-ink)}
.tr-step.on .mk2 svg{opacity:1}
.tr-step .tb{flex:1; min-width:0}
.tr-step .hd{display:flex; justify-content:space-between; gap:12px; align-items:baseline}
.tr-step .hd span:first-child{font-size:13.5px; color:var(--ink); line-height:1.35}
.tr-step .tdur{font-size:11.5px; color:var(--ink-faint); flex:none; font-feature-settings:"tnum"}
/* Space is reserved rather than collapsed: animating the height made the panel
   jump once per completed step. */
.tr-step .tout{margin:4px 0 0; font-size:12.5px; color:var(--ink-soft); line-height:1.4;
  opacity:0; transform:translateY(-2px); transition:opacity .4s ease, transform .4s ease}
.tr-step.on .tout{opacity:1; transform:none}
.tr-sum{margin:auto 15px 14px; background:var(--paper); border-radius:4px; padding:11px 13px;
  font-size:12.5px; line-height:1.55; color:var(--ink-soft);
  box-shadow:inset 0 0 0 1px var(--line);
  opacity:0; transform:translateY(4px); transition:opacity .45s ease, transform .45s ease}
.tr-sum.on{opacity:1; transform:none}
.tr-sum b{color:var(--ink); font-weight:600}

/* These live AFTER `.shell-trace{display:flex}` on purpose. Declared up beside
   .shell-body they lost on source order at equal specificity, so the trace stayed
   visible in the band where it does not fit and wrapped under the rail at 206px.
   Between 760 and 1100 there is no room for a third column, so it goes. Below 760
   the grid is one column and the trace stacks under the board, which is better than
   hiding the most persuasive panel on the page. */
@media(max-width:1100px){ .shell-trace{display:none} }
@media(max-width:760px){ .shell-trace{display:flex; border-top:1px solid var(--line)} }

/* The routine's own sentence, under the window. It is the copy that used to sit on
   six separate cards, now attached to whichever routine you are watching. */
/* The caption's split is now the RAIL's edge, not an arbitrary ratio (2026-08-11, Rokas:
   "not very aligned"). It was .44fr/.56fr, which put the description's left edge at 512px while
   the console directly above it has its only vertical rules at 224px and 796px. Nothing lined up
   with anything. It now mirrors `.shell-body`'s grid EXACTLY (224px / 1fr / 316px) with the third
   column left empty, so the name sits under the rail and the description spans precisely the main
   pane. Two edges line up instead of none, at every viewport, without a magic number.
   The empty third column is the point, not an oversight: a plain `224px 1fr` split also aligned
   the left edge but let the description run to 888px, which is about 118 characters at 15px and
   unreadable. Borrowing the trace's 316px caps it at the main pane's width for free.
   Keep this in step with `.shell-body` at every breakpoint or it drifts. */
.shell-cap{display:grid; grid-template-columns:224px minmax(0,1fr) 316px;
  gap:0; margin-top:var(--s4); align-items:start}
.shell-cap .nm{font-size:19px; font-weight:600; letter-spacing:-.025em; line-height:1.3;
  margin:0; padding-right:var(--s4); transition:opacity .3s ease}
@media(max-width:1100px){ .shell-cap{grid-template-columns:206px minmax(0,1fr)} }
.shell-cap .ds{font-size:15px; color:var(--ink-2); line-height:1.55; margin:0;
  transition:opacity .3s ease}
.shell-cap[data-swap="1"] .nm, .shell-cap[data-swap="1"] .ds{opacity:0}
/* Stacked, so the name's right padding (which exists only to hold it off the description in the
   two-column form) would just be a dead indent on the right. */
@media(max-width:760px){ .shell-cap{grid-template-columns:1fr; gap:var(--s2)}
                         .shell-cap .nm{padding-right:0} }

/* ============================================================
   C. WIRING
   The console shows one routine RUNNING. This shows what each routine is PLUGGED
   INTO: what it picks up from, and what it writes back to. That is a different
   altitude on the same data, which is the only reason it earns a section instead
   of repeating the console in a second costume.

   Structure is getminds.ai's "research methods" block in our tokens: the heading and
   the argument live in a LEFT column, and a single panel on the right holds the visual
   with a dense index sitting under it. Their index is numbered because their numbers
   key into their diagram. Ours would key into nothing, and an ordinal would imply the
   six run in sequence when only the first three do, so the slot carries each routine's
   real cadence instead. Same rhythm, no invented meaning.
   ============================================================ */
.wire-split{display:grid; grid-template-columns:minmax(0,.82fr) minmax(0,1.18fr);
  gap:var(--s6); align-items:center}
@media(max-width:1000px){ .wire-split{grid-template-columns:1fr; gap:var(--s6)} }

.wire-say h2{margin-bottom:var(--s4)}
/* The hairline rule beside the argument is the reference's tell, and it is doing real
   work: it marks this as the claim the panel then has to evidence. */
.wire-quote{margin:0; padding-left:var(--s4); border-left:1px solid var(--line);
  font-size:19px; line-height:1.6; color:var(--ink-2); max-width:44ch}
.wire-say .actions{margin-top:var(--s5)}
.wire-say .illus{margin-top:var(--s5); max-width:46ch}

/* One panel, two nested surfaces: the diagram sits on --paper, the index sits on the
   panel's own ground, so the visual reads as a thing held rather than a thing beside. */
.wire-panel{background:var(--warm); border-radius:14px; padding:var(--s3);
  box-shadow:var(--lift), inset 0 0 0 1px var(--line)}
.wire-stage{background:var(--paper); border-radius:10px; padding:var(--s5) var(--s4) 0;
  box-shadow:inset 0 0 0 1px var(--line); min-height:318px;
  display:grid; grid-template-rows:1fr auto; align-items:center; justify-items:center}
/* The sentence belongs WITH the diagram it describes, not repeated six times down the
   index. Only the live routine explains itself. */
.wire-cap{margin:var(--s5) 0 0; padding:13px 0; width:100%; text-align:center;
  font-size:12.5px; color:var(--ink-faint); box-shadow:inset 0 1px 0 var(--line)}

/* Labels get their OWN row. When they lived inside the columns they inflated the row the
   bus stretches across, so each bus ran up past the first chip and terminated in mid air. */
.wire{display:grid; align-items:center; width:100%;
  grid-template-columns:minmax(0,1fr) 34px minmax(0,auto) 34px minmax(0,1fr);
  grid-template-rows:auto auto; row-gap:11px}
.wire-lab{margin:0; align-self:end; font-size:10.5px; letter-spacing:.07em;
  text-transform:uppercase; color:var(--ink-faint); white-space:nowrap}
.wire-lab.in{grid-area:1 / 1}
.wire-lab.out{grid-area:1 / 5}
.wire-ul{list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:12px}
.wire-ul.in{grid-area:2 / 1}
.wire-ul.out{grid-area:2 / 5}
.wire-ul li{position:relative; font-size:13px; line-height:1.38; color:var(--ink-2);
  background:var(--warm); border:1px solid var(--line); border-radius:6px; padding:11px 13px}
.wire-ul.in li::after{content:""; position:absolute; left:100%; top:50%; width:17px; height:1px;
  background:var(--line)}
.wire-ul.out li::after{content:""; position:absolute; right:100%; top:50%; width:17px; height:1px;
  background:var(--line)}

/* The connector columns. The stub reaches exactly half the column width, so it meets
   the bus dead centre without anything being measured in JS. */
.wire-bus{position:relative; align-self:stretch; min-height:56px}
.wire-bus.in{grid-area:2 / 2}
.wire-bus.out{grid-area:2 / 4}
.wire-node{grid-area:2 / 3}
.wire-bus::before{content:""; position:absolute; left:50%; top:0; bottom:0; width:1px;
  background:var(--line)}
.wire-bus::after{content:""; position:absolute; top:50%; height:1px; background:var(--line)}
.wire-bus.in::after{left:50%; right:0}
.wire-bus.out::after{right:50%; left:0}

.wire-node{background:var(--warm); border:1px solid var(--line); border-radius:8px;
  box-shadow:var(--lift); padding:14px 15px; text-align:center; max-width:168px}
.wire-node svg{display:block; margin:0 auto 8px; color:var(--ink)}
.wire-node .nm{display:block; font-size:13.5px; font-weight:600; color:var(--ink);
  line-height:1.32; letter-spacing:-.01em}
.wire-node .cd{display:block; margin-top:6px; font-size:10.5px; letter-spacing:.07em;
  text-transform:uppercase; color:var(--ink-faint)}

/* ---- The routine swap (2026-08-11, Rokas: "could animate a little nicer on changing") ----
   wireDraw() replaces the stage's innerHTML in a single frame, so every change was a hard cut.
   THE BUSES AND THE STAGE FRAME DELIBERATELY DO NOT ANIMATE. Only the labels, the chips, the
   node and the caption move. The wiring is the constant and the contents are what change, so
   holding the frame while the contents swap is the diagram's own argument, and it also avoids
   the hairlines popping back to full opacity a frame before the chips have finished arriving.
   Stagger runs left to right, picks-up-from into the node into writes-back-to, which is the
   direction the work actually travels. Exit lifts 4px, entry arrives from 6px below, so the two
   halves read as one movement rather than a fade out and an unrelated fade in. */
@keyframes wireIn{ from{opacity:0; transform:translateY(6px)} to{opacity:1; transform:none} }
.wire-lab, .wire-ul li, .wire-node, .wire-cap{
  animation:wireIn .34s cubic-bezier(.22,.61,.36,1) both}
.wire-lab.in{animation-delay:0s}
.wire-ul.in li{animation-delay:.04s}
.wire-node{animation-delay:.10s}
.wire-lab.out{animation-delay:.12s}
.wire-ul.out li{animation-delay:.16s}
.wire-cap{animation-delay:.20s}
.wire-stage[data-swap="1"] .wire-lab,
.wire-stage[data-swap="1"] .wire-ul li,
.wire-stage[data-swap="1"] .wire-node,
.wire-stage[data-swap="1"] .wire-cap{
  animation:none; opacity:0; transform:translateY(-4px);
  transition:opacity .18s ease, transform .18s ease}
/* Reduced motion lands the content in its final state. The JS also skips the swap delay
   entirely, so this is the belt to that braces. */
@media(prefers-reduced-motion:reduce){
  .wire-lab, .wire-ul li, .wire-node, .wire-cap{animation:none}
  .wire-stage[data-swap="1"] .wire-lab,
  .wire-stage[data-swap="1"] .wire-ul li,
  .wire-stage[data-swap="1"] .wire-node,
  .wire-stage[data-swap="1"] .wire-cap{opacity:1; transform:none; transition:none}
}

/* The index. Two columns of six, under the visual, on the reference's rhythm. Items
   recede by stepping DOWN THE INK RAMP rather than by dropping opacity: opacity .4 put
   the resting rows at roughly 3.5:1, an accessibility failure dressed as a design
   choice. The ramp gives the same hierarchy with every state still AA. */
/* The cycle tell: a hairline that fills over one routine's dwell, then resets as the next
   one comes up. ONE line, not six segments, on purpose. Segments would restate the index
   directly below it, which already shows six routines with the current one marked, and the
   standing self-audit rule is that nothing appears twice. What the index cannot say is that
   a timer is running and the view is about to hand over, and that is all this adds.
   Duration comes from --wire-dwell, set on the element by the page script from the same
   constant that drives setInterval, so the line and the cycle cannot drift apart. */
.wire-prog{height:1px; margin:var(--s3) var(--s2) 0; background:var(--line);
  overflow:hidden; opacity:0; transition:opacity .35s ease}
.wire-prog.run{opacity:1}
.wire-prog span{display:block; height:100%; width:0; background:var(--ink-soft)}
.wire-prog.run span{animation:wireFill var(--wire-dwell, 3400ms) linear forwards}
/* Hover freezes the line because hover also pauses the cycle. */
.wire-prog.paused span{animation-play-state:paused}
@keyframes wireFill{ from{width:0} to{width:100%} }
/* No cycle runs under reduced motion, so there is nothing to indicate. */
@media(prefers-reduced-motion: reduce){ .wire-prog{display:none} }

.wire-index{display:grid; grid-template-columns:repeat(2,minmax(0,1fr));
  gap:0 var(--s5); padding:var(--s4) var(--s3) var(--s2)}
.wire-ix{display:grid; grid-template-columns:auto minmax(0,1fr); gap:10px; align-items:start;
  font:inherit; text-align:left; width:100%; background:none; border:0; cursor:pointer;
  padding:10px 0; border-top:1px solid var(--line)}
.wire-index .wire-ix:nth-child(-n+2){border-top:0}
.wire-ix:focus-visible{outline:2px solid var(--ink); outline-offset:2px}
.wire-ix .mk{width:9px; height:9px; border-radius:50%; margin-top:5px;
  border:1px solid var(--ink-faint); transition:background .2s ease, border-color .2s ease}
.wire-ix[aria-current="true"] .mk{background:var(--ink); border-color:var(--ink)}
.wire-ix .nm{display:block; font-size:13.5px; font-weight:600; line-height:1.32;
  letter-spacing:-.01em; color:var(--ink-soft); transition:color .2s ease}
.wire-ix .cd{display:block; margin-top:3px; font-size:11.5px; line-height:1.3;
  color:var(--ink-faint)}
.wire-ix:hover .nm{color:var(--ink-2)}
.wire-ix[aria-current="true"] .nm{color:var(--ink)}

@media(max-width:760px){
  /* Stacked, and the horizontal wiring goes with it. Keeping elbows here would draw
     connectors that point at nothing, which is worse than no diagram at all. */
  .wire-stage{min-height:0; padding:var(--s4) var(--s3) 0}
  .wire{grid-template-columns:1fr; grid-template-rows:none; gap:var(--s3);
    justify-items:stretch}
  .wire-lab.in, .wire-ul.in, .wire-node, .wire-lab.out, .wire-ul.out{grid-area:auto}
  .wire-bus{display:none}
  .wire-ul li::after{display:none}
  .wire-node{max-width:none}
  .wire-index{grid-template-columns:1fr; gap:0}
  .wire-index .wire-ix:nth-child(2){border-top:1px solid var(--line)}
  .wire-quote{font-size:17px}
}

/* ============================================================
   D. HOW IT WORKS
   averi.ai's spine, in our tokens. Their version is centred; ours stays left-anchored on
   the page gutter, because the editorial axis is the one thing every section here shares.
   The rule is drawn per-item from its own dot DOWN to the next, rather than as one line
   behind the list. A single full-height line has to be clipped at both ends to stop it
   overshooting the first and last dots, and that clipping breaks the moment an item wraps
   to a different number of lines.
   ============================================================ */
/* Centred (Rokas, 2026-08-09). The heading and standfirst centre; the spine centres as a
   BLOCK but its steps stay left-aligned, because centring the step text would break the
   dot-and-rule axis the whole diagram hangs off. That is also how the reference does it. */
/* `.beat` is the same centred head with no diagram under it, used by the context-layer
   section. Sharing the selector rather than duplicating the rule, so the two can never
   drift apart. Added 2026-08-10. */
.how .con-head, .beat .con-head{margin-left:auto; margin-right:auto; text-align:center}
.how .con-head h2, .beat .con-head h2{margin-left:auto; margin-right:auto}
.how .con-head .sub, .beat .con-head .sub{margin-left:auto; margin-right:auto}
.spine{list-style:none; margin:var(--s6) auto 0; padding:0; max-width:47ch}
.spine li{position:relative; padding:0 0 var(--s6) var(--s5)}
.spine li:last-child{padding-bottom:0}
.spine li::before{content:""; position:absolute; left:5px; top:15px; bottom:0; width:1px;
  background:var(--line)}
/* Armed by JS. The rule grows from its own dot downward toward the next one, which is the
   direction you are reading, and the step it belongs to settles at the same time. */
.spine[data-armed="1"] li{opacity:0; transform:translateY(14px)}
.spine[data-armed="1"] li::before{transform:scaleY(0); transform-origin:top}
.spine[data-armed="1"] li[data-in="1"]{opacity:1; transform:none;
  transition:opacity .5s ease, transform .65s cubic-bezier(.16,1,.3,1)}
.spine[data-armed="1"] li[data-in="1"]::before{transform:scaleY(1);
  transition:transform .55s cubic-bezier(.4,0,.2,1) .12s}
.spine li:last-child::before{display:none}
.spine .dot{position:absolute; left:0; top:9px; width:11px; height:11px; border-radius:50%;
  background:var(--ink)}
.spine h3{margin:0 0 6px; font-size:19px; letter-spacing:-.02em; line-height:1.3}
.spine p{margin:0; font-size:16px; line-height:1.6; color:var(--ink-soft)}
@media(max-width:760px){
  .spine li{padding-bottom:var(--s5); padding-left:var(--s4)}
  .spine h3{font-size:17.5px}
}

/* ============================================================
   F. GUIDES  (blog) + G. FAQ
   Grid is averi.ai's staggered card wall. Their covers are full-bleed photography; we
   have no photographic language, and stock imagery sitting beside our own writing is the
   same manufactured-proof move refused elsewhere in this repo. So a cover here is the
   hero's own material instead: the oak ramp, pitched differently per card, with the mark
   set into it. It takes a real image the moment one exists, via a background-image on
   .g-cover, and nothing else has to change.
   ============================================================ */
/* REBUILT 2026-08-11 off microsoft.ai's blog carousel (Rokas). Pattern ported, nothing copied.
   Full take/leave and the measurements it was built from:
   REFERENCE-NOTE-microsoft-ai-2026-08-11.md. The four moves taken:
     1. the card is HORIZONTAL, near-square cover one side, text the other
     2. headline top-anchored, meta bottom-anchored, and the space between is left EMPTY
     3. large type set TIGHT (leading ~1.05, negative tracking) at a lighter weight than our headings
     4. the next card is cut off by the viewport edge, which says "there are more" with no counter
   Left behind deliberately: their serif, their palette, their artwork, and their choice to carry
   no blurb at all (ours is doing real work, so the void moves BELOW it instead of replacing it). */

/* The track bleeds to the RIGHT viewport edge so the next card is clipped by it. `calc(-50vw +
   50%)` is exact for a centred container: at 1440 the content box ends at 1276 and this pulls it
   the remaining 164px. Only the TRACK scrolls; the document must not, and layout-audit.js asserts
   scrollWidth <= innerWidth on every run, so a regression here fails the suite rather than
   shipping. padding-right keeps the last card off the glass at the end of the scroll. */
.g-grid{display:flex; gap:var(--s4); margin-top:var(--s6); align-items:stretch;
  overflow-x:auto; overflow-y:hidden; scroll-snap-type:x mandatory; scroll-behavior:smooth;
  margin-right:calc(-50vw + 50%); padding-right:var(--s6); padding-bottom:var(--s1);
  scrollbar-width:none; -ms-overflow-style:none}
.g-grid::-webkit-scrollbar{display:none}
@media(prefers-reduced-motion:reduce){ .g-grid{scroll-behavior:auto} }

/* 62% of the wrap puts roughly a third of the next card on screen, which is the reference's
   proportion. No border and no shadow: the ref's card is a flat tinted panel and the lift our
   old cards carried is what made them read as generic content tiles. */
.g-card{position:relative; flex:0 0 clamp(300px, 62%, 700px); scroll-snap-align:start;
  display:grid; grid-template-columns:minmax(0,.46fr) minmax(0,.54fr); gap:var(--s5);
  padding:var(--s4); background:var(--warm); border-radius:20px;
  color:inherit; box-shadow:none;
  transition:background .45s ease}
.g-card:hover{background:var(--paper)}
/* THE LINK IS THE HEADING, STRETCHED OVER THE CARD, and it has to stay that way. The card is an
   <article>, not an <a>. Wrapping the whole card in an anchor (which the first version of this
   rebuild did) makes the link's accessible name the kind, the title, the blurb AND the read time
   read out as one string, which is unusable to anyone listening to it. guides.html's own CSS
   carried a comment warning about exactly this and the rewrite nearly threw it away.
   ::after gives the whole card the hit area back, so nothing is lost visually. */
.g-card h3 a{color:inherit; text-decoration:none}
.g-card h3 a::after{content:""; position:absolute; inset:0; border-radius:inherit}
.g-card h3 a:focus-visible::after{outline:2px solid var(--ink); outline-offset:4px}

/* THE COVER IS A DESIGNED PANEL, NOT A MISSING PHOTO, and that distinction is the whole reason
   this port is safe. Covers were REMOVED on 2026-08-10 because "a flat 5:2 rectangle above every
   card reads as an image that failed to load rather than as restraint", and the reference is ~45%
   image by area, so porting it literally would recreate that failure at nearly twice the size.
   So the panel is the page's own material: the oak ramp pitched differently per card, built from
   TOKENS ONLY (--line / --warm / --paper) so it inverts correctly in dark mode without a second
   palette. Four angles and four focal points, so the four cards are related but not identical.
   It still takes a real photograph the day one exists: set background-image on .g-cover and
   nothing else changes. */
/* FIRST ATTEMPT BUILT THESE FROM --line / --warm / --paper AND THEY WERE INVISIBLE. Every token
   at the light end of the oak ramp is within about 2% of white (--warm #FBFAF8, --line #E9E4DB,
   --paper #FFFFFF), so a gradient between them on a --warm card had no contrast at all and the
   panel read as the empty rectangle this whole approach exists to avoid. Caught by screenshotting
   it, not by reading the CSS.
   Fixed by mixing the INK into the ramp with color-mix, which keeps one source of truth and still
   inverts correctly in dark mode (--ink flips to near-white there, so the same percentages lighten
   the dark panel instead of darkening a light one). background-color is a real fallback for any
   engine without color-mix: it degrades to a flat tint, never to nothing. */
.g-cover{position:relative; aspect-ratio:4/5; border-radius:12px; overflow:hidden;
  background-color:var(--line);
  background-size:cover; background-position:center;
  background-image:radial-gradient(120% 90% at 22% 18%, color-mix(in srgb, var(--ink) 3%, var(--paper)), transparent 62%),
                   linear-gradient(155deg, color-mix(in srgb, var(--ink) 24%, var(--warm)), color-mix(in srgb, var(--ink) 9%, var(--warm)) 56%, var(--paper))}
.g-card:nth-child(2) .g-cover{
  background-image:radial-gradient(115% 95% at 78% 24%, color-mix(in srgb, var(--ink) 3%, var(--paper)), transparent 58%),
                   linear-gradient(205deg, color-mix(in srgb, var(--ink) 20%, var(--warm)), color-mix(in srgb, var(--ink) 7%, var(--warm)) 52%, var(--paper))}
.g-card:nth-child(3) .g-cover{
  background-image:radial-gradient(130% 85% at 40% 78%, color-mix(in srgb, var(--ink) 4%, var(--paper)), transparent 60%),
                   linear-gradient(120deg, color-mix(in srgb, var(--ink) 27%, var(--warm)), color-mix(in srgb, var(--ink) 10%, var(--warm)) 62%, var(--paper))}
.g-card:nth-child(4) .g-cover{
  background-image:radial-gradient(110% 100% at 66% 66%, color-mix(in srgb, var(--ink) 3%, var(--paper)), transparent 55%),
                   linear-gradient(235deg, color-mix(in srgb, var(--ink) 17%, var(--warm)), color-mix(in srgb, var(--ink) 6%, var(--warm)) 48%, var(--paper))}
/* Real cover artwork, 2026-08-11: generated stills animated to 4s loops, sitting on top of the
   gradient panel. THE GRADIENT STAYS UNDERNEATH ON PURPOSE. Three layers, each degrading to the
   one below: the video plays, or its poster still shows, or the token gradient shows. Nothing in
   that chain can leave an empty rectangle, which is the failure that got covers removed once.
   The Sightline mark that used to sit bottom-left is gone: it existed because the panel was
   empty, and over real artwork it is clutter. The reference carries no mark on its covers either. */
.g-vid{position:absolute; inset:0; width:100%; height:100%; object-fit:cover; display:block;
  border-radius:inherit}

/* space-between is what creates the void. The top block holds kind + headline + blurb, the foot
   holds the meta row, and everything between them is deliberately empty. */
.g-body{display:flex; flex-direction:column; justify-content:space-between;
  padding:var(--s2) var(--s2) var(--s2) 0; min-width:0}
.g-kind{display:inline-block; font-size:10.5px; letter-spacing:.08em; text-transform:uppercase;
  color:var(--ink-faint); margin:0 0 14px}
/* Weight 500, not our usual 600: at this size the lighter cut is what reads editorial rather
   than promotional, which is the single biggest thing carrying the reference's card. */
.g-card h3{margin:0; font-weight:500; font-size:clamp(22px,2.1vw,31px);
  line-height:1.06; letter-spacing:-.03em}
.g-card p{margin:14px 0 0; font-size:14.5px; line-height:1.5; color:var(--ink-soft); max-width:34ch}
/* Read time left, arrow right, same baseline. Straight from the reference. */
.g-foot{display:flex; align-items:center; justify-content:space-between; gap:var(--s3);
  margin-top:var(--s5)}
.g-meta{margin:0; font-size:12px; letter-spacing:.05em; color:var(--ink-faint)}
.g-go{flex:none; width:19px; height:19px; color:var(--ink-faint);
  transition:transform .4s cubic-bezier(.16,1,.3,1), color .4s ease}
.g-card:hover .g-go{transform:translateX(5px); color:var(--ink)}
@media(prefers-reduced-motion:reduce){ .g-card:hover .g-go{transform:none} }

/* The prev/next pair, below-left, square, panel-filled, no border. They are an ENHANCEMENT:
   the track is a native scroller and works by drag, wheel and keyboard without them, so if the
   script never runs nothing is lost. Hidden when there is nothing to scroll. */
.g-nav{display:flex; gap:10px; margin-top:var(--s4)}
.g-nav[hidden]{display:none}
.g-btn{width:44px; height:44px; display:grid; place-items:center; cursor:pointer;
  background:var(--warm); border:0; border-radius:10px; color:var(--ink-soft);
  transition:background .3s ease, color .3s ease, opacity .3s ease}
.g-btn:hover:not(:disabled){background:var(--paper); color:var(--ink)}
.g-btn:disabled{opacity:.35; cursor:default}
.g-btn:focus-visible{outline:2px solid var(--ink); outline-offset:3px}
.g-btn svg{width:17px; height:17px}

/* guides.html uses the SAME card with no carousel. An index exists to show everything at once,
   so a peeking row that hides half the library would be the pattern applied where it does not
   belong. Cards go full width and stack, which suits a horizontal card better than a 2-up grid
   and keeps the headline at the size that is doing the work. */
.g-wall{display:grid; gap:var(--s4); margin-top:var(--s6)}
/* The cover has to be RE-PROPORTIONED here, not just inherited. A wall card is the full 1112px
   against the carousel card's 700, so the same 4/5 portrait cover came out 612px tall and each
   row ran to ~660px, which turned the reference's confident void into a hole and the index into
   a very long page. Measured, then fixed by narrowing the cover column and widening its ratio:
   389px wide at 16/11 is 267px tall, so a row is ~315px and the headline gets the space instead. */
.g-wall .g-card{flex:none; width:auto; scroll-snap-align:none;
  grid-template-columns:minmax(0,.38fr) minmax(0,.62fr)}
.g-wall .g-cover{aspect-ratio:16/11}

@media(max-width:820px){
  .g-wall .g-card{grid-template-columns:1fr}
  /* One card at a time, and the split stacks: cover on top, text under it. The carousel is KEPT
     on mobile rather than collapsing to a list, because the peek is what tells a thumb it can
     swipe, and a stacked list of four tall cards is a much longer page. */
  .g-card{flex:0 0 82%; grid-template-columns:1fr; gap:var(--s3); border-radius:16px}
  .g-cover{aspect-ratio:16/10}
  .g-body{padding:0 var(--s1) var(--s1)}
  .g-card h3{font-size:22px}
  .g-foot{margin-top:var(--s4)}
  .g-grid{padding-right:var(--s4)}
}

/* FAQ. Native <details name>, which gives an exclusive accordion, keyboard support and
   correct semantics with no JavaScript at all. Where the browser is older the only
   difference is that more than one can sit open, which harms nothing. */
/* No wash here on purpose. See THE ATMOSPHERE: the FAQ is the trough that makes the
   closing material land. */
.faq-list{max-width:56ch; margin:var(--s6) auto 0}
.faq-item{background:var(--warm); border:1px solid var(--line); border-radius:10px;
  margin-bottom:10px; overflow:hidden}
.faq-item[open]{background:var(--paper); box-shadow:var(--lift)}
.faq-item summary{list-style:none; cursor:pointer; display:flex; align-items:center;
  justify-content:space-between; gap:var(--s4); padding:17px var(--s4);
  font-size:16px; font-weight:600; letter-spacing:-.01em; color:var(--ink)}
.faq-item summary::-webkit-details-marker{display:none}
.faq-item summary:focus-visible{outline:2px solid var(--ink); outline-offset:-3px}
/* The marker is drawn, not typed: a plus that loses its upright to become a minus. */
.faq-mk{position:relative; flex:none; width:14px; height:14px}
.faq-mk::before, .faq-mk::after{content:""; position:absolute; background:var(--ink-soft);
  transition:transform .3s cubic-bezier(.4,0,.2,1), opacity .3s}
.faq-mk::before{left:0; top:6.5px; width:14px; height:1.5px}
.faq-mk::after{top:0; left:6.5px; width:1.5px; height:14px}
.faq-item[open] .faq-mk::after{transform:scaleY(0); opacity:0}
.faq-item .a{padding:0 var(--s4) 19px; margin:0; font-size:15.5px; line-height:1.62;
  color:var(--ink-2); max-width:52ch}
/* One answer runs to two paragraphs as of 2026-08-11. This rule was written assuming one, so
   without it the paragraph break got the same 19px the whole block uses to sit off its bottom
   edge, and the two read as separate answers rather than one. */
.faq-item .a:not(:last-of-type){padding-bottom:14px}

/* Open/close motion (2026-08-10). Native <details> animates nothing on its own, and the
   support for animating it is still uneven, so this is built in three layers and each one
   degrades to the one below it.

   1. The panel's own surface eases between closed and open. Works everywhere.
   2. The answer fades and settles the last few pixels. Works everywhere. On a browser with
      no height animation the row still snaps, but the text ARRIVES instead of appearing,
      which is most of the perceived smoothness for none of the risk.
   3. The height eases, which is the layer that actually reads as a dropdown. It needs both
      `interpolate-size` (nothing can animate TO `auto` without it) and the
      `::details-content` pseudo, so it is feature-detected and simply absent where either
      is missing. Nothing breaks in that case: the row opens instantly, which is exactly
      the behaviour that shipped before this block existed.

   Deliberately NOT JavaScript. Zero-JS is the whole reason the FAQ is a <details name>:
   a scripted accordion would cost the keyboard handling, the exclusive-open behaviour and
   the semantics that come free here, in exchange for an animation. Bad trade. */
.faq-item{transition:background-color .3s ease, box-shadow .3s ease}
.faq-item .a{opacity:0; transform:translateY(-3px);
  transition:opacity .28s ease .04s, transform .28s cubic-bezier(.4,0,.2,1) .04s}
.faq-item[open] .a{opacity:1; transform:none}

@supports selector(::details-content) and (interpolate-size: allow-keywords){
  :root{interpolate-size:allow-keywords}
  .faq-item::details-content{block-size:0; overflow:hidden;
    transition:block-size .34s cubic-bezier(.4,0,.2,1),
               content-visibility .34s allow-discrete}
  .faq-item[open]::details-content{block-size:auto}
}

@media(prefers-reduced-motion: reduce){
  .faq-mk::before, .faq-mk::after{transition:none}
  .faq-item{transition:none}
  /* Land the answer in its final state rather than freezing it mid-fade. */
  .faq-item .a{transition:none; opacity:1; transform:none}
  .faq-item::details-content{transition:none}
}

/* ============================================================
   E. CONTACT
   Microsoft MAI's footer pattern, ported: the mark large at the foot, link columns
   across, legal set quiet at the right. Their monospaced face is theirs and stays
   theirs; ours is Instrument Sans with wide tracking on the small labels, which is
   where their air actually comes from. The CTA sits at the top of the same block,
   because a contact block that does not convert is a footer with a headline on it.
   The block is held in the closing box (averi.ai), which is the next comment down.
   ============================================================ */
/* ---------- the closing box (Rokas, 2026-08-10, averi.ai reference) ----------
   An inset rounded slab holding the CTA and the whole footer, so the page ENDS rather than
   running out.

   This reverses the previous note here, which argued no fill, no rounded lip, no shadow, on
   the grounds that a warm panel dropped onto a continuous surface is the most visible line
   on the page. That was true of a panel painted OVER the material. The box is built the
   other way round: the field is what fills it. The rounded rect is a hole cut in the page
   ground (the ::before below), so the stone is inside the box and the ground is outside it,
   and the edge is where the material stops rather than where a new fill starts.

   Gutters left and right so it reads as an object on the page, air underneath so it does not
   fuse with the bottom of the viewport. */
.contact{padding:var(--s7) 0 var(--s5);
  /* Clips the cutout's spread to this section. Without it the 100vmax ground ring paints
     over the bottom of the FAQ, which is in flow and therefore underneath. */
  overflow:hidden}
.contact-panel{position:relative; margin:0 var(--s4); padding:var(--s9) 0 var(--s6);
  border:1px solid var(--line); border-radius:20px}
.contact-panel > .wrap{position:relative; z-index:2}
/* The cutout. Ground everywhere outside the rounded rect, nothing inside it, so the field
   behind the page shows through the box and only through the box. inset:-1px puts it on the
   BORDER box (an absolute child is placed against the padding box), so the ring starts at
   the outer edge of the hairline instead of painting over it. */
.contact-panel::before{content:""; position:absolute; inset:-1px; border-radius:inherit;
  box-shadow:0 0 0 100vmax var(--ground); pointer-events:none; z-index:0}

/* ---------- the close ----------
   VERTICAL ONLY. The left wedge that used to be the first layer here now lives in the
   shader, bounded by this block's pool rather than by this block's box. As a DOM layer it
   was `inset:0`, so it ran at full strength on the panel's first row and at nothing on the
   row above it: measured as a 3.7-level step across the left 78% of the page and zero on
   the right, which is precisely a faint line on a dark ground. See "the left wedge" in
   site.js.

   What is left is the run-out: the CTA sits in open material, the small print below it does
   not. Starting at alpha 0 it has no top edge of its own to draw.
   The stops are measured, not chosen. The first version reached .16 behind the link
   columns, which was free on dark (a light texture on a dark ground lifts) and took the
   footer label, the legal block and the base line to 4.38, 4.38 and 4.27 on LIGHT, where
   the same texture is dark. Texture is free until it is behind 12.5px type.

   It runs out to --slab, the box's own surface, NOT to the page ground. Inside a box those
   are two different jobs: fading to the ground would make the bottom half of the box the
   same colour as the page around it, and a box you can only see the top of is not a box.
   Sits inside the border (inset:0 is the padding box), so the hairline stays unpainted.

   Stops pulled earlier when the box went in (2026-08-10). The line under the buttons sits at
   48.8% of the panel, which under the old ramp was still 32% open material, and on LIGHT the
   material is a DARK texture: measured 4.38 on rendered pixels, under AA. Closing the ramp by
   54% puts that line on flat slab at 4.58. It helps both themes rather than trading one for
   the other, because the veil is lighter than the material on white and darker than it on
   dark, and the type inverts with it. */
.contact-panel::after{content:""; position:absolute; inset:0; z-index:1; pointer-events:none;
  border-radius:inherit;
  background:linear-gradient(to bottom, rgba(var(--slab),0) 20%, rgba(var(--slab),.62) 34%,
    rgba(var(--slab),.96) 46%, rgb(var(--slab)) 54%)}
@media(max-width:900px){
  /* Narrow, there is less room above the links, so it closes sooner. And it closes earlier as
     a FRACTION than the desktop ramp, because the box is much taller here relative to what is
     in it: 1404px at 390 against 1000px at 1440, so the same percentage lands far lower in the
     block. Measured under the desktop stops, the line beneath the buttons sat on 34% open
     material and came back at 4.30 on light. */
  .contact-panel{padding-top:var(--s8)}
  /* Re-measured 2026-08-11 after the expansion section went in. The stops below used to be
     5 / 18 / 30 / 36. They did not move because anyone disliked them: the field samples its
     noise in DOCUMENT space, so making the page taller (9.1k to 11.8k) slid the closing box
     through different grain, and `.contact-cta .sub` landed on a darker patch and measured
     4.41 against a 4.5 floor. Stable at 4.40 to 4.41 over three runs, so a real regression and
     not the capture race that used to live in this suite.
     Proved by counterfactual before touching anything: reverting the copy change that also
     landed in this block left it at 4.41, so the page height was the cause, not the copy.
     Set from measurement, not taste. At 390 the panel is 1413px and `.sub` spans 13.1% to
     27.5% of it, so the veil has to be effectively opaque BEFORE 13.1%. It now reaches .97 at
     12%. The h2 above still sits in open material, which is the intended look: the headline
     is in the field, everything under it is on flat slab.
     Desktop is deliberately NOT touched. It passes at 4.81, `.sub` sits lower in a shorter box
     (25.1% of 1060px), and the two ramps have always been calibrated separately. Do not merge
     them into one gradient.
     If the page height changes materially again, re-run box-contrast.py and expect to re-measure
     this. That is the cost of drawing type over a live field, and it is a known one. */
  .contact-panel::after{
    background:linear-gradient(to bottom, rgba(var(--slab),0) 3%, rgba(var(--slab),.80) 8%,
      rgba(var(--slab),.97) 12%, rgb(var(--slab)) 15%)}
}

/* No max-width on the block: it constrained the button row and stacked the two
   buttons vertically. The measure belongs on the text, not on the container. */
/* ---------- typing, on the closing line only (coder.com) ----------
   The whole trick is that nothing may REFLOW while it types. A multi-line headline that
   grows a character at a time rewraps on every frame, and a heading that jumps between
   two and three lines reads as a bug, not as craft. coder solve it with two stacked
   copies of the text, one invisible, holding the box open. One copy is enough: every
   character is already in the DOM and only its INK is withheld, so the line breaks are
   final before the first character lands.
   Armed by JS rather than by default, so a script that never runs leaves a normal,
   fully readable heading instead of an invisible one. */
.tw[data-armed="1"] i{font-style:inherit; opacity:0}
.tw[data-armed="1"] i[data-on="1"]{opacity:1}
/* Zero width, so riding it along the line costs no layout. */
.tw-cur{display:inline-block; width:0; position:relative}
.tw-cur::before{content:""; position:absolute; left:.04em; bottom:.08em;
  width:.5em; height:.62em; background:currentColor}
/* Four blinks, then solid. Anything that blinks indefinitely is auto-updating content
   under WCAG 2.2.2 and would need its own pause control; four cycles stays under the
   five-second threshold and still lands the terminal-caret feel. */
.tw-cur[data-done="1"]::before{animation:twBlink 1.1s steps(1) 4}
@keyframes twBlink{0%,49%{opacity:0} 50%,100%{opacity:1}}
.contact-cta h2{margin-bottom:var(--s4)}
.contact-cta .sub{max-width:44ch}
.contact-cta .actions{margin-top:var(--s5)}
.contact-cta .fine{color:var(--ink-faint); font-size:13px; margin:var(--s5) 0 0}

.foot-grid{display:grid; grid-template-columns:minmax(0,1fr) auto auto minmax(0,.9fr);
  gap:var(--s6); margin-top:var(--s8); align-items:start}
/* One line, mark beside the wordmark rather than stacked above it (Rokas). Baseline is
   set by hand: optically the V wants to sit slightly below the cap line of the text, and
   align-items:center puts it a touch high against a face with this much overshoot. */
.foot-mark{display:flex; align-items:center; gap:14px}
.foot-mark svg{width:30px; height:30px; flex:none; margin-top:2px}
.foot-mark b{font-size:26px; font-weight:600; letter-spacing:-.035em; line-height:1.1;
  white-space:nowrap}
@media(max-width:420px){ .foot-mark b{font-size:22px} }
.foot-col{display:flex; flex-direction:column; gap:12px}
.foot-col .lbl{font-size:11px; letter-spacing:.09em; text-transform:uppercase;
  color:var(--ink-faint); margin:0 0 2px}
.foot-col a, .foot-col span.v{font-size:15px; color:var(--ink-2); text-decoration:none;
  letter-spacing:-.005em; transition:color .2s}
.foot-col a:hover{color:var(--ink)}
/* Tap target, measured 2026-08-12: 23px, one pixel under the WCAG 2.5.8 AA floor of 24.
   Same padding-block + equal negative margin-block as the nav CTA, so the hit area becomes
   35px and NOTHING MOVES: the +12px of padding and the -12px of margin cancel exactly, so each
   item still occupies 23px and .foot-col's 12px gap is unchanged. The enlarged hit boxes reach
   6px into that gap from either side, meeting without overlapping.
   Scoped to `a` on purpose, not to the `.foot-col a, .foot-col span.v` pair above it: span.v
   is the location text, not a link, and giving a non-target a tap area is noise. */
.foot-col a{padding-block:6px; margin-block:-6px}
.foot-legal{font-size:12.5px; color:var(--ink-faint); line-height:1.6; max-width:44ch}
.foot-legal p{margin:0 0 var(--s3)}
.foot-legal p:last-child{margin:0}
.foot-base{display:flex; justify-content:space-between; align-items:center; gap:var(--s4);
  flex-wrap:wrap; margin-top:var(--s7); padding-top:var(--s4);
  box-shadow:inset 0 1px 0 var(--line); font-size:12.5px; color:var(--ink-faint)}
@media(max-width:1000px){
  .foot-grid{grid-template-columns:1fr 1fr; gap:var(--s5) var(--s6)}
  .foot-mark{grid-column:1 / -1}
  .foot-legal{grid-column:1 / -1; max-width:60ch}
}
@media(max-width:760px){
  /* Narrow gutters, or the box eats the measure it is holding. */
  .contact{padding:var(--s6) 0 var(--s3)}
  .contact-panel{margin:0 var(--s3); border-radius:14px; padding-top:var(--s7)}
  .foot-grid{grid-template-columns:1fr; gap:var(--s5); margin-top:var(--s7)}
}

/* ============================================================
   THE ARTICLE
   One guide, one page. Lives here rather than in a per-page <style> block because there are
   four of these and a copy in each is four things that drift; the guides index keeps its own
   block because it is the only page shaped like a list.

   The article header deliberately carries .gl-head as well as .art-head. That is not
   decoration: site.js anchors the field's opening pool on .gl-head for any page without a
   .hero, so reusing the class is what puts an article in the same material as the other two
   pages. Change the class here and the page goes flat.

   (This block described a 96px rail beside the text until 2026-08-11. That rail is GONE, see the
   note on .art-head below. The stale description survived the restructure and was caught in a
   second audit pass, which is exactly how a later edit ends up restoring a dead assumption.)
   ============================================================ */
/* RESTRUCTURED 2026-08-11 off microsoft.ai's article template (Rokas), measured on
   /news/mai-image-2-6-.../ at 1440. What was there: title 56.9px, weight 400, leading 1.0,
   CENTRED at 64% of the viewport, then a 123px gap, then a meta strip running nearly the full
   page width (date at the far left gutter, category, authors, share at the right), then a body
   column of 683px (47%) CENTRED under it.
   The shape is what was taken. Their serif, their palette and their author bylines were not:
   we have one face, and we have no bylines to print. The fourth meta slot carries "All guides"
   instead of share links, which is a real destination rather than a button that needs a live
   URL to work.
   The old layout was a sticky 96px rail beside the text. It is gone: a rail on the left and a
   centred title above it were fighting for the same axis. */
.art-head{padding:calc(var(--s9) + 34px) 0 var(--s5); position:relative; text-align:center}
/* Not h1's 88px. That size is built for two or three words, and every title here is a
   sentence: at 88px they wrap to four lines and read as a headline rather than a title.
   Weight 500 rather than our usual 600, and leading pulled to 1.02: at this size the lighter,
   tighter cut is what reads as a title rather than as an advert. */
/* 22ch, MEASURED not calculated: `ch` is the width of the zero glyph at this element's own
   font-size, which is ~0.67em in Instrument Sans, so 22ch lands at ~890px (62% of a 1440
   viewport) against the reference's 917px (64%). At 17ch it was 685px and every title wrapped
   to three lines where theirs takes two. Re-measure if the face or the size changes; do not
   assume ch converts the way it reads. */
.art-head h1{font-size:clamp(34px,4.6vw,60px); font-weight:500; line-height:1.02;
  letter-spacing:-.035em; max-width:22ch; margin:0 auto var(--s4); text-wrap:balance}
.art-head .lede{max-width:52ch; margin:0 auto; text-wrap:pretty}

/* The meta strip. Four groups on one line, spread across the full measure, small and tracked.
   space-between rather than a fixed grid so it stays even whatever each label's length is. */
.art-meta{display:flex; flex-wrap:wrap; justify-content:space-between; align-items:baseline;
  gap:var(--s3) var(--s5); margin-top:var(--s7); padding-top:var(--s4);
  border-top:1px solid var(--line);
  font-size:11px; letter-spacing:.08em; text-transform:uppercase; color:var(--ink-faint)}
/* Tap target, measured 2026-08-12: "All guides" was 17px tall in the article meta strip, well
   under the 24px floor, and it is the only navigation out of an article besides the nav. */
.art-meta a{color:var(--ink-faint); text-decoration:none; transition:color .3s ease; padding-block:6px; margin-block:-6px}
.art-meta a:hover{color:var(--ink)}
/* Four across does not fit a phone. Left to flex-wrap it broke 3 + 1, with the first row spread
   edge to edge by space-between and one orphan under it. A 2x2 grid is the same information in
   an even block. */
@media(max-width:620px){
  .art-meta{display:grid; grid-template-columns:1fr 1fr; gap:var(--s3) var(--s4)}
}

/* position:relative + z-index IS THE BUG FIX, not tidying. Do not remove it.
   #field is a FIXED, OPAQUE canvas (`alpha:false`) at z-index:0. CSS paints a positioned
   z-index:0 element ABOVE the inline text of non-positioned elements, so the field was
   painting straight over this article's body copy: every paragraph present in the DOM at
   opacity 1 and visibility visible, and zero ink on the screen.
   It went unnoticed from 2026-08-11 to 2026-08-12 because the template lifted the two blocks
   either side of the body and never the body itself: `.art-head` has position:relative and
   `.contact-panel > .wrap` has z-index:2, so the title and the footer rendered and only the
   middle vanished, which looks like a short page rather than a broken one.
   Lifted on `.art` rather than on `.art-body` so the asides, the rows and the next-link come
   with it in one rule, matching how `.hero .inner` and `.contact-panel > .wrap` do it.
   z-index:1 clears the field (0) and stays under the grain overlay (55), the nav (60) and the
   theme button (70), all of which are meant to sit on top.
   Guarded by site/verify/article-ink.js, which measures PAINTED PIXELS rather than the DOM,
   because every existing audit passed with the text invisible. */
.art{padding:var(--s6) 0 var(--s8); position:relative; z-index:1}
/* 700px, not a ch value. The reference's column is 683px and `ch` here would be measured
   against .art-body's INHERITED font-size rather than the 18.5px its paragraphs actually use,
   which is the same trap that made headings elsewhere on this site wrap unexpectedly. */
.art-body{max-width:700px; margin:0 auto}
/* 18.5px, up from 17.5. The reference sets its body at 21.3px, which is generous even for a
   serif; this closes some of that gap without the column having to grow past 700px. */
.art-body > p{font-size:18.5px; line-height:1.65; color:var(--ink-2); margin:0 0 var(--s4)}
.art-body > p:last-child{margin-bottom:0}
.art-body h2{font-size:clamp(23px,2.5vw,30px); line-height:1.2; letter-spacing:-.03em;
  max-width:26ch; margin:var(--s6) 0 var(--s3)}
.art-body h3{font-size:19.5px; line-height:1.3; letter-spacing:-.02em;
  margin:var(--s5) 0 var(--s2)}
.art-body strong{font-weight:600; color:var(--ink)}
.art-body ul, .art-body ol{margin:0 0 var(--s4); padding-left:1.15em}
.art-body li{font-size:17.5px; line-height:1.6; color:var(--ink-2); margin:0 0 12px}
.art-body li::marker{color:var(--ink-faint)}
.art-body li:last-child{margin-bottom:0}
.art-body a{color:var(--ink); text-underline-offset:3px; text-decoration-thickness:1px;
  text-decoration-color:var(--line)}
.art-body a:hover{text-decoration-color:var(--ink)}

/* A said-out-loud aside. A rule on the left rather than a tinted box: the page paints no
   panels anywhere else and one here would be the only filled thing on it. */
.art-aside{margin:var(--s5) 0; padding:2px 0 2px var(--s4);
  box-shadow:inset 2px 0 0 var(--line)}
.art-aside p{font-size:16.5px; line-height:1.6; color:var(--ink-soft); margin:0 0 12px;
  max-width:58ch}
.art-aside p:last-child{margin:0}

/* The measure rows. This is a table by nature (a thing, and what to count for it), but a
   real table gives you a horizontal scrollbar on a phone for two columns of prose, so it is
   built as rows that stack instead. */
.art-rows{margin:var(--s5) 0; border-top:1px solid var(--line)}
.art-row{display:grid; grid-template-columns:minmax(0,10.5em) minmax(0,1fr); gap:var(--s4);
  padding:var(--s3) 0; border-bottom:1px solid var(--line); align-items:baseline}
.art-row > b{font-weight:600; font-size:16px; line-height:1.4; color:var(--ink)}
.art-row > span{font-size:16px; line-height:1.55; color:var(--ink-soft)}

/* Foot of the piece. The next thing to read, then the only call on the page that is not the
   closing block. */
.art-next{margin:var(--s7) 0 0; padding-top:var(--s4); border-top:1px solid var(--line)}
.art-next .lbl{font-size:10.5px; letter-spacing:.08em; text-transform:uppercase;
  color:var(--ink-faint); margin:0 0 var(--s3)}
.art-next a{display:block; text-decoration:none; color:var(--ink); font-size:18px;
  font-weight:500; line-height:1.35; letter-spacing:-.02em; margin:0 0 10px; max-width:44ch}
.art-next a:hover{color:var(--ink-soft)}
.art-next a:last-child{margin:0}

@media(max-width:760px){
  .art-head{padding:calc(var(--s8) + 30px) 0 var(--s4)}
  .art{padding:var(--s5) 0 var(--s7)}
  .art-grid{grid-template-columns:1fr; gap:var(--s4)}
  /* Sticky is meaningless once the rail is a line of text above the piece. */
  .art-rail{position:static; flex-direction:row; gap:var(--s4)}
  .art-body > p, .art-body li{font-size:17px}
  .art-row{grid-template-columns:1fr; gap:4px; padding:var(--s3) 0}
}

@media(prefers-reduced-motion:reduce){
  /* End state applied outright, never a frozen half-state. */
  html{scroll-behavior:auto}
  *{transition:none !important; animation:none !important}
  .bd-body .bd-r{opacity:1; transform:none}
  .tr-step{opacity:1}
  .tr-step .mk2{background:var(--ink); border-color:var(--ink)}
  .tr-step .mk2 svg{opacity:1}
  .tr-step .tout{opacity:1; transform:none}
  .tr-sum{opacity:1; transform:none}
}
