/* The system: one type scale, one rhythm between sections, page transitions, and every pressable thing answering the same way.
   Loaded last on every page, so it is the one place these decisions live. Built from an inventory of the pages on 9 Nov 2026,
   which found ~40 size/weight pairs (body text alone in nine sizes) and section gaps from 72 to 120 px. */

:root{
  /* Type scale: eight steps. Each class maps onto one of them. */
  --t-cap:12.5px;   /* mono metadata: years, labels */
  --t-sm:13.5px;    /* secondary lines, captions, footer */
  --t-ui:14.5px;    /* list rows, controls, small body */
  --t-body:16px;    /* running text */
  --t-lead:18px;    /* lead paragraphs */
  --t-h3:18px;      /* titles inside a section */
  --t-quote:22px;   /* quotes in running content */
  --t-h2:clamp(24px,2.4vw,30px); /* full-width section headings */
  --t-label:clamp(20px,1.8vw,24px); /* headings in a side column */
  /* One rhythm between sections, and one gap under a heading. */
  --sec-gap:clamp(88px,12vh,120px);
  --head-gap:24px;
}

/* Two heading roles, each one size everywhere: full-width section headings, and the labels in a side column. */
.hd2{font-size:var(--t-h2);font-weight:510;letter-spacing:-.022em;line-height:1.15}
.ab-h{font-size:var(--t-label);font-weight:510;letter-spacing:-.02em;line-height:1.2;text-wrap:balance}

/* Titles inside sections */
.pr-c h3,.ax-s h3,.rv-t1,.rv-n{font-size:var(--t-h3);font-weight:540;letter-spacing:-.014em;line-height:1.25}

/* Lead paragraphs */
main .ab-hx .ab-lede,main .pv-c,main .rd-l,main .ab-p1{font-size:var(--t-lead);line-height:1.5;letter-spacing:-.012em}

/* Running text */
.ab .ax-t,.rv-t p,.rd-r,.dl-v,.ab-me-t p{font-size:var(--t-body);line-height:1.6}

/* Rows and controls */
.wn,.wd,.an,.al,.sf-a,.rd-cv,.rx-go,.cv-act,.sx-n,.sx-u,.ro-a{font-size:var(--t-ui)}

/* Secondary lines, captions, footer */
.ky-n,.sx-m,.ct-n,.ax-o,.pq-w,.end-ft,.end-ft .lk,.end-r,.rv-r,.cl,.sx-date,.sx-day{font-size:var(--t-sm)}

/* Quotes: the page's spotlight stays large; everything inside content is one size. */
.rc-c blockquote,.wd-q blockquote,.rv-h{font-size:var(--t-quote);line-height:1.3}
.ab-q .pq-t{font-size:15px}

/* One rhythm between sections on every page. */
.sec{padding-top:var(--sec-gap)}
.ab{margin-top:var(--sec-gap)}
main .rvs{gap:var(--sec-gap);margin-top:var(--sec-gap)}
.rds{gap:var(--sec-gap);margin-top:72px}  /* the first block after an opening sits closer, as on About and Reviews */
@media (max-width:760px){:root{--sec-gap:72px}.ab{margin-top:var(--sec-gap)}}

/* ---------- Page transitions ----------
   The browser keeps the floating nav where it is and swaps only the page under it: the old page fades, the new one rises
   a few pixels into place. Same-origin navigations only; browsers without support just load the page. */
@view-transition{navigation:auto}
.isl{view-transition-name:island}
/* Nov 10: the old page used to vanish in .1s, which read as a cut. Now it fades over .16s while the new one rises in. */
::view-transition-old(root){animation:vt-out .16s cubic-bezier(.23,1,.32,1) both}
::view-transition-new(root){animation:vt-in .24s cubic-bezier(.23,1,.32,1) .04s both}
@keyframes vt-out{to{opacity:0}}
@keyframes vt-in{from{opacity:0;transform:translateY(6px)}}
/* The island morphs as a plain dark pill drawn by the group itself (the snapshots are hidden), so its corners never
   stretch; what's inside it (the pill's label, or the bar) is its own layer and swaps with the same small blur the
   label uses on scroll, centred and unstretched, instead of two labels overlapping. */
.isl[data-s=pill] .isl-c{view-transition-name:isl-in}
/* The desktop bar is the same on every page but for its highlight: a plain crossfade, no blur. */
.isl[data-s=bar] .isl-bar{view-transition-name:isl-bar}
::view-transition-group(isl-bar){animation-duration:.28s}
::view-transition-group(island){animation-duration:.28s;animation-timing-function:cubic-bezier(.32,.72,0,1);background:var(--isl);border-radius:22px;box-shadow:0 0 0 .5px var(--isl-ring),var(--isl-sh)}
::view-transition-old(island),::view-transition-new(island){animation:none;opacity:0}
/* The label layer is left where the browser puts it (no resizing, re-centring or filters on the snapshot: on his iPhone
   those drew the label ~3pt off, drifting while it blurred, then jumping into place when the transition ended). It
   doesn't move or resize; the old label steps out at once (its page is fading anyway) and the new one fades in where
   it will stay, while the pill morphs around it. */
::view-transition-group(isl-in){animation:none}
::view-transition-old(isl-in){animation:none;opacity:0}
::view-transition-new(isl-in){animation:vt-lab-in .18s ease .04s both}
@keyframes vt-lab-in{from{opacity:0}}
/* Leaving from the open menu: the menu fades with the old page, and the new page's pill comes in on its own. */
/* Same timing as the label, so the pill and its label arrive as one object (no empty grey pill first). */
html.vt-menu::view-transition-group(island){animation:vt-isl-in .18s ease .04s both}
/* Opacity only: the label is its own layer and doesn't scale with the pill, so a growing pill slid under a still label. */
@keyframes vt-isl-in{from{opacity:0}}
@media (prefers-reduced-motion:reduce){::view-transition-group(*),::view-transition-old(*),::view-transition-new(*){animation:none!important}}

/* Arriving from another page on the site (html.nav): the page transition is the only motion. The entrance (lines rising,
   blocks fading up, the status wiping in) plays once, when someone first lands; on every later page change it stacked on
   top of the crossfade and read as a stutter on iPhone Safari. */
.js.nav [data-in]{opacity:1;transform:none;transition:none}
.js.nav.ready .ln>span.in{animation:none}
.js.nav .rv-up.now{opacity:1;transform:none;transition:none}

/* Commits: rounded squares, not circles. Squares tile into an even field, so a busy stretch reads as one darker area;
   circles left a lattice of holes that read as a polka-dot pattern on top of the data. On a phone the grid keeps one
   cell per week, like desktop, at about 5px a cell, instead of a month grid of 22px dots. */
.cg .wk i{border-radius:28%}
@media (max-width:600px){.cg{grid-template-columns:18px minmax(0,1fr);column-gap:4px;row-gap:1.5px}.cg .wk{gap:1.5px}.cg .yl{font-size:10px}}

/* ---------- Every pressable thing answers the same way ----------
   Press: a small scale (controls) or a dip in opacity (text links). Hover only on real pointers. A visible ring for the keyboard. */
:where(a,button,[role=button],[tabindex="0"]):focus-visible{outline:2px solid var(--fg);outline-offset:3px;border-radius:6px}
:where(button,[role=button]){cursor:pointer}

.lk{transition:text-decoration-color .16s ease,color .16s ease,opacity .12s ease}
.lk:active{opacity:.6}
@media (hover:hover) and (pointer:fine){.lk:hover{color:var(--fg);text-decoration-color:currentColor}}

.sf-a,.cv-act,.rm-a,.ver,.sx-f,.rx-go,.rb{transition:transform .16s cubic-bezier(.23,1,.32,1),color .16s ease,opacity .16s ease}
.sf-a:active,.cv-act:active,.ver:active,.sx-f:active,.rx-go:active,.rb:active{transform:scale(.97)}
@media (hover:hover) and (pointer:fine){
  .sf-a:hover,.ver:hover{color:var(--fg)}
  .rx-go:hover{text-decoration-thickness:2px}
}
.sx-na{transition:opacity .12s ease}
.sx-na:active{opacity:.6}
@media (hover:hover) and (pointer:fine){.sx-na:hover{text-decoration-thickness:2px}}

.lg-a i{transition:transform .16s cubic-bezier(.23,1,.32,1)}
.lg-a:active i{transform:scale(.96)}

.dm{cursor:pointer}
.dm:active{transform:scale(.985)}
.dm{transition:transform .16s cubic-bezier(.23,1,.32,1)}
.ch-t{cursor:pointer}
.ch-t:active{transform:scale(.94)}

.pv-b{transition:opacity .12s ease}
.pv-b:active{opacity:.7}
.rb{cursor:pointer}
.rb:disabled{cursor:default}

/* ---------- CV ---------- */
/* The three actions say what they did: the label swaps to the result with a tick, then settles back. */
.cv-act.done::before{content:"";display:inline-block;width:.9em;height:.55em;margin:0 .45em .12em .05em;border:solid currentColor;border-width:0 0 1.6px 1.6px;transform:rotate(-45deg);vertical-align:middle;animation:cv-tick .26s cubic-bezier(.23,1,.32,1)}
@keyframes cv-tick{from{opacity:0;transform:rotate(-45deg) scale(.6)}}

/* The side panel: one primary action in solid ink (inverted in dark), the file's facts under it, two quiet actions as a pair, the date last. */
@media screen and (min-width:861px){main .cv-w{grid-template-columns:minmax(0,720px) 232px}}
main .cv-side{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:8px;align-content:start}
main .cv-side .cv-act{all:unset;box-sizing:border-box;display:flex;align-items:center;justify-content:center;gap:8px;height:40px;padding:0 12px;border-radius:10px;
  font-size:var(--t-ui);font-weight:520;letter-spacing:-.01em;white-space:nowrap;cursor:pointer;-webkit-tap-highlight-color:transparent;
  transition:transform .16s var(--out),background-color .16s ease,color .16s ease}
main .cv-side .cv-pri{grid-column:1/-1;height:44px;background:var(--fg);color:var(--bg);box-shadow:0 6px 16px -10px var(--shadow)}
main .cv-side .cv-sec2{background:var(--well);color:var(--fg)}
main .cv-side .cv-act:active{transform:scale(.97)}
main .cv-side .cv-act:focus-visible{outline:2px solid var(--fg);outline-offset:2px}
@media (hover:hover) and (pointer:fine){
  main .cv-side .cv-pri:hover{background:color-mix(in srgb,var(--fg) 86%,var(--bg))}
  main .cv-side .cv-sec2:hover{background:color-mix(in srgb,var(--well) 82%,var(--fg) 8%)}
}
main .cv-side .cv-act.done::before{content:none}
main .cv-fmt{grid-column:1/-1;margin:-2px 0 10px;text-align:center;font-size:var(--t-cap);color:var(--fg3);font-variant-numeric:tabular-nums}
main .cv-upd{grid-column:1/-1;margin:14px 0 0;padding-top:14px;box-shadow:inset 0 1px 0 var(--line);font-size:var(--t-cap);color:var(--fg3);font-variant-numeric:tabular-nums}
main .cv-two{display:contents}
/* The icon and its tick share a cell; the tick takes over with a short blur-masked crossfade, then hands back. */
.cv-ic{display:grid;width:16px;height:16px;flex:none}
.cv-ic svg{grid-area:1/1;width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round;
  transition:opacity .2s var(--out),transform .2s var(--out),filter .2s var(--out)}
.cv-ic .cv-i2{opacity:0;transform:scale(.6);filter:blur(2px)}
.cv-act.done .cv-i1{opacity:0;transform:scale(.6);filter:blur(2px)}
.cv-act.done .cv-i2{opacity:1;transform:none;filter:none}
@media (prefers-reduced-motion:reduce){.cv-ic svg{transition:opacity .15s ease}.cv-ic .cv-i2,.cv-act.done .cv-i1{transform:none;filter:none}}
@media screen and (max-width:860px){
  main .cv-side{display:grid;grid-template-columns:1fr 1fr;max-width:420px;width:100%;gap:8px}
  main .cv-side .cv-act{box-shadow:none;padding:0 12px}
  main .cv-upd{display:none}
  main .cv-fmt{margin-bottom:2px}
}

/* The sheet is one typeface: Inter throughout, with aligned figures for dates. */
.sheet :is(.cv-stamp,.cv-sub,.cv-ct,.cv-sec h2,.cv-y,.folio){font-family:var(--sans);letter-spacing:0;font-variant-numeric:tabular-nums}

/* Printing and the PDF: only the CV, always in light. The browser owns the page: real @page margins, and each sheet flows
   onto its own page, so a browser that adds its own margins (Safari) can't push content off a fixed-size sheet.
   Page numbers and the date go in the PDF's own footer (cvpdf.mjs). */
@page{size:A4;margin:13mm 15.5mm 15mm 17mm}
@media print{
  :root,:root[data-theme],:root:not([data-theme=light]){--bg:#fff;--fg:#111;--fg2:#333;--fg3:#666;--line:#ccc;color-scheme:light}
  html,body{background:#fff!important}
  body{padding:0!important}
  .ending,.tm-tip,.sx-pop,.isl,.scrim,.cv-side,.folio,.cv-stamp{display:none!important}
  .cv-p,.cv-w,.sheets{display:block!important;margin:0!important;padding:0!important;max-width:none!important}
  /* One continuous document: the browser breaks pages between entries, never inside one, never under a heading. */
  .sheet{width:auto!important;height:auto!important;aspect-ratio:auto!important;padding:0!important;overflow:visible!important;box-shadow:none!important;border-radius:0!important;animation:none!important;break-after:auto!important}
  .sheet+.sheet{margin-top:11px!important}  /* the continued list picks up at the list's own gap */
  .sheet .cv-m>li,.sheet .cv-hd,.sheet .cv-p1,.sheet .cv-d,.cv-pt li,.sheet .cv-l>li:not(:has(.cv-pt)){break-inside:avoid}
  /* A long role may continue on the next page between its points, but its title, roles and first line stay together. */
  .sheet .cv-t,.sheet .cv-ro,.sheet .cv-l>li:has(.cv-pt) .cv-d{break-after:avoid}
  .sheet{orphans:2;widows:2;-webkit-print-color-adjust:exact;print-color-adjust:exact}
  /* Safari prints a CSS pixel as 0.8pt where Chrome uses 0.75pt, so its pages run 1/15 larger and spill onto a third.
     Only WebKit knows the system font keywords; there the sheets print at 15/16 and match the PDF line for line. */
  @supports (font:-apple-system-body){.sheets{zoom:.9375}}
  .sheet .cv-sec h2{break-after:avoid}
  /* Safari won't split a flex column across pages; plain blocks it will. */
  .sheet .cv-l,.sheet .cv-m,.sheet .cv-pt{display:block!important}
  .sheet .cv-l>li+li{margin-top:11px}
  .sheet .cv-m>li+li{margin-top:2px}
  /* Rules and dashes as borders: Safari drops backgrounds unless "Print backgrounds" is ticked. */
  .sheet .cv-sec h2::after{background:none!important;height:0!important;border-top:1px solid var(--p-line)}
  .cv-pt li::before{background:none!important;height:0!important;border-top:1px solid var(--p-fg3)}
  /* Safari draws text a touch wider than it measures it, so a right-aligned date would lose its last letter at the page edge. */
  .sheet{padding-right:1.5mm!important}
}


/* ---------- Buttons: one shape ----------
   Text buttons are rounded rectangles on one radius. Icon-only buttons stay round, and the island nav stays a capsule: it is a floating bar, not a button. */
:root{--r-btn:12px}
.rm-a,main .cv-side .cv-act,.si-b,.ag-x{border-radius:var(--r-btn)}
.rm-a{height:44px;padding:0 14px 0 16px}

/* ---------- Identity ----------
   The portrait is a cut-out, so on the bare page its outline was the shirt: heavy at the bottom, open at the top, wider than the text beside it.
   A filled circle gives it a quiet, regular edge, the head is framed to fill it, and the name block centres on that circle. */
.id{gap:12px}
.me{width:48px;height:48px;border-radius:50%;background:var(--well);box-shadow:inset 0 0 0 .5px var(--ring)}
.me img{object-fit:cover;object-position:50% 0;transform:scale(1.18);transform-origin:50% 8%}

/* ---------- Apps outside work ----------
   "App Store" four times over was noise in a mono column. Shipped is the default and says nothing; only the exceptions get a tag, beside the name.
   With the status column gone, the descriptions have the row to themselves and the + centres on the icon like the rows in Selected work. */
.apb{grid-template-columns:44px minmax(0,1fr) 16px;row-gap:1px}
.an{display:flex;align-items:center;gap:8px;min-width:0}
.at{flex:none;padding:3px 6px;border-radius:6px;background:var(--well);color:var(--fg2);font-size:11.5px;font-weight:540;line-height:1;letter-spacing:0}
.apb .ch{grid-column:3;grid-row:1 / span 2;align-self:center}
.al{grid-column:2}
/* An open app: its text starts on the name's edge and stops at the + column, like the rows in Selected work. */
.aps .x .xi.xa .xt{padding:2px 40px 16px 68px;max-width:none}
@media (max-width:760px){.aps .x .xi.xa .xt{padding:2px 12px 14px 68px}}

/* ---------- Home: one way to open a section, fewer boxes ----------
   Two roles only. A section opens with one headline (.hd2), an optional link or control on the right, and the same gap
   to what follows. The two evidence bands, the logo strip and the commit grid, keep a small mono caption instead.
   Rows and demos sit on the page itself: the list rows bleed their hover by 12px so their content lines up with the headline. */
.sh{display:flex;align-items:center;justify-content:space-between;gap:16px;margin:0 0 var(--head-gap);padding:0}
.sh .hd2{margin:0;padding:0;font-family:var(--sans);font-size:var(--t-h2);font-weight:510;letter-spacing:-.022em;line-height:1.15;color:var(--fg);text-wrap:balance}
main #how{padding-top:var(--sec-gap)}
.sh .sf-a{margin-right:-4px}
.sh+*{margin-top:0!important}

/* Selected work: the headline sits above like every other section; the list keeps its panel, because the panel is what
   makes it an object and not a table of text. The first section after the opening sits a little closer. */
main .sf{margin-top:72px;padding:0;border-radius:0;background:none;box-shadow:none;corner-shape:round}
main .sf .wl{padding:8px;border-radius:26px;background:var(--surface);box-shadow:var(--card)}
@supports (corner-shape:squircle){main .sf .wl{corner-shape:squircle;border-radius:40px}}
main .aps{margin-left:-12px;margin-right:-12px}
@media (max-width:760px){main .aps{margin-left:-10px;margin-right:-10px}main .sf{margin-top:56px}main .sf .wl{padding:4px}}

/* Evidence bands: one caption style, one gap to the band. */
.lg-s .lab,.chd .cl,.chd .cr{font:400 var(--t-cap)/1.4 var(--mono);letter-spacing:-.02em;color:var(--fg3);margin:0}
.chd .cl .cn{font-weight:500;color:var(--fg)}
.lg-s{padding-top:var(--sec-gap)}
/* "9 of 9 change" stays one line: a later text-wrap:pretty on paragraphs had switched wrapping back on. */
main .ch-n{text-wrap:nowrap}

/* ---------- Phone pass ---------- */
/* Safari doesn't stretch the children of a <button> flex column, so the switch tracks shrank to their knob. Stretch them explicitly. */
.dm-sp .sp-r,.dm-sp .sp-t{width:100%;align-self:stretch}
/* The review arrows stay beside the heading on a phone, on its last line, instead of taking a row of their own. */
@media (max-width:760px){main .rc-h{flex-wrap:nowrap;align-items:flex-end}main .rc-b{flex:none;margin-bottom:-2px}}
/* Work on a phone: each role's drawing sits beside its title, where the side column puts it on desktop, instead of floating
   alone between the text and the quote with empty space on its right. */
@media (max-width:860px){
  main .rd{grid-template-columns:minmax(0,1fr) auto;column-gap:16px}
  main .rd-id{grid-column:1;grid-row:1}
  main .rd-x{grid-column:2;grid-row:1;padding-top:18px;align-self:start}
  main .rd-t,main .rd .wd-q{grid-column:1 / -1}
  main .rd .il{width:124px;height:auto}
}
/* About, How I work, on a phone: the same move as Work. The step's drawing sits beside its number and title, the text runs under both. */
@media (max-width:760px){
  main .ax-s{display:grid;grid-template-columns:minmax(0,1fr) auto;column-gap:16px;align-items:start}
  main .ax-s>.ax-il{grid-column:2;grid-row:1 / span 2;margin:0;align-self:center}
  main .ax-s>.ax-k{grid-column:1;grid-row:1;align-self:end}
  main .ax-s>h3{grid-column:1;grid-row:2}
  main .ax-s>.ax-t,main .ax-s>.ax-o{grid-column:1 / -1}
  main .ax-s .il{width:116px;height:auto}
}
/* Reviews on a phone: the drawing above the quote was a 200px box pinned left, so the drawing floated at an arbitrary inset.
   It now sits centred as the page's spotlight, a little smaller, closer to its quote. */
@media (max-width:860px){main .rx-st{row-gap:12px}main .rx-ds{justify-self:center;width:176px}}
/* CV on a phone: the two A4 sheets read as one document, so the break between them is just the gap between two roles. */
@media screen and (max-width:860px){main .sheets>.sheet:not(:last-child){padding-bottom:14px}}
/* Reviews, the people on a phone: two columns, three rows. Each person gets their own top rule, so the accent line clearly
   belongs to the cell under it and not to the person above. */
@media (max-width:860px){main .rx-ls{box-shadow:none}main .rx-b{box-shadow:inset 0 .5px 0 var(--line)}}
/* The phone menu had no way home: on desktop the portrait in the bar is Home, but the sheet only listed the four pages.
   Home is now the first tile, with the same portrait as its icon. */
.si-g{grid-template-columns:repeat(5,minmax(0,1fr))}
.si-me{display:block;width:23px;height:23px;border-radius:50%;overflow:hidden;background:var(--isl-h)}
.si-me img{display:block;width:100%;height:100%;object-fit:cover;object-position:50% 0;transform:scale(1.18);transform-origin:50% 8%}

/* Work, quotes inside a role (9 Nov). Who is speaking comes first, so the words read as someone else's before you start them:
   their initials and name, then the line, then a way to the whole review. A hairline under the initials holds the three together. */
main .wd-q{display:grid;grid-template-columns:36px minmax(0,1fr);grid-template-rows:auto auto auto;column-gap:14px;max-width:600px;margin:48px 0 0;padding:0}
main .wd-q::before{content:'';grid-column:1;grid-row:2 / 4;justify-self:center;width:1.5px;margin:8px 0 6px;border-radius:1px;background:var(--line)}
main .wd-q .wq-who{display:contents;margin:0}
.wq-av{grid-column:1;grid-row:1;display:grid;place-items:center;width:36px;height:36px;border-radius:50%;background:var(--well);color:var(--fg2);font-family:var(--mono);font-size:12px;letter-spacing:.02em}
.wq-n{grid-column:2;grid-row:1;align-self:center;display:flex;flex-wrap:wrap;align-items:baseline;gap:2px 10px;min-width:0}
main .wd-q .wq-n b{font-family:var(--sans);font-size:15px;font-weight:540;letter-spacing:-.01em;color:var(--fg)}
main .wd-q .wq-n span{font-family:var(--mono);font-size:12.5px;letter-spacing:-.02em;color:var(--fg3)}
main .wd-q blockquote{grid-column:2;grid-row:2;margin:12px 0 0;font-weight:450;letter-spacing:-.02em;color:var(--fg);text-wrap:balance}
main .wd-q blockquote p{margin:0;text-indent:-.42em}
.wq-go{grid-column:2;grid-row:3;justify-self:start;margin-top:14px;font-size:var(--t-ui);font-weight:500;color:var(--accent);text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:3px;transition:transform .16s cubic-bezier(.23,1,.32,1),text-decoration-thickness .16s ease}
.wq-go::after{content:' →'}
.wq-go:active{transform:scale(.97)}
@media (hover:hover) and (pointer:fine){.wq-go:hover{text-decoration-thickness:2px}}
@media (max-width:860px){main .wd-q{margin-top:32px;grid-template-columns:32px minmax(0,1fr);column-gap:12px}.wq-av{width:32px;height:32px;font-size:11px}.wq-n{flex-direction:column;align-items:flex-start;gap:1px}main .wd-q blockquote{margin-top:10px}}
/* Arriving on Reviews from one of those links: the sentence is marked the same way the letters mark their headline line. */
::target-text{background:color-mix(in srgb,var(--accent) 16%,transparent);color:var(--fg)}

/* Nav, 10 Nov. */
/* The pill without the ring: the label and the chevron that says it opens. */
.isl-c{padding:0 13px 0 16px}
/* Copy email in the bar, after a hairline: it's an action, not a page. The label says what happened, and keeps its width while it does. */
.isl-dv{flex:none;width:1px;height:16px;margin:0 6px 0 5px;background:var(--isl-line)}
.isl button.ni{appearance:none;-webkit-appearance:none;border:0;background:none;margin:0;font-family:inherit;cursor:pointer}
.ni-mail span{display:inline-block;min-width:var(--mw,auto);transition:opacity .14s ease,filter .14s ease}
.ni-mail.sw span{opacity:0;filter:blur(2px)}
.isl .ni-mail.on{color:var(--isl-fg)}
.isl .ni-mail:active{transform:scale(.97);transition-duration:.09s}
/* The sheet lights the page you're on, in the colour kept for current state (the island is dark in both themes, so its dark value). */
.isl .si.cur{background:rgba(138,164,255,.17);color:#c3cfff;box-shadow:inset 0 0 0 1px rgba(138,164,255,.3)}
.isl .si.cur .ic-work .c2{fill:#283044}
/* The appearance switch: three icons, the chosen one on a sliding tile (Vercel's footer). At the end of the footer row, and in the phone menu. */
.end-ft{justify-content:flex-start}
.end-ft .end-r{margin-left:auto}
.ft-th{display:inline-flex;align-items:center;flex:none}
.seg .ic{width:16px;height:16px;display:block}
/* The tile has its own colour: in dark the surface colour is almost the switch's own, and the choice disappeared. */
:root{--seg-th:var(--surface);--seg-sh:0 1px 2px var(--shadow)}
@media (prefers-color-scheme:dark){:root:not([data-theme=light]){--seg-th:rgba(236,234,231,.16);--seg-sh:none}}
:root[data-theme=dark]{--seg-th:rgba(236,234,231,.16);--seg-sh:none}
.end-ft .ft-seg{margin:0;padding:2px;border-radius:10px}
.end-ft .ft-seg i{border-radius:8px;background:var(--seg-th);box-shadow:var(--seg-sh)}
.end-ft .ft-seg button{display:grid;place-items:center;width:30px;height:26px;padding:0;color:var(--fg3);transition:color .16s ease}
.end-ft .ft-seg button[aria-pressed=true]{color:var(--fg)}
@media (hover:hover) and (pointer:fine){.end-ft .ft-seg button:hover{color:var(--fg)}}
.end-ft .ft-seg button:focus-visible{outline:2px solid var(--fg);outline-offset:-2px;border-radius:8px}
/* In the phone menu it fills its half of the row, as tall as Copy email, the icons a little bigger. */
.si-a .si-seg{display:flex;padding:3px;border-radius:14px}
.si-a .si-seg i{top:3px;bottom:3px;border-radius:11px}
.si-a .si-seg button{flex:1;display:grid;place-items:center;padding:0;height:38px}
.si-a .si-seg .ic{width:19px;height:19px}
/* Phone: the links on the left with the switch beside them, the copyright across underneath. */
@media (max-width:760px){
  .end-ft{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;row-gap:8px}
  .end-ft .end-l{grid-column:1;grid-row:1}
  .end-ft .end-r{grid-column:1 / -1;grid-row:2;margin:0}
  .end-ft .ft-th{grid-column:2;grid-row:1;align-self:center}
}
@media (min-width:761px){.isl{width:504px}} /* the bar's real width, so it never shows cropped before the script measures it */

/* Press states (Emil audit, Nov 10). The phone pill, the most tapped control on a phone, gives a little as it is pressed;
   the appearance icons dip like the old toggle did. Release eases back over .16s, the press itself is quicker. */
/* Centred by margins, not by translateX(-50%). On a phone the press animation runs on the GPU, which froze the -50% offset
   at the pill's width while the menu grew, so the menu slid right as it opened. With no translate there is nothing to
   freeze, and the press shrink (`scale`) stays centred. */
.isl{left:0;right:0;margin-inline:auto;transform:none;transition:scale .16s var(--out)}
.isl[data-s=pill]:has(.isl-c:active){scale:.97;transition-duration:.09s}
.seg button .ic{transition:transform .16s var(--out)}
.seg button:active .ic{transform:scale(.88);transition-duration:.09s}
/* The theme change briefly switches transitions off (.swap) while it reads the next look's colour; that would cut the tile's
   slide short, so the tile keeps its own. */
@media (prefers-reduced-motion:no-preference){.swap .seg>i{transition:transform .2s var(--io)!important}}
/* The island in dark: a hairline light edge so its shape holds against the near-black page and dark content under it.
   Light mode keeps its shadow, which already does that job. */
@media (prefers-color-scheme:dark){:root:not([data-theme=light]){--isl-ring:rgba(244,239,230,.17)}}
:root[data-theme=dark]{--isl-ring:rgba(244,239,230,.17)}
/* No dimming behind the open menu (his pick, Nov 10): the island grows over the page with its shadow, like the Dynamic
   Island over an app. The layer stays only to catch a tap outside the menu. It has no colour at all, because Safari on
   iOS 26 paints the strips under the clock and behind the toolbar from a full-width fixed layer's background, and a page
   can't draw there, so any dimming could never match at the edges. */
.scrim{background:none;box-shadow:none}
/* The switch's tile is a third of the switch, inset by the switch's padding; JS only says which third. Measuring the
   buttons placed it from a moment when the phone menu was still settling, leaving it off-centre. */
.seg{--p:2px}
.si-a .si-seg{--p:3px}
.seg>i{left:var(--p);width:calc((100% - 2 * var(--p)) / 3)}
/* Selected work: the open row sits inside the card, so its corners are the card's minus the gap between them (concentric),
   and the same shape. 26 − 8 = 18 on desktop, 26 − 4 = 22 on a phone; with squircle corners (Chrome) 40 − 8 and 40 − 4. */
main .sf .wl{--wr:26px;--wp:8px}
@supports (corner-shape:squircle){main .sf .wl{--wr:40px}main .sf .w{corner-shape:squircle}}
@media (max-width:760px){main .sf .wl{--wp:4px}}
main .sf .w{border-radius:calc(var(--wr) - var(--wp))}
/* Arriving on a phone: the inline script puts the island straight into its pill state; without this the label would
   play its own fade-and-blur-in (the state change from the HTML's bar), drifting through the page transition. */
.isl.still .isl-l{transition:none!important}

/* CV on a phone (Nov 10): the paper frame drops away on a phone, but the sheet kept its print sizes (12px body, 10.5px
   contacts, 9.5px stamp), small and cramped beside the rest of the site (16.5–18px at 1.55). Here it takes the site's
   reading sizes. Screen only, so the printed page and the PDF (made from the A4 print layout) don't change. */
@media screen and (max-width:760px){
  .cv-p .sheet .cv-stamp{font-size:var(--t-xs)}
  .cv-p .sheet .cv-sub{font-size:var(--t-body);margin-top:10px}
  .cv-p .sheet .cv-ct{font-size:var(--t-s);gap:9px 18px;margin-top:16px}
  .cv-p .sheet .cv-ct li{gap:7px}
  .cv-p .sheet .cv-ct svg{width:16px;height:16px}
  .cv-p .sheet .cv-sum{margin-top:22px;font-size:var(--t-body);line-height:var(--lh-body)}
  .cv-p .sheet .cv-sec{margin-top:40px}
  .cv-p .sheet .cv-sec h2{margin-bottom:14px;font-size:var(--t-s)}
  /* Skills: the label above its list; side by side left the list a narrow column at this size. */
  .cv-p .sheet .cv-sk li{grid-template-columns:minmax(0,1fr);gap:2px;padding:9px 0;font-size:var(--t-xs);line-height:var(--lh-xs)}
  .cv-p .sheet .cv-sk .cv-y{font-size:var(--t-s);line-height:var(--lh-body)}
  .cv-p .sheet .cv-l{gap:30px}
  .cv-p .sheet .cv-t b{font-size:var(--t-body)}
  .cv-p .sheet .cv-t .cv-y,.cv-p .sheet .cv-ro .cv-y{font-size:var(--t-xs)}
  .cv-p .sheet .cv-ro{margin-top:3px;font-size:var(--t-s)}
  .cv-p .sheet .cv-d,.cv-p .sheet .cv-pt li,.cv-p .sheet .cv-p1{font-size:var(--t-s);line-height:var(--lh-body)}
  .cv-p .sheet .cv-d{margin-top:8px}
  .cv-p .sheet .cv-pt li+li{margin-top:6px}
  /* Page numbers mean nothing on a page that scrolls. */
  .cv-p .folio{display:none}
}

/* =====================================================================================================================
   DESIGN SYSTEM — TYPE (Nov 10). The single source for text sizes. Every kind of text on every page takes one of
   these steps; nothing should set a font size of its own anywhere else. src/v77/scale-check.mjs lists any page
   text that is off the scale.
   Exempt, because they're drawn objects with their own scale: the keycap (.ky), the status clock (.sx-big, .rdg),
   the calendar (.cg), the demo cards' own labels (.dm, .sp-l, .sp-h, .at), avatar initials (.av, .rx-i, .rv-av,
   .wq-av), and the CV's paper sheet on desktop.
   ===================================================================================================================== */
:root{
  --t-display:52px;  --lh-display:1.1;   /* page titles */
  --t-xl:30px;       --lh-xl:1.15;       /* section headings; the About principle switch; the Reviews opening quote */
  --t-l:22px;        --lh-l:1.3;         /* quotes; Work company names */
  --t-m:18px;        --lh-m:1.45;        /* intros under a title; card titles; About pull quotes */
  --t-body:16.5px;   --lh-body:1.55;     /* body text */
  --t-s:15px;        --lh-s:1.5;         /* names, secondary text, role lines, links, buttons, list rows */
  --t-xs:13.5px;     --lh-xs:1.45;       /* captions and small notes */
  --t-mono:12.5px;   --lh-mono:1.5;      /* mono labels: dates, sources, step numbers */
}
@media (max-width:760px){:root{--t-display:31px;--t-xl:24px;--t-l:20px;--t-m:17px}}

/* Intros under a page title */
:root body :is(.hx-main>p.le,header.wk-h>p.le,.ab-lede){font-size:var(--t-m);line-height:var(--lh-m)}

/* Section headings, and the two page openers that work as headings */
:root body :is(h2.hd2,h2.ab-h){font-size:var(--t-xl);line-height:var(--lh-xl)}
/* About's principle names sit a step under the "Principles" heading (they were the same size, Nov 10 balance #6). */
:root body button.pv-b>span.pv-t{font-size:var(--t-l)}
:root body .rx-qs .rx-f>blockquote>span{font-size:var(--t-xl);line-height:1.25}

/* Quotes */
:root body :is(.rc-c>blockquote,.wd-q blockquote,.wd-q blockquote p,h2.rv-h){font-size:var(--t-l);line-height:var(--lh-l)}
:root body .rd-id>h2.rd-n{font-size:var(--t-l);line-height:1.2}
:root body figure.ab-q>blockquote.pq-t{font-size:var(--t-m);line-height:var(--lh-m)}

/* Card titles */
:root body :is(article.pr-c>h3,li.ax-s>h3,.pv-in>p.pv-c,h1.rv-t1,.rv-id>p.rv-n){font-size:var(--t-m);line-height:1.3}

/* Body */
:root body :is(section.sec>p.le,p.wk-mail,.rd-t>p.rd-l,.ab-b>p.ab-p1,.ab-b>.ab-me-t,.pv-in>p.ax-t,ul.dl .dl-v,
  article.rv .rv-t>p:not(.rv-sig),.ab-b>ul.rv-il>li>p,.hx-main>p.stt){font-size:var(--t-body);line-height:var(--lh-body)}
:root body .id p.nm{font-size:var(--t-body);line-height:1.25}

/* Secondary text, names, role lines, links, buttons, list rows */
:root body :is(.rd-id>p.rd-r,nav.toc,nav.toc a,.ro-r>.ro-a,p.sx-l1 .sx-u,button.sx-f .sx-n,button.wr .wn,button.wr .wd,button.apb .an,button.apb .al,
  .wq-n>b,.rx-f>figcaption>b,.rc-c>figcaption b,a.sf-a,a.wq-go,a.rx-go,a.rm-a,.cv-act .cv-lb){font-size:var(--t-s)}
:root body .rd-t>p.wd-ar{font-size:var(--t-s);line-height:var(--lh-body)}
:root body :is(a.sf-a,a.wq-go,a.rx-go,a.rm-a){font-weight:500}

/* Captions */
:root body :is(.rv-id>p.rv-r,li.ax-s>p.ax-o,p.sx-l2 .sx-m,p.sx-date,p.sx-date .sx-day,button.ct-r .ct-n,.ky-f>p.ky-n,.rc-c>figcaption>span:not(.av),
  figure.ab-q>figcaption.pq-w,p.rd-cv,aside.cv-side .cv-fmt,aside.cv-side .cv-upd){font-size:var(--t-xs);line-height:var(--lh-xs)}
:root body p.rd-cv{font-family:var(--sans);letter-spacing:0}

/* Mono labels */
:root body :is(.rd-id>p.rd-s,.rd-id>p.rd-y,.rv-t>p.rv-sig,.ab-b>ul.rv-il>li>span,.rx-w>span,.rx-f>figcaption>span,li.ax-s>p.ax-k,
  nav.toc .toc-y,.wq-n>span,button.pv-b .pv-n,ul.dl .dl-k,button.wr .wy,h2.lab,.chd>p.cl,.chd>p.cr){font-size:var(--t-mono);line-height:var(--lh-mono)}
:root body :is(.ro-r>.ro-b,.ro-b .ipa,.ro-b .syl,.rm-l p){font-size:var(--t-s)}
:root body li.ax-s>p.ax-k>b{font-size:var(--t-mono)}
/* Secondary in narrow columns: How I work's five steps. Control labels: the Reviews person tabs (six in a row). */
:root body li.ax-s>p.ax-t{font-size:var(--t-s);line-height:var(--lh-body)}
:root body .rx-w>b{font-size:var(--t-xs)}
/* Home's email card (Nov 10, balance #1): it was the loudest thing on Home — a 58px address over a 52px page title.
   It's a control, not a drawn object, so it's on the scale: the address at the section-heading step, the label at the
   secondary step, the padding in proportion. The keycap (lip, press) stays. */
:root body .ky .ky-a{font-size:var(--t-xl);line-height:var(--lh-xl)}
:root body .ky .ky-l{font-size:var(--t-s)}
:root body .ky .ky-c{padding:22px 26px 23px}
@media (max-width:760px){:root body .ky .ky-c{padding:18px 20px 19px}}
/* Home's commit graph (Nov 10, balance #2). It had only a small mono caption where every other section on Home has a
   heading, and its darkest weeks ran almost to black (the ramp is lighter now, in app.js). The count becomes the
   section heading; the caption stays. On a phone each year wraps onto two lines of 27 weeks, so the squares are twice
   the size (about 10px, they were under 5) and every week is still there. */
:root body .chd{align-items:flex-end;margin-bottom:24px}
:root body .chd .cc-h{margin:0}
:root body .chd .cc-h .cn{font:inherit;color:inherit;letter-spacing:inherit}
@media (max-width:760px){
  :root body .chd{align-items:flex-start;margin-bottom:18px}
  :root body .chd .cr{text-align:left}
  :root body .cg{row-gap:8px}
  :root body .cg .wk{grid-template-columns:repeat(27,minmax(0,1fr));gap:2px}
  :root body .cg .yl{align-self:start;line-height:11px}
}
/* Home's quote shelf (Nov 10, balance #4). The last card, the rest of the wall as a fading list, was the tallest thing
   in the row, so every quote card stretched to it and showed a 100–140px hole between quote and name. Now that list
   takes no height of its own: it fills what the quotes need and fades, and the row is as tall as the longest quote.
   On a wide screen the shelf ends at the column, so the next card peeks in (dimmed, as before) and fades out at the
   column's edge instead of running past the page to the window edge. The phone keeps its edge-to-edge peek. */
:root body .rc-c{min-height:0}
:root body .rc-m .rm-l{flex:1 1 0;min-height:0;overflow:hidden}
/* Things that move sideways run to the window's edge and fade there, like the logo marquee (same fade: the window's
   outer 18%); everything else stays in the column. So on a wide screen the shelf spans the window, its cards start on
   the column's left edge, the next ones peek in to the right and fade at the window edge, and cards you've passed fade
   out in the left margin. The padding gives the cards' shadows room; the margins put everything back in place.
   margin-top needs !important only to beat `.sh + *{margin-top:0!important}`. */
@media (min-width:761px){:root body main #rc{--m:max(24px,calc(50vw - 480px));width:100vw;box-sizing:border-box;padding:40px 0 60px var(--m);margin:-40px 0 -60px calc(50% - 50vw);margin-top:-40px!important;
  -webkit-mask-image:linear-gradient(90deg,transparent,#000 var(--m),#000 82%,transparent);mask-image:linear-gradient(90deg,transparent,#000 var(--m),#000 82%,transparent)}}
/* Work drawings (Nov 10, balance #5): Insight's widget is now a neutral tile, so its timer ring carries the accent. */
:root body .il .wd-ring{stroke:var(--accent)}
:root body .il .wd-trk{stroke:var(--accent);opacity:.22}
/* Reviews picker (Nov 10): three tabs to a row on a wide screen, two rows, so every name and role line fits on one line
   (in one row of six, two role lines wrapped). The phone already has two to a row. */
@media (min-width:761px){:root body main .rx-ls{grid-template-columns:repeat(3,minmax(0,1fr));gap:0 28px;box-shadow:none}:root body main .rx-b{padding:16px 0 16px;box-shadow:inset 0 .5px 0 var(--line)}}
/* CV contacts on screen (Nov 10): the four addresses can't be shortened (hiring software looks for the full linkedin.com/in/
   and github.com/ forms), and in one flowing row GitHub ended up alone on a second line. On screen they sit as two pairs
   (a phone: a list of four); print and the PDF keep their single row, which fits on A4. */
@media screen{:root body .cv-p .sheet .cv-ct{display:grid;grid-template-columns:repeat(2,max-content);gap:7px 26px}}
@media screen and (max-width:760px){:root body .cv-p .sheet .cv-ct{grid-template-columns:minmax(0,1fr);gap:9px}}
/* Print/PDF: the five contacts stay on one line, spread from margin to margin (the row was a few px short of fitting
   with the solid icons, and GitHub wrapped alone). */
@media print{:root body .cv-p .sheet .cv-ct{flex-wrap:nowrap;justify-content:space-between;column-gap:6px}:root body .cv-p .sheet .cv-ct li{white-space:nowrap}}
/* About's principle quotes are the site's quote component (as on Work), a step smaller: they sit under a principle in a
   narrow column, below its claim, and at the full quote size they'd compete with it (Nov 10). */
:root body .pv-in .wd-q{margin-top:28px}
:root body .pv-in .wd-q blockquote,:root body .pv-in .wd-q blockquote p{font-size:var(--t-m);line-height:var(--lh-m)}
