/* ═══════════════════════════════════════════════════════════════════
   Austra Cukura — portfolio stylesheet

   Paper is the material. Nothing is boxed, nothing is decorated, there
   are no lines and no second colour. One green mark is the only
   saturated thing on the site, and a piece of work earns it when you
   point at one.

   THE LADDER, and nothing off it:
     16 within a group · 32 between groups · 48 between blocks ·
     96 between sections
   Four base rungs beneath that for component interiors: 4 · 8 · 12 · 24.
   Every vertical value on the site is one of these eight numbers.
   ═══════════════════════════════════════════════════════════════════ */
@font-face { font-family:"Fraunces"; src:url("fonts/fraunces-var.woff2") format("woff2"); font-weight:100 900; font-style:normal; font-display:swap; }
@font-face { font-family:"Fraunces"; src:url("fonts/fraunces-var-italic.woff2") format("woff2"); font-weight:100 900; font-style:italic; font-display:swap; }
@font-face { font-family:"Archivo"; src:url("fonts/archivo-var.woff2") format("woff2"); font-weight:400 700; font-style:normal; font-display:swap; }

:root{
  /* Paper, ink, one green. Every ratio below measured against --paper. */
  --paper:   #F4EFE4;
  --ink:     #1A1813;   /* 15.47:1 */
  --ink-2:   #555046;   /*  6.98:1 */
  --ink-3:   #6E6659;   /*  4.94:1 — the quietest ink that still clears AA */
  --hairline:#E4DFD4;
  --hairline-2:#C9C2B4;
  --green:   #255A3E;   /*  7.01:1 */
  --focus:   #255A3E;

  --rail:     1024px;   /* the reading rail — content width, inset sits outside it */
  --gutter:     48px;   /* outer inset; never narrower than the column gutter */
  --header-h:   56px;

  /* THE LADDER */
  --xxs:      4px;
  --xs:       8px;
  --sm:      12px;
  --within:  16px;   /* a line and the line under it */
  --lg:      24px;   /* component interiors */
  --rows:    32px;   /* between rows inside a section */
  --between: 32px;   /* one group and the next */
  --xxl:     48px;   /* the deep step: hero air, footer tail */
  --section: 96px;   /* one section and the next — the deep step */
  /* THE STORY'S VERTICAL LADDER (Anna, 11 Sep 2026: "make it part of the
     design system throughout… it seems very random"). Every text box on
     this site is trimmed to cap-height and baseline, so these numbers ARE
     the air you see, and a line inside a 16/24 paragraph already shows
     about 12. Each relationship takes one step and each step doubles, so
     no two relationships can ever look alike:
       a paragraph and the next ......... 24  (a clear beat over the ~12 line)
       a heading and its own text ....... 24  (held to what it heads)
       text and the evidence it cites ... 48  (two kinds of thing meeting)
       one section and the next ......... 96  (the same step as header → story)
     They scale with the canvas below, never the paragraph step: the type
     does not change size, so neither does the air inside the argument. */
  --step-para:    24px;
  --step-head:    24px;
  --step-figure:  32px;   /* 48 for Edgefolio only, see .case-edgefolio below. 32 from 14 to 15 Sep 2026; back to 48 on Anna's "increase spacing here by half" at two caption-to-next gaps, 15 Sep */
  --step-section: 64px;   /* 96 until 14 Sep; 48 for part of 15 Sep; back to 64 on Anna's "increase the vertical spacing here by half" at the section break, 15 Sep 2026 */
  --col:     48px;   /* horizontal only — the column gutter */

  /* RADIUS — ONE VALUE FOR EVERY PICTURE ON THE SITE: 8. (Anna, 26 Aug 2026:
   "apply a corner radius to images of 8px, keep it consistent throughout".)
   16 is kept only for the frames that HOLD pictures — a browser chrome, a
   phone body, a demo panel — because those are objects with their own
   corners, and the picture inside them stays square so two radii never sit
   one inside the other. */
  --radius:     8px; /* Anna's ruling 27 Aug 2026: corner radius is 8px sitewide */
  --radius-sm:  8px;

  /* MOTION — two curves, three durations, nothing else. */
  --ease:       cubic-bezier(0.25, 0.46, 0.45, 0.94);  /* state   */
  --ease-enter: cubic-bezier(0.165, 0.84, 0.44, 1);    /* entrance */
  --dur-1: 100ms;   /* colour            */
  --dur-2: 160ms;   /* transform, filter */
  --dur-3: 400ms;   /* entrance          */
  --stagger: 100ms;
}
*{ box-sizing:border-box; }
html,body{ margin:0; padding:0; }
body{
  background:var(--paper); color:var(--ink-2);
  font-family:"Archivo", -apple-system, "Helvetica Neue", Arial, sans-serif;
  font-size:16px; line-height:24px; letter-spacing:-0.011em;
  -webkit-font-smoothing:antialiased; overflow-x:hidden;
}
img{ max-width:100%; height:auto; display:block; }

/* ── The rhythm is measured in ink, so trim the box to the ink ──────
   Half-leading — (line-height − font-size) / 2 — sits invisibly inside
   every gap and is a different size at every type size, so a declared 48
   can read as anything from 59 to 70. Trimming to cap-height and baseline
   makes a declared number mean what it says. Everything is trimmed,
   controls included: exempting them put two different pauses on one
   declared step. Unsupported browsers keep the older, looser rhythm —
   nothing moves out of place. */
:where(h1,h2,h3,h4,p,li,figcaption,blockquote,dd,dt,button,.writing .t,.writing .d){
  text-box: trim-both cap alphabetic;
}
/* Trimming costs the row its tap target, so the target comes back as a hit
   area that adds no layout height: the row's box is 35, the overlay takes
   the finger to 51, and 32 between rows leaves 16 clear so no two touch. */
.writing a{ position:relative; }
.writing a::before{ content:""; position:absolute; inset:-8px 0; }

/* ── How a picture sits on the paper ──────────────────────────────
   No border, no shadow, no outline — the page decides the box and nothing
   else. A shadow is a second signifier doing the job the paper already does. */
.pic{ border-radius:var(--radius-sm); }

/* The one exception, and it is conditional. An artwork gets a 1px edge only
   when it has none of its own: the white book cover renders at 1.00:1
   against the paper, so it does not look faint, it stops existing. This is
   not separating two things, it is saying where one thing ends. */
.pic.edged{ box-shadow:0 0 0 1px var(--hairline-2); }

/* ═══════════════════════════════════════════════════════════════════
   THE HORIZONTAL GRID — twelve columns on the rail, one gutter, no
   exceptions. A row that invents its own ratio is the horizontal
   equivalent of a gap that is off the ladder.
   At the 1024 rail: 11 gutters of 48, 12 columns of 40.67. A 4-span is
   293, an 8-span is 634, a 3-span is 210 — all on a real boundary.
   ═══════════════════════════════════════════════════════════════════ */
.grid{
  display:grid; grid-template-columns:repeat(12, minmax(0,1fr));
  gap:var(--within) var(--col); align-items:start;
}
/* An out-of-flow picture contributes no height, so in a start-aligned row it
   resolves to zero. This is the sanctioned way to ask for a stretched cell —
   there is no other, and min-height stays banned. */
.grid.stretch{ align-items:stretch; }
.c3{ grid-column:span 3; min-width:0; }
.c4{ grid-column:span 4; min-width:0; }
.c5{ grid-column:span 5; min-width:0; }
.c6{ grid-column:span 6; min-width:0; }
.c7{ grid-column:span 7; min-width:0; }
.c8{ grid-column:span 8; min-width:0; }

/* READING WIDTHS, MEASURED IN COLUMNS — never in characters.
   A `ch` cap is a perfectly good typographic measure, but this site decides
   its widths on twelve columns, and a block that stops at 58ch lands its
   right edge between two column lines: on nothing. These land on one.
   N of the twelve, for a block sitting directly on the rail: */
:root{
  --read-5: calc((100% - 11 * var(--col)) / 12 * 5 + 4 * var(--col));
  --read-6: calc((100% - 11 * var(--col)) / 12 * 6 + 5 * var(--col));
  --read-7: calc((100% - 11 * var(--col)) / 12 * 7 + 6 * var(--col));
  --read:   calc((100% - 11 * var(--col)) / 12 * 8 + 7 * var(--col));
  /* On the twelve, the seven-column cell IS the reading measure, so the text
     inside it needs no cap of its own. This token is what changes that when
     the grid collapses — as a token, not as a rule, so nothing has to win a
     specificity fight with the page section further down. */
  --reading: none;
}
/* The same widths for a block nested inside a SEVEN-column cell, where 100%
   is 7 columns rather than 12. Derived from the grid, not guessed:
     5 of 12 inside 7 = 5/7 x (100% - 6g) + 4g
     6 of 12 inside 7 = 6/7 x (100% - 6g) + 5g                            */
:root{
  --in7-5: calc(71.4286% - 0.2857 * var(--col));
  --in7-6: calc(85.7143% - 0.1429 * var(--col));
}

/* Card grids run 3-up at desktop, 2-up at tablet, 1-up at phone. */
@media (max-width:1023px){
  .grid{ grid-template-columns:repeat(6,minmax(0,1fr)); }
  .c3{ grid-column:span 3; }                     /* four-up becomes two-up */
  .c4,.c5,.c6,.c7,.c8{ grid-column:span 6; }     /* every split becomes full width */
}
@media (max-width:767px){
  .grid{ grid-template-columns:1fr; gap:var(--within); }
  .c3,.c4,.c5,.c6,.c7,.c8{ grid-column:span 1; }
}

a{ color:inherit; text-decoration:none; }
/* THE RAIL. 1024 is the CONTENT width and the 48 inset sits outside it. */
.rail{
  width:100%; max-width:calc(var(--rail) + 2 * var(--gutter));
  margin-inline:auto; padding-inline:var(--gutter);
}
/* THE OUTER AIR MUST BEAT THE INNER AIR. A constant 48 inset at a 1000px
   window is 4.8% — narrower than the 48 between two columns — and the page
   then reads as flush left with the text running off the right, however
   symmetrical the box is. So below the 1120 cap the rail shrinks with the
   window instead of holding a constant inset. The two behaviours meet
   continuously: 90% equals 1120 at 1244px, so there is no jump.
   Phones are excluded on purpose — a 375 screen wants its content edge to
   edge, and its 24 inset is already a fifteenth of the window. */
@media (min-width:768px){
  .rail{ width:min(90%, calc(var(--rail) + 2 * var(--gutter))); }
}
/* FAMILY ONLY — no font-variation-settings here, deliberately.
   `font-variation-settings` replaces the whole axis list rather than merging
   it, so a shared value here silently drops `opsz` from every heading that
   carries the class. Each size role declares its own complete axis list. */
.serif{ font-family:"Fraunces", Georgia, "Times New Roman", serif; }

/* ── Nav ─────────────────────────────────────────────────────── */
/* STICKY. The bar pins to the top of the viewport as the page scrolls
   beneath it. At rest there is no line — the page opens with the nav
   reading as part of the paper, not boxed off from it. The hairline is
   earned only once content is passing underneath: Anna's ruling, 27 Aug
   2026. <html>.nav-scrolled (toggled by controls.js off window.scrollY)
   gates it, so it fades in on ::after's own opacity rather than existing
   from paint one. z-index clears every stacking context found in this file
   and in the four case pages' inline styles — the highest anywhere is 3
   (Edgefolio's .cw-rule) — so 100 leaves clear headroom without being an
   arbitrary four-digit number.
   FULL-BLEED, NOT RAIL-WIDTH. nav.rail IS a .rail box — capped and centred
   like the content it sits above — so its own background and border would
   stop short of the viewport edge and let scrolling content peek past both
   sides. Two pseudo-elements, sized 100vw and re-centred with the
   left:50%/-50vw trick, sit behind (::before, the paper) and under
   (::after, the line) nav-inner's real content instead, so the sticky
   surface and the line both run true edge to edge while .nav-inner keeps
   the untouched .rail box for alignment — the wordmark and links never
   shift. */
nav.rail{
  position:sticky; top:0; z-index:100;
}
nav.rail::before{
  content:""; position:absolute; top:0; bottom:0; left:50%;
  width:100vw; margin-left:-50vw; background:var(--paper); z-index:-1;
}
nav.rail::after{
  content:""; position:absolute; left:50%; bottom:0;
  width:100vw; margin-left:-50vw; height:1px; background:var(--hairline);
  opacity:0; transition:opacity var(--dur-1) var(--ease);
}
html.nav-scrolled nav.rail::after{ opacity:1; }
.nav-inner{ display:flex; align-items:center; justify-content:space-between; height:var(--header-h); }
/* THE MARK IS ONE LETTER AND A DOT, AT 44. Her own size from the live site,
   restored 26 Aug 2026 — at 16px it was barely visible, because a single
   lowercase "a" carries none of the width a wordmark uses to hold its place.
   It is italic, which is what ties it to the italic in her name below it, and
   it takes the 48 cut because the optical axis answers the size the type is
   actually set at: at opsz 15 a 44px letter is drawn with the cut made for a
   caption and goes thick and flat. WONK 1 keeps the canted alternates.
   Line-height 1 so 44 of glyph sits inside the 56 header with 6 either side. */
.mark .serif{ font-variation-settings:"opsz" 48,"SOFT" 0,"WONK" 1; }
/* 32, NOT 44. 44 was right on the landing page and only there, because the
   masthead under it is 172 and the step reads 4:1. On About the h1 is 64, so
   44 came out at 1.45:1 — a nav mark and a headline in the same face, the
   same italic and the same green dot, within 58px of each other and near
   enough in size that the eye met two marks and picked neither.
   A mark's size has to work against the SMALLEST headline it ever sits above,
   not the largest. At 32 it is 2:1 on About and 5.4:1 on the landing page:
   a clear step on both, and still twice the 16 that read as barely there. */
.mark{ display:inline-flex; align-items:center; min-height:44px; flex:0 0 auto;
  white-space:nowrap; font-style:italic; font-size:32px; line-height:1;
  color:var(--ink); letter-spacing:-0.02em;
  transition:color var(--dur-1) var(--ease); }
/* The wordmark used to be the one link on the site with no hover answer. The
   dot is already green, so the letter joins it rather than the mark changing
   colour all at once. No underline: a logo is not a body link. */
.mark:hover, .mark:focus-visible{ color:var(--green); }
/* 44px HIT AREA, not layout: the glyph is 23 wide, and the finger gets the
   same overlay the nav links and Writing rows already use (closes the
   27 Aug tap-target blocker in this stylesheet). */
.mark{ position:relative; }
.mark::after{ content:""; position:absolute; top:50%; left:50%;
  width:44px; height:44px; transform:translate(-50%,-50%); }
.mark .dot{ color:var(--green); }
@media (max-width:767px){ .mark{ font-size:32px; } }

/* One designed focus state everywhere. Nothing falls back to the browser. */
a:focus-visible, button:focus-visible{
  outline:2px solid var(--focus); outline-offset:4px; border-radius:2px;
}
/* Skip link: off-screen until focused, then it lands on the paper */
.skip{
  position:absolute; left:-9999px; top:0; z-index:10;
  background:var(--paper); color:var(--ink); padding:12px 16px;
  font-size:16px; line-height:24px;
}
.skip:focus{ left:var(--gutter); top:var(--within); }
.nav-links{ display:flex; align-items:center; gap:var(--between); flex:0 0 auto; white-space:nowrap; }
/* The 44px target is a HIT AREA, not layout height — the same rule the
   Writing rows and the reading-depth control already follow. As min-height
   the link stood 44 tall around 20 of text, so the "you are here" line drew
   at the bottom of the TAP TARGET rather than under the words. Now the box
   is the text, exactly like the footer's underline, and the overlay takes
   the finger: 22 of box plus 12 above and below is 46. */
.nav-links a{
  display:inline-flex; align-items:center; position:relative;
  padding-bottom:2px; margin-bottom:-2px;   /* the line's 2px is handed back */
  font-size:13px; line-height:20px; color:var(--ink-3);
  transition:color var(--dur-1) var(--ease), background-size var(--dur-2) var(--ease);
}
.nav-links a::before{ content:""; position:absolute; inset:-12px -8px; }
/* -8px sideways too (panel, 30 Aug): "Work" is 29px of glyph, and the old
   overlay only grew the vertical axis. 29+16 clears 44; the 32px nav gap
   leaves 16 clear between neighbouring hit areas. */
.nav-links a:hover,.nav-links a:focus-visible{ color:var(--ink); }
/* WHERE YOU ARE. Both destinations are always shown — the nav never
   reshuffles. The page you are on takes the green and a 1px rule in the same
   green: the only place on the site where the accent does two jobs at once,
   because "you are here" is worth one. A case study lives under Work, so
   Work carries aria-current="page" there too (Anna, 27 Aug 2026) — same
   idiom as About, not a parallel mechanism. */
.nav-links a[aria-current="page"]{
  color:var(--green); padding-bottom:2px;
  background-image:linear-gradient(var(--green),var(--green));
  background-size:100% 1px; background-repeat:no-repeat; background-position:0 100%;
}
.nav-links a[aria-current="page"]:hover{ color:var(--green); }

/* ── Hero ────────────────────────────────────────────────────────
   THE TYPE RULE. Fraunces sets what is SPOKEN OR TITLED — the hero line,
   section and project titles, and any quotation, because a quotation is
   somebody else's voice and the change of face is what says so. Archivo
   sets what is READ: body, standfirst, labels, meta, captions, properties
   rows. A fourth serif role covers figures, below. Nothing else.
   Two families both set at display size is the font party — the eye cannot
   tell which one is the voice. */
/* THE MASTHEAD IS ONE THING, THE PAGE IS ANOTHER. Both gaps around the hero
   were the section step, so the opening line floated between two identical
   voids with no relationship to either — and on a laptop the content only
   reached the fold. The name belongs WITH the nav that labels the page, so
   that gap is the block step; the break into the evidence below is the
   section step. Unequal, and the bigger one is the real break. */
.hero{ padding-block:var(--xxl); }
h1{
  margin:0; font-weight:400; letter-spacing:-0.022em;
  font-size:64px; line-height:72px;   /* not set solid: Fraunces's descenders
       are deep, and at 64/64 line one landed in the ascenders of line two.
       72 is the next rung on the 4-grid that clears them. */
  color:var(--ink); max-width:none; text-wrap:pretty;   /* the rail sets the measure, not a ch cap */
  /* THE OPTICAL SIZE. Unset, a 64px headline is drawn with the cut made for
     15px — thick stems, low contrast — and reads as heavy and dated. Measured:
     the same word is 329px unset and 288px at opsz 144.
     WONK 1 keeps the canted alternates: the crooked M is the point, and the
     one place the face is allowed personality. SOFT 0 keeps terminals sharp.
     WONK 1 now runs on EVERY Fraunces role including the small ones (Anna,
     27 Aug 2026). The earlier note here said the small roles keep WONK 0
     because "at caption size the wonk reads as a wobble rather than a
     choice" — that was my judgement, not her ruling, and it produced a site
     where the 90POE figure "$500M" carried a straight M while every heading's
     was canted. One face, one setting. */
  font-variation-settings:"opsz" 144,"SOFT" 0,"WONK" 1;
}
/* THE EMPHASISED WORD IN THE HERO IS ITALIC AND GREEN. Her rule from the live
   site, restored 26 Aug 2026: "cukura" in the masthead and "austra" on About
   both carry it. The green is normally reserved for the full stop and the
   focus ring, and this is the one other place it is earned — it is her own
   name, once per page, at the largest size on it.
   Italic + 600 + green is three signals on one word, which the one-signifier
   rule would normally refuse. It stands because she ruled it, and because the
   word is a proper noun rather than emphasis inside a sentence. */
h1 em{
  font-style:italic; font-weight:600;
  color:var(--green); letter-spacing:-0.03em;
}
h1 .dot{ color:var(--green); }
/* The standfirst: one short line, muted, left. Not a properties row —
   pushing a lone word to the far right orphans it, and 64px straight down
   to 13px is a step the eye cannot climb. */
.lede{
  margin:var(--between) 0 0; text-wrap:pretty;
  /* Capped by the GRID, not by characters. Seven columns is about 64
     characters at 18px — inside the 45–75 the measure wants, and it ends on
     a real column line rather than between two. */
  max-width:calc((100% - 11 * var(--col)) / 12 * 7 + 6 * var(--col));
  font-size:18px; line-height:28px; color:var(--ink-3);
}

/* ── Section heads ───────────────────────────────────────────────
   Titles are the serif, one size everywhere. opsz 48 rather than 144: the
   optical axis answers the size the type is actually set at, and a 32px
   title drawn with the 64px cut goes spindly. 36 leading is 32 × 1.125,
   the same ratio the h1 uses, so descenders clear at both sizes. */
h2{
  margin:0; font-family:"Fraunces", Georgia, "Times New Roman", serif;
  font-variation-settings:"opsz" 48,"SOFT" 0,"WONK" 1;
  font-weight:400; letter-spacing:-0.022em;
  font-size:32px; line-height:36px; color:var(--ink);
}

/* ── ONE PROPORTIONAL SCALE, AND TYPE AND SPACE STEP TOGETHER ─────
   Stepped, never fluid: a clamp() on a headline changes the ratio between
   headline and body at every window width, so the page is only ever right
   at one size. Stepping the headline ALONE is the same fault one level up —
   the h1 shrinks while the section step holds, and the ratio between the
   biggest thing and the air around it drifts from 6:1 on a monitor to 2.7:1
   on a phone.

     width     h1     h2    lede   section  rows/between  within
     ≥1600     72     36     20       96         32         16
     1024–1599 64     32     18       96         32         16
     768–1023  48     28     18       96         24         16
     <768      36     24     17       48         24         16

   The within-group step never scales: it is set by the type size, and the
   type size barely moves. Section spacing is architectural, belongs to the
   canvas, and scales with it. Narrow rules come last so they win. */
@media (min-width:1600px){
  h1{ font-size:72px; line-height:80px; }
  h2{ font-size:36px; line-height:40px; }
  .lede{ font-size:20px; line-height:30px; }
}
@media (max-width:1023px){
  :root{ --gutter:32px; --between:24px; --step-figure:24px; --step-section:48px; }
  /* THE GRID IS SIX COLUMNS HERE, so a cap written against twelve cuts into
     a column that has already collapsed — measured at 768 the intro fell to
     36 characters, and at 375 to 18. Every split is full width at this size,
     so the text caps land on FOUR OF THE SIX: 70 characters at 13px and 57
     at 16px, both inside the measure. */
  /* Collapsed to six, every split is the whole rail: measured, the bio ran
     to 79 characters at 768 and 108 at 1023. And 13px and 16px cannot share
     one width — a span that reads at 57 characters in the body reads at 86
     in a caption. So the two sizes take different spans of the same six:
       13px → three of the six   (51–63 characters)
       16px → four of the six    (57–69 characters)                        */
  :root{
    --read-5: calc((100% - 5 * var(--col)) / 6 * 3 + 2 * var(--col));
    --in7-5:  calc((100% - 5 * var(--col)) / 6 * 3 + 2 * var(--col));
    --read-6: calc((100% - 5 * var(--col)) / 6 * 4 + 3 * var(--col));
    --in7-6:  calc((100% - 5 * var(--col)) / 6 * 4 + 3 * var(--col));
    --reading: calc((100% - 5 * var(--col)) / 6 * 4 + 3 * var(--col));
    --read:   100%;
  }
  h1{ font-size:48px; line-height:56px; }
  h2{ font-size:28px; line-height:32px; }
}
@media (max-width:767px){
  :root{ --gutter:24px; --section:48px; --between:24px; --rows:24px; --step-section:40px; }
  /* One column: the column IS the cap, and anything narrower is a squeeze. */
  :root{ --read-5:100%; --read-6:100%; --read:100%; --in7-5:100%; --in7-6:100%; --reading:none; }
  h1{ font-size:36px; line-height:44px; white-space:normal; }
  h2{ font-size:24px; line-height:28px; }
  .lede{ font-size:17px; line-height:26px; }
  .nav-links{ gap:var(--within); }
  /* The mark steps with everything else. This block sits LATER in the file than
     the nav's own rules, so it is the one that wins at this width — the stale
     13px left here would have silently undone the 44 above. */
  .mark{ font-size:32px; line-height:1; }
  .hero{ padding-block:var(--between) var(--section); }
}

/* ── Prose ───────────────────────────────────────────────────────
   16 between paragraphs — the within-a-group rung. A block of prose is ONE
   group, and 32 is the step BETWEEN groups: used inside a block it spread
   one argument out until it read as four separate statements. This is the
   same step the case study's story column uses, so the whole site has one
   paragraph rule and not two. */
.prose p + p, .said p + p, .receipt + p{ margin-top:var(--within); }

.prose .build{
  /* Same size as every paragraph around it — a line that stands out by being
     bigger just reads as a heading that forgot to be one. It stands out on
     the two things that cost the page nothing: it takes the step ABOVE the
     paragraph step, so the column pauses before it, and it is the only
     sentence in the column set in full ink. Air and weight, not scale. */
  margin-top:var(--xxl);
  color:var(--ink);
}

/* ── Footer ──────────────────────────────────────────────────────
   No rule above it. The footer is already the last thing on the page and
   the space above it already separates it — a line there was a second
   signifier doing work the space had done. There are no lines at all. */
footer{ border-top:0; }
/* ONE STEP ABOVE THE FOOTER, not three (Anna, 11 Sep 2026). With no line and
   no fill, the footer's own top padding and its row's top padding were
   invisible air stacked on the section step: 96 + 24 + 24 read as 144 below
   the last thing on every page. Now the section step is the whole gap. */
.foot-inner{ display:flex; flex-wrap:wrap; gap:var(--within) var(--col); align-items:center; padding-block:0 var(--xxl); }
.foot-top{ margin-top:var(--section); padding-top:0; }
.foot-inner a{
  display:inline-flex; align-items:center; min-height:44px;   /* touch target */
  font-size:13px; line-height:20px; color:var(--ink-2);   /* same size as the nav links (Anna, 5 Sep 2026) */
  transition:color var(--dur-1) var(--ease);
}
.foot-inner a .u{
  padding-bottom:2px;
  background-image:linear-gradient(var(--green),var(--green));
  background-size:0% 1px; background-repeat:no-repeat; background-position:0 100%;
  transition:background-size var(--dur-2) var(--ease);
}
.foot-inner a:hover,.foot-inner a:focus-visible{ color:var(--green); }
.foot-inner a:hover .u,.foot-inner a:focus-visible .u{ background-size:100% 1px; }
.copy{ font-size:13px; line-height:20px; color:var(--ink-3); }
.foot-links{ display:flex; align-items:center; gap:var(--within) var(--col); margin-left:auto; }
/* The footer heart: ink-3 like the rest of the credit line, never green —
   the green stays reserved for the name's full stop and the focus ring
   (26 Aug 2026 ruling), so this is punctuation, not a second accent. */
.copy .heart{ font-size:1.1em; vertical-align:-0.05em; }

/* Body links: the same underline draw, nothing else. Never on .writing —
   those rows ARE the link and are display:grid, so a line painted as a
   background stretches the whole rail and renders as a horizontal rule
   under every row. */
.prose a, .decide a{
  /* Revised same day: ink, not ink-2 — the same dark as .build and strong,
     so the page reads two colours total (body ink-2, emphasis ink) instead
     of emphasis quietly matching whichever paragraph it sits in. Text
     colour doesn't move on hover either; only the underline goes green. */
  color:var(--ink); padding-bottom:2px;
  background-image:linear-gradient(var(--hairline-2,#C9C2B4),var(--hairline-2,#C9C2B4));
  background-size:100% 1px; background-repeat:no-repeat; background-position:0 100%;
  transition:background-image var(--dur-1) var(--ease);
}
.prose a:hover, .decide a:hover{
  background-image:linear-gradient(var(--green),var(--green));
}

/* ── The entrance ────────────────────────────────────────────────
   400ms, opacity 0→1 with a 4px rise, 100ms between siblings. Four pixels
   is the whole move: it registers as the page settling, never as an
   animation. Opt-in per container, fires once, and the hidden state is
   scoped to `html.rise`, which only JavaScript ever adds — with JS off, or
   before it runs, every element is simply visible. Nothing on this site is
   hidden by a thing that might not load. */
@keyframes rise{ from{ opacity:0; transform:translateY(4px); } to{ opacity:1; transform:none; } }
html.rise [data-rise] > .r{ opacity:0; }
html.rise [data-rise] > .r.in{ animation:rise var(--dur-3) var(--ease-enter) both; }

/* ── Figures ─────────────────────────────────────────────────────
   Every number on the site is set in the serif — the fourth role, and it
   earns it: a figure is the one word in a properties row that is not prose,
   so the other voice is what stops it reading as more grey sans text. It
   stays at the row's own size, because it is a value, not a title.
   Measured: "20%" is 27.0px in Archivo and 26.5px here, so nothing moves. */
.fig{ font-family:"Fraunces",Georgia,"Times New Roman",serif;
      font-variation-settings:"opsz" 14,"SOFT" 0,"WONK" 1;
      font-weight:600; letter-spacing:-0.006em; color:var(--ink); }
.fig-lg{ font-variation-settings:"opsz" 32,"SOFT" 0,"WONK" 1; letter-spacing:-0.014em; }

/* ── The arrow that travels ──────────────────────────────────────
   Same grammar as the dot: a mark after a phrase, reserved at rest,
   answering the pointer — so it uses the same distance and the same curve.
   Allowed in exactly one place: a link that takes you FORWARD. Never on a
   link that opens a thing in place. */
.arrow{ display:inline-block; margin-left:0.24em; color:var(--ink-3);
  transition:transform var(--dur-2) var(--ease), color var(--dur-1) var(--ease); }
@media (hover:hover) and (pointer:fine){
  a:hover .arrow, a:focus-visible .arrow{ transform:translateX(3px); color:var(--ink); }
  a:active .arrow{ transform:none; }
}

/* ── No hover dot. ───────────────────────────────────────────────
   A green full stop used to slide in after a title on hover. It was a SECOND
   hover language competing with the site's only one — the word goes green —
   and it put a full stop on display copy, which is banned everywhere else on
   this site. Struck 26 Aug 2026, never to return.
   `.dot-in` is left as a no-op so any markup still carrying the class simply
   does nothing, rather than falling back to a browser default. */
.dot-in::after{ content:none; }

/* ── ONE HOVER LANGUAGE ──────────────────────────────────────────
   The word goes green and a 1px green line draws in underneath, left to
   right, over --dur-2. Every hoverable thing does this and nothing does
   anything else. The dot stays where it belongs — at rest, in the wordmark
   and after her name — never as a hover reward.
   A Writing row is display:grid and the row IS the link, so the line would
   stretch the full column; justify-self:start shrinks the title cell to its
   own text, and the line then measures the words. */
.writing .t{
  justify-self:start; padding-bottom:2px;
  background-image:linear-gradient(var(--green),var(--green));
  background-size:0% 1px; background-repeat:no-repeat; background-position:0 100%;
  transition:background-size var(--dur-2) var(--ease), color var(--dur-1) var(--ease);
}
.writing a:hover .t{ color:var(--green); background-size:100% 1px; }
/* Keyboard gets it unconditionally — focus is never a false positive. */
.writing a:focus-visible .t{ color:var(--green); background-size:100% 1px; }

/* ── The books ───────────────────────────────────────────────────
   One quiet row of four, in the same eight columns the reading uses, so
   the covers start where the reading starts. Two-up was tried and rejected:
   aligned, but the covers grew to 309 and became the loudest thing on a
   page whose whole argument is one saturated mark. They are texture, which
   is why they are greyscale.
   Written as start / span, not span + start — the shorthand `span 4` sets
   end to auto, and a later grid-column-start then collapses the pair to a
   single column. */
/* Four covers, four three-column spans, across the whole twelve. They were
   pinned to columns 5·7·9·11 to sit under a four/eight split that no longer
   exists — under a five/seven they started one column left of every other
   row on the page. On the twelve they start where the page starts and end
   where it ends, which are the only two lines every row already shares. */
/* A cover at 131px is 23% of its source: it works as texture and carries no
   information. A book row's whole job is naming the books, so the naming
   moves to type, where it is readable at any size. */
.bcap{ margin:var(--within) 0 0; display:block; }
.bcap .bt{ display:block; font-size:13px; line-height:20px; letter-spacing:-0.013em;
           color:var(--ink); text-wrap:balance; }
.bcap .ba{ display:block; font-size:13px; line-height:20px; letter-spacing:-0.013em;
           color:var(--ink-3); }

@media (max-width:767px){
  .foot-links{ margin-left:0; width:100%; }
}

/* Reduced motion means FEWER and GENTLER, not none. Colour, opacity and
   filter still fade — they aid comprehension and move nothing. Every
   transform is dropped and the entrance resolves instantly. */
@media (prefers-reduced-motion:reduce){
  *{ transition-property:color,background-color,background-image,background-size,opacity,filter,outline-color !important; }
  html.rise [data-rise] > .r,
  html.rise [data-rise] > .r.in{ opacity:1; animation:none; transform:none; }
  .dot-in::after{ transform:none; }
}
/* Every control clears 44px on a coarse pointer without anyone having to
   remember it. */
@media (pointer:coarse){
  a, button, [role="button"]{ min-height:44px; }
  /* The nav carries its 44 in the overlay above. Letting the blanket rule
     apply here would put the dead box back and drop the line away from the
     words again — the fault this whole pattern exists to prevent. */
  .nav-links a{ min-height:0; display:inline-flex; align-items:center; }
}
/* Touch fires a false hover on tap, so nothing MOVES on a touch device —
   the dot is simply always there. Gated on the pointer, never on width:
   a narrow desktop window still has a real pointer. */
@media (hover:none){
  .dot-in::after{ opacity:1; transform:none; }
}

/* ═══════════════════════════════════════════════════════════════
   THE WORK — the landing page
   Scoped to body.page-work. Everything above this line is the site;
   everything below it belongs to one page and cannot reach another.
   ═══════════════════════════════════════════════════════════════ */

/* THE MASTHEAD'S AIR IS UNEQUAL, AND THE DEEPER SIDE IS BELOW IT.
   The section step above (96) sets the masthead off from the nav mark; the
   step below has to do more work than that, because it is not separating two
   sections — it is separating the whole opening statement from the evidence,
   which is the biggest break on the page. At an equal 96 the first project's
   properties row read as the masthead's own caption (Anna, 26 Aug 2026).
   So the bottom takes the two deepest rungs together, 96 + 48.
   Both scale with the canvas: on a phone --section is 48, so this is 48 above
   and 96 below, the same relationship one size down. */
/* HERO PROPORTIONS (Anna, 15 Sep 2026: the lede grew to two lines and the
   block "looked a bit off"). The masthead now sits higher: 64 above the
   name, the full section step below, so the text cluster reads as the top
   of the page rather than something floating in the middle of it, and the
   work arrives sooner. 64 = xxl + within, the one step between 48 and 96. */
.page-work .hero{ padding-block:calc(var(--xxl) + var(--within)) var(--section); }
/* Bottom air is ONE section step now, not 96+48 (Anna, 30 Aug: "reduce the
   gap... back to when we started"): the mist's own baked-paper tail already
   supplies ~50px of quiet, so the declared 96 lands the PERCEIVED break at
   the original 144. */

/* ── THE MIST BEHIND THE MASTHEAD (Anna, 30 Aug 2026) ─────────────
   The brand loop sits under everything in the hero; the grain stays above
   it so the fibre reads as the sheet the mist is printed on. The wash holds
   the top 60% at solid paper — the name, lede and whisper never share
   ground with motion, and their contrast is untouched. */
.page-work .hero .mist, .page-about .hero .mist{
  position:absolute; z-index:0; top:calc(-1 * var(--header-h)); bottom:-45%;
  /* Centred WITHOUT a transform (18 Sep 2026). On About the hero carries
     data-rise, so rise.js gives the mist .r.in, and that animation's
     fill-mode holds its own transform — which cancelled translateX(-50%),
     left the box starting at mid-page, and let the page scroll sideways
     (300px at 1440, 188px at 375; body's overflow-x:hidden does not stop
     it). calc() lands the same box and nothing can override it. */
  left:calc(50% - 50vw); width:100vw; transform:none;
  overflow:hidden; pointer-events:none;
  /* SPREAD, NOT A BAND (Anna, 15 Sep 2026: "it looks like a line"). The
     box runs 45% past the hero so the colour has room to dissolve over
     ~300px instead of ~60px, and the mask starts fading at a third of the
     height and reaches nothing only at the bottom. */
  /* No edge and no stain (Anna, 30 Aug, both rulings): the loop itself
     bakes its clean zones — zero colour in the text corner and none in the
     bottom fifth of the frame — so the air before the work is pure paper
     with no boundary to see. Colour lives top-right, dissolving toward
     the name. Cover-cropping shifts the baked zones a little, so a soft
     mask trims the last few percent — it only ever fades colour that is
     already near paper, so no edge can appear. */
  -webkit-mask-image:linear-gradient(to bottom, #000 0%, #000 30%, rgba(0,0,0,0.55) 60%, transparent 100%);
          mask-image:linear-gradient(to bottom, #000 0%, #000 30%, rgba(0,0,0,0.55) 60%, transparent 100%);
}
.page-work .hero .mist video, .page-about .hero .mist video{ width:100%; height:100%; object-fit:cover; display:block; }
.page-about .hero .mist video{ object-position:50% 0; }   /* short hero: keep the loop's top colour, crop its baked-paper bottom */
.page-work .hero .mist::after, .page-about .hero .mist::after{
  content:""; position:absolute; inset:0;
  /* The safety veil runs HORIZONTALLY: all hero type is left-aligned and
     ends inside half the frame, so solid paper holds the text column and
     opens rightwards where the colour lives. */
  background:linear-gradient(to right, var(--paper) 0%, var(--paper) 40%, rgba(244,239,228,0) 85%);
}
@media (max-width:767px){
  /* On a phone the text spans the full width, so the veil turns vertical;
     the centre-crop of the loop keeps its colour in the upper half. */
  .page-work .hero .mist::after, .page-about .hero .mist::after{
    background:linear-gradient(to bottom, var(--paper) 0%, var(--paper) 72%, rgba(244,239,228,0.20) 100%);
  }
}
@media (prefers-reduced-transparency:reduce){ .page-work .hero .mist, .page-about .hero .mist{ display:none; } }
@media print{ .page-work .hero .mist, .page-about .hero .mist{ display:none; } }
/* ── THE DISC (Anna, 24 Sep 2026: "yes to the disc") ─────────────────
   Replaces the mist on the landing and About heroes. One flat circle in the
   brand green, hard edge, no gradient, no blur, no motion after arrival: the
   full stop after her name at building scale, so the loader's dot, the name's
   dot and the hero share one idea. Cut by the right edge of the viewport and
   by nothing else: it sits whole inside the hero's height (hero is 278px at
   1440, 213px on About), so the nav above and the first card below stay on
   paper. It never sits under type: the text block ends ~660px from the rail's
   left at 1440 and the disc starts at 1128; at 1023 and below it shrinks and
   sits in the hero's top air, right of the name, above the lede. */
.page-work .hero > .disc-wrap, .page-about .hero > .disc-wrap{ position:absolute; z-index:0; top:0; bottom:0; height:auto; left:calc(50% - 50vw); width:100vw; overflow:hidden; pointer-events:none; }
.disc{ position:absolute; display:block; border-radius:50%; background:var(--green);
  width:var(--disc, 240px); height:var(--disc, 240px); right:calc(var(--disc, 240px) * -0.3); top:var(--disc-top, 16px); }
.page-about .disc-wrap{ --disc:176px; --disc-top:16px; }
@media (max-width:1023px){ .page-work .disc-wrap, .page-about .disc-wrap{ --disc:120px; --disc-top:8px; } .disc{ right:calc(var(--disc) * -0.33); } }
@media (max-width:767px){ .page-work .disc-wrap, .page-about .disc-wrap{ --disc:96px; --disc-top:8px; } }
@media print{ .disc-wrap{ display:none; } }


/* ── PAPER GRAIN BEHIND THE MASTHEAD ─────────────────────────────
   Paper is the material this whole site claims to be made of, and until now
   that was an argument the page made in words rather than one you could see.
   This is the fibre, not a background image: monochrome fractal noise, no
   colour of its own, multiplied into the paper so it darkens the sheet very
   slightly rather than laying a grey film over it.

   It is deliberately at the edge of visible. Measured against the untextured
   sheet the lede's contrast moves by less than 0.1 — it has to, because the
   moment a texture costs a contrast ratio it has stopped being paper and
   started being decoration.

   Full-bleed, because paper does not stop at a content rail, and masked to
   nothing by the bottom of the hero so the sheet never ends on a hard line.
   Inline as a data URI: no request, no third asset to lose. */
.page-work .hero, .page-about .hero{ position:relative; isolation:isolate; }
.page-work .hero > *, .page-about .hero > *{ position:relative; z-index:1; }
.page-work .hero::before, .page-about .hero::before{
  content:""; position:absolute; z-index:1; pointer-events:none;   /* above the mist, under the type */
  top:calc(-1 * var(--header-h)); bottom:-45%;
  left:50%; width:100vw; transform:translateX(-50%);
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='480' height='480'%3E%3Cfilter id='p'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='480' height='480' filter='url(%23p)'/%3E%3C/svg%3E");
  background-size:480px 480px;   /* 480px tile: the 160px repeat read as squares (Anna, 15 Sep 2026) */
  mix-blend-mode:multiply;
  opacity:0.22;
  /* FELT texture (Anna, 30 Aug: "more grainy texture, some kind of feeling
     texture"): the mask's alpha keeps the grain light over the
     type (0.22 x 0.45 = 0.10, the AA-measured ceiling), then lets it reach full strength over the mist —
     the fibre is most present exactly where the colour is — and it melts
     out with the mist's own dissolve. */
  /* Two mask layers INTERSECTED: strength varies left-to-right (light over
     the text column, full over the colour), and the whole thing melts out
     before the gap exactly as the mist does — otherwise the grain alone
     re-stains the air the mist just vacated. */
  -webkit-mask-image:linear-gradient(to right, rgba(0,0,0,0.45) 0%, rgba(0,0,0,0.45) 50%, #000 80%),
                     linear-gradient(to bottom, #000 0%, #000 30%, rgba(0,0,0,0.55) 60%, transparent 100%);
  -webkit-mask-composite:source-in;
          mask-image:linear-gradient(to right, rgba(0,0,0,0.45) 0%, rgba(0,0,0,0.45) 50%, #000 80%),
                     linear-gradient(to bottom, #000 0%, #000 30%, rgba(0,0,0,0.55) 60%, transparent 100%);
          mask-composite:intersect;
}
/* Grain is texture, not information: anyone who has asked for less visual
   noise, or is printing, gets the clean sheet. */
@media (prefers-reduced-transparency:reduce){ .page-work .hero::before, .page-about .hero::before{ display:none; } }
@media print{ .page-work .hero::before, .page-about .hero::before{ display:none; } }

/* ── THE GALLERY MASTHEAD (Anna, 30 Aug 2026: the gallery-hang rebuild) ──
   The lens is gone. The wall opens almost empty: her name at the base h1
   size, the lede, and ONE credential line — the whisper. The name no longer
   fills the rail; the work below is what gets the size. */
.page-work .hero h1{ max-width:none; }
/* The role kicker: one quiet line binding the name to what she does, at the
   same weight as the whisper below rather than a new loud tier — the
   masthead stays name / lede / one credential, this just tags onto the
   name (8 Sep 2026). */
.page-work .hero .kicker{
  margin:var(--within) 0 0; font-size:13px; line-height:20px;
  color:var(--ink-3); letter-spacing:-0.013em;
}
/* The role outranks the domain tags on the same line — colour only, same
   size/weight, so it reads as one claim (role) plus supporting context
   (sectors) rather than five equal words. 8 Sep 2026. */
.page-work .hero .kicker .role{ color:var(--ink-2); }
.page-work .hero .lede{
  margin-top:var(--between); max-width:calc((100% - 11 * var(--col)) / 12 * 10 + 9 * var(--col)); text-wrap:balance;
  color:var(--ink-2); font-weight:500;   /* one step darker and heavier than the kicker/whisper (both ink-3, 400) —
       the statement now outranks the meta lines without competing with h1's full ink. 8 Sep 2026. */
}   /* ten columns. The lede was one sentence on one line (Anna, 5 Sep 2026); the 7 Sep positioning line runs to two, so balance splits it evenly instead of leaving a widow */
/* The whisper: her existing Edgefolio credential line, moved up to sit under
   the lede as the hero's one quiet proof. Same voice as every meta line. */
.page-work .whisper{
  margin:var(--within) 0 0; font-size:13px; line-height:20px;
  color:var(--ink-3); letter-spacing:-0.013em;
}
/* PHONE: the two meta lines wrap as two even lines instead of dropping a
   one-word orphan ("Enterprise SaaS", "Paribas · UBS") on line 2. Company
   names are joined with no-break spaces in the markup. 15 Sep 2026. */
@media (max-width:767px){ .page-work .hero .kicker, .page-work .whisper{ text-wrap:balance; } }
@media (max-width:1023px){ .page-work .hero h1{ font-size:48px; line-height:56px; } }
@media (max-width:767px){ .page-work .hero h1{ font-size:36px; line-height:44px; } }
/* THE LANDING READS LIGHTER (Anna, 23 Sep 2026: "a huge text wall here on the
   mobile", then "do 1 and 2"). Two layers of type: the name, then the sentence
   one size up; the bank line stays as the one quiet proof. The role-and-sectors
   line repeats what the sentence says (money, health), so the sectors left
   the masthead and the role joined the proof line below. h1 to lede is then
   the lede's own --between. */
/* The proof line carries the role first, one step darker, then the banks,
   joined by the same dot and spacing as every name on the line (Anna, 23 Sep:
   no large gap). On phones the role and its dot step aside. */
.page-work .whisper .role{ color:var(--ink-2); }
.page-work .whisper .sep{ color:var(--ink-3); }
.page-work .hero .lede{ font-size:24px; line-height:32px; }
@media (max-width:1023px){ .page-work .hero .lede{ font-size:20px; line-height:28px; } }
@media (max-width:767px){ .page-work .hero .lede{ font-size:18px; line-height:26px; } .page-work .whisper .role{ display:none; } }

/* No bottom padding: the footer's own section step is the one gap after the
   last project. Both used to apply, so the page ended on 192 (Anna, 11 Sep). */
.page-work .work{ padding-bottom:0; position:relative; z-index:1; }
/* z1 so the work paints above the mist's dissolving tail below the hero. */
.page-work .item{ display:block; position:relative; }
/* A case study is a four-part unit — meta, title, picture, outcome — and its
   own largest internal gap is 32. One project ending and the next beginning
   is a section break, so it takes the section step: 3x the deepest gap
   inside a block, and 48 on a phone where the internals drop to 24.
   Space, never a rule. */
.page-work .item + .item{ margin-top:var(--section); }
/* THE NUMBER SITS ON THE TITLE'S LINE, not on the sector line above it.
   (Anna, 26 Aug 2026.) The stat is the evidence FOR the title, so the two
   share a row and a baseline: title left, figure right, reading as one
   statement and its proof. Against the 13px sector line the 24px figure was
   the odd voice out and dragged that row's height with it.
   Baseline, not centre — a 32px title and a 24px figure only look level when
   they sit on the same line, which is the whole point of pairing them. */
.page-work .head{
  display:flex; justify-content:space-between;
  /* THE WITHIN-A-GROUP STEP, down two rungs from where it started (Anna,
     26 Aug 2026, twice). The sector line and the title are not two groups
     with a break between them — the label and the thing it labels are ONE
     group, and 16 is the rung for that. 32 and then 24 both still read as a
     gap, because the row is baseline-aligned to a 24px figure whose ascender
     adds air above the title's cap that no declared number accounts for. */
  gap:var(--col); margin-top:var(--within);
  /* LAST baseline, not first. The stat is two lines — figure over label — and
     plain `baseline` locks the FIGURE to the title, which left the label
     hanging below the title's line and the number floating above it.
     `last baseline` locks the LABEL to the title instead, so the label reads
     level with the title it describes and the number sits just above it,
     which is the way round Anna asked for (26 Aug 2026).
     The plain value is declared first as the fallback: a browser without
     `last baseline` drops the second line and still aligns something sane. */
  align-items:baseline;
  align-items:last baseline;
}

.page-work .item h2{
  /* Placard size, not headline size: under the picture the title is the
     label of the plate above it (30 Aug gallery hang). One line at the rail. */
  margin:0; position:relative; min-width:0;
  font-size:25px; line-height:32px; letter-spacing:-0.017em;
  max-width:none; text-wrap:pretty;
}
/* ── THE PLACARD (30 Aug 2026) ────────────────────────────────────
   Picture, then 16 to the title row — the label rung, because the placard
   and the plate are ONE group (Anna: "link it more to the image") — then 4
   to the sentence. 96 to the next piece: the air between pieces is six
   times the air inside one, so the label cannot detach.
   Title left, figure right, one line. The figure keeps the title's size:
   on the placard they are two halves of one statement. */
.page-work .head .fig{
  font-size:25px; line-height:32px; font-weight:560; letter-spacing:-0.014em;
  font-variation-settings:"opsz" 32,"SOFT" 0,"WONK" 1;
  font-variant-numeric:oldstyle-nums tabular-nums;
  color:var(--ink); transition:color var(--dur-1) var(--ease);
}
.page-work .head .cap{ font-size:13px; line-height:20px; color:var(--ink-3); margin-left:var(--xs); }
.page-work .head .stat{ margin:0; flex:0 0 auto; text-align:right; white-space:nowrap; }
.page-work .item:hover .head .fig,
.page-work .item:focus-visible .head .fig{ color:var(--green); }
/* ONE line, exactly like the placard above it (Anna, 30 Aug 2026: the
   Healthily sentence "also has to be one line exactly as the other ones").
   The rail sets the measure; `balance` only splits it evenly on viewports
   too narrow to hold it. */
/* 8, not 4: at 4 the line read as glued to the title (Anna, 11 Sep 2026). */
.page-work .said{ margin:var(--xs) 0 0; max-width:none; color:var(--ink-2); text-wrap:balance; }
/* KEYWORDS, NOT A PARAGRAPH (Anna, 23 Sep 2026: "too wordy... still do the
   keywords"). Under each title, one quiet line of what she did, lifted from the
   paragraph it replaces, in the same voice as the masthead's meta line. Shown
   at every width: it is what a reviewer scans for. */
.page-work .keys{ margin:var(--xs) 0 0; font-size:13px; line-height:20px; color:var(--ink-3); letter-spacing:-0.013em; }

@media (max-width:1239px){
  /* Stacked below the capped rail. The longest placard (Self-Service's title
     plus its figure) needs 976px of row, and the 90% rail only offers that
     from 1240 up — between 1024 and 1190 the row wrapped just one of the
     four titles, which broke "every placard reads the same" (Anna, 30 Aug).
     So on any canvas narrower than the full rail the figure sits under the
     title, the same pattern the phone already uses, and the title holds one
     line everywhere it can. */
  .page-work .head{ display:block; }
  .page-work .head .stat{ display:block; margin-top:var(--xs); max-width:none; text-align:left; }
}@media (max-width:1023px){ .page-work .item h2{ font-size:24px; line-height:32px; } }
@media (max-width:767px){
  /* One line at every width. Stacked, the sector and the outcome read as
     two ideas instead of two facts. The row holds; only the gap tightens. */
  .page-work .head{ gap:var(--sm); }
  .page-work .stat{ white-space:normal; }
}
/* THE TITLE IS WHAT ANSWERS THE POINTER. The whole block is one link, so the
   title takes the site's one hover language — the word goes green — and the
   picture lifts two pixels with it. Nothing else changes, and nothing appears
   that was not already there. */
.page-work .item h2{ transition:color var(--dur-1) var(--ease); }
.page-work .item:hover h2, .page-work .item:focus-visible h2{ color:var(--green); }
.page-work .shot{ margin-top:0; }   /* the picture opens the piece (30 Aug) */
/* SQUARE CORNERS ON THE WORK. (Anna, 26 Aug 2026: "I don't like those round
   corners on the main cards.") A 16px radius makes a screenshot read as a
   card — a rounded object sitting on the paper — and these are not cards,
   they are the work itself, printed. Square, the picture is a plate on a
   page, and the paper does the framing instead of a corner. */
.page-work .shot img{ width:100%; border-radius:var(--radius-sm); transition:transform var(--dur-2) var(--ease); }
/* The whole block is one link, so the picture is what acknowledges the
   pointer — two pixels, no shadow. The press puts it back down. */
@media (hover:hover) and (pointer:fine){
  .page-work .item:hover .shot img, .page-work .item:focus-visible .shot img{ transform:translateY(-2px); }
  .page-work .item:active .shot img{ transform:none; }
}
.page-work .item:focus-visible{ outline:1px solid var(--green); outline-offset:14px; }

/* ═══════════════════════════════════════════════════════════════
   ABOUT
   Scoped to body.page-about. Everything above this line is the site;
   everything below it belongs to one page and cannot reach another.
   ═══════════════════════════════════════════════════════════════ */

/* Two columns on the same rail: the narrow one holds the portrait and the
   quote, the wide one holds the reading. Columns come from .grid. */
/* THE PORTRAIT IS THE ONLY COLOUR ON THE PAGE, so it gets the room. Fitted
   to the height the prose left, it came out 269 wide in a column measuring
   399 — a third of its own column unused, on the one element the page is
   built around. It now fills its five columns and sets its own height; the
   reading sits beside it in seven. */
.page-about .split{ padding-top:0; align-items:start; }
.page-about .side{ display:flex; flex-direction:column; gap:var(--between); }

/* THE PORTRAIT FITS, IT DOES NOT FILL. Everywhere else the words set the row
   and the picture is cropped to whatever height they leave. That is right for
   a product shot and wrong for a person: this photograph carries her face at
   the top and the signed documents at the bottom, so any height-driven crop
   deletes one of the two things it is here to show — object-position only
   chooses which one you lose. */
.page-about .portrait{ display:block; }
.page-about .portrait .frame{ position:relative; }
/* The whole photograph, uncropped: her face at the top and the signed
   documents at the bottom both survive, because nothing here is sized by a
   height it has to be cut to fit. */
.page-about .portrait .frame img{
  display:block; width:100%; height:auto;
  border-radius:var(--radius-sm);
}
.page-about .offhours{ margin:var(--within) 0 0; font-size:13px; line-height:20px; color:var(--ink-3); }

/* ── The quote ───────────────────────────────────────────────────
   Full width, and the same row shape as everything around it: who said it
   on the left, what they said on the right. In the narrow gutter it ran to
   nine short lines beside a column that had already stopped. */
.page-about .cite{ margin:0; }
.page-about .cite .kicker{ margin:0; font-size:13px; line-height:20px; color:var(--ink-3); }
/* A quotation is somebody else speaking, so it is set in the serif — the
   same voice as the hero line and the titles, and nothing that is merely
   read. Real quote marks, and they HANG: text-indent pulls the opening mark
   out into the gutter so the first word still starts flush on the grid.
   opsz 32 answers the size it is actually set at. */
.page-about .cite blockquote{
  margin:0; font-family:"Fraunces", Georgia, "Times New Roman", serif;
  font-variation-settings:"opsz" 32,"SOFT" 0,"WONK" 1; font-weight:400;
  font-style:italic;   /* the third signal it is quoted speech, after the serif and the marks */
  font-size:24px; line-height:32px; letter-spacing:-0.014em; color:var(--green);   /* her call, 8 Sep 2026 — a second, deliberate use of the accent beyond the full stop and focus ring, to make the one line she wants to stand out actually stand out */
  max-width:none;            /* the seven-column cell does the capping — 48 characters at 24px */
  text-indent:-0.34em;       /* the italic mark is narrower, so it hangs less */
}
/* The attribution sits directly under the quote, 16 below, because that
   adjacency is the only thing that says somebody else said this. A rail
   away, the big ink text read as her own voice. */
/* THE ATTRIBUTION IS ONE LINE. It is not a paragraph, so it does not want a
   reading measure — it wants to fit. Capped at five of the twelve it had 399
   to hold a line needing 439, so it broke over two and read as prose about
   the stranger rather than as the credit under a quotation. The seven-column
   cell is 577 and holds it whole; below 1024 the cell collapses and wrapping
   there is correct. */
.page-about .cite figcaption{
  margin-top:var(--within); font-size:13px; line-height:20px; color:var(--ink-3);
  max-width:none; text-wrap:nowrap;
}
@media (max-width:1023px){ .page-about .cite figcaption{ text-wrap:pretty; } }
/* Between groups, not between sections: the stranger says it and she
   answers it — one story, two voices. A section step made them read as two
   unrelated blocks. */
/* The same width as every other 16px block in this column. It was capped a
   column short of them, which put a third right edge in a column that should
   have two: one for the reading sizes, one for the 13px notes. Nothing about
   16px justified its own measure. */
.page-about .cite .note{ margin:var(--between) 0 0; font-size:16px; line-height:24px;
                         color:var(--ink-3); max-width:var(--reading); }
@media (max-width:1023px){
  .page-about .cite .kicker{ margin-top:0; }
  .page-about .cite blockquote{ font-size:20px; line-height:28px; max-width:none; text-indent:-0.4em; }
}

/* The reading column. The only thing that was ever wrong here was the cap:
   58ch measured 531px inside a column declaring 667, so the text's right
   edge landed between two column lines — on nothing. The fix is to declare
   the columns the reading actually needs (seven), not to inflate the type
   until it fills eight. At seven columns this measures 73 characters, and
   the column does the capping. */
.page-about .prose p{ margin:0 0 var(--within); max-width:var(--reading); }
.page-about .prose p:last-child{ margin-bottom:0; }
/* The paragraph shorthand above sets margin-top:0 and outscores the base
   rule, so the build line's deeper step needs to be stated here to land. */
.page-about .prose p.build{ margin-top:var(--xxl); }
.page-about .prose strong{ font-weight:600; color:var(--ink); }   /* revised same day: ink, matching .build and the links — one emphasis colour, not body's ink-2 */

.page-about .band{ margin-top:var(--section); }
/* THE FOLD MUST CUT SOMETHING. Measured on a 900-tall laptop: the portrait
   block ended at 812 and the next section began at 908 — eight pixels below
   the fold — so the page ended on clean empty paper and read as finished.
   The portrait and the bio and the quote are one movement about the same
   person; Writing is the next subject. So this break is the block step and
   the one after it stays the section step, which lifts the quote's first
   line above the fold at every common laptop height. */
.page-about .split + .band{ margin-top:var(--xxl); }
.page-about .band > div > h2 + *{ margin-top:var(--between); }
/* …except in the header row, where the h2 and the intro are side by side. */
.page-about .head-row > h2 + *{ margin-top:0; }
.page-about .head-row + div{ margin-top:var(--between); }

/* The intro sits in the content column, like every other row's content —
   it used to run the full rail capped at six of the twelve, which put its
   right edge 41px INSIDE the column where the reading starts: past one
   line and short of the next. */
/* The same size as the quote's attribution: this is a note about the list
   under it, not a paragraph of its own. At 13px the seven-column cell would
   run it to 89 characters, so it takes the width the other 13px blocks use. */
.page-about .intro{ margin:0; color:var(--ink-3);
                    font-size:13px; line-height:20px; max-width:var(--in7-5); }
/* The shelf names itself and then says what the shelf is, one line under the
   other — the same 13px note voice as the quote's kicker. Stacked, not split
   across the row: a one-word title and its own subtitle are one thing, and
   putting the rail between them made the note read as a separate column
   rather than as the line belonging to the word above it. */
.page-about .shelf-head .intro{ margin-top:var(--within); }
.page-about .plain{ margin:0; }
.page-about .slot-fig{ margin:0; }

/* Writing: one line per row and no more. The whole row is the link, so
   there is no underline competing with anything — the title draws the
   green line on hover, the same move as everything else. */
.page-about .writing{ list-style:none; margin:0; padding:0; }
.page-about .writing li + li{ margin-top:var(--between); }
.page-about .writing .t{
  font-size:18px; line-height:28px; letter-spacing:-0.014em;
  color:var(--ink); transition:color var(--dur-1) var(--ease);
}
.page-about .writing .d{ color:var(--ink-3); }

/* The work and the books: two quiet rows, four equals each. */
.page-about .strip{ padding-top:0; margin-top:var(--section); }   /* space, never a rule */
/* An even field. The grid's row gap is 16 and its column gap 48, which reads
   as four columns rather than a row of cards — and at 2x2 below 1024 the
   three-to-one asymmetry is unmissable. The cards sit 48 apart both ways. */
.page-about .row4{ margin-top:var(--between); row-gap:var(--col); }
/* The source screenshots are all different shapes, so the frame is fixed
   and the picture fills it. Nothing ranks above another. */
.page-about .work-row .slot img{ width:100%; aspect-ratio:4/3; object-fit:cover; object-position:top left; }
.page-about .slot .cap{ margin:var(--within) 0 0; font-size:13px; line-height:20px; min-height:48px; }   /* two lines reserved, so the row bottoms stay level */
.page-about .slot .name{ display:block; color:var(--ink); }
.page-about .slot .st{ display:block; color:var(--ink-3); }
.page-about .slot:focus-visible{ outline:1px solid var(--green); outline-offset:10px; }
/* Four full-colour covers out-shouted the green dot, which is the whole
   argument of the page: one earned detail, and nothing else competing for
   it. Greyscale puts them back where they belong, as texture. The colour
   returns when you point at one, so nothing is lost. */
.page-about .books .pic{
  border-radius:2px;   /* the ONE picture that keeps a near-square corner —
     a book cover is a physical object and its corners are square */
  filter:grayscale(1) contrast(0.94);
  transition:filter var(--dur-2) var(--ease), transform var(--dur-2) var(--ease);
}
/* The colour comes BACK, it does not come back all the way. At full
   saturation the red cover is the loudest thing on a page whose whole
   argument is that one green mark is — so pointing at a book handed the
   page's one earned detail to a book jacket. At 0.45 the cover reads as
   colour again and still sits under the green. */
.page-about .books .slot:hover .pic, .page-about .books .slot:focus-visible .pic{ filter:grayscale(0.45) contrast(0.94); }
@media (hover:hover) and (pointer:fine){
  .page-about .books .slot:hover .pic, .page-about .books .slot:focus-visible .pic{ transform:translateY(-2px); }
  .page-about .books .slot:active .pic{ transform:none; }
}

@media (max-width:1023px){
  .page-about .split{ row-gap:var(--between); }
  .page-about .side{ max-width:420px; }
  .page-about .row4{ grid-template-columns:repeat(2,minmax(0,1fr)); gap:var(--between) var(--col); }
  .page-about .row4 .c3{ grid-column:span 1; }
}

/* ═══════════════════════════════════════════════════════════════
   A CASE STUDY
   Scoped to body.page-case. Everything above this line is the site;
   everything below it belongs to one page and cannot reach another.
   ═══════════════════════════════════════════════════════════════ */

/* ── Case study ────────────────────────────────────────────────────
   Five beats, not eleven rows. Every beat is the same row — label above
   its paragraph — so one vertical line runs the whole page.
   Three levels here, and the ladder covers all three:
     16 between paragraphs inside a beat
     32 between one beat and the next
     48 between page sections (credits, next project)            */
.page-case .kicker{ margin:0 0 var(--within); font-size:13px; line-height:20px; color:var(--ink-3); }
/* One step below the landing h1, so the two never tie — and the 48 cut,
   not the 144, because the axis answers the size it is set at. */
.page-case .hero h1.case{ font-size:48px; line-height:48px; letter-spacing:-0.022em; max-width:var(--read-6);
  font-variation-settings:"opsz" 48,"SOFT" 0,"WONK" 1; }
@media (max-width:767px){ .page-case .hero h1.case{ font-size:32px; line-height:36px; } }

/* Reading depth. Two words, not a segmented control — a pill is a box. */
.page-case .depth{ display:flex; align-items:center; gap:var(--within); margin-top:var(--within); }
/* The site has ONE hover language — the word goes green and a 1px green
   line draws in underneath — and the selected reading depth is the same
   move, held on. No dot: the dot stays at rest in the wordmark, never as a
   reward. The horizontal padding is gone so the line measures the words;
   the 16 between them now comes from the row's own gap. */
.page-case .depth button{
  appearance:none; background:none; border:0; padding:0 0 2px;
  margin:0 0 -2px;   /* the underline's 2px is handed straight back — decoration
                        never sets a row's height, the same rule the divider
                        below obeys. Measured: without this the step into
                        "Result" read 56 where its siblings read 51. */
  position:relative; font:inherit; font-size:13px; line-height:20px;
  color:var(--ink-3); cursor:pointer;
  background-image:linear-gradient(var(--green),var(--green));
  background-size:0% 1px; background-repeat:no-repeat; background-position:0 100%;
  transition:color var(--dur-1) var(--ease), background-size var(--dur-2) var(--ease),
             transform var(--dur-2) var(--ease);
}
@media (hover:hover) and (pointer:fine){
  .page-case .depth button:hover{ color:var(--green); background-size:100% 1px; }
}
/* Keyboard gets it unconditionally — focus is never a false positive. */
.page-case .depth button:focus-visible{ color:var(--green); background-size:100% 1px; }
/* The only real control on the site, so the only thing that gets press
   feedback: one pixel down on the way in, nothing on the way out.
   Asymmetric on purpose — the finger is deliberate, the release is not. */
.page-case .depth button:active{ transform:translateY(1px); }
/* A tap target is for the finger; the ladder is for the eye. As layout
   height, 44px stood 24 taller than its own ink and the dead half became
   rhythm — the step into "Result" read 61 where its peers read 46. Trimmed,
   the button's box is its cap height (9px at 13/20), so the overlay reaches
   18 above and below: 9 + 36 = 45, measured with elementFromPoint. */
.page-case .depth button::before{ content:""; position:absolute; inset:-18px 0; }
/* The blanket coarse-pointer rule would put the dead box back and break the
   step again on a phone, which is exactly where it was first seen. This
   control already carries its 44 in the overlay, so it opts out. */
@media (pointer:coarse){ .page-case .depth button{ min-height:0; } }
.page-case .depth button[aria-pressed="true"]{ color:var(--green); background-size:100% 1px; }
/* The divider is 14px of decoration standing beside 9px of trimmed ink, so
   it — not the type — was setting the group's height, and the extra 5 landed
   in the step below. Decoration never sets a row's height: the negative
   block margins hand its contribution back, and it still draws at 14. */
.page-case .depth .div{ width:1px; height:14px; background:var(--hairline);
             margin-block:calc((9px - 14px) / 2); }
.page-case .depth .sub{ color:var(--ink-3); }
/* Reading depth. Two blocks, one shown at a time — the skim is a summary of
   the story, not a preface to it, so the two never stack. `html.mode-skim` is
   set in the page's own <head> before paint, so nothing flashes. */
html.mode-skim .full-only{ display:none; }
html:not(.mode-skim) .skim-only{ display:none; }

/* ── One column, not two ─────────────────────────────────────────
   The story runs as one reading column and the pictures break wider. There
   is no label gutter: a 4-column strip holding one word costs a third of
   the page and pushes the reading off the screen. The label sits above its
   paragraph at 13px, so the eye goes label → sentence → label → sentence
   straight down. */
.page-case .read{ max-width:var(--read); }
.page-case .row{ display:block; }

/* ── THE SAME TWO LINES AS EVERY OTHER PAGE ──────────────────────
   The story used to run as one column with its label sitting ABOVE each
   paragraph, which is a third structure on a site that has exactly two.
   Now the label takes the five-column side and the reading takes the seven,
   so the case study lands on the page's own two lines — 208 and 655.

   The evidence still breaks wide. `display:contents` on the body makes its
   children grid items of the row itself, so a paragraph can sit in the
   reading column while the picture under it spans the whole twelve. Without
   it the pictures would be trapped inside seven columns and a case study
   whose entire argument is screenshots would lose a third of them. */
/* ── ONE COLUMN, HEADING ABOVE THE TEXT ──────────────────────────
   Her live case study's layout, restored 26 Aug 2026: "keep the layout as in
   my live case study… this one is dragging out and it's difficult to read."

   The port had split every section into a label gutter and a reading column —
   heading demoted to a 13px eyebrow at x=208, story pushed to x=655 in a
   577px ribbon. Measured against live, that is the whole complaint: five of
   the twelve columns held nothing but one small line, the story ran 50
   characters instead of 61, and the extra lines made the page taller while
   the eye had a third of the width of empty paper beside it the whole way
   down. A gutter that holds one label is not a column, it is a margin.

   Live sets the heading at 41px ABOVE its text, both flush at the same left
   edge, with the story at 701px. On this rail the eight-column reading
   measure is 700 — the same width, arrived at from the grid rather than
   copied. The heading takes the site's own h2, so it is a section heading
   here exactly as it is everywhere else. */
@media (min-width:1024px){
  .page-case .row{ display:block; }
  /* `.row .body > p`, not `.body > p`. `.page-case .body p` sets max-width to
     --reading (which is `none` at this width) and scores (0,2,1) — the same as
     `.body > p` — but it sits LATER in this file than this media block, and a
     media query adds nothing to specificity. So the tie went to source order
     and the story ran the full 1024: 88 characters, well past the measure.
     Adding `.row` takes this to (0,3,1) and settles it wherever it sits. */
  .page-case .body > *{ max-width:var(--read); }
  /* THE WCAG CEILING: 80 CHARACTERS. WCAG 2.1 SC 1.4.8 Visual Presentation
     (AAA) caps a line of text at 80 characters, and running the full rail put
     this story at 132 — the eye loses its place on the return sweep long
     before that. This is the widest the text is allowed to be, which is what
     Anna asked for: the maximum, not a designer's preference.

     BEWARE `ch`. One `ch` is the advance of the digit "0" — 9.16px here —
     while the average character in this face at this size is 7.77px. So a
     literal `80ch` renders 733px, which is 94 characters, and would blow the
     ceiling by fourteen while looking like it enforced it. Calibrated:
     67ch = 614px = 79 characters. Measured, not assumed.
     It stays in `ch` rather than px so it tracks the type if that ever
     changes — the ratio between the two units is a property of the face. */
  .page-case .row .body > p{ max-width:67ch; }
  /* A SMALLER NOTE TAKES A NARROWER COLUMN. The eight-column measure is sized
     for the story's 24px — an 18px note dropped into the same width runs 76
     characters, past the measure, because the column is fixed and the type is
     not. Six columns puts an 18px note back at 55, the same read as the story
     beside it. `.pull` sets its own width and `.flowtag` is a heading row, so
     both opt out. */
  .page-case .row .body > p[class]:not(.pull):not(.flowtag){ max-width:var(--read-6); }
  /* The evidence still breaks wide, and the caption under it keeps the
     narrower note measure. */
  .page-case .body > .stats,
  .page-case .body > .reel,
  .page-case .body > .two,
  .page-case .body > .wide,
  .page-case .body > figure{ max-width:none; }
  .page-case .body > p.cap{ max-width:var(--read-5); }
}
/* One section and the next: the block step, because each row now opens with a
   real 32px heading and 32 left the heading sitting as close to the previous
   paragraph as to its own. */
.page-case .row + .row{ margin-top:var(--step-section); }
/* The section labels are <h2> for the outline and 13px eyebrows on the
   page, which the CSS has to settle explicitly: without a family and an
   opsz here they inherit the h2 rule and get drawn with the 48pt display
   cut at 13px — thick stems built for a headline, printed caption-size.
   They stay in the serif, because a label above a paragraph is the quiet
   half of a title, but the cut now matches the size. */
/* THE SECTION HEADING IS A HEADING. It inherits the site's h2 — 32px Fraunces
   at opsz 48 — and only needs its step below stated here. It used to be
   overridden down to a 13px eyebrow, which is what turned a heading into a
   caption sitting in an empty gutter.
   `.k` stays an eyebrow: that class labels a block, it does not head one. */
.page-case .row > h2{ margin:0 0 var(--step-head); }
/* A heading owns the step to whatever opens its section. A picture opening
   a section used to add its own 48 on top (the margins collapse to the
   larger), so the heading floated off its own content. */
.page-case .row > .body > :first-child,
.page-case .row > .body > :is(style,script,template):first-child + *{ margin-top:0; }
/* a sub-head is a heading too: it sits the heading step above its text */
.page-case .body > .sub2 + *{ margin-top:var(--step-head); }
.page-case .row .k{
  margin:0 0 var(--within); font-size:13px; line-height:20px; font-weight:400;
  letter-spacing:-0.013em; color:var(--ink-3);
  font-family:"Archivo", -apple-system, "Helvetica Neue", Arial, sans-serif;
}
/* the reading measure is eight of the twelve columns, so the prose starts
   and ends on the same boundaries as every picture and every credits column */
/* No cap of its own: --read is a percentage of its container, and inside the
   seven-column grid area that container is 577, not the rail — so the calc
   folded a second time and the story came out 369 wide, 41 characters. The
   cell is the measure. */
/* The base size for ANY paragraph inside a story body — widget labels, hints,
   captions, the interactive demos' own copy. The STORY's own reading size is
   set separately below, on classless paragraphs only: raising it here swept up
   eighteen classed paragraphs across the four case studies and printed a
   widget's caption at headline size.

   ── WHY :where() ─────────────────────────────────────────────────
   Anna, 26 Aug 2026, six separate times in one day: a widget rendered wrong —
   labels overlapping titles, an accordion at 18px in a 12px box, green step
   markers colliding with the headings under them. Every one was the SAME bug.
   As `.page-case .body p` this rule scored (0,2,1) and beat every widget rule
   written the obvious way (`.hnode .step`, `.fsteps .fs-a`, `.map .role`) at
   (0,2,0). It was not just resizing them — it was wiping their margins, and
   because this site trims every text box to cap-height and baseline, a wiped
   margin is not "a bit tight", it is glyphs physically on top of each other.
   Each one got patched by raising the widget rule to (0,3,x), which works and
   does not scale: the seventh widget written normally would have broken too.

   `:where()` contributes ZERO specificity, so this whole selector scores
   (0,0,0). It still reaches every paragraph in the body and still kills the UA
   margin — but literally any rule anywhere beats it. A widget can now be
   styled the plain way and simply win. This is a default, not a decree.
   Verified: 91 paragraphs across the four case studies measured before and
   after; nothing that was correct moved. */
:where(.page-case .body p){ margin:0; font-size:16px; line-height:24px; letter-spacing:-0.011em;
                    max-width:var(--reading); }   /* none on the twelve, where the
       seven-column cell is already the measure; four-of-six once the grid
       collapses and the story would otherwise run the whole rail — measured
       at 1023 it reached 96 characters. */
.page-case .body .muted{ color:var(--ink-2); }
/* NOTE the doubled selector: `.body > * + *` scores (0,1,0) because the
   universal selector is worth nothing, so `.body p` at (0,1,1) was winning
   the tie and killing every gap between two paragraphs. `.body > p + p`
   scores (0,1,2) and settles it.
   NOTE the child combinator too, which is not cosmetic: as a descendant
   selector this reached the three stat paragraphs — siblings sitting SIDE
   BY SIDE in a grid — so the second and third numbers sat 16 lower than the
   first. A paragraph gap is a rule about one reading column and must never
   be able to cross into a grid. */
.page-case .body > p + p, .page-case .body > * + *{ margin-top:var(--within); }
/* ── TEXT TO EVIDENCE IS NOT A PARAGRAPH BREAK ───────────────────
   Measured on Edgefolio: nineteen of the twenty-three gaps on the page were
   16 — paragraph to paragraph, paragraph to picture, picture back to
   paragraph, all the same. Every gap on the ladder and nothing grouped,
   because one step cannot express two different relationships.
   Two paragraphs are one argument continuing. A paragraph and the picture
   proving it are two kinds of thing meeting. The section break is the deep
   step. The values now live in --step-para / --step-figure / --step-section
   (24 / 48 / 96, see :root, 11 Sep 2026): one ladder, each step doubling. */
.page-case .body > * + figure,
.page-case .body > * + .wide,
.page-case .body > * + .two,
.page-case .body > * + .reel{ margin-top:var(--step-figure); }
/* `.row` added so this scores (0,3,x) and cannot lose to `.body > * + p`,
   which sits later in this file and was pulling every paragraph after a
   picture back down to 16 — the exact collision this whole rule exists to
   fix. Specificity, not source order, because source order is not a thing
   the next person editing this file will think about. */
.page-case .row .body > figure + *,
.page-case .row .body > .wide + *,
.page-case .row .body > .two + *,
.page-case .row .body > .reel + *{ margin-top:var(--step-figure); }
.page-case .row .body > figure + p,
.page-case .row .body > .wide + p,
.page-case .row .body > .two + p,
.page-case .row .body > .reel + p{ margin-top:var(--step-figure); }

/* ── THE STORY'S OWN READING SIZE ─────────────────────────────────
   Anna, 26 Aug 2026: "this one is dragging out and it's difficult to read."
   The port had not made the page longer — same 2035 words, and 1,271px
   SHORTER than live — so length was never the fault. The type was: live sets
   its story at 25/37.5 and the port had inherited the site's 18/28. Two
   thousand words at 18px is a slog whatever the page height says.

   24/36 is the rung on this site's own scale nearest her live size, at the
   same 1.5 ratio she set there, and in the eight-column measure it runs 57
   characters where live ran 77.

   Classless paragraphs ONLY. Every paragraph a widget owns carries a class,
   and every paragraph of her story carries none — so `:not([class])` selects
   the story exactly, without a new class on every paragraph in four files. */
.page-case .body > p:not([class]),
.page-case .body > p.muted{ font-size:16px; line-height:24px; }
/* The paragraph step. 16 between two 16/24 paragraphs measured as barely more
   than the ~12 a line already shows, so the story read as one wall (Anna,
   11 Sep 2026: "it seems very random"). --step-para is 24: a clear beat. */
.page-case .body > p:not([class]) + p:not([class]){ margin-top:var(--step-para); }
/* AND THE PARAGRAPH AFTER A PICTURE. `.page-case .body p{margin:0}` scores
   (0,2,1) and `.body > * + *` only (0,2,0), so the reset was winning every
   time the element before a paragraph was NOT another paragraph — measured on
   Edgefolio, three paragraphs sat flush against the figure above them at 0px.
   `> * + p` also scores (0,2,1) and sits later, which settles it. The p + p
   rule above still wins its own case at (0,2,2). No !important anywhere. */
.page-case .body > * + p{ margin-top:var(--within); }
/* A FIGURE NEVER OWNS THE STEP BELOW IT. The gap after a picture belongs to
   whatever comes next, exactly like every other gap on the site — otherwise
   the two margins stack and the step is the sum of a rule nobody can see and
   one they can. Measured on Edgefolio: two figures carried their own bottom
   margin, so the ladder's 16 landed as 112 and 40. */
.page-case .body > figure{ margin-bottom:0; }

.page-case .stats{ max-width:none; }   /* three 4-spans on the one grid */
.page-case .stats p{ margin:0; }
/* the figure is the serif at the row's own size — letter-spacing comes from
   .fig-lg, so it is set once */
.page-case .stats .n{ display:block; font-size:32px; line-height:36px; font-weight:600; color:var(--ink); }
.page-case .stats .d{ display:block; margin-top:var(--within); font-size:13px; line-height:20px; color:var(--ink-3); }
.page-case .stats .q{ color:var(--ink-3); }

/* ── Pictures ────────────────────────────────────────────────────
   Prose stays narrow so it reads; evidence breaks out to the full rail so
   it lands. */
.page-case figure{ margin:var(--step-figure) 0 0; width:100%; }
.page-case figcaption{ margin-top:var(--within); font-size:13px; line-height:20px;
                       color:var(--ink-3); max-width:none; }
   /* ONE line, like every caption on a piece of work (panel, 30 Aug 2026):
      a caption is not prose, so the rail sets its width and the 45-75ch
      guidance does not apply — the same call the landing capline already
      records. The 61ch cap was wrapping nine captions across four pages. */
/* A caption that is a <p> rather than a <figcaption> — same size, same ink.
   `p.cap` ties with `.body p` on specificity and sits later, which is what
   settles it. No !important anywhere on this site. */
.page-case p.cap{ margin-top:var(--within); font-size:13px; line-height:20px;
       color:var(--ink-3); max-width:none; }

/* All eight tabs, all eight. The claim of this case study is a number, so
   the picture has to BE eight, not a sample of two. Four up, two rows, every
   tile the same size, nothing ranking above another. */
.page-case .reel{ margin-top:var(--between); row-gap:var(--between); }
.page-case .reel figure{ margin:0; }
.page-case .reel img{ width:100%; aspect-ratio:16/10; object-fit:cover; object-position:top left;
  transition:transform var(--dur-2) var(--ease); }
@media (hover:hover) and (pointer:fine){ .page-case .reel figure:hover img{ transform:translateY(-2px); } }
.page-case .reel figcaption{ margin-top:var(--xs); }

.page-case .two{ display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:var(--col); margin-top:var(--between); }
.page-case .two figure{ margin:0; }
/* the tall product shots are portraits: show the head of each, level */
.page-case .two .tall{ width:100%; aspect-ratio:4/3; object-fit:cover; object-position:top center; }
.page-case .wide img{ width:100%; }

@media (max-width:1023px){
  .page-case .reel .c3{ grid-column:span 1; }
}

/* the credits, at the end where credits belong, three columns not three rows */
.page-case .credits{ margin-top:var(--section); padding-top:0; row-gap:var(--between); align-items:start; }
.page-case .credits h3{ margin:0 0 var(--within); font-size:13px; line-height:20px; font-weight:600; color:var(--ink); }
.page-case .credits ul{ list-style:none; margin:0; padding:0; }
.page-case .credits li{ color:var(--ink-3); }
.page-case .credits li + li{ margin-top:var(--within); }
.page-case .credits .lead{ margin:0 0 var(--within); color:var(--ink); }
/* Three ragged columns: the block's height is set by the LONGEST one, and
   the two shorter ones leave trailing box under their last line. The step
   into the next block is taken from the grid's own bottom instead, so the
   ink of the longest column IS the block's bottom edge. */
.page-case .credits > div > *:last-child{ margin-bottom:0; }
.page-case .credits ul:last-child li:last-child{ margin-bottom:0; }

/* ── Square corners, and the same hover as the landing page ──────
   Matching the work index (Anna, 26 Aug 2026). A 16px radius makes a
   screenshot read as a card sitting on the paper; these are the work itself,
   printed, so the paper does the framing. Applies to every picture on a case
   study, including the ones the demos loop through. */
.page-case figure img, .page-case .shot img, .page-case .pic{ border-radius:var(--radius-sm); }

.page-case .next{ display:block; padding-top:0; margin-top:var(--section); }
.page-case .next .k{ margin:0; font-size:13px; line-height:20px; color:var(--ink-3); }
.page-case .next h2{
  margin:var(--within) 0 0; font-size:32px; line-height:36px; letter-spacing:-0.022em;
  transition:color var(--dur-1) var(--ease);
}
/* The title goes green. No dot — that was a second hover language competing
   with the site's only one, struck 26 Aug 2026. */
.page-case .next:hover h2, .page-case .next:focus-visible h2{ color:var(--green); }

/* Green arrow, hidden until hover/focus — restored from the live site's
   .cs-next a::after (Anna's original), which sits on the anchor because the
   title IS the anchor there. This rebuild's title is the h2 child, so the
   pseudo-element moves to the h2 to land on the same word it follows. Rides
   the h2's own 32px, not the original's clamp(28px,42px) — the two titles
   are already different sizes and the arrow should match ITS title. */
.page-case .next h2::after{
  content:"→"; display:inline-block; margin-left:12px; color:var(--green);
  opacity:0; transform:translateX(-8px);
  transition:opacity var(--dur-2) var(--ease), transform var(--dur-2) var(--ease);
}
.page-case .next:focus-visible h2::after{ opacity:1; transform:translateX(0); }
@media (hover:hover){
  .page-case .next:hover h2::after{ opacity:1; transform:translateX(0); }
}
/* Touch has no hover to reveal from, so the arrow stays out rather than
   sitting there as decoration with nothing to trigger it — same call as the
   live site's .cs-next a::after under (hover:none). */
@media (hover:none){
  .page-case .next h2::after{ display:none; }
}

/* ── The pull ────────────────────────────────────────────────────
   Her own voice pausing mid-argument, not a quotation — so it stays in the
   body face rather than the serif, and earns its weight from ink and air
   alone, the same move as .prose .build. Full width: a pause belongs to
   the page, not the seven-column reading cell. */
/* NOTE the child combinator and the class together: `.page-case .body p`
   scores (0,2,1) and `.body > * + *` scores (0,2,0), so a bare `.pull` at
   (0,2,0) loses both fights silently — measured, it drew at 18px with a 16
   step above it. `.page-case .body > .pull` scores (0,3,0) and settles both.
   No !important anywhere on this site. */
.page-case .body > .pull{
  grid-column:1 / -1; margin:var(--step-figure) 0 0;
  max-width:var(--read-6); font-size:24px; line-height:32px;
  letter-spacing:-0.014em; color:var(--ink); font-weight:400;
}

/* A sub-head INSIDE a body: the section label is already the serif eyebrow in
   the left column, so this one stays in the body face and earns its rank from
   weight and the deep step above it. Never a second serif size — that is the
   font party one level down. */
.page-case .body > .sub2{
  margin:var(--step-figure) 0 0; font-size:16px; line-height:24px; font-weight:600;
  letter-spacing:-0.011em; color:var(--ink);
}

/* ── The comment ─────────────────────────────────────────────────
   A native disclosure, so it needs no script. Closed, it reads as one
   quiet line — the same 13px voice as every other label on the page.
   Open, the comment sits under a hairline rule: a border, never a box,
   because a box is a second signifier the page's own language doesn't use. */
.page-case .note{ margin-top:var(--step-para); }
.page-case .note summary{
  font-size:13px; line-height:20px; color:var(--ink-3); cursor:pointer;
  list-style:none; display:inline-flex; align-items:center; gap:var(--xxs);
}
.page-case .note summary::-webkit-details-marker{ display:none; }
.page-case .note summary::before{
  content:"+"; color:var(--ink-3); font-family:"Fraunces",Georgia,serif;
  transition:transform var(--dur-2) var(--ease);
}
.page-case .note[open] summary::before{ transform:rotate(45deg); }
.page-case .note summary:hover, .page-case .note summary:focus-visible{ color:var(--green); }
.page-case .note summary:hover::before, .page-case .note summary:focus-visible::before{ color:var(--green); }
.page-case .note-body{
  margin-top:var(--within); padding-left:var(--lg);
  border-left:1px solid var(--hairline-2);
}
.page-case .note-body p{ margin:0; font-size:16px; line-height:24px; color:var(--ink-2); max-width:var(--reading); }

/* Backward-compatibility shim for the case-study page. That page's own
   markup still asks for an older set of token names, so rather than
   rewrite it, its old names are mapped here onto the values this system
   already uses at the same rung. Scoped to .page-case only — nothing
   outside the case study ever sees these names. */
/* THE RAIL, UNDER ITS OLD NAME. The case study's markup wraps every section
   in `.wrap`, which this stylesheet does not define — so the page had no
   max-width and no inset at all and every line ran the full window from
   x=0. This gives that name the rail's exact behaviour, so the page sits on
   the same centred 1024 as every other page without its markup changing. */
.page-case .wrap{
  width:100%; max-width:calc(var(--rail) + 2 * var(--gutter));
  margin-inline:auto; padding-inline:var(--gutter);
}
@media (min-width:768px){
  .page-case .wrap{ width:min(90%, calc(var(--rail) + 2 * var(--gutter))); }
}

.page-case{
  --font-body:    "Archivo", -apple-system, "Helvetica Neue", Arial, sans-serif;
  --font-display: "Fraunces", Georgia, "Times New Roman", serif;
  --font-label:   "Archivo", -apple-system, "Helvetica Neue", Arial, sans-serif;
  --fs-micro:  13px;
  --fs-small:  16px;
  --ls-label:  0;
  --ink-muted: var(--ink-3);
  --paper-white: var(--paper);
  --rule:        var(--hairline);
  --rule-soft:   var(--hairline);
  --rule-strong: var(--hairline-2);
  --case:        var(--green);
  --cw-surface:  var(--paper);
  --cw-top:      var(--paper);
  --space-2xs: 4px;
  --space-sm:  12px;
  --space-md:  16px;
  --space-lg:  24px;
  --space-xl:  32px;
  --v-tight:   16px;
  --v-section: var(--section);
}

/* ═══════════════════════════════════════════════════════════════════
   THE CASE STUDY'S OWN BLOCKS
   Fourteen classes in this page's markup had no rule anywhere: they used
   to be styled by the old global stylesheet, which did not come with the
   page. Unstyled, they fell back to browser defaults — bullet discs,
   collapsed labels, stats stacked as plain sentences. These are those
   blocks, rebuilt on the current system: two families, the ladder, and
   the same label-left / content-right shape the rest of the page uses.
   ═══════════════════════════════════════════════════════════════════ */

/* ── The hero ─────────────────────────────────────────────────── */
/* THE HEADER STARTS JUST UNDER THE NAV (Anna, 26 Aug 2026: "make it like a
   header and right below the nav bar"). 48 above the kicker read as a gap
   between two things; 24 reads as the masthead and the page belonging to each
   other. The deep step stays BELOW, where the real break is — into the work. */
/* Bottom padding removed (Anna, 27 Aug 2026, spacing-canon pass): it was
   stacking with .case-skim's own margin-top:var(--section), so the step into
   the skim was 96 + 48 = 144, not the declared 96. .case-skim owns that step
   on its own now — one declaration, the same pattern .cs-meta and .ucite
   already use for the section above them. */
.page-case .cs-header{ padding-block:var(--within) 0; }
.page-case .cs-hero-row{
  display:grid; grid-template-columns:repeat(12, minmax(0,1fr));
  gap:var(--within) var(--col); align-items:start;
}
.page-case .cs-hero-text{ grid-column:1 / span 7; min-width:0; }
.page-case .cs-hero-media{ grid-column:8 / -1; min-width:0; }
/* ── THE TWO HERO COLUMNS ARE CENTRED ON EACH OTHER ───────────────
   Measured at 1440: the demo ran 2.26x the height of the text beside it on
   Edgefolio, so start-aligned it left 306px of dead paper under the last line
   — 189 on 90POE, 121 on Healthily. Her live page holds the same two columns
   to 96, so the imbalance is the port's, not the design's.

   Taking the demo out of flow so the words set the row was tried first and
   rejected: these cells hold a caption AND a card, so pinning their children
   stacked one on top of the other, and clipping an INTERACTIVE demo hides
   controls people are meant to reach. A screenshot can be cropped; a working
   thing cannot.

   Centring was tried and is now wrong: once the hero type was raised to live's
   proportions the text column grew from 267 to 350–458, so on three of the
   four pages the WORDS are now the taller column and there is nothing left to
   centre against — centring only pushed the whole header down the page.
   Top-aligned, the kicker starts where the header starts. (Anna, 26 Aug 2026:
   "move everything higher up".) */
@media (min-width:1024px){
  .page-case .cs-hero-row{ align-items:start; }
}
/* THE KICKER SITS A REAL STEP ABOVE THE HEADLINE. Declared 16 delivered 14
   of optical gap here and 42 on live — the same number, because live's
   boxes are untrimmed and carry ~26px of hidden slack (the kicker's
   descender space plus the headline's ascender above its cap). Trimmed,
   there is no slack left to borrow and the declared number IS the air.
   32 is 0.5em of the 64px headline — the same ratio the landing page uses
   between its 13px meta and its 32px title, so both pages set a label
   against a title by one rule rather than two numbers. */
/* Raised to 48 (Anna, 27 Aug 2026). */
.page-case .cs-hero-text .kicker{ margin:0 0 var(--xxl); }
/* Live-page kicker treatment (Anna: include the live page's kicker on the
   new design). Live's .cs-header .kicker sets color:var(--case), and every
   case page resolves --case to --pop — so the kicker is always the deep
   green, not the muted ink-3 this rule inherits from .page-case .kicker.
   The leading slash is live's own ::before (content:"/ ", the space baked
   into the string rather than a margin-right), also colored --pop — a
   different mechanism from .seg-label::before's idiom, so it is reproduced
   as-is rather than borrowed, with --pop translated to mock.css's --green. */
.page-case .cs-hero-text .kicker{ color:var(--green); }
.page-case .cs-hero-text .kicker::before{ content:"/ "; color:var(--green); font-weight:500; }
/* One step below the landing page's h1, so a case study never ties with the
   site's own opening line — and the 48 cut, because the optical axis answers
   the size the type is actually SET at, not the size the face can reach. */
/* 64, not 48. Measured against live at 1440: the demo card is 505px tall on
   BOTH versions, so the hero's imbalance was never the card — it was the type
   beside it. Live sets its h1 at 67 and its lede at 25, which builds a 462px
   text column against that card. The port had 48 and 18, which built 267 and
   left 281px of dead paper. The card was never too tall; the words were too
   small to stand next to it. */
/* OPTICAL SIZE FOLLOWS THE SET SIZE (12 Sep 2026). This carried "opsz" 48
   from when the headline WAS 48px; the size was raised to 64 and the axis
   was left behind, so a 64px line was being drawn with the cut Fraunces
   designed to survive at 48 — thick stems, low contrast, the exact reason a
   big serif reads heavy and dated and nobody can say why. The axis is reset
   at every step below, because the rule is the size the type is actually
   SET at. Higher opsz draws narrower, so this can only free space, never
   overflow a cell. */
.page-case .cs-hero-title{
  margin:0; font-weight:400; letter-spacing:-0.022em;
  font-size:64px; line-height:64px; color:var(--ink);
  max-width:none; text-wrap:balance;  /* the cell is the measure; the rag is even */
  font-variation-settings:"opsz" 64,"SOFT" 0,"WONK" 1;
}
/* The lede's own cap is written against the twelve, and inside the seven-column
   hero cell that calc folds a second time — measured, it came out 26 characters
   in a cell already sized to hold it. The cell is the measure. */
/* THE STANDFIRST TAKES THE SITE'S OWN LEDE, 18/28 — no override at all.
   (Anna, 26 Aug 2026: "drop the size and proportions, apply the canon.")
   I had pushed it to 24 to match live's 25, reasoning that a lede should not
   be smaller than the body it introduces. Against a 64px headline that was
   simply too big: 0.38 of the h1, so the eye met two large voices at the top
   of the page and the headline stopped leading. At 18 the ratio is 0.28 and
   there is one voice.
   The measure comes back with it. At 24 the seven-column cell read 57
   characters; the same width at 18 would run 76, past the measure. Six
   columns puts it at 62. Size and width step together or neither works. */
/* --in7-6, NOT --read-6. The read tokens are percentages of the RAIL, and
   inside the seven-column hero cell that calc folds a second time — measured,
   the standfirst came out 265px and 30 characters. --in7-6 is the same six
   columns expressed against a seven-column parent, which is what this is:
   541px, 62 characters. Same trap that gave the story a 41-character column
   earlier in this stylesheet. */
/* THE BASIC TEXT SIZE — 16/24, the site's body. (Anna, 26 Aug 2026: "make it
   proportional, it's just the basic text.") Not the 18/28 lede role and
   certainly not the 24 it had been pushed to: against a 64px headline this
   is a standfirst, and its job is to be read after the headline, not
   alongside it. At 16 the ratio to the h1 is 0.25 — one voice leads, one
   follows. It keeps the muted ink, which is what marks it as a standfirst
   rather than the size. */
.page-case .cs-hero-text .lede{ font-size:16px; line-height:24px; max-width:var(--in7-6); }
@media (max-width:1279px){ .page-case .cs-hero-title{ font-size:48px; line-height:56px; font-variation-settings:"opsz" 48,"SOFT" 0,"WONK" 1; } }
@media (max-width:1023px){ .page-case .cs-hero-title{ font-size:40px; line-height:48px; font-variation-settings:"opsz" 40,"SOFT" 0,"WONK" 1; } }
@media (max-width:767px){ .page-case .cs-hero-title{ font-size:32px; line-height:40px; font-variation-settings:"opsz" 32,"SOFT" 0,"WONK" 1; } }

/* ── Reading depth, the segmented pair ───────────────────────────
   Not a pill and not a box: the site has ONE hover language — the word goes
   green and a 1px green line draws in under it — and the chosen depth is that
   same move, held on. Native buttons arrive with a border and a grey field,
   so all of it is stripped before the language is applied. */
/* ── THE HERO STACK, MEASURED OFF LIVE ────────────────────────────
   Her live page sets the title and its standfirst 16 apart and the controls
   24 below that. The port had 32 and 32, which is the step BETWEEN groups
   used inside one — so a headline and the sentence that finishes it read as
   two separate blocks, and the whole stack drifted apart.
   16 is the within-a-group rung: the h1 and the lede are one statement. The
   controls are a different thing, so they take the rung above it. */
/* TRIM THE TOP OF THE HEADLINE, NOT THE BOTTOM. Everything on this site is
   trimmed both ends so a declared gap means what it says — but that trim cuts
   the box at the BASELINE, and a 64px Fraunces hangs its descenders 16px below
   that. So a declared 16 under this headline measured 16 and delivered ZERO:
   the "g" of "pages" sat exactly on the lede's cap line.
   Trimming only the start keeps the cap-height alignment above and puts the
   descender back inside the box below, so the 16 is 16 of real air — which is
   what live has, and why live's 16 looks right and this one did not.
   The rule that travels is "clear the descenders". The number never travels. */
.page-case .cs-hero-title{ text-box-trim:trim-start; text-box-edge:cap alphabetic; }
/* 32. The descender is back inside the box, so this number arrives as air —
   but 16 of it is spent clearing the "g" of "pages" before any gap starts.
   Measured glyph-bottom to glyph-top: 24 declared left 16, 32 leaves 24.
   The headline and the standfirst are two sentences and want the step
   between groups, not the one inside a group. (Anna, 26 Aug 2026.) */
.page-case .cs-hero-text .lede{ margin-top:var(--rows); }
/* One step up (Anna, 26 Aug 2026). The switch is a box now, not two words —
   its border sits above the cap line of the text inside it, so a declared 24
   arrived as less than 24 of visible air under the standfirst. 32 gives the
   control room to read as a separate thing you act on, rather than the last
   line of the paragraph above it. */
/* Raised to 48 (Anna, 27 Aug 2026). */
.page-case .case-controls{ display:flex; align-items:baseline; gap:var(--within); margin-top:var(--xxl); }
/* The slash is a separator, not a word, so it is drawn rather than typed —
   nothing to read aloud, nothing to translate, and it can never be selected
   or copied with the label. Same 13px voice, one shade quieter than the word
   it precedes, because a mark that is louder than its label is decoration.
   (Anna, 26 Aug 2026.) */
.page-case .seg-label{ font-size:13px; line-height:20px; color:var(--ink-3); }
.page-case .seg-label::before{ content:"/"; margin-right:0.4em; color:var(--hairline-2); }
.page-case .seg{ display:flex; align-items:baseline; gap:var(--within); }
/* ── READING DEPTH: A PILL SWITCH ────────────────────────────────
   (Anna, 26 Aug 2026: "make it visible, make it like a pill switch.") This
   replaces the two-underlined-words version and the note above it that said a
   pill is a box — it is, and here the box is the point: the control was so
   quiet that the page's one real choice read as a footnote.

   The track is a hairline and the choice is the green, filled. Radius 999,
   not the 8 the canon gives buttons: a segmented switch IS a pill, and a
   rounded rectangle inside a rounded rectangle at two different radii is the
   thing that reads as unfinished. Paper on green measures 7.01:1. */
.page-case .seg{
  display:inline-flex; align-items:center; gap:4px;
  padding:4px; border:1px solid var(--hairline-2); border-radius:999px;
}
.page-case .seg button{
  appearance:none; background:none; border:0; margin:0; font:inherit;
  /* NO BOX TRIM. Trimming is right for type sitting on paper, where a declared
     gap should mean cap-to-baseline. It is wrong inside a filled pill: it cut
     the box to the 9px cap height, so 8px of padding either side built a 25px
     pill instead of 36 and the green looked shrink-wrapped to the letters.
     A control's box is its whole line, not its ink. */
  text-box-trim:none;
  font-size:13px; line-height:20px; padding:8px 16px; border-radius:999px;
  color:var(--ink-3); cursor:pointer; white-space:nowrap;
  transition:color var(--dur-1) var(--ease), background-color var(--dur-1) var(--ease);
}
@media (hover:hover) and (pointer:fine){
  .page-case .seg button:hover{ color:var(--ink); }
}
.page-case .seg button:focus-visible{ outline:2px solid var(--green); outline-offset:2px; }
.page-case .seg button[aria-pressed="true"]{ background:var(--green); color:var(--paper); }
.page-case .seg button[aria-pressed="true"]:hover{ color:var(--paper); }
/* "60 sec" is the qualifier inside the choice, not a second choice. It reads
   quieter by opacity rather than by a colour, so it stays legible on both the
   paper track and the green pill. */
/* Full ink, smaller size. At 0.68 opacity the sub blended to ~3.2:1 against
   the paper — under AA for 13px text (the 21 Aug register finding, reintroduced
   by the port). The quieter voice now comes from size alone. */
.page-case .seg-sub{ font-size:11px; }
/* The 44 is real height here, not a hit area: this control now has a visible
   box, and a tap target that does not match the box people can see is the
   fault that pattern normally exists to avoid. */
@media (pointer:coarse){ .page-case .seg button{ padding-block:12px; } }

/* "Read the full story" at the end of the skim: restyled to a right-aligned
   call, always green (Anna, 27 Aug 2026, spacing-canon pass). No background,
   no border — the same underline draw-in as the site's one hover language
   (.writing .t), minus the colour transition, because this label starts
   green and never changes colour on hover. The arrow is plain text inside
   the label, so it draws in with the rest of the underline rather than
   moving on its own. */
.page-case .skim-more button{
  appearance:none; background:none; border:0; padding:0 0 2px; margin:0 0 -2px;
  position:relative; font:inherit; font-size:13px; line-height:20px;
  color:var(--green); cursor:pointer;
  background-image:linear-gradient(var(--green),var(--green));
  background-size:0% 1px; background-repeat:no-repeat; background-position:0 100%;
  transition:background-size var(--dur-2) var(--ease), transform var(--dur-2) var(--ease);
}
@media (hover:hover) and (pointer:fine){
  .page-case .skim-more button:hover{ background-size:100% 1px; }
}
.page-case .skim-more button:focus-visible{
  background-size:100% 1px; outline:2px solid var(--green); outline-offset:2px;
}
.page-case .skim-more button:active{ transform:translateY(1px); }
.page-case .skim-more button::before{ content:""; position:absolute; inset:-18px 0; }
@media (pointer:coarse){ .page-case .skim-more button{ min-height:0; } }

/* ── The three figures ────────────────────────────────────────────
   A number is the one word in a row that is not prose, so it takes the
   other voice — the serif, at the row's own size. */
/* THE NUMBERS SIT IN THE HERO, BESIDE THE DEMO — not in a band under it.
   (Anna, 26 Aug 2026.) Two things follow. The evidence lands next to the claim
   it proves rather than a scroll away from it; and the text column grows by
   the height of the numbers, which is what finally balances it against a demo
   card that runs 505px whatever the words do. Measured before: 350 against
   548, a 198px hole. After: the two columns land within a rung of each other.
   The gutter drops from 48 to 24 because these three now share seven columns
   rather than twelve, and 48 between them read as three separate blocks. */
.page-case .stats-hero{
  display:grid; grid-template-columns:repeat(3, minmax(0,1fr));
  gap:var(--lg); margin-top:var(--xxl);
}
.page-case .stats-hero .stat{ min-width:0; }
.page-case .stats-hero .n{
  display:block; font-family:"Fraunces", Georgia, "Times New Roman", serif;
  font-variation-settings:"opsz" 32,"SOFT" 0,"WONK" 1;
  font-variant-numeric:oldstyle-nums tabular-nums; /* live's stat figures carry this; the port dropped it */
  font-weight:600; font-size:32px; line-height:36px; letter-spacing:-0.014em;
  color:var(--ink);
}
.page-case .stats-hero .d{
  display:block; margin-top:var(--within);
  font-size:13px; line-height:20px; color:var(--ink-3);
}
.page-case .stat-q{ color:var(--ink-3); }

/* The stat band (Anna's pick, 27 Aug 2026): the numbers leave the text
   column and close the first screen as one full-rail datum. One signifier:
   the hairline. */
.page-case .stats-band{ margin-top:var(--xxl); padding-top:var(--rows); border-top:1px solid var(--hairline); gap:var(--col); }

.page-case .stats-band .n{ font-size:48px; line-height:52px; font-variation-settings:"opsz" 48,"SOFT" 0,"WONK" 1; }


/* ── The flow panel (5 Sep 2026) ───────────────────────────────────────
   Anna's Claude Design canvas "System in Three Cards", ported verbatim as
   inline-styled markup (Untitled UI inside the cards, her tint and labels
   around them). The stylesheet adds only what the canvas could not: the
   place on the page, the entrance, the draw of the connectors, the phone
   stack. Cards are flex:1 in a row with 48px connectors, so a page may
   carry one, two, three or four of them. */
.page-case .flow{ margin:var(--xxl) 0 0; }
/* the site trims paragraph leading (text-box-trim) for its own rhythm; the product cards keep Untitled UI's line boxes */
.page-case .flow p, .page-case .flow h3, .page-case .flow span{ text-box-trim:none; }
html.rise .page-case .flow-conn.r{ opacity:1; clip-path:inset(0 100% 0 0); }
html.rise .page-case .flow-conn.r.in{ animation:flowDraw var(--dur-3) var(--ease-enter) 200ms both; }
@keyframes flowDraw{ from{ clip-path:inset(0 100% 0 0); } to{ clip-path:inset(0 0 0 0); } }
@media (prefers-reduced-motion:reduce){ html.rise .page-case .flow-conn.r, html.rise .page-case .flow-conn.r.in{ clip-path:none; animation:none; } }
@media (max-width:767px){
  /* the canvas styles are inline, so the phone stack has to outrank them */
  .page-case .flow-row{ flex-direction:column !important; gap:16px; }
  .page-case .flow-conn{ display:none !important; }
}

/* ── Owned · Scope · Method ───────────────────────────────────────
   A description list, on the page's own two lines: the term in the five
   columns, everything it describes in the seven. */
/* THREE COLUMNS ACROSS, NOT THREE ROWS DOWN (Anna, 26 Aug 2026). Stacked with
   a label gutter, Owned / Scope / Method cost three full screens of scroll and
   left the same empty left column the story had. Side by side they read as
   what they are — one set of three, comparable at a glance — and the block
   costs the height of its longest column instead of the sum of all three.
   Each term sits ABOVE its own list, so there is no gutter and no rail
   between a label and the thing it labels. */
.page-case .cs-meta{
  /* One step down from the section rung (Anna, 26 Aug 2026). Owned / Scope /
     Method is not a new section — it is the rest of the header, the credits to
     the hero above it. 96 announced it as a fresh start; 48 keeps it attached
     to the thing it describes, and the section step is saved for the break
     into the story. */
  margin:var(--xxl) 0 0;
  display:grid; grid-template-columns:repeat(3, minmax(0,1fr));
  /* 32, not the 48 column gutter. Three columns on a 1024 rail with 48
     between them left each one 309px — 40 characters at 16px, so a
     three-line statement broke into five. 32 gives back a character a line
     without the three reading as one block. */
  gap:var(--between); align-items:start;
}
.page-case .cs-meta > div{ min-width:0; }
/* THE SERIF IS FOR LARGE HEADERS ONLY (Anna, 26 Aug 2026: "add this
   decorative title only on large headers, not here"). At 13px Fraunces stops
   reading as a voice and starts reading as a decoration on a word that is
   only there to name a column. The label goes back to the body face; the
   serif keeps the h1, the section headings, quotations, and figures. */
.page-case .cs-meta dt{
  margin:0 0 var(--within);
  font-size:13px; line-height:20px; color:var(--ink-3);
  font-family:"Archivo", -apple-system, "Helvetica Neue", Arial, sans-serif;
  letter-spacing:-0.013em;
}
.page-case .cs-meta dd{ margin:0; min-width:0; }
/* Owned reads as one sentence, not a list (Anna, 27 Aug 2026) — same type
   role as the .ln lines so the three columns stay on one rhythm. */
.page-case .cs-meta dd > p{ margin:0; font-size:16px; line-height:24px; color:var(--ink-3); }
/* THE LEAD IS THE ANSWER, AND IT RANKS BY INK, NOT SIZE. At 18 in a 320px
   column it ran 36 characters and broke over two lines, so the one sentence
   that says what she owned read as the widest thing in the block rather than
   the clearest. At 16 it sits level with the lines under it and is told apart
   by full ink against their muted — the same way this site separates a
   statement from its support everywhere else. */
/* NOT BLACK. (Anna, 26 Aug 2026.) Full ink made this line the darkest thing
   in a block of supporting detail, which is more weight than a scope note
   needs — the numbers and the story already carry the page. It sits one step
   above the lines under it, which is enough: it is the first line, it stands
   alone, and 24 separates it from the list. Position does the ranking. */
/* The lead is the first line of the set, not a subhead — same ink and
   tracking as the lines under it, or it reads as accidental bold.
   (Anna, 27 Aug 2026.) */
.page-case .cs-meta .lead{
  display:block; font-size:16px; line-height:24px;
  color:var(--ink-3);
}
/* No discs. Nothing on this site is bulleted — a list is lines with air
   between them, and the air is what says they are a set. */
/* TWO RELATIONSHIPS, TWO STEPS. 16 holds the label to what it labels; the
   lead and its supporting lines share the list's own 12 rhythm, so the lead
   reads as the first line of one set rather than a stranded answer above a
   hole. (Anna, 27 Aug 2026: no gap between the lead and the lines.) */
.page-case .cs-meta .ln{ list-style:none; margin:var(--sm) 0 0; padding:0; }
.page-case .cs-meta .ln li{ font-size:16px; line-height:24px; color:var(--ink-3); }
.page-case .cs-meta .ln li + li{ margin-top:var(--sm); }
.page-case .cs-meta dd > .ln:first-child{ margin-top:0; }

/* ── The sixty-second skim ────────────────────────────────────── */
/* the same section step as the story ladder (64), and the same 4 + 8 columns
   as Scope / Method above it, so the header and the skim sit on one grid
   (Anna, 14 Sep 2026: "this somehow looks weird") */
.page-case .case-skim{ margin-top:var(--step-section); }
.case-edgefolio .case-skim{ margin-top:var(--rows); }   /* 32: 64 was too much, 24 crowded the wrapped stat captions (Anna, 15 Sep 2026, both) */
/* The demo card sat flush against the Result row's text — 0px measured,
   not eyeballed. It's proof attached to that row, so it takes the same
   rhythm as one skim-row to the next, not a bigger section break. */
.page-case .case-skim .wide{ margin-top:var(--rows); }
/* The full story needs the same section step: when the header's bottom
   padding was folded into .case-skim's margin (27 Aug 2026), full mode —
   whose first section carried no top margin of its own — fell to a 0 gap.
   Scoped to the element directly after the header so later full-only
   siblings keep their own rhythm. */
.page-case .case-skim + .full-only{ margin-top:var(--step-section); }
/* STACKED, GREEN LABEL ON TOP (Anna, 15 Sep 2026: "have it stacked, and the
   decision should be green on top, the other things as well, so it's easy to
   read"). One pattern for every case study: the label sits above its line
   in the green, the line reads at 20px in full ink, 46ch wide. */
.page-case .skim-row{
  display:grid; grid-template-columns:repeat(12, minmax(0,1fr));
  gap:var(--within) var(--col); align-items:start;
}
.case-edgefolio .skim-row{ display:block; }
.page-case .skim-row + .skim-row{ margin-top:var(--rows); }
.page-case .sk-k{
  grid-column:1 / span 4;
  font-size:13px; line-height:20px; color:var(--ink-3);
  font-family:"Archivo", -apple-system, "Helvetica Neue", Arial, sans-serif;
  letter-spacing:-0.013em;
}
/* The skim reads at the body size, like the story it summarises. It was 18 —
   between the body and a heading, which is a size with no job on this page.
   (Anna, 26 Aug 2026: regular body text throughout, except titles and the
   small text.) */
.case-edgefolio .sk-k{ display:block; margin-bottom:8px; color:var(--green); }
.page-case .sk-v{
  grid-column:5 / -1; min-width:0;
  font-size:16px; line-height:24px; letter-spacing:-0.011em; color:var(--ink-2);
}   /* body size and body ink, like every paragraph (Anna, 26 Aug and again 15 Sep 2026) */
.case-edgefolio .sk-v{ display:block; max-width:46ch; }
/* Right-aligned to the rail (Anna, 27 Aug 2026): flex + justify-content
   instead of margin-left:auto so the button itself stays the flex item,
   not a block needing its own width guess. */
.page-case .skim-more{
  display:flex; justify-content:flex-end;
  margin-top:var(--rows); font-size:13px; line-height:20px;
}

/* ── The allocator's line ─────────────────────────────────────────
   Somebody else speaking, so it is set in the serif — the same voice as
   the titles, and nothing that is merely read. */
/* 48, not 96. A quotation is a break in the argument, but it sits INSIDE a
   section — and at 96 it had a deeper step than the 48 between one section
   and the next, so a quote read as a bigger event than a new subject. A gap
   inside a thing can never exceed the gap around it. */
.page-case .ucite{ margin:var(--xxl) 0 0; }
.page-case .ucite blockquote{
  margin:0; font-family:"Fraunces", Georgia, "Times New Roman", serif;
  font-variation-settings:"opsz" 32,"SOFT" 0,"WONK" 1;
  font-weight:400; font-style:italic;
  font-size:24px; line-height:32px; letter-spacing:-0.014em; color:var(--ink);
  max-width:var(--read-6); text-indent:-0.34em;
}
.page-case .ucite figcaption{
  margin-top:var(--within); font-size:13px; line-height:20px;
  color:var(--ink-3); max-width:var(--read-5);
}

/* ── The reel's step buttons ──────────────────────────────────────
   The 44px target is a hit area, not layout height. */
.page-case .tabreel-prev, .page-case .tabreel-next{ position:relative; }
.page-case .tabreel-prev::before, .page-case .tabreel-next::before{
  content:""; position:absolute; inset:-8px;
}

@media (max-width:1023px){
  .page-case .cs-hero-text, .page-case .cs-hero-media{ grid-column:1 / -1; }
  .page-case .sk-k, .page-case .sk-v{ grid-column:1 / -1; }
  .page-case .stats-hero{ grid-template-columns:1fr; gap:var(--rows); }
  /* Three across needs three real columns' worth of width. Below the desktop
     breakpoint they stack, and the row gap becomes the step between them. */
  .page-case .cs-meta{ grid-template-columns:1fr; gap:var(--rows); }
}
/* THE COLUMN GAP IS A FLOOR, NOT A PREFERENCE. Twelve columns of minmax(0,1fr)
   can each shrink to zero, but the ELEVEN gaps between them cannot: at a 48
   gutter that is a 528px minimum the grid will not go below, whatever the
   window does. Measured at 375, every one of these grids pushed the document
   to 552 and the whole page scrolled sideways — and the four breakpoint gates
   all passed, because each child was already spanning 1/-1 and looked correct.
   Once every child is full width the columns have no job left, so the grid
   becomes one column and the gap becomes the row step. */
@media (max-width:767px){
  .page-case .cs-hero-row,
  .page-case .cs-meta > div{ display:block; }
  .page-case .skim-row{ grid-template-columns:1fr; gap:var(--within); }
  .page-case .stats-hero{ gap:var(--rows); }
  /* The band is already single-column from 1023px down; at phone widths the
     48/52 figure overwhelms a full-width column on its own, so it steps down
     with the space rather than against it. */
  .page-case .stats-band .n{ font-size:40px; line-height:44px; }
}

/* ── Confetti (the wordmark easter egg) ─────────────────────────────
   Ported from style.css 30 Aug 2026: the 27 Aug migration left these two
   rules behind in the legacy sheet, so the burst fired with invisible
   pieces — Anna: "give me back my confetti". Never retire these without
   confetti.js going too. */
.confetti-layer { position: fixed; inset: 0; pointer-events: none; z-index: 99999; overflow: hidden; }
.confetti-layer i { position: absolute; top: 0; left: 0; display: block; border-radius: 1px; will-change: transform; }


/* ── HEALTHILY TILE: THE SEARCH TYPES, THE PHONE ANSWERS ────────────────
   One picture, and the two halves have to form a sentence: a symptom
   described badly, understood, answered. With the report sitting there
   from frame one the typing caused nothing, so the phone now runs the
   real sequence — empty field, thinking, report — while the search field
   types beside it.

   Nothing here is a frame sequence. The word and its three matches are
   revealed by wiping the plate's OWN ink, so "typing" costs one transform
   and no extra images. The phone is three small screens and its loader is
   Anna's own artwork, sliced into its twelve cells and pulsed — not
   redrawn.

   THE CYCLE IS 4400ms. Everything below is a percentage of it:
     keystrokes  h·e·a·d    200 →  840   4.545% → 19.091%  steps(4)
     matches     3 rows     560 →  890  12.727% → 20.227%  steps(3)
     thinking    fades in   900 → 1100  20.455% → 25.000%
     report      resolves  2100 → 2440  47.727% → 55.455%
     rest                  2440 → 4400
   Percentages are also all of the 2280x880 artboard, so the whole thing
   scales with the tile and never leaves the grid. Every number was
   measured off the export, not guessed. (Anna, 8 Sep 2026.) */
.anim{
  position:relative; width:100%; aspect-ratio:2280/880; overflow:hidden;
  border-radius:var(--radius-sm); background:#fff;
  transition:transform var(--dur-2) var(--ease);
}
/* THE `img` QUALIFIER IS LOAD-BEARING, do not "tidy" it away: the tile sits
   inside `.page-work .shot`, whose own `img{ width:100% }` outranks a plain
   `.anim-frame` and stretches the phone across the whole plate. These
   selectors match its weight and win on order, so this block stays last. */
.anim img{ position:absolute; display:block; }
.anim img.anim-bg{ left:0; top:0; width:100%; }
.anim-hole{
  position:absolute; left:61.587%; top:18.734%;
  width:21.195%; height:116.801%; overflow:hidden;
}
/* Every screen sits in the same place, stacked; the ride-up is what puts
   the clock and battery BESIDE the notch instead of under it. */
.anim .anim-hole > *{ position:absolute; left:0; top:-1.879%; width:100%; }
.anim .anim-hole > span > img{ position:static; width:100%; height:auto; }
.anim img.anim-frame{ left:59.496%; top:12.216%; width:25.395%; z-index:2; }

/* ---- the phone's three states ---------------------------------------
   The entry screen is the floor and never animates; the other two fade in
   over it in turn, so the topmost opaque layer is always the current one. */
.anim-analysing{ opacity:0; animation:animAnalysing 4400ms infinite; }
.anim-report{ opacity:0; animation:animReport 4400ms infinite; }
@keyframes animAnalysing{
  0%,20.455%{ opacity:0; animation-timing-function:cubic-bezier(0.23,1,0.32,1); }
  25%,100%{ opacity:1; }
}
@keyframes animReport{
  0%,47.727%{ opacity:0; animation-timing-function:cubic-bezier(0.23,1,0.32,1); }
  55.455%,100%{ opacity:1; }
}

/* ---- the loader ------------------------------------------------------
   A loading screen that does not move reads as a hung app, which is worse
   than no phone at all. The motif is Anna's own export cropped to its
   3x4 grid and served as one 4KB tile sheet; each cell shows its own slice
   via background-position, so this is her artwork moving, not a redraw.
   The white ground hides the baked-in copy underneath.
   The delays run on the diagonal (row + col), which reads as a considered
   sweep rather than a spinner going round. */
.anim-load{
  position:absolute; left:41.956%; top:30.419%; width:17.156%; height:10.591%;
  background:#fff;
  display:grid; grid-template-columns:repeat(3,1fr); grid-template-rows:repeat(4,1fr);
}
.anim-load i{
  background-image:url(img/healthily-loader.webp);
  background-size:300% 400%;
  animation:animLoad 1400ms ease-in-out infinite;
}
.anim-load i:nth-child(3n+1){ background-position-x:0%; }
.anim-load i:nth-child(3n+2){ background-position-x:50%; }
.anim-load i:nth-child(3n+3){ background-position-x:100%; }
.anim-load i:nth-child(-n+3){ background-position-y:0%; }
.anim-load i:nth-child(n+4):nth-child(-n+6){ background-position-y:33.333%; }
.anim-load i:nth-child(n+7):nth-child(-n+9){ background-position-y:66.667%; }
.anim-load i:nth-child(n+10){ background-position-y:100%; }
.anim-load i:nth-child(1){ animation-delay:0ms; }
.anim-load i:nth-child(2),.anim-load i:nth-child(4){ animation-delay:90ms; }
.anim-load i:nth-child(3),.anim-load i:nth-child(5),.anim-load i:nth-child(7){ animation-delay:180ms; }
.anim-load i:nth-child(6),.anim-load i:nth-child(8),.anim-load i:nth-child(10){ animation-delay:270ms; }
.anim-load i:nth-child(9),.anim-load i:nth-child(11){ animation-delay:360ms; }
.anim-load i:nth-child(12){ animation-delay:450ms; }
@keyframes animLoad{
  0%,100%{ opacity:1; transform:scale(1); }
  45%{ opacity:.3; transform:scale(.82); }
}

/* ---- the search field ------------------------------------------------
   The two patches wear Healthily's own tokens, not the site's: the field
   fill and the panel ground are theirs. They start covering the plate's
   ink and shrink away, so "typing" costs nothing but a transform.
   The text patch is sized to the WORD's ink (17.588%-21.469%), not to a
   comfortable box around it — each quarter of a steps(4) reveal is then
   one letter. Pad it wider and the last keystroke uncovers empty field
   while the word already reads complete. */
.anim-patch{ position:absolute; }
.anim-text{
  left:17.55%; top:43.90%; width:3.95%; height:6.60%;
  background:#FAFAFA; transform-origin:100% 50%;
  animation:animType 4400ms infinite;
}
.anim-list{
  left:16.60%; top:55.90%; width:32.40%; height:31.20%;
  background:#fff; transform-origin:50% 100%;
  animation:animList 4400ms infinite;
}
/* The plate is exported WITHOUT a caret, so this is the only one on screen.
   If a future export brings the baked caret back you get two blue lines in
   the field — re-export, don't patch. */
.anim-caret-wrap{
  position:absolute; left:17.588%; top:44.32%; width:4.212%; height:5.85%;
  animation:animCaret 4400ms infinite;
}
.anim-caret{
  position:absolute; right:0; top:0; width:2px; height:100%;
  background:#2173D9;                      /* Healthily's caret blue */
  animation:animBlink 1060ms linear infinite;
}
/* Autocomplete fires at three characters — the same three the case study
   names — so the matches start landing ON the third keystroke, while the
   fourth is still being typed. Typing the list in after the word finishes
   would be a prettier sequence and the wrong behaviour.

   THE INTERVAL END IS THE LAST STEP'S END, NOT THE LAST LETTER. With
   jump-start the fourth letter lands at the START of the fourth step, so
   the keyframe runs to 840ms for the steps to be 160ms each. Ending it at
   the letter squashes every step and the word completes on the third. */
@keyframes animType{
  0%,4.545%{ transform:scaleX(1); animation-timing-function:steps(4,jump-start); }
  19.091%,100%{ transform:scaleX(0); }
}
@keyframes animCaret{
  0%,4.545%{ transform:translateX(-100%); animation-timing-function:steps(4,jump-start); }
  19.091%,100%{ transform:translateX(0); }
}
@keyframes animList{
  0%,12.727%{ transform:scaleY(1); animation-timing-function:steps(3,jump-start); }
  20.227%,100%{ transform:scaleY(0); }
}
@keyframes animBlink{ 0%,49.9%{ opacity:1; } 50%,100%{ opacity:0; } }

/* The tile is one link, so the STACK takes the two-pixel lift, not each
   layer — otherwise the plate, the screens and the frame drift apart. */
.page-work .shot .anim img{ border-radius:0; transition:none; }
@media (hover:hover) and (pointer:fine){
  .page-work .item:hover .shot .anim img,
  .page-work .item:focus-visible .shot .anim img,
  .page-work .item:active .shot .anim img{ transform:none; }
  .page-work .item:hover .anim,
  .page-work .item:focus-visible .anim{ transform:translateY(-2px); }
  .page-work .item:active .anim{ transform:none; }
}
/* THESE THREE LOOP. THEY DO NOT STOP ON INTERACTION. (Anna, 9 Sep 2026:
   "loop the animations".)

   The attract-loop ruling — play until the reader does something, then
   freeze on the answer — was written for the case-study pages, where the
   motion sits inside a page of prose someone is reading. It stays in force
   there: 90POE's ledger window and the Healthily app band still stop dead
   on the first scroll.

   On the homepage it was actively wrong, and measured so: not one tile is
   above the fold — they start at 1255, 1838, 2526 and 3070px — so the
   scroll that carried you down to the work was the same event that froze
   it. Nobody had ever seen these move. A tile IS the content here; there is
   no prose beside it to compete with, so it loops for as long as you look.

   The only thing that stops one is leaving the screen: off-view tiles are
   paused AND rewound to frame 0, so the next arrival gets the story from
   the beginning. If the observer never runs (a headless pass, an old
   browser) they simply keep playing, which is the behaviour we wanted
   anyway — the failure mode is the feature, not a frozen half-typed
   word. */
/* THE `!important` IS LOAD-BEARING, do not tidy it away. Every animated
   layer declares its motion with the `animation` SHORTHAND — which resets
   `animation-play-state` to `running` — from a heavier selector than this
   one (`.anim-eg .eg-r1 img` is 0,3,0 against this rule's 0,2,0). Without
   the flag the pause silently loses the cascade and the tiles keep
   burning keyframes off screen. Verified 9 Sep 2026: 0 of 6 paused
   before, 6 of 6 after. */
.anim.offscreen, .anim.offscreen *{ animation-play-state:paused !important; }
/* AND THEY REWIND WHEN THEY LEAVE, which is the half that was missing.
   A paused animation keeps its position, and every one of these stories
   finishes long before its cycle does: the Healthily typing is over at
   20.2% of 4400ms, the 90POE rows have all landed by 52.8% of 6000ms, the
   Edgefolio line finishes drawing at 65%. So the rest of each loop is a
   finished, static frame — and a reader arriving at a random phase was
   arriving after the ending. On Healthily that is four times out of five.
   (Anna, 9 Sep 2026: "when I scroll it is done, I can't see it, the other
   is over.")

   attract.js adds this class for exactly one reflow as a tile leaves, which
   makes the browser throw the running animations away and build them again
   at time 0 — parked, paused, ready. It must sit AFTER the pause rule: both
   are 0,2,0 and both important, so source order is what lets `none` win for
   that single frame. The flag itself is load-bearing for the same reason as
   above. */
.anim.rewind, .anim.rewind *{ animation:none !important; }
@media (prefers-reduced-motion:reduce){
  .anim-text{ animation:none; transform:scaleX(0); }
  .anim-list{ animation:none; transform:scaleY(0); }
  .anim-caret-wrap{ animation:none; transform:none; }
  .anim-caret{ animation:none; }
  .anim-analysing{ animation:none; opacity:0; }
  .anim-report{ animation:none; opacity:1; }
  .anim-load i{ animation:none; }
}

/* ── EDGEFOLIO TILE: THE FIGURES LAND, THE LINE DRAWS ───────────────────
   Her claim is "eight tabs became two pages, IN THE ORDER ALLOCATORS
   DECIDE", so the five figures land left to right — each rising into its
   own window FROM ABOVE, slightly out of focus and snapping sharp, the
   way a departure board drops into place (Anna, 8 Sep 2026: from the top,
   not up from the bottom).

   WHY THERE IS NO SLOT-MACHINE STRIP. The first build spun each window
   through the OTHER four metrics, because this screen contains no 3, 4 or
   5 and a real 0-9 drum could not be cut from her own type. Watching it
   back, the mid-spin frames showed "$2.8B" sitting under "Annualised Net
   Return" and "ar 20" under "Sharpe Ratio" — wrong values under the wrong
   labels, sliced mid-glyph. On a case about fund-data accuracy that is
   the one thing the tile must never show, even for 200ms. So the window
   now holds ONE value, its own, and sells the mechanism with travel and
   focus instead of with decoys.

   THE LINE DRAWS, IT DOES NOT SCALE. scaleY on a raster squashes stroke
   weight with it: at a third grown the return curve rendered pale, thin
   and almost flat — a broken chart, not a growing one. A left-to-right
   reveal keeps every pixel at its true weight and is what drawing a line
   actually looks like. Constant motion, so linear (motion table).
     figures  rise 700ms each, landing 220ms apart from 1000ms
     line     draws 2100 -> 3900ms, linear
     rest     3900 -> 6000ms
   Boxes measured off the export. (Anna, 8 Sep 2026.) */
/* EACH FIGURE GETS ITS OWN BOX, they do not share a band. The Sharpe
   tile carries a TWO-LINE caption, so its figure sits 21px higher and
   is shorter than the other four — one shared window clipped the top
   off "0.86" and left it floating. (Anna spotted it, 8 Sep 2026.) */
.anim-eg .eg-reel{ position:absolute; overflow:hidden; }
.anim-eg .eg-reel img{ position:absolute; left:0; top:0; width:100%; height:100%; }
.anim-eg .eg-r1{ left:15.241%; width:5.680%; top:32.216%; height:6.477%; }
.anim-eg .eg-r1 img{ animation:egLand1 6000ms infinite; }
.anim-eg .eg-r2{ left:29.803%; width:6.601%; top:32.216%; height:6.477%; }
.anim-eg .eg-r2 img{ animation:egLand2 6000ms infinite; }
.anim-eg .eg-r3{ left:44.452%; width:9.781%; top:32.557%; height:6.705%; }
.anim-eg .eg-r3 img{ animation:egLand3 6000ms infinite; }
.anim-eg .eg-r4{ left:58.925%; width:4.320%; top:32.614%; height:5.625%; }  /* the Sharpe tile sat 13px high in the export; moved down to the shared baseline, 14 Sep 2026 */
.anim-eg .eg-r4 img{ animation:egLand4 6000ms infinite; }
.anim-eg .eg-r5{ left:73.531%; width:9.013%; top:32.614%; height:5.625%; }
.anim-eg .eg-r5 img{ animation:egLand5 6000ms infinite; }
@keyframes egLand1{ 0%,5.000%{ transform:translateY(-120%); filter:blur(6px); animation-timing-function:cubic-bezier(0.16,1,0.3,1); } 16.667%,100%{ transform:translateY(0); filter:blur(0); } }
@keyframes egLand2{ 0%,8.667%{ transform:translateY(-120%); filter:blur(6px); animation-timing-function:cubic-bezier(0.16,1,0.3,1); } 20.333%,100%{ transform:translateY(0); filter:blur(0); } }
@keyframes egLand3{ 0%,12.333%{ transform:translateY(-120%); filter:blur(6px); animation-timing-function:cubic-bezier(0.16,1,0.3,1); } 24.000%,100%{ transform:translateY(0); filter:blur(0); } }
@keyframes egLand4{ 0%,16.000%{ transform:translateY(-120%); filter:blur(6px); animation-timing-function:cubic-bezier(0.16,1,0.3,1); } 27.667%,100%{ transform:translateY(0); filter:blur(0); } }
@keyframes egLand5{ 0%,19.667%{ transform:translateY(-120%); filter:blur(6px); animation-timing-function:cubic-bezier(0.16,1,0.3,1); } 31.333%,100%{ transform:translateY(0); filter:blur(0); } }

.anim-eg .eg-lines{
  position:absolute; left:20.482%; top:80.114%; width:64.693%; height:19.886%;
  animation:egDraw 6000ms infinite;
}
.anim-eg .eg-lines img{ position:absolute; left:0; top:0; width:100%; height:100%; }
@keyframes egDraw{
  0%,35.000%{ clip-path:inset(0 100% 0 0); animation-timing-function:linear; }
  65.000%,100%{ clip-path:inset(0 0 0 0); }
}
@media (prefers-reduced-motion:reduce){
  .anim-eg .eg-reel img{ animation:none; transform:none; filter:none; }
  .anim-eg .eg-lines{ animation:none; clip-path:inset(0 0 0 0); }
}

/* ── 90POE TILE: WHAT YOU BUILD ON THE LEFT LEAVES ON THE RIGHT ─────────
   Her claim is "owned the payment system 0 to 1, not a single invoice
   lost", so the verb here is CARRYING: every line item lands in the
   builder and appears in the shipped invoice IN THE SAME BEAT. Freight
   and Demurrage are drawn on both halves and share a delay — that pairing
   is the whole point, and it is what "nothing falls between them" looks
   like. Then the ledger resolves: subtotal, VAT, total.

   Third verb of four, deliberately. Healthily types, Edgefolio draws,
   this one lands. Same trick on every tile would be a fingerprint, not a
   system; Self-Service gets the fourth (a task completing).

   ONE overlay file, drawn eight times and clipped to a different row each
   time — the browser fetches it once, so eight rows cost one request.
   Entrance is Linear's curve at 520ms, opacity plus a 1% rise, on a
   6000ms cycle to match Edgefolio's slower read (Anna, 8 Sep 2026).
   Row bands measured off the two exports. (Anna, 8 Sep 2026.) */
.anim-poe .poe-row{
  position:absolute; left:0; top:0; width:100%;
  animation-duration:6000ms; animation-iteration-count:infinite;
}
/* THE TILE IS ONE SENTENCE AND IT READS LEFT TO RIGHT (Anna, 9 Sep 2026:
   "first the typed in invoice and then appears on the right — it is
   generating the invoice").

   The plate is the empty state: blank item rows, blank totals, an unset
   bank account, and an invoice document on the right carrying its header
   and vessel strip but no lines. The overlay is the same screen filled in,
   and each band below wipes one piece of it into place.

   The band that was wrong was the horizontal one. Every clip used to run
   `inset(T 0 B 0)` — full width — so a single band lit the builder row on
   the left AND the invoice line on the right in the same instant. That is
   not what the screen does. Entering a line item is the cause; the invoice
   line is the effect, and a tile that shows them simultaneously says the
   two panels are unrelated.

   So the clips are now split down the gutter. Measured off the two plates
   rather than guessed: the builder's content ends at x=1054 and the
   document's begins at x=1138 of 2048, with the dark panel edge at 1099 —
   the split sits at 1096 (53.516%), inside a gutter that is identical in
   both images, so neither half can ever leak into the other.

   Left first, six beats: three line items, the totals computing, the total,
   the bank account. Then a pause of about half a second — that gap IS the
   generate — and the document writes its two lines on the right.

   (The band at y93-110 looks like a diff and is not one: the table header
   is identical in both plates and differs only in WebP encoding. Animating
   it would flash the column titles. It is deliberately absent.) */
/* ── left: the builder, being filled in ─────────────────────────────── */
.anim-poe .poe-l1{ clip-path:inset(20.759% 46.484% 75.823% 0); animation-name:poeL1; }  /* Freight */
.anim-poe .poe-l2{ clip-path:inset(30.253% 46.484% 66.456% 0); animation-name:poeL2; }  /* Demurrage */
.anim-poe .poe-l3{ clip-path:inset(39.620% 46.484% 57.089% 0); animation-name:poeL3; }  /* Disbursements */
.anim-poe .poe-l4{ clip-path:inset(48.228% 46.484% 42.658% 0); animation-name:poeL4; }  /* Subtotal + VAT */
.anim-poe .poe-l5{ clip-path:inset(60.633% 46.484% 36.203% 0); animation-name:poeL5; }  /* Total */
.anim-poe .poe-l6{ clip-path:inset(94.304% 46.484%  2.785% 0); animation-name:poeL6; }  /* bank account */
/* ── right: the invoice, being generated ───────────────────────────── */
.anim-poe .poe-r1{ clip-path:inset(86.329% 0  7.848% 53.516%); animation-name:poeR1; }  /* Freight + route */
.anim-poe .poe-r2{ clip-path:inset(97.215% 0  0.000% 53.516%); animation-name:poeR2; }  /* Demurrage */
@keyframes poeL1{ 0%,3.000%{ opacity:0; transform:translateY(1%); animation-timing-function:cubic-bezier(0.165,0.84,0.44,1); } 11.000%,100%{ opacity:1; transform:translateY(0); } }
@keyframes poeL2{ 0%,8.000%{ opacity:0; transform:translateY(1%); animation-timing-function:cubic-bezier(0.165,0.84,0.44,1); } 16.000%,100%{ opacity:1; transform:translateY(0); } }
@keyframes poeL3{ 0%,13.000%{ opacity:0; transform:translateY(1%); animation-timing-function:cubic-bezier(0.165,0.84,0.44,1); } 21.000%,100%{ opacity:1; transform:translateY(0); } }
@keyframes poeL4{ 0%,24.000%{ opacity:0; transform:translateY(1%); animation-timing-function:cubic-bezier(0.165,0.84,0.44,1); } 32.000%,100%{ opacity:1; transform:translateY(0); } }
@keyframes poeL5{ 0%,29.000%{ opacity:0; transform:translateY(1%); animation-timing-function:cubic-bezier(0.165,0.84,0.44,1); } 37.000%,100%{ opacity:1; transform:translateY(0); } }
@keyframes poeL6{ 0%,40.000%{ opacity:0; transform:translateY(1%); animation-timing-function:cubic-bezier(0.165,0.84,0.44,1); } 48.000%,100%{ opacity:1; transform:translateY(0); } }
@keyframes poeR1{ 0%,56.000%{ opacity:0; transform:translateY(1%); animation-timing-function:cubic-bezier(0.165,0.84,0.44,1); } 65.000%,100%{ opacity:1; transform:translateY(0); } }
@keyframes poeR2{ 0%,62.000%{ opacity:0; transform:translateY(1%); animation-timing-function:cubic-bezier(0.165,0.84,0.44,1); } 71.000%,100%{ opacity:1; transform:translateY(0); } }
@media (prefers-reduced-motion:reduce){
  .anim-poe .poe-row{ animation:none; opacity:1; transform:none; }
}

/* ── THE TILES HOLD STILL ON A SMALL SCREEN ────────────────────────────
   Measured 8 Sep 2026: at 375px the tile renders 327x126, so the typed
   word is about 13px wide and an invoice row is 2px tall. The motion runs,
   costs battery, and says nothing — nobody can read a keystroke at that
   size. So below the tablet breakpoint every tile is served its ANSWER
   frame, the same one the attract loop stops on: word typed, matches
   shown, report on the phone; figures landed and the return line drawn;
   every line item carried across to the shipped invoice.

   The caret is the one thing that does NOT survive the shrink. At full
   size it says "someone is typing here"; at 126px it is a 2px blue speck
   beside a word and reads as a rendering fault, so it goes.

   This is the same end state reduced motion gets — two routes to one
   frame, which is why the frame had to be worth looking at on its own.
   It always was: it is the finished screen. */
@media (max-width:767px){
  .anim-text{ animation:none; transform:scaleX(0); }
  .anim-list{ animation:none; transform:scaleY(0); }
  .anim-caret-wrap{ animation:none; transform:none; opacity:0; }
  .anim-caret{ animation:none; }
  .anim-analysing{ animation:none; opacity:0; }
  .anim-report{ animation:none; opacity:1; }
  .anim-load i{ animation:none; opacity:1; transform:none; }
  .anim-eg .eg-reel img{ animation:none; transform:none; filter:none; }
  .anim-eg .eg-lines{ animation:none; clip-path:inset(0 0 0 0); }
  .anim-poe .poe-row{ animation:none; opacity:1; transform:none; }
}


/* ═════════════════════════════════════════════════════════════════
   THE CASE-STUDY HERO SYSTEM (moved here from projects/edgefolio.html on
   14 Sep 2026 so Healthily and Self-Service share one copy: the browser
   chrome, the split hero with the picture running to the screen edge, Owned
   in the hero, the Scope/Method pair, and the words opening in sequence).
   ═════════════════════════════════════════════════════════════════ */
/* Below 1024 the picture drops under the words on the full rail, one
   evidence step below the Read switch (the header grid's 16 row gap comes
   out of the margin). From 1024 the split rules further down take over. */
.page-case .cs-hero2{ grid-column:1 / -1; margin:calc(var(--step-figure) - var(--within)) 0 0; }
@media (min-width:1024px){ .page-case .cs-hero-row > .cs-hero-text{ grid-column:1 / -1; }
  /* the headline runs wide; the standfirst keeps the reading measure: seven
     of the twelve columns, about 65 characters */
  .page-case .cs-hero-row > .cs-hero-text .lede{ max-width:calc((100% - 11 * var(--col)) / 12 * 7 + 6 * var(--col)); } }
/* THE BROWSER. It carries the edge, the corners and the light now — the
   product inside carries none of them, so nothing is drawn over her pixels and
   the crop no longer has to pretend to be a window on its own.
   Two clearances on the shadow, both against the scene's clip. Upward:
   34 − 20 − 22 is below zero, so nothing reaches above the top edge.
   Sideways: 34 − 20 = 14, inside the 24px of scene to its left, so the clip
   has nothing to cut. That sideways number is the one that produced the grey
   band she saw. */
/* ONE BROWSER, FIVE WINDOWS (Anna, 12 Sep 2026: "put those in a browsers
   too"). The hero got its chrome first; every other whole page on the page now
   shares this exact one, so the five windows read as five screenshots taken in
   the same browser rather than five different treatments.
   The bar is sized in cqw against the browser's OWN width, then clamped. The
   clamp is the design decision: a pure percentage would have given the hero a
   33px bar and the full-rail windows a 47px one, and a browser that grows its
   chrome reads as a different browser. Clamped, every window lands at 34 on
   the desktop and shrinks together on a phone — same browser, pages at
   different scales inside it, which is what it would actually look like. */
.page-case .csb{ position:relative; overflow:hidden; container-type:inline-size;
  border:1px solid rgba(24,29,39,.08); border-radius:var(--radius-sm); background:#fff;
  box-shadow:0 1px 2px rgba(24,29,39,.06), 0 22px 34px -20px rgba(24,29,39,.22); }
.csb-bar{ display:flex; align-items:center; gap:clamp(10px,3.2cqw,23px);
  height:clamp(22px,4.6cqw,34px); padding:0 clamp(10px,3.2cqw,23px);
  background:#F6F6F4; border-bottom:1px solid rgba(24,29,39,.07); }
.csb-dots{ display:flex; gap:clamp(4px,1.1cqw,8px); flex:0 0 auto; }
.csb-dots i{ width:clamp(6px,1.15cqw,8px); height:clamp(6px,1.15cqw,8px);
  border-radius:50%; background:rgba(24,29,39,.15); }
/* Empty on purpose: a URL here would be an invented specific. */
.csb-url{ flex:1 1 auto; max-width:46%; height:clamp(12px,2.5cqw,18px);
  border-radius:999px; background:#fff; border:1px solid rgba(24,29,39,.07); }
/* The frame carries the edge, the corners and the light; her export carries
   none of them. Four classes so it beats each window's own border rule
   wherever that sits in this stylesheet. */
.page-case .csb .anim.cs-film{ border:0; border-radius:0; box-shadow:none; }
.page-case .cs-hero2 .h2-main{ position:relative; margin:0; overflow:hidden; aspect-ratio:16 / 9;
  border:0; border-radius:0; background:#fff; }
.cs-hero2 .h2-scene{ position:relative; container-type:inline-size; }
/* THE SPLIT HERO (12 Sep, her Revolut reference). The words take columns 1 to
   6; the picture takes 7 to 12 and then runs on to the window's edge, which
   is what --bleed measures: the paper left of the rail plus its inset. The
   window itself is 1.45x the space it sits in, so it is cropped by the
   screen rather than shrunk — the product stays at the homepage tile's
   scale. Card widths are rail columns (5 and 4), their positions a share of
   the scene, so they stagger on a diagonal and never touch each other. */
@media (min-width:1024px){
  /* THE ROW GAP IS OFF FROM 1024 UP (12 Sep 2026, "fix the vertical spacing").
     Above this width the hero is two columns and the only thing the row gap
     did was land on top of Owned's own 48 margin, making that step 64. Owned
     carries its own step; the gap was a second, silent one. */
  .page-case .cs-hero-row{ row-gap:0; }
  .page-case .cs-hero-row > .cs-hero-text{ grid-column:1 / span 6; }
  .page-case .cs-hero-row > .cs-hero-text .lede{ max-width:none; }
  /* a negative margin does not stretch a grid item, so the width carries the
     bleed and the item starts at its own column line */
  /* a negative margin does not stretch a grid item, so the width carries the
     bleed. The cell also starts half a gutter left of its column line, which
     is the room the live-session card breaks out into; the window itself is
     pushed back onto the column line. */
  .page-case .cs-hero2{ grid-column:7 / -1; margin:0 0 0 calc(-.5 * var(--col)); justify-self:start;
    --bleed:calc((100vw - min(90vw, var(--rail) + 2 * var(--gutter))) / 2 + var(--gutter));
    width:calc(100% + var(--bleed) + .5 * var(--col)); }
  /* THE FIGURE ALWAYS REACHES THE SCREEN EDGE; THE PRODUCT'S SCALE IS WHAT
     IS CAPPED (13 Sep 2026, Anna: "why is there a gap, let the image go
     through to the end of the screen"). The old cap sat on --bleed, which
     sizes the clip box, so above 1440 the picture stopped 144px short of the
     edge on a 1728 display. Now the bleed is never capped — the figure ends
     on the viewport edge at every width — and the browser inside it is held
     to its 1440 pixel size (1044px) so the product is never cropped harder on
     a wider screen. On a display so wide that the figure outgrows even that,
     the browser takes the figure's full width, so it still runs off the
     edge and the product simply gets larger, never more cropped. */
  /* THE CLIP STAYS, THE SHADOW GETS SMALLER (Anna, 12 Sep 2026: "fix this
     shadow it looks strange" — a hard grey band down the left side of the
     picture). The cause was the window's shadow reaching exactly as far
     sideways as the 24px of scene left of it, so the clip sliced it off
     square. I tried removing the clip and measured the result: the page then
     scrolled sideways 348px at 1440, because body's overflow-x:hidden does
     not stop this. So the clip is load-bearing and the shadow is what
     changes — every fall below reaches less far sideways than it has room
     for, and the band has nothing to cut. */
  .cs-hero2 .h2-scene{ overflow:clip; }
  /* the bleed moves to the browser: it is the object that runs off the edge */
  .page-case .cs-hero2 .h2-browser{ width:max(min(145%, 1044px), 100%); margin-left:calc(.5 * var(--col)); }
  /* the caption is words, so it stops at the rail, never at the screen edge */
  .cs-hero2 figcaption{ margin-left:calc(.5 * var(--col)); max-width:calc(100% - var(--bleed) - .5 * var(--col)); }
}
/* THE WORDS OPEN IN SEQUENCE (12 Sep 2026, "do an animation"). The site's
   own entrance already lifts the hero row as one block; this stages the four
   text blocks inside it, so the kicker, the headline, the standfirst and the
   Read switch arrive in the order they are read rather than all at once.
   That staged opening is the whole of what a landing page's first second is.
   The site's own `rise` keyframe, curve and duration, and its stagger capped
   at three steps: 0, 100, 200, 200.
   The hidden state is added by the script below, one frame before the class
   that clears it, so nothing on this page is ever hidden by something that
   might not load. */
/* THE LABEL COMES OFF IN THE HERO (Anna, 12 Sep 2026: "it says Owned and then
   it starts with I led — feels weird"). She is right, and the reason is the
   move itself: down in Owned / Scope / Method the word was doing comparative
   work, naming one of three columns you read against each other. Alone in the
   hero it has nothing to compare against, so it just announces what the next
   sentence already says — "I led design on the fund profile, end to end". Her
   sentence is untouched; only the label goes, and it stays in the markup for
   screen readers, where a description list still needs its term. */
.page-case .cs-own dt{
  position:absolute; width:1px; height:1px; margin:-1px; padding:0;
  overflow:hidden; clip-path:inset(50%); white-space:nowrap; border:0;
}
/* OWNED. One child, so it takes its whole cell rather than a third of it, and
   it keeps the 48 step that held it to the stats band before. */
.page-case .cs-hero-row > .cs-own{ grid-template-columns:1fr; grid-column:1 / -1; margin:var(--xxl) 0 0; }
/* At 1280 and up it moves into the left column on a second row and the
   picture spans both rows beside it, so the words run to the picture's own
   depth instead of stopping 152px short of it. The block that fills the paper
   is the one that says what she owned. */
@media (min-width:1280px){
  /* ROW GAP OFF, SO OWNED'S STEP IS ONE NUMBER (12 Sep 2026, "fix the vertical
     spacing"). The grid's own 16px row gap was landing on top of Owned's 48
     margin, so the drop from the Read switch to "I led design" measured 79 —
     off the ladder, and the void she flagged. With the gap at zero the margin
     is the whole step, and any slack left over by the picture beside it falls
     BELOW Owned, where the stats band's own 48-to-the-rule-plus-32 already
     absorbs it. */
  .page-case .cs-hero-row{
    /* min-content then 1fr: the picture spans both rows, and grid was handing
       its extra height to the two tracks EQUALLY — 22px into row 1 as well, so
       the step above Owned measured 70 instead of 48 at 1440 and moved with the
       window. Pinning row 1 to its content and giving row 2 the 1fr sends every
       spare pixel below Owned, where the stats band's own construction already
       absorbs it. The step above Owned is then the margin, and only the margin,
       at every width. */
    grid-template-rows:min-content 1fr; }
  .page-case .cs-hero-row > .cs-hero-text{ grid-row:1; }
  .page-case .cs-hero-row > .cs-hero2{ grid-row:1 / span 2; }
  /* THE TWO COLUMNS END LEVEL (12 Sep 2026, second spacing pass). The picture
     is 16:9 and its width is fixed by the layout, so above 1280 it runs deeper
     than the words by 44px at 1440 — and that slack was landing UNDER Owned,
     which measured the gap from Owned's last line to the stats hairline at 92
     where every narrower width had 48. Owned now sits at the bottom of its
     row, so both columns finish on the same line and the step into the stats
     band is the same 48 at every width. The air moves above Owned, between the
     reading controls and the ownership line, which is a real break in the
     reading rather than a hole before a hairline. */
  /* MOVED UP (Anna, 15 Sep 2026: "move Owned up"). Pinning Owned to the
     bottom of its row sent every spare pixel ABOVE it, which was a 44px
     break in the reading when the lede ran six lines. The lede is 27 words
     now, and the same rule opened a 203px void in the middle of the column.
     Owned sits at the TOP of its row instead, so the step above it is the
     margin and only the margin at every width, and the slack falls between
     Owned and the stats band where a hairline already closes the section. */
  .page-case .cs-hero-row > .cs-own{ grid-column:1 / span 6; grid-row:2; align-self:start; }
  /* SCOPE AND METHOD UNDER OWNED (Anna, 15 Sep 2026). Third row of the text
     column, two small columns side by side, so the paper under Owned carries
     the credits instead of a void. The picture spans all three rows. */
  .case-edgefolio .cs-hero-row{ grid-template-rows:min-content min-content 1fr; }
  .case-edgefolio .cs-hero-row > .cs-hero2{ grid-row:1 / span 3; }
  .case-edgefolio .cs-hero-row > .cs-hero-pair{ grid-column:1 / span 6; grid-row:3; align-self:start; }
}
/* The pair inside the hero: two equal columns, 13px, at every width. */
.page-case .cs-hero-row > .cs-hero-pair{
  grid-column:1 / -1; margin:var(--between) 0 0;
  grid-template-columns:1fr 1fr; gap:var(--between);
}
@media (min-width:1280px){ .page-case .cs-hero-row > .cs-hero-pair{ grid-column:1 / span 6; } }   /* the text column, under Owned, beside the picture */
.page-case .cs-hero-pair > div{ grid-column:auto; }
.page-case .cs-hero-pair dt{ margin-bottom:var(--sm); }
.page-case .cs-hero-pair .ln li{ font-size:13px; line-height:20px; }
/* THE GAP BETWEEN ITEMS MUST CLEAR THE LINE-HEIGHT INSIDE ONE (Anna, 15 Sep
   2026: "apply the spacing we have agreed on, religiously and mathematically").
   These items run to 20px line-height and wrap to two lines under 1024, so a
   4px gap put LESS air between two items than between two lines of the same
   item - proximity saying the opposite of the truth. 24 is the first rung on
   the ladder that clears 20, so 24 it is, at every width. */
.page-case .cs-hero-pair .ln li + li{ margin-top:8px; }   /* 8, not 12 (Anna, 15 Sep 2026: "reduce the vertical space between the bullet points"). Boxes are cap-trimmed, so 8 is the smallest gap that still clears the 7px between two wrapped lines of one item. */
@media (max-width:767px){ .page-case .cs-hero-row > .cs-hero-pair{ grid-template-columns:1fr; } }
/* Scope and Method are two now, not three. Two columns on the rail give each
   one 496px instead of the 309 that broke a three-line statement into five
   — her own 26 Aug measurement, now with room to spare. */
/* ON THE SAME COLUMNS AS THE STATS ABOVE THEM (12 Sep 2026). Two equal halves
   put Method's left edge at 736 while the three figures above it started at
   208, 565 and 923 — nothing in the block lined up with anything above it, and
   two grids in one header is what read as the layout not working. The pair now
   runs on the rail's own twelve columns: Scope takes 1–4, so it starts under
   "7 of 7", and Method takes 5–12, so it starts under "420+" and keeps 667px,
   wider than the 496 it had. Her 26 Aug ruling stands — neither column is near
   the 309 that broke a sentence over eleven lines. */
/* BELOW 1024 THEY STACK (Anna, 15 Sep 2026: "you have enough space, no need
   to have it so squished"). Two columns at that width give each list about
   230px, which breaks "Fund profile, onboarding and Investor Hub" across two
   lines mid-phrase - and with no marker the wrapped line reads as a separate
   item. One column at the full rail, one item per line. */
@media (max-width:1023px){
  .page-case .cs-meta-pair{ grid-template-columns:1fr; row-gap:var(--between); }
}
/* A MARKER, SO A WRAP READS AS A WRAP (Anna: "use paragraphs or bullet
   points"). The text hangs off the dot, so a continuation line lines up under
   the words rather than under the bullet. Ink, never the green - the accent
   stays where it is earned. */
/* THE DOT SITS ON THE FIRST LINE, NOT IN THE MIDDLE OF THE ITEM (Anna, 15 Sep
   2026). Absolute positioning inside the item let the dot drift toward the
   block's centre once an item wrapped. A two-column grid instead: the marker
   is a real cell, align-self:start pins it to the top, and 8px of margin
   centres it optically on a 20px first line. Deterministic at any number of
   lines. The text hangs, so a wrap lines up under the words. */
.case-edgefolio .cs-meta .ln li{ display:grid; grid-template-columns:4px minmax(0,1fr);
  column-gap:12px; padding-left:0; position:static; }
.case-edgefolio .cs-meta .ln li::before{ content:""; position:static; align-self:start;
  margin-top:8px; width:4px; height:4px; border-radius:50%;
  background:var(--ink-3); opacity:.55; }
@media (min-width:1024px){
  .page-case .cs-meta-pair{ grid-template-columns:repeat(12, minmax(0,1fr)); column-gap:var(--col); }
  .page-case .cs-meta-pair > :first-child{ grid-column:1 / span 4; }
  .page-case .cs-meta-pair > :last-child{ grid-column:5 / -1; }
}
html.wr-on .cs-hero-text > *{ opacity:0; }
html.wr-on .cs-hero-text.wr-in > *{ animation:rise var(--dur-3) var(--ease-enter) both var(--wd,0ms); }
@media (prefers-reduced-motion:reduce){
  html.wr-on .cs-hero-text > *, html.wr-on .cs-hero-text.wr-in > *{ opacity:1; animation:none; }
}

/* ── PHONE ART-DIRECTION FOR THE EDGEFOLIO TILE (15 Sep 2026) ───────────
   A 2048px desktop screenshot scaled to 327px puts the fund name at 10px
   and the figures at 7px: the first piece of work a phone reader meets is
   unreadable. Below 768px the animation stage runs at twice the card width
   and the card clips it, so the reader gets the top-left of the product —
   fund name, the first three figures, the start of the chart — at a size
   that reads. Every reel and the line keep their percentages because they
   are relative to the stage, not the card, so the choreography is intact.
   The clip lives on .shot, so nothing leaves the grid (scrollWidth ===
   innerWidth at 375, verified). */
@media (max-width:767px){
  .page-work .item .shot:has(.anim-eg){ overflow:hidden; aspect-ratio:10/7; border-radius:var(--radius-sm); }
  .page-work .shot .anim-eg{ width:200%; margin-left:-24%; border-radius:0; }   /* the export carries 12% of white plate before the fund name; pulled left so the name opens the tile */
}

/* THE HERO STACK IS CENTRED ON THE PICTURE (Anna, 15 Sep 2026: "centred in
   the size between the images… centred and moved lower"). Above 1280 the row
   gets a spare track above and below the words, each taking half the slack,
   so the title-to-Owned stack sits level with the picture's middle instead of
   starting at its top. The picture spans every track. */
/* EDGEFOLIO ONLY (Anna, 16 Sep 2026: the other case studies are not finished
   and must not be pushed down): the body carries .case-edgefolio. */
@media (min-width:1280px){
  /* THE FIRST SPARE TRACK KEEPS ROOM FOR THE KICKER (16 Sep 2026). The kicker
     is pinned to the row's top, so when a long title fills the row (Healthily,
     90POE, Self-Service: three and four lines at 72px) the stack started at
     the row's top and the title was drawn over the kicker. 68 = the kicker's
     20px line plus the 48 step it has under it on every other page. With
     slack to spare the two tracks still split it evenly, so Edgefolio's
     centring is untouched above 1280 and moves 4px at 1280. */
  .case-edgefolio .cs-hero-row{ grid-template-rows:minmax(68px, 1fr) min-content min-content min-content 1fr; }
  .case-edgefolio .cs-hero-row > .cs-hero-text{ grid-row:2; }
  .case-edgefolio .cs-hero-row > .cs-own{ grid-row:3; }
  .case-edgefolio .cs-hero-row > .cs-hero-pair{ grid-row:4; }
  .case-edgefolio .cs-hero-row > .cs-hero2{ grid-row:1 / span 5; }
  /* THE KICKER STAYS PUT (Anna, 15 Sep 2026: "always should stay in the same
     position"). It is lifted out of the centred stack and pinned to the top
     of the row, so the breadcrumb line sits where it does on every other page
     while the title, lede, controls and Owned centre on the picture. */
  .case-edgefolio .cs-hero-row{ position:relative; }
  .case-edgefolio .cs-hero-row > .cs-hero-text > .kicker{ position:absolute; top:0; left:0; margin:0; }
}

/* TWO BEATS SIDE BY SIDE (Anna, 15 Sep 2026: "proportionally, not vertically,
   but horizontally"). Problem and Constraints share the rail as two columns
   on the case-study grid; on a phone they stack. The row gap between them
   is gone because they are one row. */
.page-case .skim-pair{ display:grid; grid-template-columns:repeat(12, minmax(0,1fr)); column-gap:var(--col); align-items:start; }
/* ON THE STATS' OWN COLUMNS (Anna, 15 Sep 2026: "23 on top and 2 below").
   Problem takes 1-4 and starts under "7 of 7"; Constraints takes 5-12 and
   starts under "420+", the same 4 + 8 split Scope and Method use at the foot. */
.page-case .skim-pair > .skim-row:first-child{ grid-column:1 / span 4; }
.page-case .skim-pair > .skim-row:last-child{ grid-column:5 / span 4; }   /* stops before the third stat column (Anna, 15 Sep 2026: "it should stop before the 20% starts") */
.page-case .skim-pair > .skim-row + .skim-row{ margin-top:0; }
.page-case .skim-pair + .skim-row{ margin-top:var(--rows); }
/* THE DECISION COMES AFTER THE FIRST IMAGE (Anna, 16 Sep 2026, Healthily:
   "move the decision lower, that's how it is in Edgefolio"). Edgefolio's own
   inline rules, shared here so the three other case studies keep the same
   rhythm: 32 from the caption above to the beat, the figure step under it. */
.case-edgefolio .case-skim > .beat{ margin-top:var(--between); }
.case-edgefolio .case-skim > .beat + figure, .case-edgefolio .case-skim > .beat + .skim-shots{ margin-top:var(--step-figure); }
.case-edgefolio .skim-pair + .skim-shots, .case-edgefolio .skim-pair + .wide{ margin-top:var(--step-figure); }   /* the first image takes the figure step after the pair, as Edgefolio's does (48, measured) */   /* the beat after the pair keeps the row step; the pair is a div, so the row-to-row rule never matched it (16 Sep 2026) */
@media (max-width:767px){ .page-case .skim-pair{ grid-template-columns:1fr; row-gap:var(--rows); } .page-case .skim-pair > .skim-row{ grid-column:auto; } }

/* CREDITS: Scope and Method at the foot of the story (15 Sep 2026). */
.page-case .cs-credits-wrap{ margin-top:var(--rows); }   /* 16, with the Read-the-full-story link sitting between the caption and the credits at 16 too, so the whole run is about half what it was (Anna, 15 Sep 2026) */
.case-edgefolio .case-skim .skim-more{ margin-top:var(--step-para); }   /* 24 on every case study: Edgefolio's own inline rule already set it there (16 Sep 2026) */
.page-case .cs-credits{ margin-top:0; }

/* THE HERO LADDER (Anna, 15 Sep 2026: "not enough contrast… make it larger,
   the subtext larger, the font smaller"). Three sizes doing three jobs above
   1280: the title at 72 is the statement, the lede at 20 in full ink is the
   claim, and Owned drops to 13 as the credit line it is. Hierarchy from size
   and ink, never from a new accent. */
@media (min-width:1280px){
  .case-edgefolio .cs-hero-title{ font-size:72px; line-height:76px; font-weight:600; font-variation-settings:"opsz" 144,"SOFT" 0,"WONK" 1; }   /* 600: "not bold enough" at 400 (Anna, 15 Sep 2026) */
  .case-edgefolio .cs-hero-text .lede{ font-size:20px; line-height:30px; color:var(--ink); max-width:none; }
  .case-edgefolio .cs-hero-row > .cs-own dd > p{ font-size:13px; line-height:20px; color:var(--ink-3); max-width:none; }   /* full column, never squeezed: it is the ownership line (Anna, 15 Sep 2026) */
}

/* STAT CAPTIONS AT BODY SIZE (Anna, 15 Sep 2026: "it can be in normal text
   body size"). The figure is the title; the line under it reads like a
   sentence, at the size every sentence on the page uses. */
.case-edgefolio .stats-hero .d{ font-size:16px; line-height:24px; }

/* A PULL QUOTE AFTER A FIGURE GETS THE FULL SECTION STEP (Anna, 15 Sep 2026:
   "double the spacing here"). 96, not the 48 figure step: the quote is a
   change of voice, and it needs the air of a new section before it. */
.case-edgefolio .row .body > figure + .ucite{ margin-top:var(--section); }

/* CREDITS MATCH THE SKIM PAIR (Anna, 15 Sep 2026: "match the second
   screenshot style to the first one… this is on top and this is at the end").
   Same two four-column slots as Problem and Constraints, same green label
   with 8 under it, same body ink. The bullets stay. */
.page-case .cs-credits{ grid-template-columns:repeat(12, minmax(0,1fr)); column-gap:var(--col); }
.page-case .cs-credits > div:first-child{ grid-column:1 / span 4; }
.page-case .cs-credits > div:last-child{ grid-column:5 / span 4; }
.page-case .cs-credits dt{ margin:0 0 var(--xs); color:var(--green); }
.page-case .cs-credits .ln li{ color:var(--ink-2); }
.page-case .cs-credits .ln li + li{ margin-top:var(--xs); }
@media (max-width:767px){ .page-case .cs-credits{ grid-template-columns:1fr; } .page-case .cs-credits > div{ grid-column:auto; } }

/* IN THE FULL STORY THE CREDITS TAKE THE SECTION STEP (Anna, 15 Sep 2026:
   "the same spacing as in other places"). After the skim the link above them
   already carries the rhythm, so 32 stays there. */
html:not(.mode-skim) .page-case .cs-credits-wrap{ margin-top:var(--section); }   /* 96, the full section break, after the Reflection: Anna asked for more twice on 15 Sep 2026 */
/* ONE VERB, EVERYWHERE: THINGS LAND IN READING ORDER (14 Sep 2026; into the
   shared sheet 16 Sep 2026 for the three case-study heroes). A reel is a box
   cut from her own export, dropped back into place over a patch of the
   surface it came from. The stagger lives in animation-delay, which a looping
   animation keeps on every pass; attract.js parks and rewinds the .anim off
   screen. Used by the case-study heroes. */
.anim-land .eg-reel{ position:absolute; overflow:hidden; background:#fff; }
.anim-land .eg-reel img{ position:absolute; left:0; top:0; width:100%; height:100%; animation:landIn 6000ms infinite both; animation-delay:var(--d,0ms); }
@keyframes landIn{
  0%{ transform:translateY(-120%); filter:blur(6px); animation-timing-function:cubic-bezier(0.16,1,0.3,1); }
  11.667%,100%{ transform:none; filter:none; }
}
@media (prefers-reduced-motion:reduce){ .anim-land .eg-reel img{ animation:none; transform:none; filter:none; } }

/* ── HEALTHILY: THE SYMPTOM CHECKER TYPES (16 Sep 2026) ─────────────────
   Her four web exports of the checker at 2x. The base is the empty page; the
   reels are boxes cut from the other three files, put back where they came
   from. Letter by letter, for real (Anna: "make it actually type it out by
   the first letter, second letter, third letter"): the focused field comes in
   with its typed word hidden under a patch of its own blank grey, then h, e, a
   are three separate cuts that appear one at a time, 250ms apart, the cursor
   stepping along behind them and blinking at 1Hz. The dropdown opens as one
   piece with the site's entrance (400ms, 4px). One
   seven-second clock for every reel:
   pencil and field standing by from 0 · 1.5s focus · 1.6 / 1.85 / 2.1s h, e, a ·
   2.5s the dropdown · hold to 7s. */
.anim-hh2 .r-pencil img{ opacity:0; animation:hhPencil 7000ms infinite both; }
.anim-hh2 .r-field img, .anim-hh2 .r-blank img{ animation:hhFocus 7000ms infinite both; }
.anim-hh2 .r-l1 img{ opacity:0; animation:hhL1 7000ms infinite both; animation-timing-function:step-end; }
.anim-hh2 .r-l2 img{ opacity:0; animation:hhL2 7000ms infinite both; animation-timing-function:step-end; }
.anim-hh2 .r-l3 img{ opacity:0; animation:hhL3 7000ms infinite both; animation-timing-function:step-end; }
.anim-hh2 .r-cursor{ animation:hhCaret 7000ms infinite both; animation-timing-function:step-end; }
.anim-hh2 .r-cursor img{ opacity:0; animation:hhBlink 7000ms infinite both; animation-timing-function:step-end; }
.anim-hh2 .r-list img{ opacity:0; animation:hhList 7000ms infinite both; }
/* the pencil and the field are there from the start, standing by (Anna, 16 Sep 2026) */
@keyframes hhPencil{ 0%,20.5%{opacity:1} 21.4%,100%{opacity:0} }
@keyframes hhFocus{ 0%,20.5%{opacity:0} 21.4%,100%{opacity:1} }
@keyframes hhL1{ 0%,22.85%{opacity:0} 22.86%,100%{opacity:1} }
@keyframes hhL2{ 0%,26.42%{opacity:0} 26.43%,100%{opacity:1} }
@keyframes hhL3{ 0%,29.99%{opacity:0} 30%,100%{opacity:1} }
/* the caret box moves in three steps: after h, after he, after hea (its rest position) */
@keyframes hhCaret{ 0%,22.85%{transform:translateX(-820%)} 22.86%{transform:translateX(-530%)} 26.43%{transform:translateX(-240%)} 30%,100%{transform:none} }
@keyframes hhBlink{ 0%,21.4%{opacity:0} 21.5%{opacity:1} 28.6%{opacity:0} 35.7%{opacity:1} 42.9%{opacity:0} 50%{opacity:1} 57.1%{opacity:0} 64.3%{opacity:1} 71.4%{opacity:0} 78.6%{opacity:1} 85.7%{opacity:0} 92.9%{opacity:1} 100%{opacity:0} }
/* the dropdown opens as one piece, the site's entrance: 400ms, 4px (Anna, 16 Sep 2026: "the dropdown looks strange" in three parts) */
@keyframes hhList{ 0%,35.714%{opacity:0;transform:translateY(4px);animation-timing-function:cubic-bezier(0.165,0.84,0.44,1)} 41.429%,100%{opacity:1;transform:none} }
@media (prefers-reduced-motion:reduce){ .anim-hh2 .eg-reel, .anim-hh2 .eg-reel img{ animation:none; } .anim-hh2 .r-pencil img, .anim-hh2 .r-cursor img{ opacity:0; } .anim-hh2 .r-l1 img, .anim-hh2 .r-l2 img, .anim-hh2 .r-l3 img, .anim-hh2 .r-list img{ opacity:1; } }

/* EDGEFOLIO'S OWN FIGURE STEP (Anna, 16 Sep 2026: today's rules are for
   Edgefolio because it is finished; the other case studies keep their last
   pushed state until their designs are updated). */
@media (min-width:1024px){ .case-edgefolio{ --step-figure:48px; } }

/* THE AGENTS LINK IN THE NAV (Anna, 23 Sep 2026: "put it next to Work…
   how I design with agents… and then About me"). The agent case left the
   Work list, where it blended in, and became its own door. Short label on
   phones so the nav stays one line. */
/* Nav: "AI manager" with a tiny org chart, one dot over three (23 Sep 2026).
   On hover the three agents take their handoff in turn, hard cuts only. */
.nav-links .nl-ico{ width:16px; height:16px; margin-right:6px; vertical-align:-2px; overflow:visible; }
.nav-links .nl-ico path{ fill:none; stroke:currentColor; stroke-width:1; }
.nav-links .nl-ico circle{ fill:currentColor; stroke:none; }
@keyframes nl-hand{ 0%{ visibility:hidden; } 100%{ visibility:visible; } }
@media (hover:hover) and (prefers-reduced-motion:no-preference){
  .nav-links a:hover .nl-ico .c{ animation:nl-hand 1ms steps(1) both; }
  .nav-links a:hover .nl-ico .c1{ animation-delay:120ms; }
  .nav-links a:hover .nl-ico .c2{ animation-delay:240ms; }
  .nav-links a:hover .nl-ico .c3{ animation-delay:360ms; }
}

