/* ==========================================================================
   Helen Lim — Portfolio Stylesheet
   Structure:
     1. Design tokens (:root)
     2. Base / reset
     3. Layout utilities (.wrap)
     4. Navigation
     5. Footer
     6. Home: hero + network graph
     7. Cards & project grid (home + work pages)
     8. Case study pages
     9. About page
    10. Responsive
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Design tokens — "Specimen Plate" system
   Warm plate paper, one cobalt ink worked at every depth, Inter for display
   and reading with Space Mono reserved for data, hairline rounded frames.
   Old Paper & Ink token names are kept as aliases so every downstream rule
   resolves to the new values without touching layout.
   -------------------------------------------------------------------------- */
@import url("https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600;1,700&family=Space+Mono:wght@400;700&display=swap");

:root {
  /* Layout */
  --nav-height: 64px;
  --content-max: 1200px;   /* page-level container */
  --reading-max: 900px;    /* long-form (case study) column */
  --gutter: clamp(1.25rem, 5vw, 4rem);

  /* Spacing — the 4px ramp (tokens snapped to the nearest step) */
  --space-xs: 0.5rem;   /* s2 ·  8px */
  --space-sm: 1rem;     /* s4 · 16px */
  --space-md: 2rem;     /* s6 · 32px */
  --space-lg: 3rem;     /* s7 · 48px */
  --space-xl: 6rem;     /* s9 · 96px — separations are generous */

  /* Type — two families. Inter carries display and reading alike, so the
     hierarchy is made by size, weight, line-height and tracking rather than
     by changing voice; Space Mono is data only. The display and body tokens
     resolve to the same family on purpose: they stay separate so the
     intent of each selector is still legible, and so display could be
     re-pointed later without touching reading text. Which of the two a
     selector takes is not a free choice — see the family rule under the
     type scale below. */
  --font-body: "Inter", -apple-system, BlinkMacSystemFont, sans-serif;
  --font-display: "Inter", -apple-system, BlinkMacSystemFont, sans-serif;
  --font-data: "Space Mono", ui-monospace, SFMono-Regular, Menlo, monospace;

  /* ---- Type scale --------------------------------------------------------
     One ramp for the whole site. Every font-size in this stylesheet resolves
     to one of these; a raw value below is a deliberate exception and says so
     where it stands.

     Which family a selector takes follows one rule:
       --font-display  headings, and UI set as tracked capitals
       --font-body     running text, and UI set as words
       --font-data     labels, tags, figures, breadcrumbs — data, not prose

     Fluid only above ~25px, where the travel is worth the complexity. The
     clamps are written as `rem + vw` rather than bare `vw`: a bare `4.5vw`
     reached its own floor at 782px and its ceiling at 1156px, so the hero was
     frozen across more than half the range it was meant to scale over. The
     sum form spreads the same travel evenly from 360px to 1440px. */
  --text-display:    clamp(2.2rem, 1.85rem + 1.56vw, 3.25rem);  /* 35.2 → 52  */
  --text-page-title: clamp(1.6rem, 1.38rem + 0.96vw, 2.25rem);  /* 25.6 → 36  */
  --text-heading:    1.5rem;      /* 24   */
  --text-lead:       clamp(1.25rem, 1.15rem + 0.42vw, 1.5rem);  /* 20   → 24  */
  --text-subhead:    1.35rem;     /* 21.6 */
  /* Same value as --text-body-lg, kept apart on purpose: one governs the
     size of a titled item, the other the size of a lead paragraph, and they
     should be free to move independently. The pairing below is the same. */
  --text-title:      1.125rem;    /* 18   */
  --text-body-lg:    1.125rem;    /* 18   */
  --text-body:       1rem;        /* 16   */
  --text-small:      0.875rem;    /* 14   */
  --text-xs:         0.8125rem;   /* 13   */
  /* 12px is the floor for anything set in capitals. Space Mono's uniform
     stroke plus positive tracking stops the glyphs forming word-shapes, so
     small caps cost more here than they would in Inter — the mono labels used
     to sit at 11px and the two filter legends at 10px. */
  --text-ui:         0.75rem;     /* 12 — Inter controls and navigation */
  --text-label:      0.75rem;     /* 12 — Space Mono capitals           */
  --text-micro:      0.6875rem;   /* 11 — reserved; nothing uses it     */

  /* ---- Leading ---- */
  --leading-body:    1.65;
  --leading-tight:   1.4;
  --leading-compact: 1.3;
  --leading-heading: 1.15;
  --leading-display: 1.02;

  /* ---- Tracking — negative as the type grows, positive for capitals ---- */
  --track-display: -0.04em;
  --track-title:   -0.03em;
  --track-head:    -0.02em;
  --track-lead:    -0.015em;
  --track-item:    -0.01em;
  --track-caps:     0.08em;   /* Space Mono capitals */
  --track-ui:       0.12em;   /* Inter capitals      */

  /* Ground & ink — a soft ivory, warm by three points across the channels and
     no more, so it reads as paper rather than as beige. The atmospheric field
     below is cool; the small temperature difference between the two is what
     makes the page feel lit rather than filled. The old plate paper (#F3EFE6)
     was warm enough to read as cream, which is what this replaces. */
  --color-bg: #FAFAF9;          /* near-white ground */
  --color-surface: #E9E8E5;     /* paper sunk */
  --color-card: #FBFAF8;        /* paper lifted — overlays, insets */
  --color-border: #D6D4D0;      /* rule */
  --color-border-strong: #ADABA6;
  --color-mark: #8A8884;         /* grey glyphs — the lightest grey that
                                    still clears 3:1 on the ground */
  --color-text: #1A1815;        /* ink */
  /* Both secondary tones are darker than a flat page would need. The field
     below carries real colour now, and these have to clear AA at its densest
     point rather than only on bare paper — muted lands at 5.57:1 there and
     8.40:1 on ivory, faint at 4.54:1 and 6.85:1. That compresses the gap
     between them slightly; it is the price of a background with weather in it. */
  --color-text-muted: #44484B;
  --color-text-faint: #515558;

  /* The accent at depth — one hue, five steps */
  --cobalt-abyss: #002B74;
  --cobalt-deep:  #013994;
  --cobalt:       #2848A6;
  --cobalt-lift:  #4B60C3;
  --cobalt-sky:   #6C7BE2;
  --cobalt-04: rgba(40, 72, 166, 0.04);
  --cobalt-08: rgba(40, 72, 166, 0.08);
  --cobalt-16: rgba(40, 72, 166, 0.16);
  --cobalt-32: rgba(40, 72, 166, 0.32);
  /* The page behind a dialog: a light, cool grey with only a trace of the
     cobalt in it, so it sits with the site's colours without tinting the
     page blue. */
  --scrim: rgba(64, 72, 92, 0.22);

  /* Legacy accent aliases — everything collapses to depths of the one ink */
  --accent-coral: var(--cobalt);
  --accent-celadon: var(--cobalt);
  --accent-celadon-ink: var(--cobalt-deep);
  --accent-indigo: var(--cobalt);
  --accent-sage: var(--cobalt-lift);
  --accent-ochre: var(--cobalt);

  /* The one hue that is not the accent. Reserved for the contact pill, where
     the point is that the line is an invitation rather than a status: a
     second colour used once says that, where another depth of cobalt would
     just read as more of the same. Set at the accent's own weight so it sits
     beside it rather than shouting over it. */
  --moss:    #1B6B4E;
  --moss-08: rgba(27, 107, 78, 0.08);

  /* Interaction washes — surfaces, not marks */
  --wash: var(--cobalt-08);
  --wash-active: var(--cobalt-16);

  /* Nav dot accents */
  --dot-home: var(--cobalt);
  --dot-work: var(--cobalt);
  --dot-creative: var(--cobalt);
  --dot-about: var(--cobalt);

  /* Radii — containers take r-md, content inside them takes r-sm */
  /* A squircle hugs its corner tighter than a circular arc of the same
     radius, so the ramp is scaled up to keep the softness it reads with. */
  /* Each corner scales with the screen and stops at its full size, so a
     desktop or tablet keeps exactly these and a phone gets the same
     proportion rather than the same pixels — 44px on a 335px-wide cover read
     as a pill. About 24px on a 375px phone for the large one. */
  --radius-sm: min(10px, 2.4vw);
  --radius-md: min(22px, 3.8vw);
  --radius-lg: min(44px, 6.4vw);   /* panels — the fun dialog */
  /* Picture surfaces take the dialog's own corner, not a scaled version of
     it. A corner is read against the other corners on screen rather than
     against the shape it belongs to, so the same arc is what makes two
     things look related — shrinking it in proportion just makes the smaller
     one look sharper. Only the thumbs are capped, where 44px would close
     into a circle. */
  --radius-media: var(--radius-lg);   /* covers, carousel photo, case images */
  --radius-media-sm: 12px;            /* 54px thumbs — as round as they go */
  --radius-pill: 999px;

  /* Motion */
  --dur-quick: 240ms;
  --dur-settle: 400ms;
  --ease-settle: cubic-bezier(0.2, 0.8, 0.2, 1);
}

/* Selection: a 22% cobalt wash that pulls every glyph to full ink. */
::selection {
  background: rgba(40, 72, 166, 0.22);
  color: var(--color-text);
}

/* One focus treatment everywhere. */
:where(a, button, [tabindex]):focus-visible {
  outline: 2px solid var(--cobalt);
  outline-offset: 3px;
  border-radius: var(--radius-sm);
}

/* --------------------------------------------------------------------------
   2. Base / reset
   -------------------------------------------------------------------------- */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

/* --------------------------------------------------------------------------
   Scrollbars — the thumb, and nothing else.

   No track, no groove, no buttons: on a page this quiet the channel a
   scrollbar sits in is a second rule running down the edge of everything,
   and it reads as chrome rather than as paper. What is left is a light grey
   thumb that firms up slightly when reached for.

   `scrollbar-color` inherits, so the root sets it once for the page, the case
   columns, the filter panel and the hobby dialogs alike. `scrollbar-width`
   does not inherit, and neither do the WebKit pseudo-elements, so both go on
   `*` to reach every scroller rather than only the page. The thumb is drawn
   inside a transparent border so it sits away from the very edge and reads as
   a floating bar rather than a filled slot.
   -------------------------------------------------------------------------- */
html {
  scrollbar-color: var(--color-border) transparent;
}

* {
  scrollbar-width: thin;
}

*::-webkit-scrollbar {
  width: 12px;
  height: 12px;
}

*::-webkit-scrollbar-track,
*::-webkit-scrollbar-corner {
  background: transparent;
}

*::-webkit-scrollbar-thumb {
  background-color: var(--color-border);
  border: 3px solid transparent;
  background-clip: padding-box;
  border-radius: var(--radius-pill);
}

@media (hover: hover) {
  *::-webkit-scrollbar-thumb:hover {
    background-color: var(--color-border-strong);
  }
}

/* The ground lives on the root, not on body, so the atmospheric layers below
   can sit at z-index -1 — behind every element on the page, including ones
   that establish their own stacking order — without disappearing behind a
   body fill. */
html {
  scroll-behavior: smooth;
  background: var(--color-bg);
}

body {
  position: relative; /* anchors the atmospheric layers below */
  font-family: var(--font-body);
  color: var(--color-text);
  background: transparent;
  overflow-x: hidden; /* the hero graph bleeds past the right edge */
  line-height: var(--leading-body);
  -webkit-font-smoothing: antialiased;
  font-variant-numeric: tabular-nums;
}

/* ---- Atmosphere ----------------------------------------------------------
   The ground is near-white and mostly stays that way. Rather than one weak
   wash spread over the whole page, the colour is gathered into a few discrete
   areas — a slab low on the left, a second on the right, a warm one at the
   foot — each strong enough to read as an area of blue, with clear white
   between them. Nothing spans the page, so the middle stays paper.

   Painted onto the page, not pinned to the window: the layer spans the whole
   document and travels with it, so each area of colour stays over the content
   it was placed against and scrolls up and away like any other mark on the
   paper. The two cool areas are sized in viewport units so they land on the
   hero the way they were composed; the warm note sits at the document's foot,
   so it arrives with the footer rather than glowing along the screen edge.

   The flats are left clean — no noise layer over the paper. ---------- */
body::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: -1;
  background-image:
    /* The centre sits off-page, so only the outer edge of the blue crosses
       the left margin, low on the left below the title. */
    radial-gradient(32vw 26vh at -3vw 74vh, rgba(140, 172, 202, 0.26), transparent 72%),
    /* the warm note, held to the foot of the page so the rest reads cool */
    radial-gradient(58vw 22vh at 50% calc(100% + 4vh), rgba(226, 208, 182, 0.34), transparent 74%);
  background-size: auto, auto;
  background-repeat: no-repeat, no-repeat;
}

/* The one place the field is allowed to gather: loosely around and behind the
   network, so the graph sits in the page rather than on it. Painted onto the
   document like the layer above, so it stays with the hero and scrolls away
   with it. Still weak enough that the strongest point measures 4.67:1 for the
   faintest text tone. */
body::after {
  content: "";
  position: absolute;
  /* Pulled up past the top of the page so the band arrives already in
     progress — its upper half is cut off by the page edge rather than
     resolving into a shape that sits complete on the paper. */
  top: -26vh;
  left: 50%;
  width: min(1180px, 88vw);
  height: min(380px, 42vh);
  pointer-events: none;
  z-index: -1;
  /* A wide, flat band rather than a round blob, laid along a slight
     downward-right slant. The ellipse can't be angled from inside the
     gradient, so the box carries the tilt and the paint stays axis-aligned
     within it. The centring translate rides along on the same transform. */
  transform: translateX(-50%) rotate(9deg);
  transform-origin: 50% 50%;
  background:
    radial-gradient(50% 50% at 62% 50%, rgba(138, 180, 214, 0.30), transparent 72%);
}

/* Continuous corners, site-wide. A circular corner meets the straight edge
   with a jump in curvature — the "corner inside the corner" you can see on a
   plain border-radius. A squircle ramps the curvature so the edge tapers into
   the turn. corner-shape only bites where there is a radius, so this is inert
   on square elements, and browsers without it fall back to the circular arc. */
* {
  corner-shape: squircle;
}

/* Circles and pills keep a true arc: a superellipse at 50%, or at a pill
   radius, is a different shape rather than a smoother one. */
.logo-mark,
.logo-circle,
.view-project-btn,
.footer-links a,
.fun-close {
  corner-shape: round;
}

/* Inter for every heading; only weight and tracking step */
h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 600;
  letter-spacing: var(--track-head);
}

img {
  max-width: 100%;
  display: block;
}

/* No browser lets a form control inherit type from the page, so any button
   that does not name a face drops out of the system and renders in the UA
   default. Family only — each control keeps whatever size it sets for
   itself, and the selectors that want the data voice still override this. */
button, input, select, textarea {
  font-family: inherit;
}

/* --------------------------------------------------------------------------
   3. Layout utilities
   .wrap gives every page the same centered container and left edge.
   -------------------------------------------------------------------------- */
.wrap {
  width: 100%;
  padding-inline: var(--gutter);
}

/* Entrance reveal (applied by js/site.js) */
.reveal {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity var(--dur-settle) var(--ease-settle),
              transform var(--dur-settle) var(--ease-settle);
  transition-delay: var(--reveal-delay, 0ms);
}

.reveal.revealed {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  .reveal {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* --------------------------------------------------------------------------
   4. Site nav — the mark on the left, the two section pages on the right,
   sitting on the same hairline rule the sections used to hang from.
   -------------------------------------------------------------------------- */
.site-nav {
  position: relative;
  z-index: 20;
  /* transparent: on the home page the hero graph shows straight through */
  background: transparent;
}

.nav-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-md);
  padding-block: 0.9rem;
}

/* The mark is the way home; nothing else marks the landing page. */
.nav-mark {
  display: inline-flex;
  flex: none;
  /* nudged down so the mark sits optically centred against the nav links */
  margin-top: 3px;
  transition: transform var(--dur-settle) var(--ease-settle);
}

@media (hover: hover) {
  .nav-mark:hover {
    transform: rotate(90deg);
  }
}

.nav-mark img {
  width: 44px;
  height: 44px;
}

.nav-links {
  display: flex;
  align-items: center;
  gap: var(--space-md);
}

/* Agate caps in the display voice — the same register the section labels
   carried, now as links rather than a switch. A rule draws underneath on
   hover and stays drawn on the page you are reading. */
.nav-link {
  position: relative;
  font-family: var(--font-display);
  font-size: var(--text-ui);
  font-weight: 600;
  letter-spacing: var(--track-ui);
  text-transform: uppercase;
  color: var(--color-text-muted);
  text-decoration: none;
  padding-block: 0.35rem;
  transition: color var(--dur-quick) var(--ease-settle);
}

.nav-link::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: var(--cobalt);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--dur-quick) var(--ease-settle);
}

.nav-link[aria-current="page"] {
  color: var(--cobalt);
}
@media (hover: hover) {
  .nav-link:hover {
    color: var(--cobalt);
  }
}

.nav-link[aria-current="page"]::after {
  transform: scaleX(1);
}
@media (hover: hover) {
  .nav-link:hover::after {
    transform: scaleX(1);
  }
}

.meta-group {
  display: inline-flex;
  align-items: center;
  gap: 1.25rem;
}

/* Panels: WORK and ABOUT content, swapped by the tabs. Transparent so the
   hero graph spills through beneath; the mouse falls through empty areas
   to the graph nodes, while real content re-enables its own events. */
.site-panel {
  display: none;
  position: relative;
  z-index: 1;
  padding-block: var(--space-sm) var(--space-xl);
  pointer-events: none;
}

/* The panel rises into place each time it is switched to. Going from
   display:none to block restarts the animation on its own, so no JS is
   needed to replay it. No fill-mode on purpose: once it finishes the element
   falls back to its normal styles, leaving no transform behind — a lingering
   one would become a containing block, which silently re-parents any fixed or
   sticky descendant to the panel instead of the viewport. */
.site-panel.active {
  display: block;
  animation: panel-rise var(--dur-settle) var(--ease-settle);
}

@keyframes panel-rise {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: translateY(0); }
}

@media (prefers-reduced-motion: reduce) {
  .site-panel.active { animation: none; }
}

.site-panel .card-link,
.site-panel .about-main {
  pointer-events: auto;
}

/* About: one column, the content starting at the shared left gutter.
   Matches the work grid's offset below the title. */
#panel-about .about-main {
  margin-top: var(--space-md);
}

/* --------------------------------------------------------------------------
   5. Footer
   -------------------------------------------------------------------------- */
footer {
  padding: var(--space-lg) var(--gutter);
  /* Transparent, so the atmospheric field carries through to the bottom of
     the page. An opaque fill here cut the footer out of the gradient and left
     it sitting on a flat panel of its own. */
  background: transparent;
  color: var(--color-text);
  position: relative; /* slides over the sticky hero like the panels */
  z-index: 1;
}

/* The rule above the footer, matching the WORK/ABOUT divider exactly: same
   cobalt wash, same inset to the page gutter. A pseudo-element for the same
   reason as that one — a border on the footer spans its whole box, and the
   gutter here is padding, which sits inside the border. */
footer::before {
  content: "";
  position: absolute;
  left: var(--gutter);
  right: var(--gutter);
  top: 0;
  border-top: 1px solid var(--cobalt-32);
}

/* Two columns: name + message on the left, back-to-top + icons on the right */
.footer-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-md);
  flex-wrap: wrap;
}

/* The mark beside the footer name (44px floor, clear space via gap).
   On hover it turns a quarter. The quatrefoil is four-fold symmetric, so 90°
   lands it exactly back on itself — the turn is plainly visible while it
   happens, then resolves with nothing out of place, and only the hand-drawn
   wobble in the line gives away that it moved at all. Pure transform, so it
   stays on the compositor and never reflows the footer row. */
.footer-mark {
  width: 44px;
  height: 44px;
  flex: none;
  transition: transform var(--dur-settle) var(--ease-settle);
}

@media (hover: hover) {
  .footer-mark:hover {
    transform: rotate(90deg);
  }
}

/* The mark is also the way home — to the top of the landing page. */
.footer-mark-link {
  display: flex;
  flex: none;
}

.footer-left {
  display: flex;
  align-items: center;
  gap: 0.8rem; /* ≥ 11px clear space at the 44px size */
}

.footer-name {
  font-family: var(--font-data);
  font-size: var(--text-label);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--color-text-faint);
}

/* The credit line sits under the greeting now, so the gap between them moves
   here — it belongs to whichever of the two is on top. */
.footer-msg {
  font-size: var(--text-body-lg);
  margin-bottom: 0.3rem;
}

.footer-right {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.6rem;
}

.back-to-top {
  font-family: var(--font-body);
  font-size: var(--text-ui);
  font-weight: 600;
  /* Not --track-ui: that tracking is for capitals, and this reads "Back to
     Top". Sentence case wants next to nothing. */
  letter-spacing: 0.02em;
  color: var(--cobalt);
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  background-image: linear-gradient(var(--cobalt), var(--cobalt));
  background-repeat: no-repeat;
  /* anchored right, so the rule draws right to left */
  background-position: 100% 100%;
  background-size: 0% 1.5px;
  transition: background-size var(--dur-quick) var(--ease-settle);
}

@media (hover: hover) {
  .back-to-top:hover {
    color: var(--cobalt);
    background-size: 100% 1.5px;
  }
}

.footer-links {
  display: flex;
  gap: 0.5rem;
}

/* Each mark in a grey ring; mark and ring both turn cobalt on hover. The ring
   is the hit area, so these stay a thumb-sized target. */
.footer-links a {
  color: var(--color-mark);
  display: inline-grid;
  place-items: center;
  width: 2.375rem;
  height: 2.375rem;
  border: 1px solid var(--color-border-strong);
  border-radius: 50%;
  transition: color var(--dur-quick) var(--ease-settle),
              border-color var(--dur-quick) var(--ease-settle);
}

.footer-links a:focus-visible {
  color: var(--cobalt);
  border-color: var(--cobalt);
}
@media (hover: hover) {
  .footer-links a:hover {
    color: var(--cobalt);
    border-color: var(--cobalt);
  }
}

.footer-links svg {
  width: 17px;
  height: 17px;
}

/* --------------------------------------------------------------------------
   6. Home: hero + network graph
   The graph fills the hero behind the text; a mask fades it out toward the
   left/top so the intro text sits on a clean background.
   -------------------------------------------------------------------------- */
#hero {
  position: relative;
  /* the CTA reaches past the hero box, so the hero paints above the stage
     section below it — otherwise the later sibling would cover it */
  z-index: 1;
  min-height: 62vh;
  display: flex;
  align-items: center;
}

/* Full-width wrap so the title aligns to the shared left gutter */
#hero .wrap {
  width: 100%;
}

/* Driven by js/site.js during the scroll transition; 1 until it starts. */
#hero {
  --hero-fade: 1;
}

/* Both fade out as the graph takes the foreground; once faded, the CTA
   stops taking clicks so it cannot be hit invisibly. */
.intro-text,
.map-cta {
  opacity: var(--hero-fade);
}

/* The cue stops taking the pointer only once it has actually faded out of the
   way — it sits over the lifted graph and would otherwise swallow hovers
   meant for the field. It used to give up its clicks as soon as the graph
   lifted, which is 2% of the transition: about ten pixels of scroll, with the
   cue still at full opacity and plainly a button. */
#hero.cta-spent .map-cta {
  pointer-events: none;
}

/* Lifted above the intro column once it starts moving, and hinted to the
   compositor so the per-frame transform stays on the GPU. */
#hero.graph-lifted #graphContainer {
  z-index: 2;
}

/* Past the stage the field goes with its panels. Same threshold, set by
   js/site.js, and the same duration as `.explore-ui` opposite, so the two
   leave together rather than one trailing the other. `visibility` is held
   until the fade is over, then switched, so a canvas at zero opacity is not
   still catching the pointer. */
body.explore-past #graphContainer {
  opacity: 0;
  visibility: hidden;
  transition: opacity var(--dur-settle) var(--ease-settle),
              visibility 0s linear var(--dur-settle);
}

#graphContainer {
  opacity: 1;
  visibility: visible;
  transition: opacity var(--dur-settle) var(--ease-settle), visibility 0s;
  will-change: transform;
  /* Fixed, and boxed by js/graph.js: the hero's own box, extended upward by
     the headroom the bloom needs, in the page's coordinates at the top of the
     document. js/site.js does all of its moving with a transform, so the
     field is never scrolled natively and then pulled back a frame late —
     which is what made it shake while the story scrolled past. The bloom is
     still cropped by the top of the page and stops short of the section
     divider, so it never bleeds into Work/About. */
  position: fixed;
  left: 0;
  top: 0;
  width: 0;
  height: 0;
  z-index: 0;
  pointer-events: none; /* circles re-enable their own events */
  -webkit-mask-image: linear-gradient(
    105deg,
    transparent 0%,
    transparent 24%,
    rgba(0, 0, 0, 0.45) 36%,
    #000 50%
  );
  mask-image: linear-gradient(
    105deg,
    transparent 0%,
    transparent 24%,
    rgba(0, 0, 0, 0.45) 36%,
    #000 50%
  );
}

#networkGraph {
  width: 100%;
  height: 100%;
  display: block;
  opacity: 0;
  transition: opacity 0.8s ease;
  pointer-events: auto; /* the canvas answers hover for the readout */
}

.intro-text {
  position: relative;
  z-index: 1;
  max-width: 34rem;
}

/* Display type: the name, in the same gray as the nav */
.intro-text h1 {
  font-size: var(--text-display);
  font-weight: 600;
  line-height: var(--leading-display);
  letter-spacing: var(--track-display);
  color: var(--color-text-muted);
  margin-bottom: var(--space-xs);
}

.intro-text p {
  font-size: var(--text-body-lg);
  line-height: var(--leading-body);
  color: var(--color-text-muted);
}

.intro-current {
  margin-top: var(--space-md);
}

/* The subtitle is one line from a tablet up — held there, since it runs a
   touch past the column's 34rem — and breaks after "systems" on a phone
   (.br-phone, switched on in the 640px rules). */
.intro-subtitle { white-space: nowrap; }
.br-phone { display: none; }

/* --------------------------------------------------------------------------
   Availability pill — where I am, and how to reach me.

   The same cobalt wash and pill radius the filter switches use, kept to one
   line of small type so it reads as a status rather than as another sentence
   under the subtitle. The dot is the thing to point at; the line beside it
   changes to the way to get in touch.
   -------------------------------------------------------------------------- */
/* Scoped through .intro-text: `.intro-text p` sets reading size and muted ink
   on every paragraph in the hero, and would otherwise win on specificity and
   leave this at 1.05rem grey. */
.intro-text .status-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.3rem 0.85rem 0.3rem 0.7rem;
  border-radius: var(--radius-pill);
  corner-shape: round;
  background: var(--cobalt-08);
  font-size: var(--text-ui);
  line-height: var(--leading-tight);
  color: var(--cobalt-deep);
}

/* A live dot: it holds its size and sends a ring outward, so the pill does not
   reflow on every beat. Sized in em so it tracks the type beside it. */
.status-dot {
  position: relative;
  flex: none;
  width: 0.5rem;
  height: 0.5rem;
  padding: 0;
  border: 0;
  border-radius: 50%;
  corner-shape: round;
  background: var(--cobalt);
}

/* The two readings are stacked in one grid cell, so each is laid out as if it
   were the only one and neither adds to the other's size. That is what lets
   them cross-fade: taking the outgoing line out of the flow instead would
   leave both in it while it faded, and the pill would balloon to hold both
   before settling. The cell takes the width of the longer line, so the pill
   is one size in both states and the swap changes nothing but the words. */
/* Both pills sit in one grid cell, so neither adds to the other's height and
   the line below never moves. `place-items: start` is what keeps them from
   being stretched to fill it: each pill takes the size of its own words in
   both directions, which is the whole point of a pill. Stretching only shows
   on a narrow screen, where the longer line wraps and would otherwise leave
   the shorter pill standing at double height. */
.status-stack {
  display: grid;
  place-items: start;
  margin-top: var(--space-sm);
}

.status-stack .status-pill { grid-area: 1 / 1; }

/* The two pills take turns on one nine-second cycle. Long enough to read
   without hurrying, slow enough not to nag at the corner of the eye.

   Only opacity, transform and pointer-events are animated, never visibility,
   so the email stays in the page for a screen reader and for search the whole
   time. The pill that is faded out stops taking clicks, so the address is
   only clickable while it is the one being shown. */
/* ease-in-out rather than --ease-settle: settle is an ease-out, which drops
   most of the opacity in the first moments and reads as a cut with a tail.
   Easing in as well as out keeps both ends of the change gentle. */
.status-pill-role    { animation: status-a 9s cubic-bezier(0.4, 0, 0.4, 1) infinite; }
.status-pill-contact { animation: status-b 9s cubic-bezier(0.4, 0, 0.4, 1) infinite; }

/* Each holds for about four seconds, and the change takes 700ms. Opacity only:
   one pill fades out where it stands and the other fades in, with nothing
   moving. 7.8% of nine seconds is that 700ms. */
@keyframes status-a {
  0%, 44.4%     { opacity: 1; pointer-events: auto; }
  52.2%, 92.2%  { opacity: 0; pointer-events: none; }
  100%          { opacity: 1; pointer-events: auto; }
}

@keyframes status-b {
  0%, 44.4%     { opacity: 0; pointer-events: none; }
  52.2%, 92.2%  { opacity: 1; pointer-events: auto; }
  100%          { opacity: 0; pointer-events: none; }
}

.intro-text .status-pill-contact {
  background: var(--moss-08);
  color: var(--moss);
}

.status-pill-contact .status-dot { background: var(--moss); }

/* The address takes the pill's own colour rather than standing out from it:
   the whole line is the message, and the green already marks it as one. No
   rule under it either. */
.intro-text .status-pill-contact a {
  color: inherit;
  text-decoration: none;
}

/* The pill's words are one flex item, not several. A bare text node beside an
   element becomes an anonymous flex item of its own, so "Contact me at" and
   the address were two items with the pill's 0.55rem gap driven between them,
   which read as a stray space before the email. Wrapped, the gap applies only
   between the dot and the line, and the words space themselves normally. */
.status-line {
  min-width: 0;
}

/* No cycling. The line settles on the contact reading, which is the one worth
   landing on if only one can be shown. */
@media (prefers-reduced-motion: reduce) {
  .status-pill-role    { animation: none; opacity: 0; pointer-events: none; }
  .status-pill-contact { animation: none; opacity: 1; pointer-events: auto; }
}

/* CTA anchored to the graph side of the hero */
/* The primary action is cobalt text, not an object: a rule draws
   underneath on hover and the arrow travels the way it will take you. */
/* Centred on the page, the label over a stack of three chevrons — the lure
   to keep going down. Set as the nav is: this is a way through the page, not
   a readout, so it takes the display face and the caps tracking every other
   piece of navigation carries. It used to be Space Mono at weight 400, which
   made the site's largest call to action the one control set as data. */
.map-cta {
  position: absolute;
  /* Sits near the bottom of the first screen rather than the bottom of the
     hero box, so it lands where the eye ends up before scrolling. */
  top: calc(100vh - var(--nav-height) - 5.5rem);
  left: 50%;
  transform: translateX(-50%);
  z-index: 2;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 0.5rem;
  background: none;
  border: none;
  cursor: pointer;
  font-size: var(--text-ui);
  font-weight: 600;
  font-family: var(--font-display);
  letter-spacing: var(--track-ui);
  text-transform: uppercase;
  color: var(--cobalt);
  padding: 0.5rem 0.25rem;
  /* A cobalt glow breathing under the whole cue */
  animation: cta-glow 2.8s var(--ease-settle) infinite;
}

/* One bare V — no stem, no box. */
.cta-chevrons {
  display: flex;
  flex-direction: column;
  align-items: center;
}

.chev {
  width: 16px;
  height: 9px;
  display: block;
  transition: transform var(--dur-quick) var(--ease-settle);
}

/* Reached for, the V steps down a little — the way the button will take you. */
.map-cta:focus-visible .chev {
  transform: translateY(4px);
}
@media (hover: hover) {
  .map-cta:hover .chev {
    transform: translateY(4px);
  }
}

@keyframes cta-glow {
  0%, 100% { filter: drop-shadow(0 0 3px rgba(40, 72, 166, 0.16)); }
  50%      { filter: drop-shadow(0 0 10px rgba(40, 72, 166, 0.42)); }
}

@media (prefers-reduced-motion: reduce) {
  .map-cta { animation: none; }
  .chev { transition: none; }
}

/* --------------------------------------------------------------------------
   Network explore — the scroll stage below the hero. Scaffolding for now:
   height only, so the CTA has somewhere to travel to.
   -------------------------------------------------------------------------- */
/* --------------------------------------------------------------------------
   Landing page only: the scroll settles on the field.

   The graph's transition is driven by scroll position, so a free flick used
   to leave it stopped halfway — grown but not arrived, with the panels not
   yet shown. Two snap points fix where a scroll comes to rest: the top of the
   page, and the top of the stage, which is exactly the position the
   transition finishes at. Between them the scroll behaves normally; near
   either end it settles onto it.

   `proximity`, never `mandatory`: it nudges a scroll that has come to rest
   nearby, and leaves a deliberate one alone. The footer is not a snap target,
   so once past the stage the page scrolls freely to the bottom — reaching it
   is a second gesture rather than something a single flick falls through.

   Scoped to the home page: the case studies and the about page are ordinary
   documents and must keep ordinary scrolling.
   -------------------------------------------------------------------------- */
/* The class sits on <html> because that is the scroll container here: a
   `scroll-snap-type` on <body> propagates to the viewport for no one, and
   would silently do nothing. */
html.page-home {
  scroll-snap-type: y proximity;
}

/* Off while the hero cue is carrying the page down to the story. The stage's
   snap position lies part-way along that journey, and proximity snapping
   cannot tell a scroll travelling through it from one that has stopped near
   it — so it catches at the page mid-glide. Set and cleared by js/site.js. */
html.gliding {
  scroll-snap-type: none;
}

/* Only the stage is a start-aligned snap position. The top of the page was one
   too, which made the first screen sticky: a small scroll to reach the cue at
   the bottom of it was pulled straight back, and a slightly larger one was
   taken all the way down. The hero now scrolls freely and the snapping only
   has an opinion once the field is being approached. */
html.page-home #network-explore {
  scroll-snap-align: start;
}

/* The footer is a snap position too, aligned by its end so it settles at the
   foot of the document. It has to be one: only a few hundred pixels of scroll
   exist past the stage, and with nothing to catch it down here `proximity`
   would keep pulling a scroll back up to the field and the footer would be
   awkward to reach. */
html.page-home footer {
  scroll-snap-align: end;
}

#network-explore {
  position: relative;
  /* No fixed height any more: the stage is as tall as the story inside it.
     js/site.js holds the field still for `stage.offsetHeight - innerHeight`,
     so the beats' own heights are the only timing there is — add a beat and
     the field simply stays put for a screen longer. The floor is here for the
     case where the track is missing or has not laid out yet. */
  min-height: 135vh;
}

/* --------------------------------------------------------------------------
   The narrative — four beats scrolled past the held field.

   Not an overlay. The beats are ordinary blocks in the stage's flow, so they
   travel with the page while the graph behind them does not, and the text
   physically moves past a picture that stays still. That is the difference
   between scrollytelling and a slideshow, and it is also why the copy is real
   content: in the flow it is in reading order, reachable by keyboard, present
   for a screen reader and readable with no JavaScript at all. The old reading
   column was none of those things — .explore-ui is visibility:hidden until a
   class arrives.
   -------------------------------------------------------------------------- */
.story-track {
  position: relative;
  /* #hero is a positioned z-index: 1 and the graph lifts to 2 inside it, so
     the beats have to clear both. .explore-ui takes 5 for the same reason. */
  z-index: 5;
  /* Quiet before the first line. It used to be long enough that the beat lit
     only once the field had finished travelling, which read as a wait: the
     hero copy was gone, the graph was still on its way, and there was nothing
     to look at in between. The line now arrives while the field is still
     settling, so the story picks up where the hero left off. */
  padding-top: 36vh;
  /* Room past the close for the field to travel back to centre and then to be
     explored. This is the length of the exploration state: the panels leave
     when the stage's foot nears the fold, so what is set here is how long
     they stay. */
  padding-bottom: 190vh;
  /* The field beneath keeps answering hover between the beats; only the line
     being read takes the pointer. */
  pointer-events: none;
}

/* A beat holds a screen, so one line is being read at a time and the next is
   a scroll away rather than already half-read at the bottom of the last. */
.story-beat {
  min-height: 85vh;
  display: grid;
  align-items: center;
  /* The story keeps the left of the page; the field steps to the right to
     make the room — see NARRATIVE_SHIFT in js/site.js.

     Two bounds, and the smaller wins. The first is a measure: never wider
     than 30rem, because this is one line of prose at a time. The second is
     the room actually there, and it has to be, because the field is not a
     fixed width — it settles at 78% of the smaller viewport axis, so the
     space to its left is 66vw − 39vmin once the shift has moved it. Setting
     only the measure put the text over the field from 1280px down. The floor
     stops the column collapsing; below the query further down there is no
     room to share at all and the story takes the whole width instead. */
  width: min(
    clamp(16rem, 40vw, 30rem),
    max(15rem, 66vw - 39vmin - var(--gutter) - 1.5rem)
  );
  margin-inline-start: var(--gutter);
  opacity: 0.14;
  transition: opacity var(--dur-settle) var(--ease-settle);
}

/* The beat crossing the middle of the screen is the one being read. The others
   stay on the page at low contrast rather than being taken off it: they are
   one story, and a reader scrolling back should find the earlier lines where
   they left them. Opacity only, never visibility or display, so every beat
   stays in the accessibility tree whatever the scroll position — the same
   reason the two status pills in the hero cross-fade the way they do. */
.story-beat.is-active {
  opacity: 1;
  pointer-events: auto;
}

/* Set exactly as the line under the hero title is — same size, same leading,
   no tracking. --text-lead was a step too loud for four screens of it, and
   its negative tracking belongs to display type rather than to prose at this
   size. The story now reads in the same voice the page opens in. */
.story-beat p {
  font-size: var(--text-body-lg);
  line-height: var(--leading-body);
  color: var(--color-text);
}

/* The privacy line under the count: a footnote to the sentence, so smaller
   and quieter than it, close enough beneath to read as belonging to it. */
.story-beat p.story-note {
  margin-top: var(--space-sm);
  font-size: var(--text-ui);
  line-height: var(--leading-body);
  color: var(--color-text-faint);
}

/* The close. One size with the beats above it now, so what marks it out is
   weight, ink and room rather than scale: the deepest cobalt on the page, a
   step up in weight, and a screen of quiet under it with nothing following.
   All existing tokens — nothing was drawn for it. */
/* The same measure as the beats above it, and a little more room beneath —
   the pause the story ends on. */
.story-beat-close {
  min-height: 95vh;
}

/* The break between the two sentences is a decision about where the thought
   turns, the same as .title-break on the work and about pages — and it holds
   on the same condition: only while the line it makes still fits. The first
   sentence sets at 297px; below about 337px of viewport the column is
   narrower than that, and the break would strand a word rather than set a
   line. 22rem is the nearest step clear of it. */
@media screen and (max-width: 22rem) {
  .close-break { display: none; }
}

.story-beat-close p {
  font-family: var(--font-display);
  font-size: var(--text-body-lg);
  font-weight: 600;
  line-height: var(--leading-body);
  color: var(--cobalt-deep);
}

/* No gutter for the field to step aside into: the story takes the full width
   and the field stays behind it. The condition is the width *and* the shape,
   because the room beside the field is 66vw − 39vmin — a tall window runs out
   of it while still being wide. js/site.js watches this same query and stops
   shifting the field when it matches, so the two can never disagree.

   This is deliberately not the 900px the panels dock at. The panels have the
   same problem and dock too late; unifying the two thresholds belongs to the
   responsive pass, and this is the value that pass should settle on. */
@media (max-width: 1100px), (max-aspect-ratio: 13/10) {
  .story-beat,
  .story-beat-close {
    width: auto;
    /* still one line of prose at a time: a tablet's full width is too long
       a line to read comfortably */
    max-width: 34rem;
    margin-inline: var(--gutter);
  }
}

/* No cross-fade to follow: every beat is simply legible, and the story is read
   by scrolling rather than by watching. */
@media (prefers-reduced-motion: reduce) {
  .story-beat {
    opacity: 1;
    transition: none;
  }
}

/* The line the field leaves behind. Anchored to the foot of the stage rather
   than fixed to the viewport, so it scrolls up into the space the graph has
   just vacated and then travels on to the footer with everything else — no
   second piece of scroll state to keep in step with the fade.

   Set as the page's own display voice with the rule that draws under it on
   hover, the same as every other link here. */
.explore-outro {
  position: absolute;
  left: 50%;
  bottom: 24vh;
  transform: translateX(-50%);
  max-width: min(90vw, 34rem);
  text-align: center;
  font-family: var(--font-display);
  font-size: var(--text-body-lg);
  font-weight: 500;
  /* No tracking: --track-lead is drawn for display sizes and pinches this at
     18px. Leading stays tight — one centred line, not a paragraph. */
  line-height: var(--leading-tight);
  color: var(--cobalt);
  text-decoration: none;
  background-image: linear-gradient(var(--cobalt), var(--cobalt));
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 0% 1.5px;
  transition: background-size var(--dur-quick) var(--ease-settle);
}

.explore-outro:focus-visible {
  background-size: 100% 1.5px;
}
@media (hover: hover) {
  .explore-outro:hover {
    background-size: 100% 1.5px;
  }
}

/* --------------------------------------------------------------------------
   Network explore — the readout and the controls flanking the settled field.

   Two columns of bare text in the margins, not floating cards: the graph is
   the plate and these are its marginalia, so they take the page's own paper
   and a single hairline rule rather than a panel with a shadow. They sit in
   the gutters the graph's 78% fit leaves clear, which is why they can be
   opaque without covering anything.
   -------------------------------------------------------------------------- */
.explore-ui {
  position: fixed;
  inset: 0;
  z-index: 5;
  /* One panel, held in the right margin and centred on the field's height,
     so it reads as the field's companion rather than as chrome pinned to
     the top of the window. The field takes its own position from where this
     lands (EXPLORE_SHIFT in js/site.js). */
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  justify-content: center;
  padding: clamp(4.5rem, 9vh, 6rem) clamp(1.5rem, 5vw, 6rem) var(--space-md);
  /* The field beneath still answers hover and drag — only the panel takes
     the pointer. */
  pointer-events: none;
  opacity: 0;
  visibility: hidden;
  /* In from the right, the side it lives on. Small: an arrival, not an
     entrance. */
  transform: translateX(1.5rem);
  transition: opacity var(--dur-settle) var(--ease-settle),
              transform var(--dur-settle) var(--ease-settle),
              visibility 0s linear var(--dur-settle);
}

body.explore-live .explore-ui {
  opacity: 1;
  visibility: visible;
  transform: none;
  transition: opacity var(--dur-settle) var(--ease-settle),
              transform var(--dur-settle) var(--ease-settle),
              visibility 0s;
}

/* One object. The switches, what they amount to and the hint are a single
   instrument, so they share one region — drawn as an outline only, a cobalt
   hairline at the site's middle radius, on the page's own paper. */
.explore-panel {
  width: clamp(264px, 21vw, 300px);
  max-height: 100%;
  overflow-y: auto;
  pointer-events: auto;
  padding: 1.25rem 1.25rem 1rem;
  border: 1px solid var(--cobalt-16);
  border-radius: var(--radius-md);
}

/* The panel's one title, in the About page's heading voice ("Hi, I'm
   Helen!"), and named for what the cue at the top of the page promised. */
.explore-title {
  font-family: var(--font-display);
  font-size: var(--text-heading);
  font-weight: 500;
  letter-spacing: var(--track-head);
  line-height: var(--leading-heading);
  color: var(--color-text);
  margin: 0 0 1rem;
}

/* Every label in the panel: mono capitals in the accent. Where a group has
   counts, the unit they are in is said once, here, at the right — so the
   numbers in the rows can be bare. */
.explore-h {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  width: 100%;
  font-family: var(--font-data);
  font-size: var(--text-label);
  font-weight: 400;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--cobalt);
  margin: 0 0 0.35rem;
  padding: 0;
}

.explore-group {
  border: 0;
  margin: 0 0 0.85rem;
  padding: 0;
  min-width: 0;
}

/* ---- Switches ------------------------------------------------------------
   Six of one thing: a row with a mark that says whether it is in, its name,
   and what it is worth in the network. Pressed is the resting state — the
   plain reading is the whole network until you take something out. The mark
   is round, like the dots it filters: filled when in, a ring when out.
   -------------------------------------------------------------------- */
.toggle {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  width: calc(100% + 1rem);
  margin: 0 -0.5rem;
  padding: 0.32rem 0.5rem;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  font-family: var(--font-body);
  font-size: var(--text-small);
  font-weight: 500;
  line-height: var(--leading-tight);
  color: var(--color-text);
  cursor: pointer;
  text-align: left;
  transition: background var(--dur-quick) var(--ease-settle),
              color var(--dur-quick) var(--ease-settle);
}

@media (hover: hover) {
  .toggle:hover { background: var(--cobalt-04); }
}

.toggle-box {
  flex: none;
  position: relative;
  width: 0.7rem;
  height: 0.7rem;
  border-radius: 50%;
  /* a true circle — the site's squircle corners would square it off */
  corner-shape: round;
  border: 1.5px solid var(--cobalt);
  background: var(--cobalt);
  transition: background var(--dur-quick) var(--ease-settle),
              border-color var(--dur-quick) var(--ease-settle);
}


.toggle-name { flex: 1 1 auto; }

/* The count beside each switch, in the body face like the rest of the row. */
.toggle-n {
  font-size: var(--text-label);
  font-variant-numeric: tabular-nums;
  color: var(--color-text-faint);
}

/* Out: the circle empties, the words and the count step back. The same row, further away — still plainly something you can put
   back. */
.toggle[aria-pressed="false"] { color: var(--color-mark); }
.toggle[aria-pressed="false"] .toggle-box {
  background: transparent;
  border-color: var(--color-border-strong);
}
.toggle[aria-pressed="false"] .toggle-n { color: var(--color-mark); }
@media (hover: hover) {
  .toggle[aria-pressed="false"]:hover .toggle-box { border-color: var(--cobalt); }
}


/* ---- Result ---------------------------------------------------------------
   What the selection amounts to, set apart by the panel's one rule. The undo
   lives in its header, next to the consequence it would reverse. */
.explore-result {
  border-top: 1px solid var(--cobalt-16);
  padding-top: 1.35rem;
  margin-top: 0.25rem;
}

.explore-result-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-bottom: 0.35rem;
}

.explore-result-head .explore-h { width: auto; margin: 0; }

/* Always present, so it is found before it is needed; disabled, not hidden,
   while there is nothing to undo — the panel never shifts under the pointer. */
.explore-reset {
  appearance: none;
  border: 0;
  background: none;
  padding: 0;
  font-family: var(--font-body);
  font-size: var(--text-ui);
  font-weight: 500;
  color: var(--cobalt);
  cursor: pointer;
  border-bottom: 1px solid var(--cobalt-32);
  transition: color var(--dur-quick) var(--ease-settle),
              border-color var(--dur-quick) var(--ease-settle),
              opacity var(--dur-quick) var(--ease-settle);
}
@media (hover: hover) {
  .explore-reset:hover { border-bottom-color: var(--cobalt); }
}
.explore-reset:disabled {
  color: var(--color-mark);
  border-bottom-color: transparent;
  cursor: default;
  opacity: 0.7;
}


/* One measure to a line at a switch row's face, size, weight and height, so
   the result reads as the last group in one list. The figure leads, in the
   accent, as every number in the story does. */
.explore-lines {
  list-style: none;
  margin: 0;
  padding: 0;
}
.explore-lines li {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.32rem 0;
  font-family: var(--font-body);
  font-size: var(--text-small);
  font-weight: 500;
  line-height: var(--leading-tight);
  color: var(--color-text);
}

.explore-empty {
  margin: 0;
  font-size: var(--text-small);
  line-height: var(--leading-tight);
  color: var(--color-text-muted);
}

/* ---- Hint -----------------------------------------------------------------
   The one instruction, under the figures. Stays put: it is the only place
   the panel says the dots can be reached, and it should be there whenever
   someone looks for it. */
.explore-hint {
  margin: 0.6rem 0 0;
  font-size: var(--text-xs);
  line-height: var(--leading-tight);
  color: var(--color-text-faint);
}

/* A figure in the prose that the field can answer. Underlined in the accent
   so it reads as reachable, and left otherwise as the running text it sits in:
   same face, same size, same colour. A button rather than a span, because it
   does something — which also means the keyboard reaches it and focus works
   the same as hover, without any of that having to be arranged in script. */
.figure-probe {
  appearance: none;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  cursor: help;
  border-bottom: 1.5px solid var(--cobalt-32);
  transition: border-color var(--dur-quick) var(--ease-settle);
}

.figure-probe:focus-visible {
  border-bottom-color: var(--cobalt);
}
@media (hover: hover) {
  .figure-probe:hover {
    border-bottom-color: var(--cobalt);
  }
}

/* A number is the evidence in the sentence, so it takes the accent and
   bold — in the sentence's own face, so it reads as part of the line rather
   than a switch into a data font. Not a different size: it should sit in the
   line, not break it. */
.figure {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--cobalt-deep);
}

/* Narrow screens: no margin to stand in, so the panel docks at the foot of
   the field, full measure, and its two groups of switches sit side by side
   so the field keeps most of the screen. The key gives way first. */
@media (max-width: 900px) {
  .explore-ui {
    align-items: stretch;
    justify-content: flex-end;
    padding: 0 var(--gutter) var(--space-sm);
  }
  .explore-panel {
    width: auto;
    max-height: 54vh;
    padding: 1rem 1rem 0.75rem;
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    column-gap: 1.25rem;
  }
  .explore-title,
  .explore-result { grid-column: 1 / -1; }
  .explore-title { font-size: var(--text-title); margin-bottom: 0.6rem; }
  .explore-group { margin-bottom: 0.6rem; }
  .toggle { padding-block: 0.3rem; }
}

/* Phones and tablets: the panel is the switches and nothing else — the two
   groups side by side, docked whole at the foot of the screen, never
   scrolling. The figures and the hint give way; the graph above them is the
   result, and js/site.js fits it into the room left over (PANEL_DOCK_MAX). */
@media (max-width: 900px) {
  .explore-panel {
    max-height: none;
    overflow: visible;
    padding: 0.9rem 1rem 0.4rem;
    column-gap: 1rem;
  }
  .explore-result { display: none; }
}

/* Tablets: room for the result as a third column beside the two groups of
   switches — set as a group of its own, its label level with theirs, with
   a hairline between each column instead of a rule above. The hint stays a phone-and-desktop thing: three columns
   of rows line up only if none of them carries a paragraph. */
@media (min-width: 641px) and (max-width: 900px) {
  .explore-panel { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .explore-result {
    display: block;
    grid-column: auto;
    border-top: 0;
    padding-top: 0;
    margin: 0 0 0.6rem;
  }
  .explore-result-head { margin-bottom: 0.35rem; }
  .explore-result .explore-hint { display: none; }
  /* A hairline between the three, in the panel's own cobalt wash. */
  .explore-group + .explore-group,
  .explore-result {
    border-left: 1px solid var(--cobalt-16);
    padding-left: 1rem;
  }
}

/* Phones and tablets: once it has arrived, the graph is shown at full
   strength — behind the story as well as above the docked panel. Before the
   cue it is hidden (#hero:not(.cta-spent), which outranks this), and past
   the stage it still leaves (body.explore-past). */
@media (max-width: 900px) {
  body:not(.explore-past) #graphContainer { opacity: 1; }
}


@media (prefers-reduced-motion: reduce) {
  .explore-ui { transition: none; transform: none; }
}

/* Set by js/shelf.js while a dialog is open, so the page behind it does not
   scroll. Named for the full-map modal this once belonged to; that component
   is gone and the fun dialog is the only thing that raises it now. */
body.modal-open {
  overflow: hidden;
}

/* The retired full-map modal's two copy styles are kept: index.html still
   carries that copy in a hidden .network-copy block, marked for reuse in the
   scroll experience. Everything else the modal owned has been removed. */
.map-subtitle {
  color: var(--color-text-muted);
  margin-top: var(--space-xs);
  font-size: var(--text-body);
}

.map-notes {
  max-width: 62ch;
  margin-top: var(--space-sm);
}

.map-notes p {
  font-size: var(--text-body);
  line-height: var(--leading-body);
  color: var(--color-text);
  margin-top: var(--space-xs);
}

#case-wrapper {
  position: relative;
}

/* Case header: breadcrumb on the left, the mark (and the live-project link,
   inserted by js/site.js) on the right. */
.case-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-sm);
  margin-bottom: var(--space-md);
}

/* The mark leads the breadcrumb on the left. */
.case-header-left {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  min-width: 0;
}

.case-header-right {
  display: flex;
  align-items: center;
  gap: var(--space-sm);
  flex: none;
}

/* Breadcrumb — mono agate, the trail faint and the link in cobalt. */
.crumbs {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
  font-family: var(--font-data);
  font-size: var(--text-label);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--color-text-faint);
  min-width: 0;
}

.crumbs a {
  color: var(--cobalt);
  text-decoration: none;
  background-image: linear-gradient(var(--cobalt), var(--cobalt));
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 0% 1px;
  transition: background-size var(--dur-quick) var(--ease-settle);
}

@media (hover: hover) {
  .crumbs a:hover {
    background-size: 100% 1px;
  }
}

.crumb-sep {
  color: var(--color-border-strong);
}

/* The mark, top right. Turns a quarter on hover, the same gesture the
   footer mark makes — the quatrefoil is four-fold symmetric, so it lands
   back on itself. Pure transform, so the header never reflows. */
.case-mark {
  width: 44px;
  height: 44px;
  flex: none;
  transition: transform var(--dur-settle) var(--ease-settle);
}

@media (hover: hover) {
  .case-mark:hover {
    transform: rotate(90deg);
  }
}

@media (prefers-reduced-motion: reduce) {
  .case-mark { transition: none; }
  .case-mark:hover { transform: none; }
}

.ext-link .ext-icon {
  width: 13px;
  height: 13px;
  transition: transform var(--dur-quick) ease;
}

@media (hover: hover) {
  .ext-link:hover .ext-icon {
    transform: translate(3px, -3px);
  }
}

/* Graph tooltip — no container at all: bare text floating on the field,
   knocked out of the dots behind it by a paper-coloured text-shadow. */
.tooltip {
  position: absolute;
  /* Name and relationship sit on one line, so the readout must not wrap and
     must be measurable — graph.js reads its width to keep it on the plate. */
  white-space: nowrap;
  background: none;
  border: 0;
  box-shadow: none;
  padding: 0;
  border-radius: 0;
  color: var(--color-text);
  font-size: var(--text-small);
  pointer-events: none;
  opacity: 0;
  transition: opacity 180ms var(--ease-settle);
  z-index: 9999;
}

.tooltip strong {
  font-family: var(--font-display);
  /* the site's heading weight ("Hi, I'm Helen!"); 600 read heavy at this size */
  font-weight: 500;
  line-height: var(--leading-compact);
  text-shadow: 0 0 3px var(--color-bg), 0 0 7px var(--color-bg),
               0 0 14px var(--color-bg);
}

/* The relationship follows the name on the same line, set in mono caps and a
   step down, so the two read as one label rather than as two lines of
   caption. The dots no longer carry the group as colour, which makes this the
   only place a relationship is named on hover. */
.tooltip .tooltip-type {
  margin-left: 0.5em;
  font-family: var(--font-data);
  font-size: var(--text-label);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--cobalt);
  text-shadow: 0 0 3px var(--color-bg), 0 0 7px var(--color-bg),
               0 0 14px var(--color-bg);
}

/* --------------------------------------------------------------------------
   7. Cards & project grid
   -------------------------------------------------------------------------- */
/* Page title — the display voice at reading width, one step down from the
   hero so the grid below it still leads. The italic carries the phrase the
   page is really about. */
.page-title {
  /* In em, not rem: the size is viewport-scaled, so a box measured in the
     title's own type keeps the same words on the same line at every width
     rather than reflowing as the font grows. 20em is what holds the first
     line of the longest of these titles. */
  max-width: 20em;
  margin-top: var(--space-lg);
  font-size: var(--text-page-title);
  font-weight: 600;
  line-height: var(--leading-heading);
  letter-spacing: var(--track-title);
  color: var(--color-text-muted);
}

.page-title em {
  font-style: italic;
  color: var(--cobalt);
}

/* These titles are composed, not just wrapped: the break is a decision about
   where the thought turns. It only holds while the line it makes still fits —
   below that the box is narrower than the first line, and a forced break
   would strand a word instead of setting one. */
@media screen and (max-width: 34rem) {
  .page-title .title-break { display: none; }
}

.project-grid {
  display: grid;
  /* minmax(0, …): equal columns whatever they hold — a tag that will not
     break would otherwise widen its own column past its share. */
  grid-template-columns: repeat(2, minmax(0, 1fr));
  /* Rows sit close: each card already carries the room its description fades
     into on hover, so a full step between rows on top of that read as a gap. */
  gap: 1.25rem var(--space-lg);
  margin-block: var(--space-md) var(--space-lg);
}

.card-link {
  text-decoration: none;
  color: inherit;
  display: block;
}

/* A card with no case study behind it: hovers like the rest, but the pointer
   stays an arrow so it doesn't promise a click. */
.card-link-soon {
  cursor: default;
}

/* De-boxed cards: image leads, text sits on bare paper. Every category
   carries the one accent — projects differ by cover art, not by hue. */
.card {
  height: 100%;
  transition: transform var(--dur-quick) var(--ease-settle);
}

.card.hidden,
.card-link.hidden {
  display: none !important;
}

.card-media {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-media);
  overflow: hidden;
  /* Tight to the cover, so the whole text block — tags, title, copy — reads
     as one caption sitting under the image. */
  margin-bottom: var(--space-xs);
}

/* The caption sits a little in from the frame. The cover's corners are deep
   (--radius-media), so its straight side only begins well below the curve;
   text flush with the outer edge read as hanging outside the picture. Inset
   by margin, not padding: the tag row clips its leading separator at its
   own edge (see .pill-row), and padding would move that edge. */
.card > .pill-row,
.card > h3,
.card > p {
  margin-inline: 0.875rem;
}

.card-img {
  width: 100%;
  aspect-ratio: 16 / 9;
  background: var(--color-surface);
  object-fit: cover;
  display: block;
}

/* --------------------------------------------------------------------------
   Skeletons — the wait, shaped like what is coming.

   The boxes already hold their final shape: every image and clip here
   declares its aspect ratio, so nothing moves when the picture arrives and
   there is no separate placeholder layout to build. What was missing was any
   sign that something is on its way — a flat grey panel reads as a broken
   image. This adds a slow highlight travelling across that grey, and takes
   it away the moment the media paints.

   The work grid goes further: until every cover can paint, each card —
   cover, tags, title, copy — is held as a skeleton, and js/site.js lifts
   .is-loading on all of them at once, so the projects arrive together
   instead of text first and clips trickling in after. The words keep their
   space while hidden, so nothing reflows when they appear.
   -------------------------------------------------------------------------- */
/* Tinted with the accent's own wash rather than grey paper, so the wait
   reads as part of the page. .case-image.img-skeleton outranks the case
   frame's background shorthand, which would otherwise wipe the shimmer. */
.img-skeleton,
.case-image.img-skeleton,
.project-grid.is-loading .card-media,
.project-grid.is-loading .pill-row,
.project-grid.is-loading .card h3,
.project-grid.is-loading .card p {
  background-color: var(--wash);
  background-image: linear-gradient(
    100deg,
    rgba(250, 250, 249, 0) 38%,
    rgba(250, 250, 249, 0.7) 50%,
    rgba(250, 250, 249, 0) 62%
  );
  background-repeat: no-repeat;
  background-size: 200% 100%;
  animation: img-shimmer 1.4s linear infinite;
}

@keyframes img-shimmer {
  from { background-position: 100% 0; }
  to   { background-position: 0 0; }
}

/* The grid's text becomes bars of its own length: the words stay in the
   flow, uncoloured, so each bar is exactly as tall and wide as what replaces
   it. */
.project-grid.is-loading .pill-row,
.project-grid.is-loading .card h3 {
  width: fit-content;
  max-width: 100%;
}

.project-grid.is-loading .pill-row,
.project-grid.is-loading .pill,
.project-grid.is-loading .pill + .pill::before,
.project-grid.is-loading .card-link .card h3,
.project-grid.is-loading .card-link .card p {
  color: transparent;
  border-radius: 4px;
}

/* The title's hover underline is drawn with background-size, which would
   squash the shimmer to a hairline mid-load. */
.project-grid.is-loading .card-link .card h3 { background-size: 200% 100%; }

/* No rule around a cover that is not there yet — the shimmer is the shape.
   The border keeps its width, so the frame does not shift when it returns. */
.project-grid.is-loading .card-media { border-color: transparent; }

/* Covers wait unseen behind the shimmer, then fade in together. */
.project-grid.is-loading .card-img { opacity: 0; }
.card-img { transition: opacity var(--dur-quick) var(--ease-settle); }

/* Still a placeholder, just a still one. */
@media (prefers-reduced-motion: reduce) {
  .img-skeleton,
  .case-image.img-skeleton,
  .project-grid.is-loading .card-media,
  .project-grid.is-loading .pill-row,
  .project-grid.is-loading .card h3,
  .project-grid.is-loading .card p {
    background-image: none;
    animation: none;
  }
}

/* The card answers the pointer by drawing back: the whole thing — cover,
   tags, title, copy — eases down a couple of percent, and a cobalt rule
   draws under the title. Scaling the card rather than the image means the
   type recedes with it, so the card reads as one object rather than a frame
   with a moving picture in it. Only transform and background-size animate,
   so it stays on the compositor and the grid never reflows. Keyboard focus
   does the same. */
.card-link:focus-visible .card {
  transform: scale(0.975);
}
@media (hover: hover) {
  .card-link:hover .card {
    transform: scale(0.975);
  }
}

/* The cover clip sits inside the card's anchor, and a <video> will happily
   take a click that was meant for the link. Nothing here is meant to be
   operated, so it is made transparent to the pointer and every click,
   anywhere on the cover, opens the case study. */
.card-link video.card-img {
  pointer-events: none;
}

/* Stand-in cover for projects whose art does not exist yet — a designed
   state, not a hole. Plain paper, with the mark in the skeleton's gray and
   a single highlight coursing through its lines (images/logo-loading.svg
   carries the colours and the motion). */
.card-img-soon {
  background-color: var(--color-bg);
  display: grid;
  place-content: center;
  justify-items: center;
  gap: var(--space-xs);
}

.card-soon-mark {
  width: 72px;
  height: 72px;
}

@media (max-width: 700px) {
  .card-soon-mark {
    width: 44px;
    height: 44px;
  }
}

.card-soon-label {
  font-family: var(--font-data);
  font-size: var(--text-label);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--cobalt);
}

/* A cobalt rule draws under the title, left to right. The round arrow in the
   preview is the card's only arrow now — an inline one beside the title would
   be the same signal said twice. */
.card h3 {
  font-size: var(--text-title);
  /* the About page's heading weight ("Hi, I'm Helen!"), at the card's own size */
  font-weight: 500;
  margin-bottom: 0.25rem;
  background-image: linear-gradient(var(--cobalt), var(--cobalt));
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 0% 1.5px;
  transition: background-size var(--dur-quick) var(--ease-settle),
              color var(--dur-quick) var(--ease-settle);
}

.card-link:focus-visible .card h3 {
  background-size: 100% 1.5px;
  color: var(--cobalt);
}
@media (hover: hover) {
  .card-link:hover .card h3 {
    background-size: 100% 1.5px;
    color: var(--cobalt);
  }
}

/* The description is held back until the card is reached for. Its space is
   still reserved, so revealing it never reflows the grid or nudges the
   neighbouring cards. Focus reveals it identically to hover. */
.card p {
  font-size: var(--text-small);
  color: var(--color-text-muted);
  opacity: 0;
  transition: opacity var(--dur-quick) var(--ease-settle);
}

.card-link:focus-visible .card p {
  opacity: 1;
}
@media (hover: hover) {
  .card-link:hover .card p {
    opacity: 1;
  }
}

/* A touch screen has no hover to bring the description in, so it is left
   out there altogether: a card is its cover, tags and title, and the rows
   close up around them instead of keeping room for copy that would never
   arrive. */
@media (hover: none) {
  .card p {
    display: none;
  }
  .project-grid {
    row-gap: var(--space-md);
  }
}

/* Tags are data, not controls — so they carry no container. Cobalt caps in
   mono, with a dot between them doing the separating an oval used to do. */
.pill-row {
  /* Each tag carries its separator on its left and is pulled left by its
     width, and the row clips — so whichever tag begins a line, the first or
     one that has wrapped, has its separator cut off, and a "|" never leads a
     line. The column gap gives back what the pull takes between two tags on
     the same line. */
  display: flex;
  flex-wrap: wrap;
  overflow: hidden;
  column-gap: var(--pill-sep);
  margin-bottom: 0.25rem; /* the title sits right up under the tags */
  --pill-sep: 1.45rem;   /* half a rem either side of the bar, as before */
}

.pill {
  /* A tag never breaks inside itself; a row wraps only between tags. An
     inline-block rather than inline, because the tags sit flush in the
     markup with no space between them — as plain inlines that left the row
     nowhere to break at all. */
  display: inline-block;
  white-space: nowrap;
  font-family: var(--font-data);
  font-size: var(--text-label);
  font-weight: 400;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--cobalt);
}

/* The separator belongs to the pair, not to either tag: it only appears
   between two of them, never leading or trailing (see .pill-row). */
.pill {
  position: relative;
  margin-left: calc(-1 * var(--pill-sep));
  padding-left: var(--pill-sep);
}
.pill::before {
  content: "|";
  position: absolute;
  left: 0;
  width: var(--pill-sep);
  text-align: center;
  /* The same cobalt wash every other hairline on the site is drawn in, so the
     separator stays subordinate to the labels it divides rather than reading
     as a third tag. */
  color: var(--cobalt-32);
  font-weight: 400;
}

/* Phones and tablets: every tag row on one line. Each card measures its own
   width (a size container), and the tags are set to whatever size lets the
   longest row — "INTERACTIVE DATA VISUALIZATION | DATA ANALYSIS", 43
   characters of Space Mono at about 0.65em each with the tighter tracking,
   plus one separator — fit it, between 9px and their usual size. Still
   allowed to wrap, between tags, if a card is ever narrower than that. */
@media (max-width: 900px) {
  .card { container-type: inline-size; }
  .pill-row { --pill-sep: 1rem; }
  .pill {
    /* the card's width, less the caption inset either side and one separator */
    font-size: clamp(9px, calc((100cqi - 2.75rem) / 28.5), var(--text-label));
    letter-spacing: 0.04em;
  }
}


/* --------------------------------------------------------------------------
   8. Case study pages
   -------------------------------------------------------------------------- */
#case-wrapper {
  padding-block: var(--space-lg) var(--space-xl);
}

.case-container {
  max-width: var(--reading-max);
  margin-inline: auto;
}

.case-title {
  font-size: var(--text-page-title);
  font-weight: 600;
  /* the same gray as the name on the landing page */
  color: var(--color-text-muted);
  margin-bottom: var(--space-sm);
}

/* The live project sits with the title as a mark, not as a button. */
.case-title-link {
  display: inline-flex;
  vertical-align: baseline;
  margin-left: 0.55rem;
  color: var(--cobalt);
}

.case-title-link svg {
  width: 20px;
  height: 20px;
  transition: transform var(--dur-quick) var(--ease-settle);
}

@media (hover: hover) {
  .case-title-link:hover svg {
    transform: translate(2px, -2px);
  }
}

/* --------------------------------------------------------------------------
   Case rail — the section tokens as a standing index in the left margin.
   Fixed off the centred reading column rather than laid out beside it, so
   the column's own width is untouched; it only appears once the margin is
   wide enough to hold it.
   -------------------------------------------------------------------------- */
/* Narrow: no margin to stand in, so the rail joins the flow above the case
   and carries the back link alone. */
.case-rail {
  margin-bottom: var(--space-md);
}

.case-rail .case-rail-list,
.case-rail::after {
  display: none;
}

/* Wide enough for the margin to hold it: the rail leaves the flow and stands
   beside the reading column, section index and divider included. */
@media screen and (min-width: 1400px) {
  .case-rail {
    position: fixed;
    /* js/site.js walks `top` down with the page until the nav has gone */
    top: calc(var(--nav-height) + var(--space-lg));
    /* level with the nav's mark, on the shared page gutter */
    left: var(--gutter);
    width: 9.5rem;
    margin-bottom: 0;
    /* No overflow box here: it clipped the divider, which sits outside the
       rail's own bounds. The list is short enough not to need one. */
    z-index: 15;
  }

  /* Scoped through the rail so it outranks the base .case-rail-list rule
     further down, which sets display for the column layout. */
  .case-rail .case-rail-list { display: flex; }

  /* The divider belongs to a rail with something in it — beside a lone back
     link it would be a stub. */
  .case-rail:has(.case-rail-list)::after { display: block; }
}

/* A hairline between the rail and the column it indexes, tucked close to the
   rail and running its own height — no further. In the same cobalt wash the
   nav's rule uses, so it reads on the ground. */
.case-rail::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  right: calc(var(--space-xs) * -1);
  width: 1px;
  background: var(--cobalt-32);
}

/* Answers the pointer the way Back to Top does: a cobalt rule draws under it.
   This one runs right to left, anchored at the right edge — the way back is
   leftward, and the arrow says so, so the rule travels with it rather than
   against it. The arrow used to slide instead, which said the same thing in a
   second language and moved the target while it was being aimed at. It holds
   still now; only the rule arrives. */
.case-rail-back {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-family: var(--font-display);
  font-size: var(--text-ui);
  font-weight: 600;
  letter-spacing: var(--track-ui);
  text-transform: uppercase;
  color: var(--cobalt);
  text-decoration: none;
  background-image: linear-gradient(var(--cobalt), var(--cobalt));
  background-repeat: no-repeat;
  /* Anchored right: the rule grows from that edge, so it unrolls leftward. */
  background-position: 100% 100%;
  background-size: 0% 1.5px;
  transition: background-size var(--dur-quick) var(--ease-settle);
}

.case-rail-back:focus-visible {
  background-size: 100% 1.5px;
}
@media (hover: hover) {
  .case-rail-back:hover {
    background-size: 100% 1.5px;
  }
}

/* No rule down the side: weight and colour alone carry the section you are
   in, and the labels start flush with the nav's own left edge. */
.case-rail-list {
  list-style: none;
  margin: var(--space-sm) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

.case-rail-list a {
  display: block;
  /* A nav, not a readout: Inter at a reading weight, and set as written
     rather than in caps. The section titles it indexes are authored in title
     case and put into caps by .tag-label-filled, so the rail simply does not
     transform them. */
  font-family: var(--font-body);
  font-weight: 400;
  font-size: var(--text-ui);
  /* Sentence case, so barely any — see .back-to-top. */
  letter-spacing: 0.01em;
  line-height: var(--leading-tight);
  color: var(--color-text-faint);
  text-decoration: none;
  transition: color var(--dur-quick) var(--ease-settle);
}

@media (hover: hover) {
  .case-rail-list a:hover {
    color: var(--color-text);
  }
}

.case-rail-list a.active {
  font-weight: 600;
  color: var(--cobalt);
}
@media (prefers-reduced-motion: reduce) {
  .case-rail-back,
  .case-title-link svg { transition: none; }
}

.case-image {
  width: 100%;
  background: var(--color-surface);
  /* The same cover the work card shows, one page along — it would read as a
     different component if its corner changed on the way through. */
  border-radius: var(--radius-media);
  margin-block: var(--space-lg);
  aspect-ratio: 16 / 9;
  object-fit: cover;
}

/* Replaced elements only: .case-image also dresses an empty <div> on the
   case studies that have no artwork yet. */
video.case-image { display: block; }

.case-context {
  margin-bottom: var(--space-lg);
}

.case-paragraph {
  font-size: var(--text-body);
  line-height: var(--leading-body);
  color: var(--color-text);
  margin-top: 0.75rem;
}

.case-paragraph a {
  text-decoration: underline;
  color: inherit;
}

.case-meta {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--space-md);
  margin-top: var(--space-md);
}

.case-meta-item p {
  font-size: var(--text-body);
  margin-top: var(--space-xs);
  line-height: var(--leading-tight);
  color: var(--color-text-muted);
}

/* Section and property labels — CONTEXT, TEAM, ROLE, TIMELINE, SKILLS.
   No container: mono caps in cobalt is enough to mark them as labels, and
   an outline around a single word reads as a control rather than a heading. */
.tag-label {
  display: inline-block;
  font-family: var(--font-data);
  font-size: var(--text-label);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--cobalt);
  margin-bottom: var(--space-xs);
}

.tag-label-filled {
  display: inline-block;
  /* The same mark as .tag-label above — mono agate in the accent. It keeps
     its own name and its section top margin, but nothing else sets it
     apart: a section title and a property label are the same voice here. */
  font-family: var(--font-data);
  font-size: var(--text-label);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--cobalt);
  margin-top: var(--space-xl);
  margin-bottom: var(--space-xs);
}

/* The one rule weight and wash the site uses everywhere — the nav's line,
   the rail's divider, and this. */
hr {
  border: none;
  border-top: 1px solid var(--cobalt-32);
  margin: var(--space-lg) 0;
}

.case-section {
  margin-bottom: var(--space-lg);
}

.case-heading {
  font-size: var(--text-heading);
  font-weight: 500;
  margin: 0.75rem 0 1rem;
}

/* A topic inside a section — one step under .case-heading. It stays out of
   the rail, which indexes sections only. */
.case-subhead {
  margin: var(--space-lg) 0 0;
  /* the same step as .case-heading: a topic inside a section is titled at the
     size a section is, so a case that uses both does not read as three ranks */
  font-size: var(--text-heading);
  font-weight: 500;
  letter-spacing: var(--track-item);
}

/* First in its section, it follows the heading rather than opening a gap. */
.case-heading + .case-subhead {
  margin-top: var(--space-md);
}

/* --------------------------------------------------------------------------
   Case study — editorial passages.

   Three pieces, kept because the case actually needs them: a closing line set
   a step up from body copy, a diagram wrapper with a mono caption, and the
   worked examples. Everything else here is type and whitespace. They use the
   same tokens as the rest of the site, and none of them is a container: a box
   around every idea reads as a slide deck rather than as writing.
   -------------------------------------------------------------------------- */

.case-turn {
  margin: var(--space-lg) 0;
  font-family: var(--font-display);
  font-size: var(--text-lead);
  font-weight: 500;
  line-height: var(--leading-tight);
  letter-spacing: var(--track-lead);
  color: var(--cobalt-deep);
  max-width: 34ch;
}

/* Diagrams sit on the page's own paper, full measure, with a caption under
   them. The SVGs inside are drawn to a viewBox and scale to whatever column
   they are given.

   The caption is Inter, not Space Mono. It is the one caption on the site
   that runs to full sentences over several lines, and mono capitals set at
   label size stop forming word-shapes — legible for "TIMELINE" and hard work
   for a paragraph. Mono is kept for the marks that are genuinely data: tags,
   figures, breadcrumbs, the index beside each example. */
.case-diagram {
  margin: var(--space-lg) 0;
}

.case-diagram svg {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
}

.diagram-caption {
  margin-top: var(--space-sm);
  font-family: var(--font-body);
  font-size: var(--text-xs);
  line-height: var(--leading-tight);
  color: var(--color-text-faint);
}

/* The worked examples. Numbered in mono down the side, so two of them read as
   a pair of specimens rather than as two more sections. */
.case-example {
  display: grid;
  grid-template-columns: 3.5rem 1fr;
  gap: var(--space-sm);
  margin-top: var(--space-lg);
  padding-top: var(--space-md);
  border-top: 1px solid var(--color-border);
}

.example-index {
  font-family: var(--font-data);
  font-size: var(--text-label);
  letter-spacing: var(--track-caps);
  color: var(--cobalt);
  padding-top: 0.35rem;
}

.example-title {
  font-family: var(--font-display);
  font-size: var(--text-title);
  font-weight: 600;
  letter-spacing: var(--track-item);
  margin-bottom: 0.4rem;
}

.example-body p { font-size: var(--text-body); line-height: var(--leading-body); margin-bottom: 0.6rem; }
.example-body p:last-child { margin-bottom: 0; }

/* Narrow screens: the index stops holding its own column and sits above. */
@media (max-width: 700px) {
  .case-example { grid-template-columns: 1fr; gap: 0.35rem; }
}

.case-bullets,
.case-ordered-bullets {
  padding-left: 1.5rem;
  margin-top: var(--space-xs);
}

.case-bullets { list-style-type: disc; }

.case-bullets li,
.case-ordered-bullets li {
  font-size: var(--text-body);
  line-height: var(--leading-body);
  color: var(--color-text);
  margin-bottom: var(--space-xs);
}

.data-columns {
  display: flex;
  gap: var(--space-md);
  margin-top: 1.5rem;
}

.data-item {
  flex: 1;
}

.data-item h4 {
  font-size: var(--text-title);
  font-weight: 600;
  margin: var(--space-xs) 0;
}

.data-item p {
  font-size: var(--text-body);
  line-height: var(--leading-body);
  color: var(--color-text);
  margin-bottom: 0.75rem;
}

.viz-row {
  display: flex;
  align-items: flex-start;
  gap: 1.5rem;
  margin-bottom: var(--space-sm);
}

.viz-caption {
  font-size: var(--text-body);
  line-height: var(--leading-body);
  color: var(--color-text);
  margin-top: var(--space-md);
  flex: 1;
}

.placeholder-img {
  width: 100%;
  max-width: 380px;
  height: auto;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  margin-bottom: var(--space-sm);
}

.placeholder-img-viz {
  width: 100%;
  max-width: 500px;
  height: auto;
  object-fit: contain;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  margin-top: 0.75rem;
}

.placeholder-img-large {
  width: 100%;
  height: auto;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  margin-top: 1.25rem;
}

.placeholder-img-large-no-border {
  width: 100%;
  height: auto;
  margin-top: 1.25rem;
}

/* A demo clip inside a case section: the bordered images' corner, with a
   light hairline so the recording reads as one framed screen. */
.case-demo-video {
  display: block;
  border: 1px solid #E5E5E5;
  border-radius: var(--radius-sm);
}

/* The way out to the live thing. Not a panel and not a ruled-off footer: the
   heading and the action simply close the column, sitting on its left edge
   like everything above them, with the section's own space holding them
   apart. The filled box was the only element on a case page that began
   somewhere other than the measure, and the only rounded panel on the page. */
.full-project-section {
  margin-top: var(--space-xl);
}

/* The NDA notice is a condition on the work, not part of it, so it keeps a
   soft cobalt wash to set it apart, with a mono label and a quiet aside. */
.nda-notice {
  background: var(--cobalt-08);
  text-align: left;
  padding: var(--space-sm) var(--space-md);
  border-radius: var(--radius-md);
  margin-bottom: var(--space-lg);
}

.full-project-section h2,
.nda-notice h2 {
  font-size: var(--text-heading);
  font-weight: 500;
  margin-bottom: var(--space-sm);
  color: var(--color-text);
}

/* Set as a .tag-label, so it reads in the same voice as CONTEXT above it. */
.nda-notice h2 {
  margin-bottom: 0;
  font-family: var(--font-data);
  font-size: var(--text-label);
  font-weight: 400;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--cobalt);
}

.nda-notice .nda-p {
  margin-top: var(--space-xs);
  font-size: var(--text-small);
  color: var(--color-text-muted);
}

.nda-notice a {
  color: var(--cobalt);
  font-weight: 500;
  text-decoration: none;
}

/* The same text action the rail's way back and Back to Top use: mono-free
   display capitals in the accent, with a rule that unrolls under the words.
   The pill was the site's only filled control, and nothing else on a case
   page is boxed. */
.view-project-btn {
  display: inline-block;
  color: var(--cobalt);
  font-family: var(--font-display);
  font-size: var(--text-ui);
  font-weight: 600;
  letter-spacing: var(--track-ui);
  text-transform: uppercase;
  text-decoration: none;
  background-image: linear-gradient(var(--cobalt), var(--cobalt));
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 0% 1.5px;
  transition: background-size var(--dur-quick) var(--ease-settle),
              color var(--dur-quick) var(--ease-settle);
}

.view-project-btn:focus-visible {
  background-size: 100% 1.5px;
}
@media (hover: hover) {
  .view-project-btn:hover {
    background-size: 100% 1.5px;
  }
}

.view-project-btn:active { color: var(--cobalt-deep); }

@media (prefers-reduced-motion: reduce) {
  .view-project-btn { transition: none; }
}

/* --------------------------------------------------------------------------
   BlackRock case study — Optro palette.

   Scoped to body.case-br so none of it reaches another page. Prose keeps the
   site's own type and ink. Everything that is not prose (labels, figures,
   diagrams, the lines that close an argument) takes the Optro navy and lime:
   an enterprise ledger with a computational edge. Lime is never ink on paper,
   where it has no contrast; it is a fill, or ink on navy.
   -------------------------------------------------------------------------- */
.case-br {
  --br-lime: #E3FF52;
  --br-navy: #14365A;
  --br-paper: #F2F2EE;
  --br-white: #FFFFFF;
  --br-gray: #9DA9B6;
  --br-rule: rgba(20, 54, 90, 0.16);
  --br-rule-strong: rgba(20, 54, 90, 0.38);
  --br-ink-soft: rgba(20, 54, 90, 0.64);
  --br-on-navy-soft: rgba(255, 255, 255, 0.72);
  --br-on-navy-rule: rgba(255, 255, 255, 0.16);
  --br-chip: 2.25rem;
}

/* ---- Headings ------------------------------------------------------------
   Section labels are left to the site's own .tag-label-filled, so they
   match CONTEXT: mono capitals in the site cobalt, nothing added. Headings
   stay in the site's ink rather than the case navy, to match the About page. */
.case-br .case-heading { color: var(--color-text); }

/* ---- Closing lines ---------------------------------------------------------
   The one recurring mark: a navy cell with lime ink beside the line that
   closes a section's argument. */
.case-br .case-turn {
  display: grid;
  grid-template-columns: 1.75rem minmax(0, 38ch);
  column-gap: 1rem;
  align-items: start;
  max-width: none;
  color: var(--br-navy);
}

.case-br .case-turn::before {
  content: "\2192" / "";
  display: grid;
  place-items: center;
  width: 1.75rem;
  height: 1.75rem;
  margin-top: 0.05rem;
  background: var(--br-navy);
  color: var(--br-lime);
  font-family: var(--font-data);
  font-size: var(--text-small);
  line-height: 1;
}

/* Held to one line while the column has room; it wraps only when it doesn't. */
.case-br .case-turn.br-turn-wide { grid-template-columns: 1.75rem minmax(0, 1fr); }

/* The case's closing line inverts the mark: navy arrow on lime. Its words
   go to the site's ink, like the case's headings, rather than navy. */
.case-br .case-turn.br-turn-lime { color: var(--color-text); }

.case-br .case-turn.br-turn-lime::before {
  background: var(--br-lime);
  color: var(--br-navy);
}

.case-br .br-coda {
  margin-top: var(--space-md);
  max-width: none;
  font-size: var(--text-body-lg);
  line-height: var(--leading-tight);
  font-weight: 500;
  color: var(--br-navy);
}

/* ---- Layout: two columns --------------------------------------------------
   Prose beside prose, or prose beside the visual it sets up. */
.br-cols {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-lg);
  align-items: start;
  margin-top: 0.75rem;
}

.br-cols > .case-paragraph,
.br-col > .case-paragraph:first-child { margin-top: 0; }

/* ---- The challenge ------------------------------------------------------ */
.br-challenge-body {
  container-type: inline-size;
  display: grid;
  grid-template-columns: 3rem minmax(0, 1fr);
  gap: 1.25rem;
  align-items: start;
  /* The same 12px a .case-heading takes under its label, so the question sits
     where every other section's heading does. */
  margin-top: 0.75rem;
}

.br-challenge-mark {
  display: grid;
  place-items: center;
  width: 3rem;
  height: 3rem;
  background: var(--br-lime);
  color: var(--br-navy);
  font-family: var(--font-data);
  font-size: var(--text-subhead);
  font-weight: 700;
  line-height: 1;
}

/* One line at every width: the question is about 33.9em long, so the size
   is the column beside the mark divided by 35, capped at the page title. */
.br-challenge-q {
  --br-q-mark: 4.25rem; /* mark + gap */
  white-space: nowrap;
  font-family: var(--font-display);
  font-size: min(var(--text-page-title), calc((100cqw - var(--br-q-mark)) / 35));
  font-weight: 500;
  line-height: var(--leading-heading);
  letter-spacing: var(--track-title);
  color: var(--color-text);
}

/* ---- Requested solution/statement -> underlying problem --------------------
   Two paper cells, the request above the problem beneath it, so the
   reframing reads downward from what was asked to what was wrong. */
.br-flow {
  list-style: none;
  margin: var(--space-lg) 0 0;
  padding: 0;
  display: grid;
  gap: 0;
  max-width: 28rem;
}

.br-flow li {
  position: relative;
  padding: 1rem 1.1rem;
  background: var(--br-paper);
  border: 1px solid var(--br-rule);
  font-family: var(--font-data);
  font-size: var(--text-label);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--br-ink-soft);
}

/* The problem is what the reframing arrives at, so it takes the navy. */
/* Joined: the problem cell sits directly under the request, sharing its
   edge, so the two read as one reframed record. */
.br-flow li + li { border-top: 0; }

.br-flow li:last-child {
  background: var(--br-navy);
  border-color: var(--br-navy);
  color: var(--br-lime);
  font-weight: 700;
}


/* ---- Prioritize / Diagnose / Route ------------------------------------------
   A numbered cell per question, on a paper panel beside the prose. */
.br-steps {
  list-style: none;
  margin: 0;
  padding: var(--space-md);
  background: var(--br-paper);
}

.br-steps li {
  position: relative;
  display: grid;
  grid-template-columns: var(--br-chip) minmax(0, 1fr);
  column-gap: 1rem;
  align-items: start;
}

.br-steps li + li { margin-top: 1.5rem; }

.br-steps-n {
  display: grid;
  place-items: center;
  width: var(--br-chip);
  height: var(--br-chip);
  background: var(--br-navy);
  color: var(--br-lime);
  font-family: var(--font-data);
  font-size: var(--text-label);
  font-weight: 700;
  line-height: 1;
}

.br-steps-body {
  min-height: var(--br-chip);
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.br-steps-k {
  font-family: var(--font-data);
  font-size: var(--text-label);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--br-ink-soft);
}

.br-steps-q {
  margin-top: 0.15rem;
  font-family: var(--font-display);
  font-size: var(--text-body-lg);
  font-weight: 500;
  line-height: var(--leading-tight);
  color: var(--br-navy);
}


/* The decision model: outlined like the reframing cells. */
.br-model { border: 1px solid var(--br-rule); }

/* ---- The backlog -----------------------------------------------------------
   A routing diagram of the backlog on the page's own ground. It is drawn twice:
   left to right for wide screens, and as a stacked tree for narrow ones, so
   the labels stay readable without sideways scrolling. */
.br-backlog {
  margin: var(--space-lg) 0;
}

.br-route {
  display: block;
  width: 100%;
  height: auto;
}

.br-route-narrow { display: none; max-width: 30rem; }

/* ---- Root-cause examples ---------------------------------------------------
   Read as a case trace rather than a grid of cells: the four steps run down
   one column, their names set in the margin beside it. No rule runs down
   them — the margin of names is what holds the steps together. The
   assumption is struck through where it sits, the way it would be corrected
   on paper; the decision is the one label filled in lime. */
.case-br {
  --br-trace-k: 10.5rem; /* the margin the step names sit in */
}

.case-br .case-example {
  grid-template-columns: 2.25rem minmax(0, 1fr);
  gap: 0.75rem;
  border-top: 0;
}

.case-br .example-index {
  padding-top: 0.3rem;
  font-weight: 700;
  color: var(--br-ink-soft);
}

.case-br .example-title {
  margin-bottom: 1rem;
  color: var(--br-navy);
}

.br-trace {
  margin: 0;
}

.br-trace > div {
  display: grid;
  grid-template-columns: var(--br-trace-k) minmax(0, 1fr);
}

.br-trace dt {
  padding: 0.3rem 1rem 0 0;
  text-align: right;
  font-family: var(--font-data);
  font-size: var(--text-label);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--br-ink-soft);
}

.br-trace dd {
  position: relative;
  margin: 0;
  max-width: 60ch;
  padding: 0 0 1.25rem 1.25rem;
  font-size: var(--text-body);
  line-height: var(--leading-body);
  color: var(--color-text);
}

.br-trace .is-assumed dd {
  color: var(--color-text-faint);
  text-decoration: line-through;
  text-decoration-color: var(--br-rule-strong);
  text-decoration-thickness: 1px;
}

.br-trace .is-decision dd {
  padding-bottom: 0;
  font-weight: 500;
  color: var(--br-navy);
}

/* DECISION is the one name with a fill. */
.br-trace .is-decision dt {
  justify-self: end;
  align-self: start;
  margin-right: 1rem;
  padding: 0.2rem 0.4rem;
  background: var(--br-lime);
  line-height: 1.2;
  color: var(--br-navy);
}

/* ---- The question changed -------------------------------------------------
   Side by side: the old question on paper, the new one on navy. */
.br-question {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 2.5rem minmax(0, 1.5fr);
  margin: var(--space-md) 0 var(--space-lg);
}

.br-q-from {
  display: flex;
  align-items: center;
  padding: var(--space-md);
  background: var(--br-paper);
  font-size: var(--text-body-lg);
  line-height: var(--leading-tight);
  color: var(--br-ink-soft);
}

.br-q-arrow {
  display: grid;
  place-items: center;
  font-family: var(--font-data);
  font-size: var(--text-subhead);
  font-weight: 700;
  line-height: 1;
  color: var(--br-navy);
}

.br-q-to {
  display: flex;
  align-items: center;
  padding: var(--space-md);
  background: var(--br-navy);
  font-family: var(--font-display);
  font-size: var(--text-body-lg);
  font-weight: 500;
  line-height: var(--leading-tight);
  letter-spacing: var(--track-lead);
  color: var(--br-white);
}

/* ---- The role: the diagram on the page's own ground ------------------------
   The names sit inside their circles, so on a narrow screen the drawing
   keeps a readable width and scrolls sideways rather than shrinking. */
/* Scaled to the column at every width, never scrolled: no image or diagram
   in a case study scrolls sideways. On a phone that draws it small. */
.case-br .case-diagram.br-role {
  margin: 0 0 var(--space-lg);
}

/* ---- Impact ---------------------------------------------------------------- */
/* Three qualitative outcomes, secondary to the figure: a row of cells with a
   mono name over a short description. */
.br-outcomes {
  list-style: none;
  margin: var(--space-md) 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 1px;
  background: var(--br-rule);
  border: 1px solid var(--br-rule);
}

.br-outcomes li {
  padding: 1rem 1.1rem 1.1rem;
  background: var(--br-white);
  font-size: var(--text-small);
  line-height: var(--leading-body);
  color: var(--color-text-muted);
}

.br-outcome-k {
  display: block;
  margin-bottom: 0.4rem;
  font-family: var(--font-data);
  font-size: var(--text-label);
  letter-spacing: var(--track-caps);
  color: var(--br-navy);
}

/* ---- Narrow ----------------------------------------------------------------
   Every side-by-side layout stacks, and the arrows turn to point down. */
@media screen and (max-width: 48rem) {
  .br-cols { grid-template-columns: 1fr; gap: var(--space-md); }


  /* The step names leave the margin and sit above their text. */
  .br-trace > div {
    grid-template-columns: 1fr;
    padding: 0 0 1.25rem 1rem;
  }
  .br-trace dt { padding: 0 0 0.3rem; text-align: left; }
  .br-trace dd { padding: 0; }
  .br-trace .is-decision { padding-bottom: 0; }
  .br-trace .is-decision dt { justify-self: start; margin: 0 0 0.4rem; }

  .br-route-wide { display: none; }
  .br-route-narrow { display: block; }

  .br-question { grid-template-columns: 1fr; }
  .br-q-arrow { padding: 0.5rem 0; }
  .br-q-arrow { transform: rotate(90deg); }

  .br-outcomes { grid-template-columns: 1fr; }

  .br-challenge-body { grid-template-columns: var(--br-chip) minmax(0, 1fr); gap: 1rem; }
  .br-challenge-mark { width: var(--br-chip); height: var(--br-chip); font-size: var(--text-body); }
  .br-challenge-q { --br-q-mark: 3.25rem; }

}

/* --------------------------------------------------------------------------
   9. About page
   -------------------------------------------------------------------------- */
/* Meta rows are data: mono agate */
/* The place/employer/school line under the About heading. Mono agate like
   every other meta row on the site — tags, captions, dates — but held in
   faint ink rather than cobalt, so it reads as a quiet aside to the
   introduction instead of competing with it. */
/* Scoped through the parent so it outranks `.about-intro-text p`, which
   otherwise sets the body size and full ink on this paragraph. */
.about-intro-text .about-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.25rem;
  margin-bottom: var(--space-sm);
  font-family: var(--font-data);
  font-size: var(--text-label);
  line-height: var(--leading-compact);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  /* Mono is the data voice, and it carries the accent wherever it appears —
     the same cobalt as the project pills and the fun titles. */
  color: var(--cobalt);
}

.about-meta span {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

/* Inherits the row's faint ink via currentColor, so the glyphs and the
   labels stay the same gray. */
/* Drawn at 24px, set at 14 — the glyphs carry the accent in the file, so
   there is no fill to inherit any more. */
.about-meta .meta-icon {
  width: 14px;
  height: 14px;
  flex: none;
  display: block;
}

/* Sections */
.about-section {
  margin-bottom: var(--space-xl);
  scroll-margin-top: calc(var(--nav-height) + var(--space-md));
}

.about-section h2 {
  /* the case studies' section heading (.case-heading): same size and weight,
     so a heading on this page reads in the same voice as one in a case */
  font-size: var(--text-heading);
  font-weight: 500;
  margin-bottom: var(--space-sm);
}

/* Intro: text + profile photo */
.about-intro {
  display: flex;
  gap: var(--space-lg);
  align-items: flex-start;
  justify-content: space-between;
}

.about-intro-text {
  flex: 1;
  /* fill the row up to the photo column (gap provides the breathing room) */
  max-width: none;
}

.about-intro-text p {
  font-size: var(--text-body);
  line-height: var(--leading-body);
  color: var(--color-text);
  margin-bottom: var(--space-sm);
}

/* My Identity. The row and the text column are .about-intro and
   .about-intro-text themselves, so the heading, body copy, column widths,
   gap and stacking all come from the intro above. These rules add only what
   the intro has no equivalent for. */
.about-identity {
  /* The plate column runs the height of the text, so the plate can sit
     centred beside it. */
  align-items: stretch;
}

/* The same width as .about-photo-stack, with the plate set level with the
   top of the text, as the profile photo is in the intro. */
.about-identity-visuals {
  width: min(300px, 38%);
  flex: none;
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
}

/* The text column ends on its last line rather than that paragraph's bottom
   margin, so the paper beside it ends there too. */
.about-identity .about-intro-text > :last-child {
  margin-bottom: 0;
}

/* The plate: the annotated blueprint with two rows beneath it, dancheong
   then munsal, each led to by a line from the mark. The photographs sit in
   those rows. Positions are percentages of the plate so they stay on the
   rows drawn in images/identity-logo-blueprint.svg (1000 x 1300) at any
   width. */
.about-identity-plate {
  position: relative;
  width: 100%;
  aspect-ratio: 1000 / 1300;
  margin: 0;
}

.about-identity-plate-drawing {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
}

/* Set like the carousel thumbnails: cropped to a square, the same small
   corner and hairline. */
.about-identity-plate-photo {
  position: absolute;
  display: block;
  left: 6%;
  width: 14%;
  height: auto;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  border-radius: var(--radius-media-sm);
  border: 1px solid var(--color-border);
}

.about-identity-plate-photo.is-dancheong { top: calc(880 / 1300 * 100%); }
.about-identity-plate-photo.is-munsal { top: calc(1100 / 1300 * 100%); }

/* The row labels, set in the page rather than the SVG so they take the same
   label type as 단청 · DANCHEONG and 문살 · MUNSAL in the text beside them.
   Each is centred on its photo, just to its right. */
.about-identity-plate-label {
  position: absolute;
  left: 23.2%;
  transform: translateY(-50%);
  font-family: var(--font-data);
  font-size: var(--text-label);
  line-height: var(--leading-compact);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--cobalt);
  white-space: nowrap;
}

.about-identity-plate-label.is-dancheong { top: calc(950 / 1300 * 100%); }
.about-identity-plate-label.is-munsal { top: calc(1170 / 1300 * 100%); }

/* Munsal is drawn in moss in the blueprint, so both its labels, the one in
   the plate and the one in the text, take the same green. */
.about-identity .about-identity-label.is-munsal,
.about-identity-plate-label.is-munsal {
  color: var(--moss);
}

/* 단청 | DANCHEONG, 문살 | MUNSAL: the site's mono label, as in .about-meta,
   set directly under its subheading. Scoped through the row so it outranks
   `.about-intro-text p`. */
.about-identity .about-identity-label {
  margin: 0 0 var(--space-xs);
  font-family: var(--font-data);
  font-size: var(--text-label);
  line-height: var(--leading-compact);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--cobalt);
}

/* My Values, My Perspective: each opens its passage, so it carries the
   space above. */
.about-identity-subhead {
  margin: var(--space-md) 0 0.35rem;
  /* Set as the experience titles are (.exp-title): body size, regular
     weight, untracked. */
  font-family: var(--font-body);
  font-size: var(--text-body);
  font-weight: 400;
  line-height: var(--leading-body);
  letter-spacing: normal;
  color: var(--color-text);
}

/* --------------------------------------------------------------------------
   My Identity, as a disclosure inside the bio.

   It is a continuation of the introduction, not a section of the page, so the
   trigger is the site's text action — cobalt, body face, a rule drawn under
   the words on hover, exactly as Back to Top and the case rail's way back —
   and not a control with an edge of its own.
   -------------------------------------------------------------------------- */
.identity-toggle {
  display: inline-flex;
  align-items: flex-start;
  gap: 0.45rem;
  /* Under the carousel, clear of the thumbnails. */
  margin: var(--space-md) 0 0;
  text-align: left;
  padding: 0;
  border: 0;
  background: none;
  font-family: var(--font-body);
  font-size: var(--text-body);
  line-height: var(--leading-body);
  color: var(--cobalt);
  cursor: pointer;
}

/* The rule belongs to the words; the chevron stands outside it. */
.identity-toggle-label {
  background-image: linear-gradient(var(--cobalt), var(--cobalt));
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 0% 1px;
  transition: background-size var(--dur-quick) var(--ease-settle);
}

.identity-toggle:focus-visible .identity-toggle-label {
  background-size: 100% 1px;
}
@media (hover: hover) {
  .identity-toggle:hover .identity-toggle-label {
    background-size: 100% 1px;
  }
}

/* Open, it reads "Close my identity", and the rule draws back the other way:
   right to left. */
.identity-toggle[aria-expanded="true"] .identity-toggle-label {
  background-position: 100% 100%;
}

/* The same chevron the home cue uses, turned over when the panel is open. */
.identity-chev {
  width: 11px;
  height: auto;
  flex: none;
  /* the label can wrap in the photo column, so the mark stays on line one */
  margin-top: 0.5em;
  transition: transform var(--dur-quick) var(--ease-settle);
}

.identity-toggle[aria-expanded="true"] .identity-chev {
  transform: rotate(180deg);
}

/* The height is animated from the script, so the box clips while it moves.
   `hidden` is what actually takes the closed content out of the tab order —
   the clipping alone would leave it reachable. */
.identity-panel {
  overflow: hidden;
  transition: height var(--dur-settle) var(--ease-settle);
}

.identity-panel[hidden] {
  display: none;
}

/* The page's own measure, the same grid #experience and #shelf-root stand in:
   a label column exactly as wide as the photo above it, then the content. So
   My Identity, My Values and My Perspective sit in that margin and their text
   starts on the line "Hi, I'm Helen!" starts on. */
.identity-panel-inner {
  padding-top: var(--space-md);
  display: grid;
  grid-template-columns: min(300px, 38%) minmax(0, 1fr);
  gap: var(--space-md) var(--space-lg);
  align-items: start;
}

/* Placed rather than flowed: the mark keeps the label column to itself and
   every passage stands in the content column, whatever each one holds. */
.identity-head { grid-column: 1; }
.identity-body { grid-column: 2; }

.identity-label {
  /* the same step as "Hi, I'm Helen!" above it (.about-section h2) */
  font-size: var(--text-heading);
  font-weight: 500;
  margin: 0;
}

/* Above the passage it opens. The content column runs continuously, so each
   of these carries the space that separates it from the passage before
   rather than getting it from a grid row it does not have. */
.identity-body .identity-label {
  margin-top: var(--space-md);
  margin-bottom: 0.35rem;
}

/* The first one opens the column, so it has nothing to be held off from. */
.identity-body .identity-label:first-child {
  margin-top: 0;
}

.identity-body > :last-child {
  margin-bottom: 0;
}

/* The plate stands in the label column, under My Identity, at the width that
   column was drawn for — the same 300px the photo above it takes. Capped, so
   it keeps that size when the column goes full width at narrow screens. */
.identity-panel .about-identity-plate {
  width: 100%;
  max-width: 300px;
  margin: 0;
}

/* The disclosure now follows the closing line, so it is offered after the bio
   has finished rather than interrupting it. The trigger keeps its own space
   above (see .identity-toggle); the panel is the last thing in the column, so
   nothing below it needs holding off. */

@media (prefers-reduced-motion: reduce) {
  .identity-toggle-label,
  .identity-chev,
  .identity-panel {
    transition: none;
  }
}

/* Photo stack: main photo + caption + clickable thumbnail carousel */
/* The deck, its caption, then the way into My Identity — one column, since
   clicking or swiping the pile is the whole interface. */
.about-photo-stack {
  width: min(300px, 38%);
  margin: 0;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
}

/* --------------------------------------------------------------------------
   Photo deck. The frames are stacked, not laid in a row: each one sits a
   little further up and to the right of the one on top of it and tips over by
   the same step, so the pile reads as photographs on a table. That corner
   showing past the top card is the only affordance — no dots, no thumbnails,
   no card around the card.

   One formula does the whole stack: a card's offset and tilt are the peek
   step multiplied by its depth. Hover simply makes the step bigger, and
   because the transform is what transitions, every layer opens together.
   -------------------------------------------------------------------------- */
.photo-deck {
  position: relative;
  /* A little narrower than its column, so the card that peeks out past the
     top-right corner stays inside the column rather than reaching the text
     beside it or the title above. */
  width: 95%;
  cursor: pointer;
  /* the step, one card deep — only one card ever shows under the top one */
  --peek-x: 8px;
  --peek-y: -7px;
  --peek-r: 1.8deg;
  /* a horizontal drag is the deck's; a vertical one is still the page's */
  touch-action: pan-y;
}

/* More of the pile on hover: the same step, opened up. */
.photo-deck:focus-visible {
  --peek-x: 14px;
  --peek-y: -12px;
  --peek-r: 3deg;
}
@media (hover: hover) {
  .photo-deck:hover {
    --peek-x: 14px;
    --peek-y: -12px;
    --peek-r: 3deg;
  }
}

.deck-stack {
  position: relative;
  aspect-ratio: 3 / 4; /* all carousel photos are 1125x1500 */
  margin: 0;
  padding: 0;
  list-style: none;
}

.deck-card {
  position: absolute;
  inset: 0;
  transform-origin: 50% 90%; /* tips about its foot, the way a photo would */
  transform:
    translate(calc(var(--d, 0) * var(--peek-x)), calc(var(--d, 0) * var(--peek-y)))
    rotate(calc(var(--d, 0) * var(--peek-r)));
  transition: transform 420ms var(--ease-settle), opacity 420ms var(--ease-settle);
}

/* Below the one peeking card there is nothing left to show. */
.deck-card.is-buried { opacity: 0; }

/* Out from under the pile: left, a little smaller, turned. It does not leave
   the page and it does not fade — from here it drops back down behind the
   stack, which is the half of the move that puts it away. */
.deck-card.is-out {
  transform: translateX(-56%) scale(0.92) rotate(-6deg);
  transition-duration: 240ms;
}

/* Put back in the pile, or brought in from the left to start a step: placed
   without animating, so only the move that follows is seen. */
.deck-card.no-anim { transition: none; }

/* The portrait the stack has always shown: same ratio, same corner, same
   hairline. The shadow is only deep enough to separate one frame from the
   one under it. */
.deck-card img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: var(--radius-media);
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  box-shadow: 0 2px 12px rgba(26, 24, 21, 0.08);
  user-select: none;
  -webkit-user-drag: none;
}

/* Nothing moves on its own account; the pile simply sits open a little. */
@media (prefers-reduced-motion: reduce) {
  .deck-card { transition: none; }
}

.about-photo-caption {
  font-size: var(--text-xs);
  color: var(--color-text-faint);
  margin-top: 0.5rem;
  min-height: 2.2em; /* two lines: captions swap without shifting the row */
  transition: opacity 260ms var(--ease-settle);
}

/* The caption changes its text while it is down, so the words never swap in
   plain view. */
.about-photo-caption.is-swapping {
  opacity: 0;
  transition-duration: 160ms;
}

@media (prefers-reduced-motion: reduce) {
  .about-photo-caption { transition: none; }
}

/* "Lately..." rotating line */
.lately-line {
  margin-top: var(--space-sm);
}

.lately-rotator {
  display: inline-block;
  font-style: italic; /* voice moment */
  color: var(--color-text-muted);
  opacity: 1;
  transform: none;
  transition: opacity 260ms var(--ease-settle), transform 260ms var(--ease-settle);
}

.lately-rotator.out {
  opacity: 0;
  transform: translateY(6px);
}

/* Professional roles */
/* Experience: Work + Education row lists (logo | title/details | date) */
/* With the row rules gone, the logo ring is what gives the list its
   structure — so it takes the cobalt wash the divider uses, and a little
   more size to hold the column on its own. */
.logo-circle,
.logo-mark {
  width: 52px;
  height: 52px;
  border-radius: 50%;
  /* The site's own rule colour, not the cobalt one: the badge is a ring
     drawn around a logo rather than an accented object, so it stays neutral
     and lets the artwork inside it carry the only colour. */
  border: 1px solid var(--color-border);
  flex-shrink: 0;
}

/* blank badge for rows without a logo file yet */
.logo-circle {
  background: var(--color-surface);
}

/* No fill behind the ring. Transparent artwork sits on the page's own paper,
   which is what keeps the badge a drawn outline rather than a grey disc.

   The ring is a wrapper and the logo is a child of it, rather than the ring
   being drawn on the image itself. That is not tidiness. A `border-radius` on
   a replaced element clips its content to a circle inscribed in the *content*
   box, and padding shrinks that box: padding a logo to make it smaller shrank
   the clip by exactly as much, so the corners of a square mark were sliced no
   matter what value was used. Clipping on the wrapper happens where the ring
   is drawn, which is what one would expect it to do. */
.logo-mark {
  display: grid;
  place-items: center;
  overflow: hidden;
  background: none;
}

/* The artwork. Sized as a share of the badge rather than inset with padding,
   so the number says how big the logo is instead of how much space is left
   around it. */
.logo-art {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}

/* A seal already drawn to its own circular edge: it fills the badge, and the
   ring comes off so the two outlines do not sit on top of each other. */
.logo-mark-bare { border: 0; }
.logo-art-fill  { object-fit: cover; }

/* Each logo gets one number: how far it sits in from the ring.

   Each value is the share of the badge the whole file occupies, worked back
   from how large its mark should read: the files carry different amounts of
   margin, so the same mark size needs a different share from each. The two
   translations take up the last fraction of a pixel where a file's own
   margins are not quite even. Both are under 1px, and every number here was
   measured from the alpha channel rather than judged by eye.

   These are tied to the current files. Re-exporting a logo at a different
   size or with different margins means re-measuring its share. */
.logo-blackrock { width: 73.1%; height: 73.1%; translate: -0.6px 0.8px; }
.logo-sofi      { width: 69.3%; height: 69.3%; }
.logo-aaa       { width: 89.7%; height: 89.7%; translate: 0.7px 1.0px; }
.logo-cfa       { width: 71.0%; height: 71.0%; }

/* Section ends where the my-story photo column begins — rows and dates
   stop at the photo's left edge instead of running full width */
/* Every section below the intro inherits the intro's measure: a label column
   exactly as wide as the photo above it, then the same gap. So the subheads
   sit in that margin and their content starts on the line "Hi, I'm Helen!"
   starts on. */
#experience,
#shelf-root {
  display: grid;
  grid-template-columns: min(300px, 38%) minmax(0, 1fr);
  gap: var(--space-md) var(--space-lg);
  align-items: start;
}

h2.exp-subhead {
  /* no size or weight of its own — it takes .about-section h2, the same step
     the fun heading sits at, so the two label columns read as one voice */
  margin-top: 0;
  margin-bottom: 0;
  /* clears the first row's own padding, so the label reads level with the
     entry beside it rather than floating above it */
  padding-top: 0.9rem;
}

.exp-list {
  list-style: none;
  padding: 0;
  margin-top: 0;
}

/* No rules between rows — the logo rings carry the rhythm instead. */
.exp-list > li {
  display: flex;
  align-items: flex-start;
  gap: var(--space-sm);
  padding-block: 0.9rem;
}

.exp-main {
  flex: 1;
  min-width: 0;
  /* the date sits above the title, so the pair is centred against the logo
     circle as a stack rather than the title alone */
  min-height: 52px;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.exp-title {
  font-size: var(--text-body);
  color: var(--color-text);
}

/* Agate eyebrow: the range dates the entry before you read it */
.exp-date {
  font-family: var(--font-data);
  font-size: var(--text-xs);
  color: var(--cobalt);
  white-space: nowrap;
  display: block;
  margin-bottom: 0.15rem;
}

.exp-detail {
  font-size: var(--text-xs);
  color: var(--color-text-muted);
  margin-top: 0.3rem;
}

.exp-detail em {
  margin-right: 0.35rem;
}

.exp-involvements {
  margin-top: 0.9rem;
}

.exp-sublist {
  list-style: none;
  padding: 0;
  font-size: var(--text-xs);
  color: var(--color-text-muted);
  line-height: var(--leading-body);
  margin-top: 0.2rem;
}

.highlight {
  color: var(--accent-indigo);
  font-weight: 500;
}


/* --------------------------------------------------------------------------
   Fun — shelf of hobbies (rendered by js/shelf.js)
   Matches the Experience section width; items rest on the plank and lift on
   hover/focus, opening a hobby panel. Minimal black-and-white per the mockup.
   -------------------------------------------------------------------------- */

.fun-heading {
  font-size: var(--text-title);
  font-weight: 600;
  margin-bottom: 0.25rem;
}

.fun-subtext {
  font-size: var(--text-small);
  color: var(--color-text-muted);
}

/* Surface line where items sit, as a distance up from the scene bottom */
.shelf-scene {
  --shelf-surface: 6.7%; /* plank's ledge (where items rest), as % of stage height */
  overflow-x: clip; /* the plank art's transparent margins bleed past the stage */
}

.shelf-stage {
  position: relative;
  width: 100%;
  aspect-ratio: 1137 / 330; /* visible plank spans full width + headroom for the plant */
}

/* shelf.png has ~10% transparent margins each side and ~27% below the lip;
   oversize + offset it so the VISIBLE plank spans the stage edge-to-edge. */
.shelf-plank {
  position: absolute;
  left: -12.5%;
  bottom: -22%;
  width: 124%;
  max-width: none;
  height: auto;
  z-index: 1;
  pointer-events: none;
  user-select: none;
}

/* Objects on the shelf: bottoms align to the surface line */
.shelf-item,
.shelf-decor {
  position: absolute;
  bottom: var(--shelf-surface);
}

.shelf-decor {
  height: auto;
  pointer-events: none;
  user-select: none;
}

.shelf-item {
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
  line-height: 0;
}

/* On a touch screen the shelf is drawn to fit the width, never scrolled, so
   on a phone some objects are under 20px across. Each is given an unseen
   margin to its hit area so a thumb can still find it; the art itself is
   untouched. */
@media (hover: none) {
  .shelf-item::after {
    content: "";
    position: absolute;
    inset: -10px;
  }
}

/* One-word descriptor under the object, drawn only on hover/focus. Mono
   agate in the accent, the voice the captions and pills already use. */
.shelf-label {
  position: absolute;
  top: calc(100% + 0.4rem);
  /* js/shelf.js measures where the object's ink actually sits and sets
     --label-x; the box centre is only the fallback. */
  left: var(--label-x, 50%);
  white-space: nowrap;
  font-family: var(--font-data);
  font-size: var(--text-label);
  letter-spacing: var(--track-caps);
  line-height: var(--leading-compact);
  text-transform: uppercase;
  color: var(--cobalt);
  opacity: 0;
  pointer-events: none;
  /* Sits low and floats up as it fades in */
  --label-shift: -50%;
  transform: translate(var(--label-shift), 6px);
  transition: opacity var(--dur-quick) var(--ease-settle),
              transform var(--dur-quick) var(--ease-settle);
}

/* The plant's label is hung from its right edge instead of its centre, then
   pulled in a little so it tucks under the pot rather than the outer leaf. */
.shelf-item[data-label-align="right"] .shelf-label {
  --label-shift: calc(-100% - 14px);
}

/* The label and the lift answer a pointer that can hover, and the keyboard.
   On a touch screen a tap would leave them stuck on behind the panel it
   opens — and the panel says what the object is anyway. */
.shelf-item:focus-visible .shelf-label {
  opacity: 1;
  transform: translate(var(--label-shift), 0);
}
@media (hover: hover) {
  .shelf-item:hover .shelf-label {
    opacity: 1;
    transform: translate(var(--label-shift), 0);
  }
  .shelf-item:hover img {
    transform: translateY(-8px);
  }
}

@media (prefers-reduced-motion: reduce) {
  .shelf-label { transition: none; }
}

.shelf-item img {
  width: 100%;
  height: auto;
  display: block;
  transition: transform 260ms var(--ease-settle);
}

/* Hover/focus: the raw object lifts 8px and nothing else happens — no glow,
   no cast shadow. The lift and the label are the whole response. */
.shelf-item:focus-visible img {
  transform: translateY(-8px);
}

.shelf-item:focus-visible {
  outline: none;
}

/* ---- Fun modal -------------------------------------------------------- */
.fun-modal {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-md);
  opacity: 0;
  transition: opacity var(--dur-settle) ease;
}

.fun-modal[hidden] { display: none; } /* the hidden attribute must win over display:flex */
.fun-modal.open { opacity: 1; }

.fun-modal-backdrop {
  position: absolute;
  inset: 0;
  background: var(--scrim);
}

/* Lifted paper floating on a soft three-layer shadow — no border */
.fun-dialog {
  position: relative;
  background: var(--color-card);
  border: 0;
  border-radius: var(--radius-lg);
  /* Landscape: wider than it is tall, and taking most of the screen so the
     images are worth looking at. */
  max-width: min(1280px, 80vw);
  width: 100%;
  max-height: 90vh;
  /* The content scrolls in .fun-dialog-scroll, held off the top and bottom
     by the corner radius, so the scrollbar starts and ends where the edge is
     straight rather than running out over the curves. */
  display: flex;
  flex-direction: column;
  overflow: hidden;
  padding: var(--radius-lg) 0;
  /* Depth only. The cobalt ring that used to sit outside these read as an
     outline drawn around the panel rather than as light under it. */
  box-shadow: 0 2px 10px -2px rgba(26, 24, 21, 0.12),
              0 30px 70px -24px rgba(26, 24, 21, 0.50);
  transform: translateY(16px) scale(0.98);
  transition: transform var(--dur-quick) var(--ease-settle);
}

.fun-modal.open .fun-dialog { transform: none; }

/* Focused on open so the keyboard starts inside it; the dialog is not a
   control, so it shows no ring. */
.fun-dialog:focus { outline: none; }

/* The rest of the dialog's 48px inset, inside the scroller. */
.fun-dialog-scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: calc(var(--space-lg) - var(--radius-lg)) var(--space-lg);
}

/* Dismiss control: a small grey cross in a grey ring, the same ring the
   footer icons wear; cross and ring both turn cobalt on hover. It sits in the
   top-right corner, inside the curve, and stays put while the title and
   content scroll. It has no fill, so content shows through as it scrolls by. */
.fun-close {
  --close-size: 1.625rem;
  position: absolute;
  z-index: 1;
  top: 1.25rem;
  right: 1.25rem;
  width: var(--close-size);
  height: var(--close-size);
  display: grid;
  place-items: center;
  padding: 0;
  border: 1px solid var(--color-border-strong);
  border-radius: 50%;
  background: transparent;
  color: var(--color-mark);
  cursor: pointer;
  transition: color var(--dur-quick) var(--ease-settle),
              border-color var(--dur-quick) var(--ease-settle);
}

.fun-close svg {
  width: 8px;
  height: 8px;
}

.fun-close:focus-visible {
  color: var(--cobalt);
  border-color: var(--cobalt);
}
@media (hover: hover) {
  .fun-close:hover {
    color: var(--cobalt);
    border-color: var(--cobalt);
  }
}

/* Same voice as "When I'm not working..." — the heading the shelf sits under */
.fun-modal-title {
  font-family: var(--font-display);
  font-size: var(--text-subhead);
  /* the About page's heading weight ("Hi, I'm Helen!"), at its own size */
  font-weight: 500;
  letter-spacing: var(--track-head);
  line-height: var(--leading-compact);
  color: var(--color-text);
  margin-right: var(--space-md);
}

.fun-note {
  margin-top: 0.4rem;
  font-size: var(--text-small);
  color: var(--color-text-muted);
}

/* Gallery: two-column masonry of images, small caption under each */
.fun-grid {
  margin-top: var(--space-md);
  columns: 3;
  column-gap: var(--space-sm);
}

.fun-tile {
  display: block;
  width: 100%;
  margin: 0 0 var(--space-sm);
  break-inside: avoid;
  padding: 0;
  border: none;
  background: none;
  text-align: left;
}

/* ---- One-shape variant --------------------------------------------------
   Opted into per panel with `uniform: true` in js/shelf.js. A masonry earns
   its ragged edge when the pictures are different kinds of thing; when they
   are fourteen versions of the same shot, the varying heights read as
   untidiness rather than as rhythm.

   A real grid rather than `columns`, because column flow sizes each track
   independently and cannot line rows up across them. The tracks stay at
   three to keep the panel's composition, and the images take a 3:4 box —
   the ratio all but one of them already are, so the crop costs nothing on
   the rest. Figures align to the top of their row, so a caption running to
   two lines lengthens its own cell without pushing its neighbours' pictures
   out of line. */
/* An image flagged `columnStart` in js/shelf.js heads its own column, so
   the order down the masonry can be set by hand. Only while there are
   columns to move between — in the single phone column a forced break would
   push the rest out sideways. */
@media screen and (min-width: 641px) {
  .fun-grid:not(.fun-grid-uniform) .fun-tile-col-start { break-before: column; }
}

.fun-grid-uniform {
  columns: auto;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-sm);
  align-items: start;
}

.fun-grid-uniform .fun-tile {
  margin-bottom: 0;
}

.fun-grid-uniform .fun-tile img {
  aspect-ratio: 3 / 4;
  object-fit: cover;
}

.fun-tile img {
  width: 100%;
  height: auto;
  display: block;
  border-radius: var(--radius-md);
}

/* Captions are mono agate */
.fun-cap {
  display: block;
  font-family: var(--font-data);
  font-size: var(--text-label);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--cobalt);
  margin-top: 0.35rem;
}

/* Book covers: a row of covers, each with a glow tinted to its art */
.fun-covers {
  margin-top: var(--space-md);
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-md);
  align-items: start;
}

.fun-cover {
  --glow-rgb: 201, 194, 176; /* neutral hairline tone unless the book sets one */
  margin: 0;
  /* rows share one baseline: cover box, then caption */
  display: grid;
  grid-template-rows: auto auto;
}

/* Uniform 2:3 box so every cover is the same size and the row lines up,
   regardless of each file's exact dimensions or caption length. */
/* The per-book glow stays: sampled from the art, it is data rather than
   decoration — the one colour outside the palette allowed on the site. */
.fun-cover img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 2 / 3;
  object-fit: cover;
  border-radius: var(--radius-md);
  box-shadow: 0 0 0 1px rgba(26, 24, 21, 0.07),
              0 0 18px 6px rgba(var(--glow-rgb), 0.45);
}

.fun-cover figcaption {
  font-size: var(--text-xs);
  color: var(--color-text-muted);
  margin-top: 0.7rem;
  line-height: var(--leading-tight);
  align-self: start;
}

/* Books list */
.fun-books {
  list-style: none;
  padding: 0;
  margin-top: var(--space-md);
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
}

/* One row per book: the cover holds the left column at a fixed width so
   every row lines up, and the write-up runs beside it. */
.fun-book {
  display: grid;
  grid-template-columns: 140px minmax(0, 1fr);
  gap: var(--space-md);
  align-items: start;
}

.fun-book-cover {
  width: 100%;
  height: auto;
  /* The covers' own proportion, taken from the files: every one is
     1410 x 2250. A generic 2:3 box was a touch squarer than that, and with
     object-fit: cover it trimmed about 6% off the top and bottom of each
     jacket. Matching the files exactly means cover crops nothing. */
  aspect-ratio: 1410 / 2250;
  object-fit: cover;
  border-radius: var(--radius-md);
  /* A hairline holds the edge; no glow behind the art. */
  box-shadow: 0 0 0 1px rgba(26, 24, 21, 0.07);
}

.fun-book-title {
  font-family: var(--font-display);
  font-size: var(--text-title);
  /* the About page's heading voice ("Hi, I'm Helen!"), at its own size */
  font-weight: 500;
  letter-spacing: var(--track-head);
}

/* Mono agate in the accent, the same voice the captions and pills carry */

/* The score in the accent, the unearned marks left hollow beside it */
.fun-book-rating {
  margin-top: 0.5rem;
  font-size: var(--text-small);
  /* Spacing between star glyphs, not tracking on text. */
  letter-spacing: 0.18em;
  color: var(--cobalt);
}

.fun-book-note {
  font-size: var(--text-body);
  line-height: var(--leading-body);
  color: var(--color-text);
  margin-top: 0.6rem;
  /* Holds its place while the write-up is still to come */
  min-height: 3lh;
}

/* Passages from the book, in its own words. Italic is what marks a voice as
   someone else's; the size and leading are the write-up's, so a quote reads
   as part of the same column rather than as a pull-quote set apart from it.
   A book's status line takes the same set, so it reads with the quotes. */
.fun-book-quote,
.fun-book-status {
  margin: 0.6rem 0 0;
  font-size: var(--text-body);
  line-height: var(--leading-body);
  font-style: italic;
  color: var(--color-text-muted);
}

/* Paragraphs within one passage sit closer than two passages do. */
.fun-book-quote p + p { margin-top: 0.5rem; }

/* The line in a passage worth carrying away, marked as a reader would mark
   it: a band of the site's blue laid behind the words, which keep their own
   ink. The wash is the accent at sixteen per cent — the stronger of the two
   washes the site uses, since the pill's eight per cent is too faint to read
   as a highlight on a line of italic. `clone` gives each wrapped line its
   own ends, so a phrase that breaks across two lines is two neat bands
   rather than one band with its corners cut off at the break. */
.fun-book-quote mark {
  background: var(--cobalt-16);
  color: inherit;
  padding: 0.05em 0.2em;
  border-radius: 3px;
  -webkit-box-decoration-break: clone;
  box-decoration-break: clone;
}

/* A rule between books — the site's own hairline, the same cobalt wash the
   titles and the hr are drawn in. The list's gap is split around it, so the
   line sits in the middle of the space rather than against the next cover. */
.fun-book + .fun-book {
  border-top: 1px solid var(--cobalt-32);
  padding-top: var(--space-md);
}

/* Simple list (fitness, communities) */
.fun-list {
  list-style: none;
  padding: 0;
  margin-top: var(--space-md);
  display: flex;
  flex-direction: column;
  gap: 0.55rem;
}

.fun-list li {
  font-size: var(--text-body);
  color: var(--color-text);
  padding-left: 1.1rem;
  position: relative;
}

.fun-list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.55em;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent-ochre);
}

/* Spotify */
.fun-spotify {
  margin-top: var(--space-md);
  border-radius: var(--radius-md);
  overflow: hidden;
}
.fun-spotify iframe { display: block; border: 0; }

@media (prefers-reduced-motion: reduce) {
  .shelf-item img,
  .fun-modal,
  .fun-dialog,
  .fun-close,
  .fun-tile img { transition: none; }

  /* The marks' turn is pure decoration — it carries no meaning that
     would be lost, so it is dropped rather than snapped. */
  .footer-mark,
  .nav-mark { transition: none; }
  .footer-mark:hover,
  .nav-mark:hover { transform: none; }
}


/* --------------------------------------------------------------------------
   10. Responsive
   -------------------------------------------------------------------------- */
@media screen and (max-width: 768px) {

  .intro-text {
    max-width: 100%;
  }

  /* Mute the graph on small screens where it sits behind the text */
  #graphContainer {
    opacity: 0.35;
  }

  .case-meta {
    grid-template-columns: repeat(2, 1fr);
  }

  .data-columns,
  .viz-row {
    flex-direction: column;
  }

  /* no margin to sit in at this width — labels return above their content */
  #experience,
  #shelf-root,
  .identity-panel-inner {
    display: block;
  }

  .identity-body {
    margin-top: var(--space-md);
  }

  h2.exp-subhead {
    margin-top: var(--space-md);
    padding-top: 0;
  }

  .exp-list,
  .shelf-scene {
    margin-top: var(--space-sm);
  }

}

/* Tablets keep the desktop hero as it is: the name with the graph opening
   to its right. Phones: the name alone. There is no room beside it for the graph, and
   under it the graph only competes, so the graph stays out of sight until
   the reader has gone past the cue — scrolled or tapped — and arrives with
   the story instead. cta-spent is set by js/site.js once the cue has faded.
   The name block is centred in the screen above the cue. */
/* The hero cue's wording: a question beside the graph on a wide screen, an
   invitation on a phone. */
.cta-label-phone { display: none; }

@media screen and (max-width: 640px) {
  .cta-label-wide { display: none; }
  .cta-label-phone { display: inline; }

  /* body size on a phone, so "helping people navigate complex systems" holds
     its line on an ordinary phone */
  .intro-text p.intro-subtitle { white-space: normal; font-size: var(--text-body); }
  .br-phone { display: inline; }

  /* About on a phone: one column — the greeting, the photo, then the story.
     The figure and the text column are both dissolved (display: contents)
     so their parts can be placed on their own: the name, photo and
     caption, the where/what line, the paragraphs, then the way into My
     Identity, which
     belongs after the text it expands on. Tablets keep the desktop's two
     columns. */
  .about-intro {
    flex-direction: column;
    gap: 0;
  }
  .about-photo-stack,
  .about-intro-text { display: contents; }
  /* The greeting comes first, over the photo, as its heading; the
     where/what line sits under the photo and its caption, then the story. */
  /* room under it for the card that peeks up past the top one */
  .about-intro-text > h2 { order: -1; margin-bottom: 1.75rem; }
  .about-photo-stack .photo-deck { width: min(250px, 70%); }
  .about-photo-caption { margin-bottom: 0.75rem; }
  /* order 0, like the photo and caption: after them in source order */
  .about-intro-text > .about-meta { margin-bottom: var(--space-md); }
  .about-intro-text > p:not(.about-meta) { order: 1; }
  .identity-toggle { order: 2; }

  /* My Identity stacks: text first, then the plate at a narrow width. */
  .about-identity-visuals {
    order: 1;
    width: min(300px, 100%);
    justify-content: flex-start;
  }
  /* One project to a row on a phone. */
  .project-grid {
    grid-template-columns: minmax(0, 1fr);
  }
  #hero {
    min-height: calc(100svh - var(--nav-height) - 6rem);
  }
  #hero:not(.cta-spent) #graphContainer {
    opacity: 0;
  }
}

@media screen and (max-width: 640px) {
  /* The footer's right-hand group wraps under the name on a phone, so it
     lines up on the left with everything above it instead of hanging off
     its own right edge. */
  /* ...and it comes first, above the sign-off: the way back up and the
     links are what a reader at the foot of a phone screen reaches for. Order
     only — the markup keeps its sequence for keyboard and screen readers. */
  /* Case studies on a phone: the cover comes straight after the title, and
     the details follow it as one left-aligned column — Team, Role,
     Timeline, Skills — each label over its text. .case-head groups the
     three only so they can be reordered here; flex margins do not collapse,
     so the gaps are set outright to the ones the page had. */
  .case-head {
    display: flex;
    flex-direction: column;
  }
  .case-head .case-title { margin-bottom: 0; }
  .case-head .case-image {
    order: 1;
    margin-block: var(--space-md);
  }
  .case-head .case-meta {
    order: 2;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-sm);
    margin: 0 0 var(--space-md);
  }

  /* The challenge question stops holding itself to one line and wraps at
     an ordinary heading size; on a phone one line made it a few pixels
     tall. */
  .br-challenge-q {
    white-space: normal;
    font-size: var(--text-lead);
  }

  .footer-right { align-items: flex-start; order: -1; }
  /* Stacked at every phone width, not only once the row runs out of room —
     otherwise the reorder above showed as a third layout between about 500
     and 640px, side by side the wrong way round. Two layouts: this, and the
     desktop row. */
  .footer-row {
    flex-direction: column;
    align-items: flex-start;
  }
  /* The contact line breaks after its greeting, so the pill shrinks to its
     longer line instead of wrapping out to the full width of the screen. */
  .status-lead { display: block; }

  /* Hobby panels on a phone: one picture to a row, every layout — the
     masonry, the even grid, the book covers — so each image is shown at the
     panel's full width instead of as a thumbnail. A book's cover sits above
     its write-up rather than beside it. */
  .fun-grid { columns: 1; }
  .fun-grid-uniform,
  .fun-covers { grid-template-columns: minmax(0, 1fr); }
  .fun-book { grid-template-columns: minmax(0, 1fr); }
  /* The books panel reads as a list on a phone: titles, ratings and notes,
     without the cover art, which at a phone's width pushed each write-up
     a whole screen apart from the next. */
  .fun-book-cover { display: none; }
  /* ...and the panel itself takes the phone's width, with a phone's margins
     inside it, so the pictures get the room instead of the padding. */
  .fun-modal { padding: 1rem; }
  .fun-dialog { max-width: none; }
  .fun-dialog-scroll { padding-inline: 1.25rem; }

}

/* --------------------------------------------------------------------------
   Liberation Day case study — the article's own system, as specimens.

   Scoped to body.case-tf. The page keeps the portfolio's chrome exactly as
   every other case has it: cobalt labels, Inter prose, the same rules and
   rhythm. What takes the article's ink is only the reproductions of it — the
   chart stills, the tooltips, the fund list, the specimen plate.

   Every value below is transcribed from the project's DESIGN-SYSTEM.md rather
   than eyeballed, including the three rules that hold the system together:
   serif is prose and sans is instruments, the accent means loss or hover and
   nothing else, and a signed figure always carries its sign.
   -------------------------------------------------------------------------- */
.case-tf {
  --tf-red:      #b2182b;  /* accent: subhead, hover, loss */
  --tf-gain:     #16653f;
  --tf-paper:    #f2f1ee;
  --tf-ink:      #2b2a28;
  --tf-ink-soft: #4a4843;
  --tf-muted:    #6b665e;
  --tf-rule:     #ddd9d3;

  /* Off-token greys the article uses inside SVG and on controls. */
  --tf-tip-bg: #faf9f7;   /* one step lighter than paper */
  --tf-base:   #c4c0b9;   /* baseline + event markers */
  --tf-axis:   #b5b0a8;   /* axis domain and ticks */
  --tf-dotted: #a9a49c;   /* crosshair, dotted button underline */
  --tf-btn:    #cfcbc4;   /* button borders */

  --tf-serif: "Cormorant Garamond", Georgia, serif;
  --tf-radius: 5px;       /* one radius, everywhere */
}

/* A section that opens straight on a topic, with no heading of its own,
   takes the same step under its label that .case-heading would — otherwise
   those sections sit a good deal lower than the ones that have a heading.
   Scoped to this case so no other one is touched. */
.case-tf .tag-label-filled + .case-subhead {
  margin-top: 0.75rem;
}

/* The question the article is built to answer, set a step above the site's
   closing line: it is the transition into the project, not a summary. */
.case-tf .tf-question {
  font-size: clamp(1.4rem, 1.2rem + 0.85vw, 1.9rem);
  /* the full measure of the column, not a narrow pull-quote */
  max-width: none;
  margin-top: var(--space-md);
}

/* --------------------------------------------------------------------------
   The data flow. Type under hairlines, converging. No boxes: an outline
   around each source would turn three notes into a system diagram.
   -------------------------------------------------------------------------- */
.tf-flow { margin: var(--space-lg) 0; }

.tf-sources {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-md);
}

.tf-sources li {
  border-top: 1px solid var(--cobalt-32);
  padding-top: var(--space-xs);
}

.tf-src-k {
  display: block;
  font-family: var(--font-data);
  font-size: var(--text-label);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--cobalt);
  margin-bottom: 0.35rem;
}

.tf-src-d {
  display: block;
  font-size: var(--text-small);
  line-height: var(--leading-tight);
  color: var(--color-text-muted);
}

/* Fixed height, non-uniform scale: the bracket is a connector, so it should
   not grow taller as the column widens. Only horizontals and verticals stay
   true under that stretch, which is why it is a bracket and not three curves. */
.tf-converge {
  display: block;
  width: 100%;
  height: 56px;
}

.tf-drop {
  display: block;
  width: 12px;
  height: 36px;
  margin-inline: auto;
}

.tf-stage { text-align: center; }

.tf-stage-k {
  display: block;
  font-family: var(--font-display);
  font-size: var(--text-title);
  font-weight: 600;
  letter-spacing: var(--track-item);
}

.tf-stage-d {
  display: block;
  margin-top: 0.2rem;
  font-family: var(--font-data);
  font-size: var(--text-xs);
  color: var(--color-text-faint);
}

.tf-stage-out .tf-stage-k { color: var(--cobalt-deep); }

/* Stacked, the three sources no longer sit side by side, so the bracket has
   nothing to gather — only the stem into Python is left. */
@media screen and (max-width: 620px) {
  .tf-sources { grid-template-columns: 1fr; gap: var(--space-sm); }
  .tf-converge { height: 24px; }
  .tf-bracket { display: none; }
}

/* --------------------------------------------------------------------------
   Rebasing, at postage-stamp size. Two sparklines and an arrow: the point is
   that the second pair starts from the same place, which needs no axis. The
   lines take fund colours, never the accent — red here would read as a loss.
   -------------------------------------------------------------------------- */
.tf-rebase {
  margin: var(--space-md) 0 0;
  display: flex;
  align-items: flex-start;
  /* room for the arrow, drawn in the gap as .tf-states draws its own */
  gap: var(--space-md);
  max-width: 30rem;
}

.tf-rebase-panel { flex: 1 1 0; min-width: 0; }

.tf-rebase-fig { position: relative; }

.tf-rebase-fig svg {
  display: block;
  width: 100%;
  height: auto;
  background: var(--tf-paper);
  border-radius: var(--tf-radius);
}

/* Named above the paper in the same mono capitals as the tooltip strip and
   the chart states below. */
.tf-rebase-k {
  display: block;
  font-family: var(--font-data);
  font-size: var(--text-label);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--cobalt);
  margin-bottom: 0.4rem;
}

/* The step between the two, hung off the second sparkline so it sits level
   with the pictures rather than the labels. */
.tf-rebase-panel + .tf-rebase-panel .tf-rebase-fig::before {
  content: "\2192";
  position: absolute;
  top: 50%;
  left: calc(var(--space-md) / -2);
  transform: translate(-50%, -50%);
  font-size: var(--text-body);
  line-height: 1;
  color: var(--color-mark);
}

/* --------------------------------------------------------------------------
   The six funds as a table: ticker, sector, and why each earned a line on the
   chart. Headed in the same cobalt mono capitals as the figure labels, ruled
   in the article's own hairline, and never boxed. Each ticker carries the
   round chip the article's filter buttons use, in that fund's line colour;
   SPY's grey is the system's own decision — the benchmark is deliberately
   neutral, and the accent is spent on loss and hover rather than identity.
   -------------------------------------------------------------------------- */
.tf-funds-wrap {
  margin: var(--space-md) 0 var(--space-lg);
  overflow-x: auto;
}

.tf-funds {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-small);
  line-height: var(--leading-tight);
}

.tf-funds th,
.tf-funds td {
  text-align: left;
  vertical-align: top;
  padding: 0.7rem 1.25rem 0.7rem 0;
  border-bottom: 1px solid var(--tf-rule);
}

.tf-funds th:last-child,
.tf-funds td:last-child { padding-right: 0; }

/* On a phone the gutters give their width back to the relevance column. */
@media screen and (max-width: 620px) {
  .tf-funds th,
  .tf-funds td { padding-right: 0.75rem; }
}

/* The narrowest phones: a size down and tighter, so the table fits its
   column instead of scrolling. The header may wrap here. */
@media screen and (max-width: 360px) {
  .tf-funds { font-size: var(--text-xs); }
  .tf-funds th,
  .tf-funds td { padding-right: 0.5rem; }
  .tf-funds thead th { white-space: normal; letter-spacing: 0.04em; }
  /* "Semiconductors", "manufacturing": long single words break with a
     hyphen rather than hold a column open */
  .tf-funds td { hyphens: auto; }
}

.tf-funds thead th {
  padding-top: 0;
  padding-bottom: 0.4rem;
  font-family: var(--font-data);
  font-size: var(--text-label);
  font-weight: 400;
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--cobalt);
  white-space: nowrap;
}

.tf-funds tbody th { font-weight: 400; white-space: nowrap; }
.tf-funds td { color: var(--color-text); }
.tf-funds td:last-child { color: var(--color-text-muted); }

.tf-tk {
  display: flex;
  align-items: center;
  gap: 6px;
  font-family: var(--font-data);
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--color-text);
}

.tf-tk i {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: rgb(var(--line));
  flex: 0 0 auto;
}

/* --------------------------------------------------------------------------
   The tooltip. One shell, used by the glossary crop and the specimen plate,
   exactly as the article uses one shell for its map, scatter and fund
   tooltips. Content order is fixed: title, meta, row, note.
   -------------------------------------------------------------------------- */
.tf-tip {
  background: var(--tf-tip-bg);
  border: 1px solid var(--tf-rule);
  border-radius: var(--tf-radius);
  padding: 8px 11px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
  font-family: var(--font-body);
  font-size: 0.8125rem;
  line-height: 1.5;
  color: var(--tf-ink);
  max-width: 280px;
}

.tf-tip-title { display: block; font-weight: 600; color: var(--tf-ink); }
.tf-tip-meta  { display: block; color: var(--tf-muted); }
.tf-tip-row   { display: block; color: var(--tf-ink-soft); }
.tf-tip-row b { font-weight: 600; }

/* The sign carries the meaning; colour only reinforces it. */
.case-tf .is-down { color: var(--tf-red); }
.case-tf .is-up   { color: var(--tf-gain); }

/* --------------------------------------------------------------------------
   Glossary. A crop of the article: one sentence in its serif at the body
   weight it actually uses, the term dotted in currentColor — not in the
   accent, which is reserved for hover — and the card shown open, since a
   case study cannot hover.
   -------------------------------------------------------------------------- */
/* Sized to its own content rather than to the column: the sentence runs to
   about 30 characters, so a full-measure ground left most of the paper empty
   and the crop read as a banner. It is a detail lifted out of the article,
   and it is set at the size of one. */
.tf-gloss {
  margin: var(--space-md) 0 var(--space-lg);
  width: min(30rem, 100%);
  background: var(--tf-paper);
  border-radius: var(--tf-radius);
  padding: 1.25rem;
}

.tf-gloss-line {
  font-family: var(--tf-serif);
  font-size: clamp(1.05rem, 0.98rem + 0.32vw, 1.3rem);
  font-weight: 500;
  line-height: 1.6;
  color: var(--tf-ink);
  margin: 0;
  max-width: 30ch;
}

.tf-gloss-term {
  border-bottom: 1px dotted currentColor;
  cursor: help;
}

/* .term-tip is one step narrower than the chart shell. */
.tf-gloss-card {
  margin-top: 0.7rem;
  margin-left: clamp(0rem, 5vw, 3rem);
  max-width: 230px;
}

/* How much the tooltip carries at each stop. The article's own tooltip at
   three points in it, on the same paper the glossary crop and the chart
   stills stand on — so this reads as the thing itself rather than a list of
   the fields it holds. Labelled above the paper and stepped through with
   arrows, as .tf-states names its own. */
.tf-progress {
  margin: var(--space-md) 0;
  display: flex;
  align-items: stretch;
  gap: var(--space-sm);
}

.tf-prog-step {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

/* Stretched, so the three grounds match however many rows each tooltip has. */
.tf-prog-fig {
  flex: 1;
  background: var(--tf-paper);
  border-radius: var(--tf-radius);
  padding: var(--space-sm);
}

/* In the strip it fills its ground; the 280px shell width is the article's
   own limit and has no bearing here. */
.tf-prog-fig .tf-tip {
  max-width: none;
}

.tf-prog-k {
  display: block;
  font-family: var(--font-data);
  font-size: var(--text-label);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--cobalt);
  margin-bottom: 0.4rem;
}

.tf-prog-step ul { list-style: none; margin: 0; padding: 0; }

.tf-prog-step li {
  font-family: var(--font-body);
  font-size: var(--text-small);
  line-height: 1.5;
  color: var(--color-text-muted);
}

.tf-prog-arrow {
  flex: 0 0 auto;
  align-self: center;
  color: var(--color-mark);
  font-size: var(--text-body);
}

@media screen and (max-width: 620px) {
  .tf-progress { flex-direction: column; gap: 0.75rem; }
  .tf-prog-arrow { display: none; }
}

/* --------------------------------------------------------------------------
   Three states of the fund chart, read left to right, drawn with the chart's
   own chrome: a #c4c0b9 baseline and April 2 marker, per-fund line colours,
   a dot at every point, and the ticker set at the line end the way the
   article labels its series. Each one is named above its ground, as the
   tooltip strip above it is.
   -------------------------------------------------------------------------- */
.tf-states {
  list-style: none;
  margin: var(--space-md) 0 var(--space-lg);
  padding: 0;
  display: flex;
  align-items: stretch;
  /* room for an arrow to sit in, between one state and the next */
  gap: var(--space-md);
}

/* Column flex so the three grounds match: the last state carries a row of
   controls the other two don't, and the stills stretch to meet it. */
.tf-states li {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

.tf-states .tf-state-fig { flex: 1; }

/* What the chart is showing at that point, set above its ground in the same
   mono capitals the tooltip strip labels its own with. Two lines of room are
   held whether or not they are used, so a label that wraps in a narrow column
   cannot leave the three grounds starting at different heights. */
.tf-state-n {
  display: block;
  font-family: var(--font-data);
  font-size: var(--text-label);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  line-height: var(--leading-tight);
  min-height: calc(2 * var(--leading-tight) * 1em);
  /* Set to the bottom of that room, so a one-line label sits as far off its
     ground as the tooltip strip's labels do. Those run at body leading; this
     runs tight, so the half-leading it gives up is added back underneath. */
  display: flex;
  align-items: flex-end;
  color: var(--cobalt);
  margin-bottom: calc(0.4rem + (var(--leading-body) - var(--leading-tight)) * 0.5em);
}

/* The step from one state to the next, drawn in the gap rather than added to
   the markup: it is punctuation, not a fourth item in the list. Hung off the
   ground so it sits level with the charts and not with the labels above. */
.tf-state-fig { position: relative; }

.tf-states li + li .tf-state-fig::before {
  content: "\2192";
  position: absolute;
  top: 50%;
  left: calc(var(--space-md) / -2);
  transform: translate(-50%, -50%);
  font-size: var(--text-body);
  line-height: 1;
  color: var(--color-mark);
}

.tf-state-fig {
  background: var(--tf-paper);
  border-radius: var(--tf-radius);
  padding: 0.5rem 0.55rem 0.45rem;
}

.tf-state-fig svg {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
}

/* Sans for every instrument label, per the article's first rule. */
.tf-ax,
.tf-mark,
.tf-end {
  font-family: var(--font-body);
}

.tf-ax   { font-size: 7px; fill: var(--tf-ink-soft); }
.tf-mark { font-size: 7px; fill: #555; }
.tf-end  { font-size: 7px; font-weight: 600; }

/* The article's filter pill, shrunk: border and chip in the fund's colour. */
.tf-state-controls {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
  margin-top: 0.5rem;
}

.tf-state-controls span {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  font-family: var(--font-body);
  font-size: 0.5625rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--tf-ink);
  border: 1px solid rgb(var(--line));
  background: rgba(var(--line), 0.1);
  border-radius: var(--tf-radius);
  padding: 0.1rem 0.28rem;
}

.tf-state-controls i {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: rgb(var(--line));
}

/* Inactive is the same chip dimmed, never a different colour — the identity
   stays visible while the line is off. */
.tf-state-controls .is-off {
  border-color: var(--tf-btn);
  background: transparent;
  color: var(--tf-muted);
}

.tf-state-controls .is-off i { opacity: 0.3; }

.tf-state-c {
  display: block;
  margin-top: 0.5rem;
  font-size: var(--text-xs);
  line-height: var(--leading-tight);
  color: var(--color-text-faint);
}

/* Stacked, there is no gap alongside for an arrow to stand in. */
@media screen and (max-width: 700px) {
  .tf-states {
    flex-direction: column;
    gap: var(--space-sm);
  }

  .tf-states li + li .tf-state-fig::before { content: none; }
}

/* The section's closing line, given the full measure. */
.case-tf .tf-guide {
  font-size: clamp(1.4rem, 1.2rem + 0.85vw, 1.9rem);
  max-width: none;
}

/* --------------------------------------------------------------------------
   Specimen plate. The article's elements on the article's paper, laid out as
   one plate rather than as five more cards.
   -------------------------------------------------------------------------- */
.tf-board {
  margin: var(--space-md) 0 var(--space-lg);
  background: var(--tf-paper);
  border-radius: var(--radius-md);
  padding: var(--space-md);
  color: var(--tf-ink);
}

/* One band per system: named on the left, drawn on the right. The hairline
   between bands is the article's own 1px #ddd9d3 rule, so the plate is set in
   the language it documents. */
.tf-spec {
  display: grid;
  grid-template-columns: 7rem minmax(0, 1fr);
  gap: var(--space-md);
  align-items: start;
  padding: var(--space-md) 0;
  border-top: 1px solid var(--tf-rule);
}

.tf-spec:first-child {
  padding-top: 0;
  border-top: 0;
}

.tf-spec:last-child { padding-bottom: 0; }

/* The article's own figure-title mark: sans, uppercase from CSS, 0.08em. */
.tf-spec-k {
  display: block;
  font-family: var(--font-body);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--tf-muted);
}

/* The value under each specimen, so the plate is the reference itself. */
.tf-spec-v {
  display: block;
  margin-top: 0.3rem;
  font-family: var(--font-data);
  font-size: var(--text-xs);
  letter-spacing: 0.02em;
  color: var(--tf-muted);
}

.tf-spec-type {
  display: grid;
  grid-template-columns: 1.3fr 1fr;
  gap: var(--space-md);
}

/* Both columns run the full height of the band and drop their caption to the
   floor, so the two names sit on one line however tall the specimen above
   each of them happens to be. */
.tf-spec-type > div {
  display: flex;
  flex-direction: column;
}

.tf-spec-type > div > .tf-spec-v {
  margin-top: auto;
  /* auto eats the shared top margin, so the gap is held here instead */
  padding-top: 0.5rem;
}

.tf-serif-lg {
  font-family: var(--tf-serif);
  font-size: clamp(1.5rem, 1.2rem + 1.1vw, 2rem);
  font-weight: 500;
  line-height: 1.05;
  margin: 0;
  text-wrap: balance;
}

.tf-serif-desc {
  font-family: var(--tf-serif);
  font-style: italic;
  font-weight: 500;
  font-size: 1.05rem;
  line-height: 1.35;
  color: var(--tf-red);
  margin: 0.45rem 0 0;
  max-width: 34ch;
}

/* .fig-title in the article: sans, 0.75rem, 600, uppercase, 0.08em, muted. */
.tf-fig-title {
  display: block;
  font-family: var(--font-body);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--tf-muted);
  margin-bottom: 0.6rem;
}

.tf-axis-spec {
  display: flex;
  justify-content: space-between;
  font-family: var(--font-body);
  font-size: 0.75rem;
  color: var(--tf-ink-soft);
  border-top: 1px solid var(--tf-axis);
  padding-top: 0.35rem;
  max-width: 12rem;
}


/* Every band reads the same way: the specimen, then its name and value. */
.tf-swatches,
.tf-series,
.tf-uikit {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm) var(--space-md);
  font-family: var(--font-body);
  font-size: var(--text-xs);
  color: var(--tf-ink);
}

/* The series colours are swatches like any other: a line colour is a token,
   and it is shown at the same size as the ones above it. */
.tf-swatches li,
.tf-series li {
  flex: 0 1 auto;
  min-width: 5.5rem;
}

.tf-swatches i,
.tf-series i {
  display: block;
  width: 100%;
  height: 2rem;
  border-radius: var(--tf-radius);
  margin-bottom: 0.35rem;
}

/* --------------------------------------------------------------------------
   Data UI. The patterns as the article draws them, not their CSS written out.
   Every value below is copied from the project's own rules.
   -------------------------------------------------------------------------- */
.tf-uikit li {
  flex: 0 1 auto;
  min-width: 9rem;
}

/* A common floor, so three specimens of different heights sit on one line. */
.tf-ui-stage {
  display: flex;
  align-items: center;
  min-height: 3.25rem;
  margin-bottom: 0.15rem;
}

/* .term: a dotted underline in the running colour, on the serif it appears in. */
.tf-ui-term {
  font-family: var(--tf-serif);
  font-size: 1.05rem;
  font-weight: 500;
  color: var(--tf-ink);
  border-bottom: 1px dotted currentColor;
  cursor: help;
}

/* .stock-button.active: the fund filter switched on — border in the series
   colour, that colour at 0.22 behind it, and the round chip at full strength. */
.tf-ui-filter {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 30px;
  padding: 4px 10px;
  border: 1px solid rgb(var(--line));
  border-radius: var(--tf-radius);
  background: rgba(var(--line), 0.22);
  font-family: var(--font-body);
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--tf-ink);
}

.tf-ui-filter::before {
  content: "";
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: rgb(var(--line));
}

/* Narrow: the name returns above the band it labels. */
@media screen and (max-width: 700px) {
  .tf-spec {
    display: block;
  }

  .tf-spec-body {
    margin-top: var(--space-sm);
  }

  .tf-spec-type {
    display: block;
  }

  .tf-spec-type > div + div {
    margin-top: var(--space-md);
  }
}
