/* =========================================================================
   Palettes + the theme switch.
   Loaded AFTER each page's own <style> so these tokens win over the
   hardcoded values still sitting in the case-study stylesheets.
   ========================================================================= */

/* ---- the four palettes · corners of the mark, in click order ---- */
:root,
[data-theme="plum"] { --paper:#3a1326; --hi:#b2d2ff; --base:#a8bbd6; --slot:#462d39; }
[data-theme="green"]{ --paper:#514d31; --hi:#dcd3ba; --base:#ded0a4; --slot:#46442d; }
/* cream is the one light theme: hi/base darkened to clear AA on a pale ground,
   and --muted mixes toward the INK rather than the paper (see below). */
[data-theme="cream"]{ --paper:#e9e3d9; --hi:#ac4708; --base:#97552c; --slot:#46302d;
                      --muted-mix:#3a2a1c; }
[data-theme="teal"] { --paper:#122f36; --hi:#dcc6d7; --base:#d2aaca; --slot:#2d4246; }

/* ---- roles the case studies use, derived so every palette gets them free ---- */
:root{
  --ink:    var(--hi);
  --body:   var(--base);
  --accent: var(--hi);
  /* On dark themes "muted" means mixing toward the ground. On a light ground
     that washes text out instead, so --muted-mix lets cream pull the other way. */
  --muted:  color-mix(in srgb, var(--base) 68%, var(--muted-mix, var(--paper)));
  --line:   color-mix(in srgb, var(--base) 22%, transparent);
  --card:   color-mix(in srgb, var(--base)  6%, transparent);
}

/* The root carries the ground too, not just <body>. Without it the canvas is
   the browser default until body's styles resolve, which shows as a white
   flash between pages. theme.js sets data-theme on <html> during head parse,
   so the right palette is there from the very first painted frame. */
html{ background:var(--paper); }
body{ background:var(--paper); }

/* ---- text under an asset, one rule for every case study -----------------
   A LABEL names the asset: label font, all caps, base color.
   A DESCRIPTION says something about it: label font, sentence case, italic.
   Scoped to .shell article so the home page and prototypes are untouched. */
.shell article figcaption{
  font-family:var(--mono); font-style:italic; text-transform:none; color:var(--body); }
/* the Role / Timeline / Team labels and the contents label: base color, not the dimmest */
.shell article .meta .k, .shell .toc .lbl{ color:var(--body); }
/* every other label in a case study: section labels, result and stat labels, quote credits */
.shell article .band, .shell article .act,
.shell article .result .k, .shell article .result .l, .shell article .statgrid .l,
.shell article blockquote cite{ color:var(--body); }
.shell article .t,
.shell article figcaption.label{
  font-family:var(--mono); font-size:11px; font-weight:500; letter-spacing:.1em;
  text-transform:uppercase; font-style:normal; color:var(--body); text-align:center; }
.ph{ background:var(--slot); border:1px dashed var(--line); color:var(--muted); }

/* ---- the switch · pinned bottom-left, outside the page margins ----
   Four outlined cells; one filled square translates between the corners,
   so the fill genuinely moves rather than cross-fading. */
.mark{
  position:fixed; left:24px; bottom:24px; z-index:60;
  width:16px; height:16px; padding:0; border:0; background:none;
  cursor:pointer; -webkit-tap-highlight-color:transparent;
}
.mark i{
  position:absolute; width:7px; height:7px;
  border:1px solid var(--base); transition:border-color .45s ease;
}
.mark i:nth-child(1){ left:0;  top:0 }
.mark i:nth-child(2){ right:0; top:0 }
.mark i:nth-child(3){ left:0;  bottom:0 }
.mark i:nth-child(4){ right:0; bottom:0 }
.mark .fill{
  position:absolute; left:0; top:0; width:7px; height:7px;
  background:var(--hi);
  transition:transform .38s cubic-bezier(.65,.05,.25,1), background-color .45s ease;
}
[data-theme="green"] .mark .fill{ transform:translate(9px,0) }
[data-theme="cream"] .mark .fill{ transform:translate(0,9px) }
[data-theme="teal"]  .mark .fill{ transform:translate(9px,9px) }
.mark:hover i{ border-color:var(--hi) }
/* invisible margin around the icon: getting close is enough to open the
   label (and it makes a 44px touch target out of a 16px mark) */
.mark::before{ content:""; position:absolute; inset:-14px }
/* on hover the switch names itself, in the palette it would change: the box
   wipes open from the left (theme.js adds .tip-on), and closes the same way. */
.mark .mark-tip{
  position:absolute; left:26px; top:50%; transform:translateY(-50%);
  padding:6px 10px; border-radius:0; white-space:nowrap; pointer-events:none;
  font-family:var(--mono, "JetBrains Mono", ui-monospace, monospace);
  font-size:11px; font-weight:500; letter-spacing:.12em; line-height:1; text-align:left;
  color:var(--paper); background:var(--hi);
  clip-path:inset(0 100% 0 0);
  transition:clip-path .16s ease, background-color .45s ease, color .45s ease;
}
.mark.tip-on .mark-tip{ clip-path:inset(0 0 0 0);
  transition:clip-path .16s cubic-bezier(.2,.7,.2,1), background-color .45s ease, color .45s ease }
@media(hover:none){ .mark .mark-tip{ display:none } }
@media(prefers-reduced-motion:reduce){ .mark .mark-tip, .mark.tip-on .mark-tip{ transition:none } }
.mark:focus-visible{ outline:2px solid var(--hi); outline-offset:6px }

/* ---- cross-fade the ground and type on change ---- */
html, body, nav, .site-nav, .slot, .ph, .etym hr, .rule-v,
.word, h1, h2, h3, p, .lede, .eyebrow, .byline, .role,
.toc a, .toc .lbl, figcaption, .contact a, .cta, .copy,
table.metrics td, table.metrics .m, .callout{
  transition:background-color .45s ease, color .45s ease, border-color .45s ease;
}

/* Links carry a fast colour transition so hover feels responsive (.18–.2s),
   but that made them beat the .45s ground to the new palette. While the theme
   is actually changing, theme.js adds .theming and they move in step instead. */
html.theming .site-nav a,
html.theming .foot a, html.theming .contact a, html.theming .etym .ext,
html.theming .shell article .ext, html.theming .shell article .jump,
html.theming .mobile-menu-toggle, html.theming .mobile-menu-close{
  transition:color .45s ease;
}

@media(prefers-reduced-motion:reduce){
  .mark .fill{ transition:none }
}

/* =========================================================================
   Site nav — one component, shared by every page.
   Each page supplies its own container width (nav .shell / .site-nav .nav-in);
   everything below is common.
   ========================================================================= */
.site-nav{ position:sticky; top:0; z-index:40; background:var(--paper); }
.site-nav ul{
  list-style:none; width:100%; display:flex;
  justify-content:space-between; align-items:center; line-height:1.3;
}
.site-nav li{ display:flex }

.site-nav a{
  position:relative; font-family:var(--mono); font-weight:500;
  font-size:var(--fs-16); line-height:1.3; color:var(--base);
  text-decoration:none; transition:color .2s;
}
/* absolute rule, so hovering can't change the nav's height. It draws in from
   the left in step with the typing: theme.js adds .typed as the first letter
   goes and sets --ul-dur to the word's typing time, so line and letters land
   together. Leaving retracts it quickly. */
.site-nav a::after{
  content:""; position:absolute; left:0; right:0; bottom:-4px; height:1px;
  background:currentColor; transform:scaleX(0); transform-origin:left center;
  transition:transform .18s cubic-bezier(.4,0,.2,1);
}
.site-nav a:hover, .site-nav a:focus-visible{ color:var(--hi) }
.site-nav a.typed::after{ transform:scaleX(1);
  transition:transform var(--ul-dur, .26s) linear }
/* the typed copy sits over a hidden copy, so the link never changes width */
.site-nav a .nsz{ visibility:hidden }
.site-nav a .ntype{ position:absolute; left:0; top:0; white-space:nowrap }
@media(prefers-reduced-motion:reduce){
  .site-nav a::after{ transition:none }
}

/* the wordmark is the fifth member of the same space-between row, so its
   slot is always reserved and revealing it never moves the links */
.site-nav a.brand{
  position:relative; display:inline-block;
  font-family:var(--display); font-weight:700; font-size:var(--fs-20);
  line-height:1.3; color:var(--hi);
}
.site-nav a.brand::after{ content:none }        /* no hover rule on the wordmark */
.site-nav a.brand:hover, .site-nav a.brand:focus-visible{ color:var(--hi) }
.site-nav a.brand .sizer{ visibility:hidden }   /* holds the link at "pannisy" width */
/* typed text is absolute, so " zhao" spills into the gap instead of
   widening the link and redistributing every other nav item */
.site-nav a.brand .type{ position:absolute; left:0; top:0; white-space:nowrap }

/* home only: the wordmark fades in once the headword scrolls under the nav */
.site-nav.reveal a.brand{
  opacity:0; transform:translateY(-4px); pointer-events:none;
  transition:opacity .28s ease, transform .28s ease, color .45s ease;
}
.site-nav.reveal.show-brand a.brand{ opacity:1; transform:none; pointer-events:auto }

@media(prefers-reduced-motion:reduce){
  .site-nav a.brand{ transform:none }
}

/* Compact navigation is shared by home, about, and every case study. */
.mobile-menu-toggle,.mobile-menu-close{display:none}
.mobile-site-menu{display:none}
@media(max-width:760px), (max-aspect-ratio:4/5){
  .site-nav .mark{position:relative;top:auto;right:auto;bottom:auto;left:auto;z-index:auto;flex:none;margin-left:auto}
  /* In the nav, reveal the label toward the left so it never crosses MENU. */
  .site-nav .mark .mark-tip{left:auto;right:26px;clip-path:inset(0 0 0 100%)}
  .site-nav .mark.tip-on .mark-tip{clip-path:inset(0 0 0 0)}
  .site-nav > .shell,.site-nav > .nav-in{display:flex;align-items:center;justify-content:space-between;gap:20px}
  .site-nav ul{width:auto;flex:1;flex-wrap:nowrap;justify-content:flex-start}
  .site-nav li:not(:first-child){display:none}
  .site-nav a.brand,.mobile-menu-brand{font-size:20px}
  .mobile-menu-toggle,.mobile-menu-close{
    position:relative;display:inline-block;flex:none;min-width:4ch;min-height:44px;padding:11px 0;
    border:0;background:none;color:var(--base);cursor:pointer;
    font:500 16px/1.3 var(--mono);text-align:left;text-decoration:none;
    -webkit-tap-highlight-color:transparent;
  }
  .mobile-menu-toggle::after,.mobile-menu-close::after{
    content:"";position:absolute;left:0;right:0;bottom:5px;height:1px;background:currentColor;
    transform:scaleX(0);transform-origin:left center;transition:transform .18s cubic-bezier(.4,0,.2,1);
  }
  .mobile-menu-toggle:hover,.mobile-menu-toggle:focus-visible{color:var(--hi)}
  .mobile-menu-close{color:var(--paper)}
  .mobile-menu-close:hover,.mobile-menu-close:focus-visible{color:var(--paper);opacity:.7}
  .mobile-menu-toggle.typed::after,.mobile-menu-close.typed::after{
    transform:scaleX(1);transition:transform var(--ul-dur,.26s) linear;
  }
  .mobile-menu-toggle .nsz,.mobile-menu-close .nsz{visibility:hidden}
  .mobile-menu-toggle .ntype,.mobile-menu-close .ntype{position:absolute;left:0;top:11px;white-space:nowrap}
  .mobile-site-menu{
    position:fixed;inset:0;z-index:80;display:flex;flex-direction:column;
    min-height:100vh;min-height:100dvh;padding:20px var(--pad,24px) max(24px,env(safe-area-inset-bottom));
    overflow-y:auto;background:var(--hi);color:var(--paper);
    clip-path:inset(0 0 100% 0);visibility:hidden;pointer-events:none;
    transition:clip-path .48s cubic-bezier(.76,0,.24,1),visibility 0s linear .48s;
  }
  .mobile-site-menu.is-open{clip-path:inset(0 0 0 0);visibility:visible;pointer-events:auto;transition:clip-path .48s cubic-bezier(.76,0,.24,1),visibility 0s}
  html.menu-arriving .mobile-site-menu{transition:none}
  .mobile-menu-top{display:flex;align-items:center;justify-content:space-between;gap:20px;min-height:36px}
  .mobile-menu-brand{font:700 20px/1.3 var(--display);color:var(--paper);text-decoration:none}
  .mobile-menu-links{display:flex;flex-direction:column;align-items:flex-start;gap:8px;margin:clamp(62px,12vh,120px) 0 40px}
  .mobile-menu-links a{max-width:100%;font:700 clamp(42px,11vw,80px)/1.08 var(--display);letter-spacing:-.05em;color:var(--paper);text-decoration:none;overflow-wrap:anywhere;transition:opacity .2s,transform .2s}
  .mobile-menu-links a:hover,.mobile-menu-links a:focus-visible{opacity:.7;transform:translateX(6px)}
  .mobile-menu-contact{display:flex;flex-direction:column;align-items:flex-start;gap:8px;margin-top:auto;padding-top:28px}
  .mobile-menu-contact a{position:relative;display:inline-flex;align-items:center;gap:12px;padding-bottom:4px;color:var(--paper);text-decoration:none;font:300 16px/1.4 var(--mono);transition:opacity .18s ease}
  .mobile-menu-contact a:hover,.mobile-menu-contact a:focus-visible{opacity:.7}
  .mobile-menu-contact a::after{content:"";position:absolute;left:0;right:0;bottom:-3px;height:1px;background:currentColor;transform:scaleX(0);transform-origin:left center;transition:transform .2s cubic-bezier(.4,0,.2,1)}
  .mobile-menu-contact a:hover::after,.mobile-menu-contact a:focus-visible::after{transform:scaleX(1)}
  .mobile-menu-contact .ico{width:20px;height:20px;flex:none;color:inherit}
  html.mobile-menu-open,body.mobile-menu-open{overflow:hidden}
}
html.menu-arriving::after{content:"";position:fixed;inset:0;z-index:79;background:var(--hi);pointer-events:none}
@media(max-width:380px){.mobile-site-menu{padding-inline:20px}.mobile-menu-links a{font-size:40px}}
@media(max-width:760px) and (max-height:650px){
  .mobile-menu-links{margin:36px 0 24px;gap:4px}
  .mobile-menu-links a{font-size:clamp(36px,8vh,52px)}
  .mobile-menu-contact{gap:6px}
}
@media(max-width:700px){
  .shell article .foot a{max-width:100%}
  .shell article figure img,.shell article figure video{max-width:100%}
  .shell article table.metrics td{overflow-wrap:anywhere}
}
@media(prefers-reduced-motion:reduce){
  .mobile-site-menu,.mobile-site-menu.is-open,.mobile-menu-toggle::after,.mobile-menu-close::after,.mobile-menu-links a,.mobile-menu-contact a::after,.case-image-close::after{transition:none}
}

/* Keep case-study prose comfortable on phones while leaving desktop intact. */
@media(max-width:600px){
  :root{--pad:16px}
  .shell article h1{font-size:clamp(29px,8vw,34px);line-height:1.12}
  .shell article h2{font-size:clamp(25px,7vw,29px);line-height:1.18}
  .shell article .lede{font-size:20px;line-height:1.45}
  .shell article p,.shell article ul.clean li{font-size:17px;line-height:1.55}
  .shell article .meta .v{font-size:15px;line-height:1.5}
  .shell article table.metrics td{font-size:16px}
  .shell article figure.chart{overflow-x:auto;overscroll-behavior-inline:contain;-webkit-overflow-scrolling:touch}
  .shell article figure.chart::after{content:"swipe →";position:sticky;left:0;display:block;margin-top:8px;font:500 11px var(--mono);letter-spacing:.04em;color:var(--body)}
  .shell article figure.chart > svg{display:block;width:720px!important;max-width:none!important;height:auto}
  /* above the image, centred: below it the hint read as part of the caption */
  .shell article figure.mobile-zoomable::before{content:"tap image to enlarge";display:block;margin-bottom:8px;text-align:center;font:500 11px var(--mono);letter-spacing:.04em;color:var(--body)}
}

.shell article figure.mobile-zoomable img,.shell article figure.mobile-zoomable video{cursor:zoom-in}

.case-image-viewer{box-sizing:border-box;position:fixed;inset:0;width:100vw;max-width:none;height:100dvh;max-height:none;margin:0;padding:56px 0;border:0;border-radius:0;background:var(--paper);color:var(--base);overflow:auto}
/* scoped to [open]: an unscoped display would beat the UA's
   dialog:not([open]){display:none} and leave the viewer painted after close */
.case-image-viewer[open]{display:flex}
.case-image-viewer::backdrop{background:rgba(0,0,0,.72);backdrop-filter:blur(6px)}
/* margin:auto centres in both axes and, unlike align-items:center, still lets a
   taller-than-viewport image scroll to its top edge */
.case-image-viewer img,.case-image-viewer video{display:block;flex:none;width:100%;max-width:none;max-height:none;height:auto;margin:auto}
/* theme.js pins the element to the clip's own ratio; object-fit keeps the
   frame flush inside it if the two ever disagree, so the native controls can
   never sit against empty letterbox instead of the video. */
.case-image-viewer video{object-fit:contain;background:#000}
/* Matches the mobile menu's CLOSE, minus the typing signature: the underline
   draws on hover instead of keystroke-by-keystroke. */
.case-image-close{position:fixed;top:10px;right:16px;z-index:1;padding:11px 0;min-height:44px;
  border:0;background:none;color:var(--base);cursor:pointer;
  font:500 16px/1.3 var(--mono);text-align:left;-webkit-tap-highlight-color:transparent}
.case-image-close::after{content:"";position:absolute;left:0;right:0;bottom:5px;height:1px;
  background:currentColor;transform:scaleX(0);transform-origin:left center;
  transition:transform .18s cubic-bezier(.4,0,.2,1)}
.case-image-close:hover,.case-image-close:focus-visible{color:var(--hi)}
.case-image-close:hover::after,.case-image-close:focus-visible::after{transform:scaleX(1)}

/* =========================================================================
   Ordinary links — one hover for all of them: the colour shifts and an
   underline draws in from the left, the same gesture as the nav tabs.
   Typing stays a nav-only signature, and the outline rail keeps its own
   hover (colour + rails), so nothing competes with it.
   ========================================================================= */
.foot a, .contact a, .etym .ext, .shell article .ext, .shell article .jump{
  position:relative; text-decoration:none; transition:color .18s ease;
}
.foot a::after, .contact a::after, .etym .ext::after, .shell article .ext::after,
.shell article .jump::before{
  content:""; position:absolute; left:0; right:0; bottom:-3px; height:1px;
  background:currentColor; opacity:1;
  transform:scaleX(0); transform-origin:left center;
  transition:transform .2s cubic-bezier(.4,0,.2,1);
}
.foot a:hover::after, .foot a:focus-visible::after,
.contact a:hover::after, .contact a:focus-visible::after,
.etym .ext:hover::after, .etym .ext:focus-visible::after,
.shell article .ext:hover::after, .shell article .ext:focus-visible::after,
.shell article .jump:hover::before, .shell article .jump:focus-visible::before{
  transform:scaleX(1);
}
.foot a:hover, .foot a:focus-visible,
.etym .ext:hover, .etym .ext:focus-visible,
.shell article .ext:hover, .shell article .ext:focus-visible,
.shell article .jump:hover, .shell article .jump:focus-visible{ color:var(--accent) }
/* The case-study pager is [key cap][title], so the shared underline would run
   under the cap as well. Draw it under the title instead, and start it from the
   side the arrow points, so the wipe travels the way the reader is going. */
.foot a::after{ content:none }
.foot a .ptitle{ position:relative }
.foot a .ptitle::after{
  content:""; position:absolute; left:0; right:0; bottom:-3px; height:1px;
  background:currentColor; transform:scaleX(0); transform-origin:left center;
  transition:transform .2s cubic-bezier(.4,0,.2,1);
}
.foot a.next .ptitle::after{ transform-origin:right center }
.foot a:hover .ptitle::after, .foot a:focus-visible .ptitle::after{ transform:scaleX(1) }
@media(prefers-reduced-motion:reduce){ .foot a .ptitle::after{ transition:none } }

/* The two ends of the sequence point back to the index rather than to another
   case study, so they carry the site mark instead of a chevron and set their
   label as an eyebrow — a different kind of destination, not a fifth project. */
.foot a.index .ptitle{
  font-family:var(--mono); font-weight:500; font-size:13px;
  letter-spacing:.12em; text-transform:uppercase;
}
.foot .cap svg{ display:block; width:15px; height:15px; fill:var(--base, var(--body)) }

/* the jump link carried a permanent rule and an arrow in ::after */
.shell article .jump{ border-bottom:0 }
@media(prefers-reduced-motion:reduce){
  .foot a::after, .contact a::after, .etym .ext::after, .shell article .ext::after,
.shell article .jump::before{
    transition:none;
  }
}

/* =========================================================================
   Case-study outline — collapsed to rails, expands on hover.
   The column stays 210px and every row keeps its height, so the dash sits
   exactly where its label will appear. Nothing reflows on hover.
   ========================================================================= */
/* The "Contents" label is taken out of the flow so it can't push the rails
   down — otherwise rail 1 can't line up with the first line of the article,
   and the rails would shift when the label appears on hover. */
.toc .lbl{
  position:absolute; top:56px; left:0; margin:0;   /* flush with the dashes */
  opacity:0; transition:opacity .2s ease;
}

/* no border-left indicator: colour alone marks the active rail */
.toc #toc a{ position:relative; border-left:0 }

/* COLLAPSED — the label leaves the flow, so every rail sits on the same
   22px pitch instead of inheriting a row height that varies with how long
   the label is and whether it wraps. */
.toc #toc a{ display:block; height:22px; padding-top:0; padding-bottom:0 }
/* The row height snaps between states, so whichever element is LEAVING is
   hidden instantly — otherwise you watch the rails slide apart before the
   labels arrive. Only the arriving element fades. */
.toc #toc a span{
  position:absolute; left:0; top:0; white-space:nowrap;
  opacity:0; pointer-events:none; transform:translateY(-4px);
  transition:opacity 0s;                      /* collapsing: gone at once */
}
/* each row's reveal starts a beat after the one above it; --i is set per link
   in theme.js, and the fallback keeps it working if that never runs */
.toc #toc a{ --d:calc(60ms + var(--i, 0) * 34ms) }
/* the dash: same width and same left edge for every rail; the active one
   is --hi, the rest --base */
/* --hi and --base are nearly the same brightness (1.00–1.27 depending on
   palette), so on a 2px mark they're indistinguishable. Inactive rails are
   mixed halfway to the ground, which separates them on every palette while
   staying visible. */
.toc #toc a::before{
  content:""; position:absolute; left:12px; top:10px;
  width:22px; height:2px; border-radius:1px;   /* matches the 22px theme mark */
  background:color-mix(in srgb, var(--base) 50%, var(--paper));
  transition:opacity .22s ease .04s, background-color .2s ease;
}
.toc #toc a.sub::before{ left:12px }
.toc #toc a.active::before{ background:var(--hi) }

/* While the pointer is on the outline, the row under it takes the highlight
   and the scroll-tracked row lets go — otherwise two rows read as "current"
   at once. :not(:hover) means hovering the active row keeps it highlighted
   rather than dimming the thing you're pointing at. */
.toc #toc:hover a.active:not(:hover){ color:var(--muted) }
.toc #toc:hover a.active:not(:hover)::before{
  background:color-mix(in srgb, var(--base) 50%, var(--paper));
}

/* EXPANDED — labels return to the flow and the list reads as before */
.toc:hover #toc a,
.toc:focus-within #toc a{ height:auto; padding-top:5px; padding-bottom:5px }
.toc:hover #toc a span,
.toc:focus-within #toc a span{
  position:static; white-space:normal; opacity:1; pointer-events:auto;
  transform:none;
  transition:opacity .24s ease var(--d), transform .24s ease var(--d);
}

/* hover (or keyboard focus) reveals the outline as-is */
/* "Contents" leads the cascade; the rows follow via --d above */
.toc:hover .lbl,
.toc:focus-within .lbl{ opacity:1; transition-delay:.04s }

.toc:hover #toc a::before,
.toc:focus-within #toc a::before{ opacity:0; transition:opacity 0s }

@media(prefers-reduced-motion:reduce){
  .toc .lbl,.toc #toc a span,.toc #toc a::before{ transition:none }
  .toc #toc a span{ transform:none }
}

/* Rail 1 lines up with the first line of the article.
   `top` matches the nav height so the column doesn't start 18px low before
   you've even scrolled (sticky engages immediately when the threshold sits
   below the element's natural position). */
.toc{ position:sticky; top:66px; align-self:start; height:max-content; padding-top:46px }

/* EXPANDED alignment — "Contents" is the first thing you read when the panel
   opens, so it lines up with the first line of the article; the section list
   drops below it. Collapsed, the label is hidden and rail 1 takes that line
   instead. The rails are already gone by the time this shift happens, so the
   move is never visible. */
.toc:hover, .toc:focus-within{ padding-top:86px }

/* Captured media in case studies — same framing as figure images, so light
   screenshots and recordings don't read as holes in a dark ground. */
figure video.shot{
  display:block; width:100%; height:auto;
}

/* Flow diagram — inline SVG so it recolours with the palette */
figure svg.flow{ display:block; width:100%; height:auto; overflow:visible }

/* Mascot strip — one gorilla per unlockable feature */
.mascots{
  display:grid; grid-template-columns:repeat(4, 1fr); gap:20px 12px; margin:0;
}
.mascots figure{ margin:0; display:flex; flex-direction:column; align-items:center; gap:6px }
.mascots img{ display:block; width:100%; height:auto; max-width:110px }
.mascots figcaption{
  margin:0; font-family:var(--mono); font-size:10.5px; letter-spacing:.06em;
  text-transform:uppercase; color:var(--muted); text-align:center;
}
@media(max-width:560px){ .mascots{ grid-template-columns:repeat(3, 1fr) } }

/* =========================================================================
   Page transitions — a short cross-fade between pages.
   The .pt class is added by theme.js, so without JS the body is never
   hidden and the site degrades to plain navigation.
   ========================================================================= */
/* Preferred path: the browser cross-fades snapshots of the two pages on the
   compositor. Smoother than animating opacity over a live subtree, and the
   click navigates immediately — no scripted delay before the fetch starts. */
@view-transition{ navigation:auto }
::view-transition-old(root),
::view-transition-new(root){ animation-duration:.14s; animation-timing-function:ease }
/* The nav and the mark deliberately have NO view-transition-name. Naming them
   would hold them still across a navigation, but it also creates a stacking
   context, which promotes the sticky nav to its own compositing layer. The
   page's ground comes from body's background propagated to the canvas, which
   repaints on the main thread — so a promoted nav animates its palette a frame
   or two out of step, and the bar visibly changes colour before the page. They
   cross-fade with everything else instead. */

/* Fallback for browsers without cross-document view transitions. theme.js adds
   .pt only there, so the two never run at once. Only the page's own content
   crosses over: opacity on <body> would take the nav and mark with it, since a
   child cannot opt out of its parent's fade. The entrance is an animation
   rather than a JS-toggled transition so it needs no rAF tick — a page opened
   in a background tab still arrives faded in. */
html.pt body > *:not(.site-nav):not(.mark):not(.mobile-site-menu):not(dialog){
  animation:pt-fade-in .09s ease both;
}
/* The exit is an animation too, not a transition. The entrance leaves opacity
   under animation control (fill:both), and a transition cannot interpolate
   away from an animated value — it would cut to 0 instead of fading. */
html.pt.pt-out body > *:not(.site-nav):not(.mark):not(.mobile-site-menu):not(dialog){
  animation:pt-fade-out .07s ease both;
}
@keyframes pt-fade-in{ from{ opacity:0 } to{ opacity:1 } }
@keyframes pt-fade-out{ from{ opacity:1 } to{ opacity:0 } }

/* =========================================================================
   Home intro. The headword's letters are wrapped in spans and revealed with
   `visibility`, so the word holds its full width from the first frame and the
   line never reflows as it types. Everything else waits, then fades in.
   Both classes are added by theme.js, so a page that never runs it is never
   left hidden.
   ========================================================================= */
.tw,.tw-el{ visibility:hidden }
.tw.on,.tw-el.on{ visibility:visible }
/* html.intro is set during head parse, so the page is hidden from the first
   painted frame. theme.js swaps it for the per-element .intro-hide below in
   one synchronous block once it has staged everything. */
html.intro .site-nav, html.intro .mark,
html.intro .entry .phon, html.intro .entry .defs, html.intro .entry .etym,
html.intro main > #work, html.intro main > .case, html.intro footer{ opacity:0 }
/* the headword waits too, until its letters are wrapped and individually hidden */
html.intro .entry .word{ visibility:hidden }

/* The blocks arrive one after another rather than all at once, and the page's
   own content drifts in from the left — the direction the caret just travelled.
   The nav and the mark are chrome, so they only fade; moving them would read
   as the frame being unstable. */
.intro-hide{ opacity:0;
  transition:opacity .36s ease, transform .36s cubic-bezier(.2,.7,.2,1) }
.intro-hide.slide{ transform:translateX(-10px) }
.intro-hide.in{ opacity:1; transform:none }
/* the caret rides the last revealed glyph; a box-shadow rather than a border
   so it takes up no width and cannot nudge the text */
.tw.at{ box-shadow:1px 0 0 currentColor; animation:tw-blink .9s steps(1,end) infinite }
@keyframes tw-blink{ 50%{ box-shadow:none } }

@media(prefers-reduced-motion:reduce){
  ::view-transition-old(root),::view-transition-new(root){ animation:none }
  html.pt body > *{ animation:none; opacity:1 }
  .tw,.tw-el{ visibility:visible }
  .tw.at{ box-shadow:none; animation:none }
  .intro-hide{ opacity:1; transition:none }
}
