/* ═══════════════════════════════════════════════════════════════════════════
   Portfolio — built on Slate tokens. Everything here derives from them:
   no new colours, the three text steps, the 30/38/52px ladder, one easing.
   ═══════════════════════════════════════════════════════════════════════════ */

:root{
  --page:600px;
  --gutter:24px;
}

html{
  scrollbar-gutter:stable;             /* the page never jumps sideways when a scrollbar appears */
  -webkit-text-size-adjust:100%;
  text-size-adjust:100%;
  scroll-behavior:smooth;
}
@media (prefers-reduced-motion:reduce){html{scroll-behavior:auto}}

/* ── Commit Mono ───────────────────────────────────────────────────────────
   Two weights on disk, so each face claims a weight range. Slate's 500 and
   600 resolve to a real cut instead of the browser smearing a fake bold:
   anything under 550 is Regular, anything above is Bold. Emphasis below 700
   is carried by ink (--text vs --text-2), the Slate way.                  */

@font-face{font-family:"Commit Mono";src:url(fonts/CommitMono-400-Regular.woff2) format("woff2");font-weight:100 549;font-style:normal;font-display:swap}
@font-face{font-family:"Commit Mono";src:url(fonts/CommitMono-400-Italic.woff2) format("woff2");font-weight:100 549;font-style:italic;font-display:swap}
@font-face{font-family:"Commit Mono";src:url(fonts/CommitMono-700-Regular.woff2) format("woff2");font-weight:550 900;font-style:normal;font-display:swap}
@font-face{font-family:"Commit Mono";src:url(fonts/CommitMono-700-Italic.woff2) format("woff2");font-weight:550 900;font-style:italic;font-display:swap}

/* Trial: Office Times Round for the headline only. Licence is personal use. One weight,
   no italic, 68 glyphs (no curly quotes or dashes), so it stays on the heading. */
/* One weight, one style, declared honestly. Claiming 100–900 made the browser
   think a bold existed, so it never synthesised one for the type toolbar. */
@font-face{font-family:"Office Times Round";src:url(fonts/OfficeTimesRound-Regular.woff2) format("woff2");font-weight:400;font-style:normal;font-display:swap}

body{
  height:auto;min-height:100%;
  font-synthesis:none;
  /* Site-wide tracking for body text. Mono is set loose; this tightens it
     everywhere, dialogs and palette included. The headline sets its own. */
  letter-spacing:-.4px;
  /* The alternates from the download: smarter kerning (ss03), arrows (ss04), tabular numbers (ss05). */
  font-feature-settings:"ss03" 1,"ss04" 1,"ss05" 1;
  font-variant-ligatures:contextual;
  text-rendering:optimizeLegibility;
}
/* Browsers reset letter-spacing on form controls, and font:inherit doesn't
   cover it, so buttons and inputs would otherwise ignore the tracking above. */
button,input,textarea,select{letter-spacing:inherit}
em,cite{font-style:italic}
cite{color:var(--text)}

::selection{background:var(--inv);color:var(--inv-text)}

a{color:inherit;text-decoration:none;-webkit-tap-highlight-color:transparent}

.skip{
  position:fixed;left:16px;top:12px;z-index:600;
  height:30px;padding:0 12px;border-radius:7px;display:inline-flex;align-items:center;
  background:var(--inv);color:var(--inv-text);
  translate:0 -48px;transition:translate .18s var(--ease);
}
.skip:focus-visible{translate:0 0}

/* ── The one container ─────────────────────────────────────────────────── */

.page{
  width:100%;max-width:calc(var(--page) + var(--gutter) * 2);margin:0 auto;
  padding:
    max(56px, env(safe-area-inset-top))
    max(var(--gutter), env(safe-area-inset-right))
    max(40px, env(safe-area-inset-bottom))
    max(var(--gutter), env(safe-area-inset-left));
  display:flex;flex-direction:column;
}
main{display:flex;flex-direction:column;gap:56px}

@media (max-width:640px){
  :root{--gutter:16px}
  .page{padding-top:max(20px, env(safe-area-inset-top))}
  main{gap:44px}
}

/* ── Entrance. Short rise, staggered by --i. Once, on load. ───────────── */

.reveal{animation:rise .5s var(--ease) both;animation-delay:calc(var(--i, 0) * 45ms + 40ms)}
@keyframes rise{from{opacity:0;translate:0 6px;filter:blur(2px)}to{opacity:1;translate:0 0;filter:blur(0)}}

/* ── Header ────────────────────────────────────────────────────────────── */

.head{display:flex;align-items:center;gap:10px;height:52px;margin-bottom:40px}
@media (max-width:640px){.head{margin-bottom:28px}}

.mark{
  width:30px;height:30px;border-radius:50%;flex:none;display:grid;place-items:center;
  background:var(--inv);color:var(--inv-text);
  font-size:11px;font-weight:700;line-height:1;
  /* Commit Mono's caps are optically centred in a line-height:1 box, so no nudge. */
  user-select:none;-webkit-user-select:none;
  transition:scale .18s var(--ease),rotate .6s var(--ease);
}
.mark:hover{scale:1.04}
.mark:active{scale:.94}
.mark.spin{rotate:360deg}

.who{display:flex;flex-direction:column;min-width:0;line-height:1.3}
.name{font-weight:700}

.icon-btn .ic{display:grid;place-items:center}
/* One text button, not a row of glyphs: the menu holds the theme, the view and
   the rest, so the header says so in words. The button box meets the column
   edge, the same rule the icon buttons followed. */
.menu-btn{
  height:30px;padding:0 9px;margin-right:0;border-radius:7px;flex:none;
  font-size:13px;color:var(--text-2);
  transition:background .15s,color .15s;
}
#copy-email{margin-right:-6px}   /* the pair reads as one group, not two islands */
#copy-email{position:relative}
/* Same ink and shape as the tooltips, pinned above the button. */
.copy-pop{
  position:absolute;left:50%;bottom:calc(100% + 6px);z-index:30;pointer-events:none;white-space:nowrap;
  padding:4px 7px;border-radius:6px;background:var(--inv);color:var(--inv-text);
  font-size:11.5px;line-height:1.35;box-shadow:var(--sh-2);
  opacity:0;translate:-50% 3px;transition:opacity .12s,translate .14s var(--ease);
}
#copy-email[data-copied] .copy-pop{opacity:1;translate:-50% 0}
.menu-btn:hover{background:var(--hover);color:var(--text)}
.menu-btn:focus-visible{box-shadow:var(--ring)}

/* ── Intro ─────────────────────────────────────────────────────────────── */

.intro{display:flex;flex-direction:column;gap:14px}

/* Still used by the timeline's ongoing roles. */
.dot{position:relative;width:6px;height:6px;border-radius:50%;background:var(--text);flex:none}
.dot::after{
  content:"";position:absolute;inset:0;border-radius:50%;background:var(--text);
  animation:ping 2.4s var(--ease) infinite;
}
@keyframes ping{0%{opacity:.35;scale:1}70%,100%{opacity:0;scale:3}}

.lede{
  margin:0;font-family:"Office Times Round",Georgia,"Times New Roman",serif;
  font-size:26px;line-height:1.2;font-weight:400;letter-spacing:-.01em;
  text-wrap:balance;
}
/* Monospace is set wide by design; a touch of tracking at display size keeps
   the headline reading as a line rather than a row of cells. */
.lede em{color:var(--text-2)}
@media (max-width:640px){.lede{font-size:22px}.section-sub{display:none}}

.prose{
  margin:0;font-size:14px;line-height:1.75;color:var(--text-2);
  text-wrap:pretty;hanging-punctuation:first allow-end last;
}

/* ── Type system ───────────────────────────────────────────────────────────
   Two faces, one job each:
     Office Times Round  the headline.
     Commit Mono         everything else: section titles (~/name, like a path),
                         body copy, labels, dates, the terminal.
   Sentences keep the site's mono tracking; only the line height opens up, since
   a monospace needs more air than a proportional face to read as prose. */

.prose,.row-detail p{font-size:13.5px;line-height:1.65}

/* Underlines that know their place: thin, offset clear of descenders,
   and they only reach full contrast on hover. */
.link{
  color:var(--text);
  text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:4px;
  text-decoration-color:var(--line-2);text-decoration-skip-ink:auto;
  transition:text-decoration-color .15s;
  border-radius:2px;
}
.link:hover{text-decoration-color:currentColor}

/* ── Section heading ───────────────────────────────────────────────────── */

.section{display:flex;flex-direction:column;gap:6px;min-width:0;scroll-margin-top:24px}
.section-head{display:flex;align-items:baseline;gap:8px;min-height:30px}
/* Section headings share the headline serif, a step down in size. It has one
   weight, so they are set at 400; size and the serif do the work bold used to.
   Counts and hints stay mono and sit on the title's baseline. */
.section-title{
  position:relative;margin:0;
  font-family:var(--mono);font-size:13px;line-height:30px;font-weight:700;letter-spacing:-.4px;color:var(--text);
}
.section-title .pfx{color:var(--text-3)}   /* the ~/ is the prompt, the name is the point */
.count{font-size:11px;color:var(--text-3)}
.section-sub{font-size:12px;color:var(--text-3)}

/* The # hangs in the gutter so titles stay flush with everything else. */
.anchor{
  position:absolute;right:100%;top:50%;translate:-6px -50%;
  width:16px;height:20px;display:grid;place-items:center;border-radius:4px;
  font-family:var(--mono);font-size:13px;line-height:1;letter-spacing:-.4px;
  color:var(--text-3);font-weight:400;opacity:0;transition:opacity .15s,color .15s;
}
.section-head:hover .anchor,.anchor:focus-visible{opacity:1}
.anchor:hover{color:var(--text)}
@media (hover:none){.anchor{display:none}}

/* ── Rows ──────────────────────────────────────────────────────────────── */

.rows{list-style:none;margin:0;padding:0;display:flex;flex-direction:column}

/* The hover fill bleeds 10px past the text column so the words stay aligned
   with the heading above while the target is generous. */
.row{
  display:flex;align-items:center;gap:12px;min-height:38px;padding:0 10px;margin:0 -10px;
  border-radius:7px;color:var(--text);
  transition:background .15s;
}
.rows button.row{width:calc(100% + 20px)}
.row:hover{background:var(--hover)}
.row:focus-visible{background:var(--hover);box-shadow:inset 0 0 0 1px var(--text-3)}

.row-title{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.row-desc{margin-left:1ch;color:var(--text-3);transition:color .15s}
.row:hover .row-desc{color:var(--text-2)}

/* A hairline leader from title to meta — the eye travels along it. */
.row-fill{flex:1;min-width:16px;height:1px;background:var(--line);transition:background .15s}
.row:hover .row-fill{background:var(--line-2)}

.row-meta{flex:none;font-size:12px;color:var(--text-3)}
.row:hover .row-meta{color:var(--text-2)}

/* The arrow lives in the row's bleed, so at rest the meta column is flush
   with the text edge above it. On hover the meta steps aside to make room. */
.row{position:relative;transition:background .15s,padding-right .2s var(--ease)}
.row:hover,.row:focus-visible{padding-right:30px}   /* the leader shortens; nothing overlaps */
.row-go{
  position:absolute;right:8px;top:50%;margin-top:-7px;width:14px;height:14px;color:var(--text-2);
  opacity:0;translate:-4px 4px;transition:opacity .15s,translate .2s var(--ease);
}
.row:hover .row-go,.row:focus-visible .row-go{opacity:1;translate:0 0}

/* A borderless pill's ink is inset by its padding; pull it out so its text
   ends on the same edge as the dates above and below. */
.row .pill[data-t="1"],.row .pill[data-t="2"]{margin-right:-7px}

/* Phone: title and description stack on the left, meta stays top-right,
   baselines of title and meta shared. Two lines per row, not three. */
@media (max-width:640px){
  .row{align-items:baseline;gap:16px;padding-top:9px;padding-bottom:10px;line-height:1.4}
  .row:hover,.row:focus-visible{padding-right:10px}
  .row-title{flex:1;white-space:normal;text-wrap:pretty}
  .row-desc{display:block;margin-left:0;margin-top:1px}
  .row-fill,.row-go{display:none}
  .row .pill{translate:0 -1px}
  .exp .row{padding-right:30px}
  .exp .row-go{display:block;opacity:1;translate:0 0}
  .row-detail p{max-width:none}
}

/* A build in progress: a sliver runs the length of a tiny track, hesitates at
   each end, and starts again. Ink only — the pill's step already carries the
   emphasis, so this adds motion, not colour. */
.build{
  display:block;position:relative;width:14px;height:3px;flex:none;
  border-radius:2px;background:var(--line-2);overflow:hidden;
}
.build i{
  position:absolute;inset:0 auto 0 0;width:6px;border-radius:2px;background:var(--text-2);
  animation:build 1.6s cubic-bezier(.5,0,.5,1) infinite;
}
@keyframes build{
  0%,8%{translate:0}
  50%,58%{translate:8px}
  100%{translate:0}
}
@media (prefers-reduced-motion:reduce){.build i{animation:none;width:9px}}

/* ── Folders ───────────────────────────────────────────────────────────────
   Work and Projects as two monochrome folders. The whole drawing is sized in
   em, and the folder's font-size is 5% of its own width (a size container),
   so the art scales as one object: 10px → a 200 × 150 folder on desktop,
   smaller on a phone, with nothing re-laid-out.

   Greys are mixed from the Slate text and ground tokens, so both themes come
   for free. Only the mix amounts differ: in light the back is the darker
   panel, in dark it is the dimmer one, which is what reads as "behind".   */

:root{
  --fd-back:color-mix(in srgb,var(--text) 27%,var(--bg));
  --fd-front-a:color-mix(in srgb,var(--text) 12%,var(--surface));
  --fd-front-b:color-mix(in srgb,var(--text) 19%,var(--surface));
  --fd-hi:rgba(255,255,255,.6);
  --fd-edge:rgba(20,20,26,.06);
  --fd-shadow:rgba(20,20,26,.16);
  /* The one place motion overshoots. A card that lands exactly where it is
     going reads as placed; one that settles reads as lifted. */
  --spring:cubic-bezier(.3,1.4,.55,1);
}
:root[data-theme="dark"]{
  --fd-back:color-mix(in srgb,var(--text) 16%,var(--bg));
  --fd-front-a:color-mix(in srgb,var(--text) 21%,var(--bg));
  --fd-front-b:color-mix(in srgb,var(--text) 15%,var(--bg));
  --fd-hi:rgba(255,255,255,.14);
  --fd-edge:rgba(255,255,255,.04);
  --fd-shadow:rgba(0,0,0,.55);
}

.folders{min-width:0}

/* ── View toggle ───────────────────────────────────────────────────────────
   Two words on the section's title line, like the timeline's hint: the
   current view in full ink, the other one quiet. List view moves the rows out
   of the dialogs and onto the page, so there is only ever one copy of them. */

.folders .section-head{margin-bottom:6px}
.view-sw{display:flex;align-items:baseline;gap:6px;font-size:12px}
.view-sep{color:var(--line-2)}
.view-btn{
  padding:0;border:0;background:none;font:inherit;color:var(--text-3);cursor:pointer;
  text-decoration:underline 1px transparent;text-underline-offset:3px;
  transition:color .15s var(--ease),text-decoration-color .15s var(--ease);
}
.view-btn:hover{color:var(--text-2)}
.view-btn[aria-pressed="true"]{color:var(--text);text-decoration-color:var(--line-2);cursor:default}
.folders[data-view="list"] .fd-grid{display:none}

.lists{display:flex;flex-direction:column;gap:28px}
.lists .section-head{margin-bottom:-2px}
.fd-grid{display:grid;grid-template-columns:1fr 1fr;gap:24px}
@media (max-width:640px){.fd-grid{gap:12px}}

.folder{
  container-type:inline-size;position:relative;
  display:flex;flex-direction:column;align-items:center;gap:0;
  padding:22px 0 14px;border-radius:12px;text-align:center;
  -webkit-tap-highlight-color:transparent;
}
.folder:focus-visible{box-shadow:var(--ring)}

.fd{
  position:relative;display:block;flex:none;
  font-size:min(10px,4.6cqw);width:20em;height:15em;
  letter-spacing:normal;        /* an illustration: its tiny type keeps its own spacing */
  transition:scale .15s var(--ease);
}
.folder:active .fd{scale:.97}

/* Back panel and its tab. */
.fd-back{
  position:absolute;left:.4em;right:.4em;top:1.6em;bottom:.6em;z-index:0;
  border-radius:1.2em;background:var(--fd-back);
}
.fd-back::before{
  content:"";position:absolute;left:1.2em;top:-1.1em;width:7.2em;height:2.2em;
  border-radius:.8em .8em 0 0;background:var(--fd-back);
}

/* Front panel. A few degrees of tilt even at rest gives it the slightly
   wider top edge a real folder has; opening tips it further toward you. */
.fd-front{
  position:absolute;left:0;right:0;bottom:0;height:11.2em;z-index:4;
  border-radius:1em 1em 1.2em 1.2em;
  background:linear-gradient(var(--fd-front-a),var(--fd-front-b));
  box-shadow:inset 0 1px 0 var(--fd-hi),inset 0 0 0 1px var(--fd-edge),0 1px 2px var(--fd-shadow);
  transform-origin:50% 100%;transform:perspective(40em) rotateX(-5deg);
  transition:transform .34s var(--ease),box-shadow .34s var(--ease);
}
/* The pressed seam near the top edge. */
.fd-front::before{
  content:"";position:absolute;left:1.4em;right:1.4em;top:.9em;height:1px;
  background:var(--fd-hi);opacity:.7;
}

/* Ground shadow: tightens as the folder opens and lifts. */
.fd::after{
  content:"";position:absolute;left:10%;right:10%;bottom:-.9em;height:1.6em;z-index:-1;
  border-radius:50%;background:radial-gradient(closest-side,var(--fd-shadow),transparent);
  opacity:.55;transition:opacity .34s var(--ease),scale .34s var(--ease);
}

/* Preview cards. Rest: tucked in, fanned a little, tops peeking out.
   Open: they rise and fan out together, and drift a
   few pixels with the pointer (--px, set in main.js). */
.pv{
  --px:0;
  position:absolute;left:50%;top:1.5em;width:14.4em;height:10em;margin-left:-7.2em;
  display:flex;flex-direction:column;gap:.55em;padding:.8em .9em;overflow:hidden;
  border-radius:.7em;background:var(--surface);
  box-shadow:0 0 0 1px var(--line),0 1px 2px var(--fd-shadow);
  transform-origin:50% 100%;text-align:left;
  translate:var(--x0) 0;rotate:var(--r0);
  transition:translate .42s var(--spring),rotate .42s var(--spring),box-shadow .3s var(--ease);
}
/* All three share one timing, no stagger: they pop out as a single gesture. */
.pv.a{z-index:1;--x0:-1.1em;--r0:-6deg;--x1:-5.2em;--y1:-4.2em;--r1:-13deg}
.pv.c{z-index:2;--x0:1.2em;--r0:6deg;--x1:5.2em;--y1:-3.8em;--r1:12deg}
.pv.b{z-index:3;--x0:0em;--r0:-1deg;--x1:0em;--y1:-5.6em;--r1:-1.5deg}
/* Only two cards (Projects): they straddle the centre instead of leaving the
   right-hand slot empty. */
.fd:not(:has(.pv.c)) .pv.a{--x0:-.7em;--r0:-5deg;--x1:-2.9em;--y1:-4.6em;--r1:-8deg}
.fd:not(:has(.pv.c)) .pv.b{--x0:.7em;--r0:3deg;--x1:2.9em;--y1:-5.2em;--r1:6deg}

@media (hover:hover){
  .folder:hover .fd-front{transform:perspective(40em) rotateX(-24deg);box-shadow:inset 0 1px 0 var(--fd-hi),inset 0 0 0 1px var(--fd-edge),0 -.4em 1.4em -.6em var(--fd-shadow)}
  .folder:hover .pv{translate:calc(var(--x1) + var(--px) * .8em) var(--y1);rotate:var(--r1);box-shadow:0 0 0 1px var(--line),0 .6em 1.4em -.6em var(--fd-shadow)}
  .folder:hover .fd::after{opacity:.35;scale:1.08 1}
}
.folder:focus-visible .fd-front,.folder[aria-expanded="true"] .fd-front{transform:perspective(40em) rotateX(-24deg);box-shadow:inset 0 1px 0 var(--fd-hi),inset 0 0 0 1px var(--fd-edge),0 -.4em 1.4em -.6em var(--fd-shadow)}
.folder:focus-visible .pv,.folder[aria-expanded="true"] .pv{translate:calc(var(--x1) + var(--px) * .8em) var(--y1);rotate:var(--r1);box-shadow:0 0 0 1px var(--line),0 .6em 1.4em -.6em var(--fd-shadow)}
/* Closing, the cards drop back together; no stagger on the way in. */
.folder:not(:hover):not(:focus-visible):not([aria-expanded="true"]) .pv{transition-delay:0ms;transition-duration:.28s;transition-timing-function:var(--ease)}

/* Work's cards are the real thing, shrunk: a window bar, the site's hero and
   a line where the caption goes. The shot is a small 2:1 crop, not the tall
   full-page capture the dialog loads. */
.pv-win{gap:.45em}
.pv-shot{
  display:block;aspect-ratio:2/1;border-radius:.3em;overflow:hidden;flex:none;
  background:var(--surface-2);box-shadow:0 0 0 1px var(--line);
}
.pv-shot img{display:block;width:100%;height:100%;object-fit:cover;object-position:top center}

/* Mini interfaces on the cards. Everything in em, all ink from tokens. */
.pv-top{display:flex;align-items:center;gap:.3em;height:.8em;flex:none}
.pv-top i{width:.4em;height:.4em;border-radius:50%;background:var(--line-2)}
.pv-top b{margin-left:.35em;font-weight:400;font-size:.72em;line-height:1;color:var(--text-3);white-space:nowrap}
.pv-h{font-size:1.05em;line-height:1.1;font-weight:700;color:var(--text);letter-spacing:-.02em;white-space:nowrap}
.pv-h em{font-style:normal;font-weight:400;color:var(--text-2)}
.pv .sk{display:block;height:.42em;border-radius:.21em;background:var(--line);flex:none}
.pv-sw{display:flex;gap:.3em;flex:none}
.pv-sw i{flex:1;height:1.7em;border-radius:.3em}
.pv-bars{display:flex;align-items:flex-end;gap:.3em;height:3.4em;flex:none}
.pv-bars i{flex:1;border-radius:.15em;background:var(--line-2)}
.pv-bars i:last-child{background:var(--text)}
.pv-field{display:block;height:1.35em;border-radius:.35em;box-shadow:inset 0 0 0 1px var(--line-2);flex:none}
.pv-btn{
  display:inline-flex;align-items:center;justify-content:center;align-self:flex-start;height:1.5em;padding:0 .8em;
  border-radius:.4em;background:var(--inv);color:var(--inv-text);font-size:.72em;line-height:1;white-space:nowrap;
}
.pv-ui{display:flex;align-items:center;gap:.5em;margin-top:auto}
.pv-ui .pv-btn{margin-left:auto}
.pv-pill{font-size:.72em;line-height:1;padding:.3em .6em;border-radius:99em;box-shadow:inset 0 0 0 1px var(--line-2);color:var(--text)}
.pv-cols{display:grid;grid-template-columns:repeat(3,1fr);gap:.35em;flex:1;min-height:0}
.pv-cols > span{display:flex;flex-direction:column;gap:.3em;padding:.3em;border-radius:.35em;background:var(--surface-2);box-shadow:inset 0 0 0 1px var(--line)}
.pv-cols i{height:1.1em;border-radius:.25em;background:var(--surface);box-shadow:0 0 0 1px var(--line)}
.pv-rows{display:flex;flex-direction:column;flex:1}
.pv-rows i{display:flex;align-items:center;justify-content:space-between;gap:.6em;height:1.7em;border-bottom:1px solid var(--line)}
.pv-rows i:last-child{border-bottom:0}
.pv-rows s{display:block;width:45%;height:.42em;border-radius:.21em;background:var(--line)}
.pv-rows b{font-weight:400;font-size:.72em;color:var(--text);font-variant-numeric:tabular-nums}

/* Label under the art. */
.fd-label{display:flex;align-items:baseline;gap:8px;margin-top:16px;font-weight:700}

/* ── Kind words ────────────────────────────────────────────────────────── */

.kws{list-style:none;margin:0;padding:0;border-top:1px solid var(--line)}
.kw{position:relative;padding:18px 0 16px;border-bottom:1px solid var(--line)}
.kws:not([data-all]) .kw:nth-child(n+4){display:none}
.kws-all{
  align-self:flex-start;display:inline-flex;align-items:baseline;gap:6px;height:30px;margin-top:6px;padding:0 2px;
  border:0;background:none;font:inherit;font-size:12px;color:var(--text-2);cursor:pointer;border-radius:4px;
}
.kws-all:hover{color:var(--text)}
.kws-all:focus-visible{outline:none;box-shadow:var(--ring)}
.kw-fig{margin:0;display:flex;flex-direction:column;gap:12px}
.kw-quote{margin:0}
.kw-quote p{
  margin:0;font-size:13.5px;line-height:1.65;color:var(--text);
  text-wrap:pretty;hanging-punctuation:first allow-end last;
  display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2;overflow:hidden;
}
.kw[data-open] .kw-quote p{display:block;-webkit-line-clamp:unset;line-clamp:none}
.kw-who{display:flex;align-items:center;gap:8px;min-width:0;font-size:12px}
.kw-name{font-weight:700;color:var(--text);white-space:nowrap}
.kw-role{color:var(--text-3);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.kw-more{
  position:absolute;right:0;bottom:16px;height:22px;padding:0 2px;border:0;background:none;
  font:inherit;font-size:12px;color:var(--text-2);cursor:pointer;border-radius:4px;
  text-decoration:underline;text-decoration-color:var(--line-2);text-underline-offset:4px;
}
.kw-more:hover{color:var(--text);text-decoration-color:currentColor}
.kw-more:focus-visible{outline:none;box-shadow:var(--ring)}
.kw-more[hidden]{display:none}
/* The video sits as a thumbnail, then opens to the full column once it plays. */
.kw-video{
  position:relative;width:min(100%,240px);aspect-ratio:16/9;border-radius:8px;overflow:hidden;
  background:var(--surface);box-shadow:0 0 0 1px var(--line);transition:width .34s var(--ease);
}
.kw[data-playing] .kw-video{width:100%}
.kw-video video{display:block;width:100%;height:100%;object-fit:cover}
.kw-play{position:absolute;inset:0;display:grid;place-items:center;padding:0;border:0;background:none;color:#fff;cursor:pointer}
.kw-play:focus-visible{outline:none;box-shadow:inset 0 0 0 2px #fff}
.kw-play-ic{
  width:36px;height:36px;display:grid;place-items:center;padding-left:2px;border-radius:50%;
  background:rgba(20,20,26,.55);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);
  transition:scale .2s var(--ease),background .15s var(--ease);
}
.kw-play:hover .kw-play-ic{scale:1.08;background:rgba(20,20,26,.7)}
.kw-dur{position:absolute;left:8px;bottom:8px;padding:2px 6px;border-radius:4px;background:rgba(20,20,26,.55);font-size:11px;line-height:1.4}
.kw-play[hidden]{display:none}
@media (prefers-reduced-motion:reduce){.kw-video{transition:none}}
@media (max-width:640px){.kw-role{white-space:normal}.kw-who{flex-wrap:wrap}.kw-more{position:static;margin-top:8px}}

/* ── Folder dialog ─────────────────────────────────────────────────────────
   Native <dialog>: top layer, inert page behind, Escape for free. It grows
   out of the folder it came from and shrinks back into it (main.js).     */

.fdlg{
  width:min(600px,calc(100vw - 32px));max-height:min(78vh,720px);
  padding:0;border:0;border-radius:12px;background:var(--surface);color:var(--text);
  box-shadow:var(--sh-4);overflow:hidden;
}
.fdlg[open]{display:flex;flex-direction:column}
.fdlg::backdrop{background:var(--overlay, rgba(20,20,26,.32))}
.fdlg:focus-visible{box-shadow:var(--sh-4)}
.fdlg-head{display:flex;align-items:center;gap:8px;height:52px;flex:none;padding:0 12px 0 20px;border-bottom:1px solid var(--line)}
.fdlg-ic{width:20px;height:20px;display:grid;place-items:center;color:var(--text-2);margin-left:-2px}
.fdlg-title{margin:0;font-size:13px;font-weight:700}
.fdlg-head .section-sub{margin-right:4px}
.fdlg-body{flex:1;min-height:0;overflow-y:auto;padding:8px 20px 12px;overscroll-behavior:contain}
@media (max-width:640px){
  .fdlg{width:calc(100vw - 16px);max-height:86vh;margin:auto auto 8px}
  .fdlg-head{padding-left:16px}
  .fdlg-body{padding:6px 16px 10px}
}

/* ── Browser windows (Work) ────────────────────────────────────────────────
   The Work dialog widens to hold a 2×2 grid. Every window is 2:1, so stills
   and scrollable full-page captures line up at rest. A scrollable one shows
   the hero first and scrolls in place; the scroll chains back to the dialog
   at either end. */

.fdlg-wide{width:min(1120px,calc(100vw - 48px));max-height:calc(100vh - 48px)}
/* The windows are raised surfaces, so the ground under them recedes to the page
   colour. The head stays on --surface, like every other dialog head. */
.fdlg-wide .fdlg-body{padding:20px 20px 8px;background:var(--bg)}

.bw-grid{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:1fr 1fr;gap:20px 20px}
.lists .bw-grid{grid-template-columns:1fr;gap:24px}
/* List view: three sites, then the third fades into the page under a "View more". */
.peek .bw:nth-child(n+4){display:none}
.peek-more{
  position:relative;z-index:2;display:flex;justify-content:center;align-items:flex-end;
  height:340px;margin-top:-340px;padding-bottom:4px;
  background:linear-gradient(to bottom,transparent,color-mix(in srgb,var(--bg) 70%,transparent) 45%,var(--bg) 85%);
  pointer-events:none;
}
/* A blur under the fade that builds in from the top, so the image softens
   before it disappears rather than just washing out. */
.peek-more::before{
  content:"";position:absolute;inset:0;z-index:-1;
  backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);
  mask-image:linear-gradient(to bottom,transparent,#000 50%);
}
.peek-more .btn{pointer-events:auto}
.lists .bw:focus{outline:none}
@media (max-width:640px){
  .bw-grid{grid-template-columns:1fr;gap:20px}
  .fdlg-wide{width:calc(100vw - 16px);max-height:calc(100vh - 16px)}
  .fdlg-wide .fdlg-body{padding:14px 12px 4px}
}

.bw-card{
  display:flow-root;background:var(--surface);border-radius:11px;
  box-shadow:0 0 0 1px var(--line),var(--sh-1),0 14px 32px -16px rgba(20,20,26,.18);
}
.bw-bar{display:flex;align-items:center;gap:12px;height:38px;padding:0 14px}
.bw-dots{display:flex;gap:5px}
.bw-dots i{width:7px;height:7px;border-radius:50%;background:var(--line-2)}
.bw-client{font-size:12px;color:var(--text);padding-top:1px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.bw-client b{font-weight:700}
.bw-pct{margin-left:auto;font-size:10.5px;color:var(--text-3);font-variant-numeric:tabular-nums}
.bw-vp-wrap{position:relative}
.bw-vp{
  position:relative;margin:0 6px 6px;aspect-ratio:2/1;overflow:hidden;border-radius:6px;
  background:var(--surface-2);box-shadow:0 0 0 1px var(--line);
}
.bw-vp img{display:block;width:100%;height:auto}
/* Scrollable windows don't take the wheel until you click (or tab) into them,
   so scrolling the page past them never gets caught. Live, they scroll and wear
   a quiet ring; leaving the window or pressing Esc hands the wheel back. */
.bw[data-scroll] .bw-vp{overflow-y:hidden;cursor:pointer;scrollbar-width:thin;scrollbar-color:var(--line-2) transparent;transition:box-shadow .15s var(--ease)}
.bw[data-live] .bw-vp{overflow-y:auto;cursor:auto;box-shadow:0 0 0 1px var(--text-3)}
.bw-vp:focus-visible{outline:none;box-shadow:var(--ring)}
/* Under the window: when and with whom on the left, what on the right. */
.bw-meta{
  display:flex;justify-content:space-between;align-items:baseline;gap:16px;
  margin:12px 0 0;padding:0 2px 14px;font-size:12px;line-height:17px;
}
.bw-when{color:var(--text)}
.bw-with{color:var(--text-3)}
.bw-services{color:var(--text-3);text-align:right}

/* The empty slot: a window with nothing loaded yet. */
.bw-empty .bw-card{background:transparent;box-shadow:0 0 0 1px var(--line)}
.bw-empty .bw-vp{display:grid;place-items:center;background:transparent;box-shadow:none;
  background-image:repeating-linear-gradient(-45deg,var(--line) 0 1px,transparent 1px 9px)}
.bw-soon{font-size:11px;color:var(--text-3);background:var(--surface);padding:4px 8px;border-radius:6px;box-shadow:0 0 0 1px var(--line)}

/* The hint: a glass pill that pops in after the dialog lands, a mouse whose
   wheel rolls, and letters that ride a wave. It reads "click to scroll" until
   the window is live, then "scroll me" until you do. */
.bw-hint{
  position:absolute;left:50%;bottom:16px;display:flex;align-items:center;gap:7px;
  height:28px;padding:0 12px 0 10px;border-radius:999px;pointer-events:none;
  /* Fixed light glass, not theme tokens: it sits on a screenshot, and the
     screenshot doesn't change with the theme. A dark pill vanished on dark heroes. */
  background:rgba(255,255,255,.86);color:#1b1b1f;font-size:11.5px;letter-spacing:-.2px;
  -webkit-backdrop-filter:blur(10px) saturate(1.4);backdrop-filter:blur(10px) saturate(1.4);
  box-shadow:0 0 0 1px rgba(255,255,255,.5) inset,0 0 0 1px rgba(20,20,26,.08),0 8px 24px -6px rgba(0,0,0,.45);
  translate:-50% 0;transition:opacity .25s var(--ease),translate .3s var(--ease),scale .3s var(--ease);
  animation:bw-in .5s var(--spring,cubic-bezier(.3,1.4,.55,1)) .45s backwards;
}
.bw[data-live][data-scrolled] .bw-hint{opacity:0;translate:-50% 8px;scale:.9}
@keyframes bw-in{from{opacity:0;translate:-50% 10px;scale:.8}}
.bw-mouse{width:9px;height:13px;border-radius:5px;box-shadow:inset 0 0 0 1.25px currentColor;display:flex;justify-content:center;padding-top:2.5px;box-sizing:border-box}
.bw-mouse i{width:1.5px;height:3px;border-radius:1px;background:currentColor;animation:bw-wheel 1.4s var(--ease) infinite}
@keyframes bw-wheel{0%{translate:0 0;opacity:1}60%{translate:0 4px;opacity:0}61%,100%{translate:0 0;opacity:0}75%,100%{opacity:1}}
.bw-wave{display:flex}
.bw-wave i{font-style:normal;display:inline-block;animation:bw-wave 1.4s var(--ease) infinite;animation-delay:calc(var(--i) * 55ms)}
@keyframes bw-wave{0%,50%,100%{translate:0 0}22%{translate:0 -3px}}
@media (prefers-reduced-motion:reduce){
  .bw-hint{animation:none}
  .bw-mouse i,.bw-wave i{animation:none}
}

/* ── Expandable rows (Work) ────────────────────────────────────────────────
   The chevron sits where the arrow does. Open rows keep it, so the state is
   visible at rest; the detail slides on grid-template-rows, which animates
   from 0fr without measuring anything.                                   */

.row[aria-expanded="true"]{padding-right:30px}
.row[aria-expanded="true"] .row-go{opacity:1;translate:0 0}
.row-go.chev{translate:0 -3px}
.row-go.chev svg{transition:rotate .2s var(--ease)}
.row[aria-expanded="true"] .row-go.chev svg{rotate:180deg}

.row-detail{display:grid;grid-template-rows:0fr;transition:grid-template-rows .24s var(--ease)}
.row[aria-expanded="true"] + .row-detail{grid-template-rows:1fr}
.row-detail-in{min-height:0;overflow:hidden}
.row-detail p{
  margin:0;padding:4px 0 12px;color:var(--text-2);   /* face and size come from the type system rule */
  text-wrap:pretty;max-width:56ch;
}
.metrics{margin:0 0 14px;display:flex;flex-wrap:wrap;gap:6px 24px}
.metrics > div{display:flex;flex-direction:column;gap:1px}
.metrics dt{font-size:11px;color:var(--text-3)}
.metrics dd{margin:0;font-size:12.5px;color:var(--text);font-variant-numeric:tabular-nums}
.row-detail + .row,.exp + li .row{margin-top:0}

/* Phone: the Work chevron lines up with the title's first line, not the row's middle.
   It is always visible here, so it also drops the 3px resting nudge that desktop uses
   while the chevron is hidden (.row-go.chev is more specific than .exp .row-go). */
@media (max-width:640px){
  .exp .row-go,.exp .row-go.chev{top:18px;margin-top:-7px;translate:0 0}
}

/* ── Page report ───────────────────────────────────────────────────────── */

.report{
  position:fixed;left:50%;top:8vh;z-index:201;width:min(600px,calc(100vw - 32px));
  max-height:84vh;display:flex;flex-direction:column;overflow:hidden;
  background:var(--surface);border-radius:12px;box-shadow:var(--sh-4);
  opacity:0;pointer-events:none;transform:translate(-50%,8px) scale(.985);
  transition:opacity .18s,transform .22s var(--ease);
}
.report.open{opacity:1;pointer-events:auto;transform:translate(-50%,0) scale(1)}
/* Two lines of text, so the bar sizes to its content with room to breathe
   rather than sitting in a 52px row meant for one line. */
.r-head{display:flex;align-items:center;gap:12px;min-height:52px;flex:none;padding:16px 12px 16px 20px;border-bottom:1px solid var(--line)}
.r-title{margin:0;font-size:13px;font-weight:700}
.r-sub{font-size:12px;color:var(--text-3)}
.r-body{flex:1;min-height:0;overflow-y:auto;padding:16px 20px 20px;display:flex;flex-direction:column;gap:20px}
.r-h{display:flex;align-items:baseline;gap:8px;margin:0 0 8px;font-size:12px;font-weight:700}
.r-h .count{font-weight:400}
.r-body .stats{background:var(--bg)}
.r-body .stat{flex-basis:120px;padding:10px 14px 11px}
.r-body .stat-label{display:flex;align-items:baseline;gap:6px;overflow:visible}
.r-body .stat-label .sub{font-size:11px;color:var(--text-3);overflow:hidden;text-overflow:ellipsis}
.r-body .stat-value{font-size:18px}
.r-body .stat-foot .pill{margin-left:-7px}
.r-body .stat-foot .pill[data-t="3"],.r-body .stat-foot .pill[data-t="4"]{margin-left:0}
.r-meters{display:flex;flex-direction:column;gap:10px}
.chk{display:flex;align-items:center;gap:10px;min-height:30px;padding:0 0 0 2px;border-bottom:1px solid var(--line);font-size:12.5px;color:var(--text-2)}
.chk:last-child{border-bottom:0}
.chk .ic{width:14px;height:14px;flex:none;display:grid;place-items:center;color:var(--text-2)}
.chk .label{flex:1;min-width:0}
.chk .val{flex:none;font-size:11.5px;color:var(--text-3);font-variant-numeric:tabular-nums}
.chk[data-pass="false"]{color:var(--text)}
.chk[data-pass="false"] .ic{color:var(--text)}
.chk[data-pass="false"] .val{color:var(--text);font-weight:700}
.r-foot{
  display:flex;align-items:center;gap:6px;flex-wrap:wrap;min-height:38px;flex:none;padding:0 20px;
  border-top:1px solid var(--line);background:var(--surface-2);font-size:11.5px;color:var(--text-3);
}
.r-foot .link{color:var(--text-2)}
.r-foot .src{margin-left:auto}
@media (hover:none){.r-foot .src{display:none}}      /* view-source: does not open on phones */
@media (max-width:520px){
  .r-head{padding-left:16px}
  .r-sub{display:none}
  .r-body{padding:14px 16px 16px}
  .r-foot{padding:0 16px}
  .chk{padding:6px 0 6px 2px;align-items:flex-start;line-height:1.45}
  .chk .ic{height:18px}
  .chk .val{display:none}
}
.r-foot .link:hover{color:var(--text)}

/* ── Timeline ──────────────────────────────────────────────────────────────
   The one element that leaves the 600px column. body is an inline-size
   container, so 100cqw is the viewport minus the scrollbar gutter — 100vw
   would include it and scroll the page sideways by 15px. The canvas pads
   itself so the first year sits flush with the text column above.       */

body{container-type:inline-size}

.tl{
  --yr:148px;--fade:96px;--pad:24px;--tail:48px;
  /* Inset from the window edges, capped on very wide screens. Centred on the
     column, so the first year still lands on its left edge. */
  --inset:64px;--w:min(100cqw - var(--inset) * 2, 1280px);
  position:relative;width:var(--w);margin:10px 0 0 calc(50% - var(--w) / 2);
  user-select:none;-webkit-user-select:none;
}
@media (max-width:640px){.tl{--yr:96px;--fade:28px;--tail:36px;--inset:0px}}   /* a phone has no room to spare */

.tl-scroll{
  overflow-x:auto;overflow-y:hidden;outline:none;cursor:grab;
  overscroll-behavior-x:contain;        /* a trackpad fling never becomes "back" in the browser */
  scrollbar-width:none;
}
.tl-scroll::-webkit-scrollbar{display:none}
.tl[data-dragging] .tl-scroll{cursor:grabbing}

.tl-canvas{position:relative}
.tl-canvas::after{content:"";position:absolute;left:0;right:0;bottom:0;height:1px;background:var(--line);transition:background .15s}

/* Ruler: a label per year, a 7px tick per year, a 3px tick per quarter.
   Past years in --text-2, this year in full ink, the future in --text-3. */
.tl-ruler{position:absolute;left:0;right:0;top:0;height:36px;border-bottom:1px solid var(--line);transition:border-color .15s}
.tl-scroll:focus-visible .tl-ruler{border-color:var(--text-3)}
.tl-scroll:focus-visible .tl-canvas::after{background:var(--text-3)}
.tl-ruler::after{
  content:"";position:absolute;left:var(--pad);right:calc(var(--pad) - 1px);bottom:0;height:3px;
  background:repeating-linear-gradient(to right,var(--line-2) 0 1px,transparent 1px calc(var(--yr) / 4));
}
.tl-year{position:absolute;top:0;width:0;height:36px}
.tl-year::after{content:"";position:absolute;left:0;bottom:0;width:1px;height:7px;background:var(--line-2)}
.tl-year span{position:absolute;left:0;top:9px;translate:-50% 0;font-size:11.5px;line-height:16px;color:var(--text-3);white-space:nowrap}
.tl-year[data-when="past"] span{color:var(--text-2)}
.tl-year[data-when="now"] span{color:var(--text);font-weight:700}

/* Today: a hairline in full ink, and a flag you can hover for the date. */
.tl-now{position:absolute;top:36px;bottom:0;width:1px;margin-left:-.5px;background:var(--text);z-index:1;pointer-events:none}
.tl-flag{position:absolute;top:23px;width:7px;height:13px;margin-left:-3.5px;border-radius:2px 2px 3.5px 3.5px;background:var(--text);z-index:2}
.tl-flag::before{content:"";position:absolute;inset:-8px -8px -2px}   /* a hit area bigger than the ink */

/* Pointer guide with a month readout. */
.tl-guide{position:absolute;top:26px;bottom:0;width:1px;margin-left:-.5px;background:var(--line-2);z-index:1;pointer-events:none;opacity:0;transition:opacity .15s}
.tl-guide span{
  position:absolute;bottom:100%;left:0;translate:-50% 0;margin-bottom:1px;padding:1px 6px;border-radius:5px;
  background:var(--inv);color:var(--inv-text);font-size:11px;line-height:16px;white-space:nowrap;
}
.tl[data-guide] .tl-guide{opacity:1}

/* Bars. */
.tl-bar{
  position:absolute;height:30px;z-index:2;display:flex;align-items:center;gap:12px;
  /* Right padding lives on the bar, not the dates: a sticky label can only
     travel to its container's content edge, so this keeps it off the border. */
  padding-right:11px;
  font-size:12.5px;white-space:nowrap;color:var(--text);
}
/* The pill is painted by a layer of its own, so it can be faded out at its end
   without taking the label with it: the bar's length is the dates, and a label
   longer than that simply carries on past the edge at full ink. */
.tl-bar::before{
  content:"";position:absolute;inset:0;z-index:0;
  border:1px solid var(--line);border-radius:7px;background:var(--surface);
  transition:border-color .15s,background .15s;
}
.tl-bar > *{position:relative;z-index:1}
/* The fall-off is short and weighted late: the pill holds its ink almost to the
   end, then goes in the last few pixels, so the edge still reads as an edge. */
.tl-bar[data-over]::before{
  -webkit-mask-image:linear-gradient(90deg,#000 calc(100% - 14px),rgba(0,0,0,.72) calc(100% - 7px),transparent);
  mask-image:linear-gradient(90deg,#000 calc(100% - 14px),rgba(0,0,0,.72) calc(100% - 7px),transparent);
}
.tl[data-dragging] .tl-bar{pointer-events:none}

/* The label sticks to the inside of the fade, so a role that started off
   screen still says what it is. It is never trimmed to fit: the bar's width is
   the dates and nothing else, so a long role simply runs past the end and
   dissolves there (--cut, set per bar in main.js, is the distance from the
   label's own left edge to the end of the bar). */
.tl-in{position:sticky;left:var(--fl);flex:none;display:flex;align-items:baseline;gap:1ch;padding-left:11px}
.tl-org{color:var(--text-3)}
/* The years and the Now tag sit at the bar's right edge, out of the flex flow,
   so an overflowing label cannot push them along with it. */
.tl-now-tag,.tl-dates{
  position:absolute;right:11px;top:50%;translate:0 -50%;
  font-size:11.5px;transition:opacity .15s;
}
.tl-bar.live .tl-now-tag{right:calc(var(--tail) + 11px)}
.tl-now-tag{display:inline-flex;align-items:center;gap:7px;color:var(--text)}
.tl-now-tag .dot{width:5px;height:5px}
.tl-dates{color:var(--text-2)}

/* Once the sticky label has slid along, it would run into whatever sits at the
   bar's right edge, so that steps aside. */
.tl-bar[data-tight] .tl-dates,.tl-bar[data-tight] .tl-now-tag{opacity:0}

/* Ongoing: the bar runs past today and dissolves. */
.tl-bar.live::before{-webkit-mask-image:linear-gradient(to right,#000 calc(100% - var(--tail)),transparent);mask-image:linear-gradient(to right,#000 calc(100% - var(--tail)),transparent)}


/* Side work: hatched and dashed, so it reads as alongside rather than instead. */
.tl-bar.side::before{
  border-style:dashed;border-color:var(--line-2);
  background:repeating-linear-gradient(-45deg,var(--line) 0 1px,transparent 1px 6px),var(--bg);
}
.tl-bar.side .tl-title{color:var(--text-2)}



.sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}

/* Edges. Two parts, each doing one job.
   1. The track is masked to transparent at both ends, so bars and ticks fade
      into the real page ground rather than into a painted copy of it. (A
      painted gradient banded visibly in Chrome; a mask does not.)
   2. Over that, three backdrop blurs, each stronger and narrower, so the blur
      ramps instead of starting at a seam. Their masks also feather top and
      bottom, or the blur samples past the edge and leaves a smudged line.
   Both ease away when you reach an end: the mask via registered properties,
   so the gradient stops themselves can transition.                        */

@property --fl{syntax:"<length>";inherits:true;initial-value:0px}
@property --fr{syntax:"<length>";inherits:true;initial-value:0px}
.tl{--fl:var(--fade);--fr:var(--fade);transition:--fl .24s var(--ease),--fr .24s var(--ease)}
.tl[data-start]{--fl:0px}
.tl[data-end]{--fr:0px}
.tl-scroll{
  -webkit-mask-image:linear-gradient(to right,transparent,#000 var(--fl),#000 calc(100% - var(--fr)),transparent);
  mask-image:linear-gradient(to right,transparent,#000 var(--fl),#000 calc(100% - var(--fr)),transparent);
}

.tl-fade{position:absolute;top:-12px;bottom:-12px;width:var(--fade);z-index:3;pointer-events:none;transition:opacity .24s var(--ease)}
.tl-fade.l{left:0}
.tl-fade.r{right:0;scale:-1 1}
.tl-fade i{
  position:absolute;inset:0;
  -webkit-mask-composite:source-in;mask-composite:intersect;
}
.tl-fade i:nth-child(1){--w:100%;-webkit-backdrop-filter:blur(1px);backdrop-filter:blur(1px)}
.tl-fade i:nth-child(2){--w:66%;-webkit-backdrop-filter:blur(2.5px);backdrop-filter:blur(2.5px)}
.tl-fade i:nth-child(3){--w:33%;-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}
.tl-fade i{
  -webkit-mask-image:linear-gradient(to right,#000,transparent var(--w)),linear-gradient(to bottom,transparent,#000 12px,#000 calc(100% - 12px),transparent);
  mask-image:linear-gradient(to right,#000,transparent var(--w)),linear-gradient(to bottom,transparent,#000 12px,#000 calc(100% - 12px),transparent);
}
.tl[data-start] .tl-fade.l,.tl[data-end] .tl-fade.r{opacity:0}

/* When a bar is mostly off the left edge, its sticky label runs into its own
   dates. The dates step aside first. */


.tl-hint kbd{margin-left:3px}
.tl-hint kbd + kbd{margin-left:-1px}
@media (hover:none){.tl-hint{display:none}}

/* ── Skills ────────────────────────────────────────────────────────────────
   A slow marquee of outlined chips. The track holds the list twice and slides
   by exactly one copy, so the loop has no seam. The ends fade and blur out
   instead of clipping hard. main.js eases the speed down on hover by playback
   rate, so the chips never jump the way swapping the duration would.      */

.marquee{
  --fade:72px;
  position:relative;overflow:hidden;margin:4px 0 0;padding:4px 0;
  mask-image:linear-gradient(90deg,transparent,#000 var(--fade),#000 calc(100% - var(--fade)),transparent);
}
/* A soft blur over each end, on top of the fade, so chips dissolve rather than dim. */
.marquee::before,.marquee::after{
  content:"";position:absolute;top:0;bottom:0;width:var(--fade);z-index:1;pointer-events:none;
  backdrop-filter:blur(3px);-webkit-backdrop-filter:blur(3px);
}
.marquee::before{left:0;mask-image:linear-gradient(90deg,#000,transparent)}
.marquee::after{right:0;mask-image:linear-gradient(270deg,#000,transparent)}
@media (max-width:640px){.marquee{--fade:40px}}

.mq-track{display:flex;width:max-content;animation:mq 40s linear infinite}
@keyframes mq{to{translate:-50% 0}}
.mq-set{display:flex;gap:8px;margin:0;padding:0 8px 0 0;list-style:none}
.sk{
  display:flex;align-items:center;gap:7px;height:30px;padding:0 11px 0 10px;
  border:1px solid var(--line);border-radius:2px;background:var(--surface);   /* the photos' corner */
  font-size:13px;color:var(--text);white-space:nowrap;
}
.sk-ic{flex:none;color:var(--text-2)}
.sk-logo{padding:0 12px}   /* a wordmark that is its own label */

@media (prefers-reduced-motion:reduce){
  .mq-track{animation:none}
  .marquee{overflow-x:auto;scrollbar-width:none}
  .mq-set[aria-hidden]{display:none}
}

/* ── Behind the screen ─────────────────────────────────────────────────────
   Instant-film prints fanned in a loose arc. The fan is a little wider than
   the text column, so the outer prints spill ~40px past it on each side. It is
   sized off its own width (a size container) so the three prints always fit
   edge to edge; on a phone it shrinks back to the column.
   Each print carries its own lean (--r), drop (--y) and nudge (--x), a touch
   irregular on purpose, so it reads as placed by hand, not laid out.     */

:root{--paper:#fff}
:root[data-theme="dark"]{--paper:color-mix(in srgb,var(--text) 90%,var(--bg))}

.shots{
  --count:3;--overlap:.1;
  container-type:inline-size;
  /* The column plus 40px each side. The 48px floor from the viewport edge leaves
     room for the outer prints' lean, which pushes a corner out past the row. */
  width:min(100cqw - 96px,680px);margin:14px 0 0 calc(50% - min(100cqw - 96px,680px) / 2);
  display:flex;justify-content:center;align-items:flex-start;padding:4px 0 48px;
}
@media (max-width:640px){
  /* No room to spill on a phone: the fan matches the column, and the 16px gutter
     takes the corner a 7° lean pushes out. */
  .shots{width:calc(100cqw - 64px);margin-left:calc(50% - (100cqw - 64px) / 2)}
}

.shot{
  /* The natural size at 12% overlap, floored. The negative margins between
     prints take up whatever the row overflows by, so the overlap grows as the fan narrows. */
  --w:max(calc(100cqw / (var(--count) - (var(--count) - 1) * var(--overlap))),96px);
  --lift:14px;--grow:1.08;   /* the hover pose; main.js reads these to place the caption */
  position:relative;flex:none;width:var(--w);margin:0 0 0 calc((100cqw - var(--count) * var(--w)) / (var(--count) - 1));
  --foot:max(calc(var(--w) * .12),36px);   /* room for the 12px caption on two lines when the print is narrow */
  padding:calc(var(--w) * .055) calc(var(--w) * .055) var(--foot);
  background:var(--paper);border-radius:2px;
  box-shadow:0 0 0 1px var(--line),0 1px 2px rgba(20,20,26,.06),0 8px 18px -8px rgba(20,20,26,.22);
  transform-origin:50% 90%;
  rotate:var(--r);translate:var(--x) var(--y);
  /* Leaving: settle first, then drop back under the neighbours. */
  transition:rotate .3s var(--ease),scale .3s var(--ease),translate .3s var(--ease),box-shadow .3s var(--ease),z-index 0s linear .3s;
}
.shot:first-child{margin-left:0}   /* overlap lives between prints, so the row spans the fan exactly */
.shot-photo{
  display:block;aspect-ratio:4 / 5;border-radius:1px;background:var(--scene);
  box-shadow:inset 0 0 0 1px rgba(0,0,0,.08);overflow:hidden;
}
.shot-photo img{display:block;width:100%;height:100%;object-fit:cover}

/* The caption is printed on the film, not floated above it: it lives in the
   deeper bottom border and appears when the print comes forward. Same 12px
   as the site's other small captions, whatever the print size. */
.shot-cap{
  position:absolute;left:.5em;right:.5em;bottom:0;height:var(--foot);
  display:grid;place-items:center;text-align:center;
  font-size:12px;line-height:1.1;color:var(--text-2);
  letter-spacing:-.2px;overflow:hidden;text-wrap:balance;
  opacity:0;transition:opacity .2s var(--ease);
}
@media (max-width:640px){.shot-cap{font-size:10px}}   /* a phone print is ~125px wide */
.shot:is(:hover,:active) .shot-cap{opacity:1;transition-duration:.28s}

/* Arriving: straighten, grow a little, lift, and come to the front at once. */
.shot:is(:hover,:active){
  z-index:10;rotate:0deg;scale:var(--grow);translate:var(--x) calc(var(--y) - var(--lift));
  box-shadow:0 0 0 1px var(--line),0 2px 4px rgba(20,20,26,.08),0 22px 36px -14px rgba(20,20,26,.35);
  transition:rotate .38s var(--spring),scale .38s var(--spring),translate .38s var(--spring),box-shadow .38s var(--ease),z-index 0s;
}

/* ── Contact ───────────────────────────────────────────────────────────── */

#contact{gap:10px}

/* ── Terminal ──────────────────────────────────────────────────────────────
   The contact section as a small shell. It is a Slate surface, not a skin:
   same hairline frame and 8px radius as any box on the page, grey window
   dots (no traffic lights, there is no accent), the site's own mono type.
   Output lines use the three text steps: the echoed command in full ink,
   results in --text-2, hints in --text-3.                                 */

.term{
  margin-top:8px;overflow:hidden;
  background:var(--surface);border:1px solid var(--line);border-radius:8px;
  transition:border-color .15s var(--ease);
}
.term:focus-within{border-color:var(--line-2)}

.term-head{display:flex;align-items:center;gap:10px;height:32px;padding:0 12px;border-bottom:1px solid var(--line);background:var(--surface-2)}
.term-dots{display:flex;gap:6px}
.term-dots i{width:9px;height:9px;border-radius:50%;background:var(--line-2)}
.term-title{flex:1;margin-right:48px;text-align:center;font-size:11.5px;color:var(--text-3)}   /* 48px ≈ the dots, so the title centres on the window */

.term-log{
  height:208px;overflow-y:auto;padding:12px 14px 4px;
  font-size:13px;line-height:1.65;color:var(--text-2);
  overscroll-behavior:contain;scroll-padding-bottom:4px;
}
.term-log > div{white-space:pre-wrap;overflow-wrap:anywhere;min-height:1.65em}
.term-log .cmd{color:var(--text)}
.term-log .cmd::before{content:"❯ ";color:var(--text-3)}
.term-log .dim{color:var(--text-3)}
.term-log .ink{color:var(--text)}
.term-log .err{color:var(--text)}
.term-log .err::before{content:"✗ ";color:var(--text-3)}
.term-log .ok::before{content:"✓ ";color:var(--text-3)}
.term-log .gap{min-height:.6em}
/* Help rows: the command column is a fixed number of characters wide, so every
   description starts on the same column, the way a real --help does. */
.term-log .row2{display:grid;grid-template-columns:15ch minmax(0,1fr);gap:0 1ch}
.term-log .row2 > :first-child{color:var(--text)}
.term-log a{color:var(--text);text-decoration:underline;text-decoration-color:var(--line-2);text-underline-offset:3px;transition:text-decoration-color .15s}
.term-log a:hover{text-decoration-color:currentColor}
/* Commands in output are clickable and read as commands, not links. */
.term-log button.run{color:var(--text);border-radius:3px;transition:background .15s}
.term-log button.run:hover{background:var(--hover)}

.term-line{display:flex;align-items:center;gap:1ch;height:38px;padding:0 14px;font-size:13px}
.term-prompt{color:var(--text-3);flex:none;cursor:text;user-select:none}
.term-field{position:relative;flex:1;min-width:0;display:flex;height:100%}
.term-in{
  flex:1;min-width:0;height:100%;padding:0;border:0;outline:none;background:none;
  font-size:13px;color:var(--text);caret-color:var(--text);caret-shape:block;
}
.term-in:focus-visible{box-shadow:none}
.term-in::placeholder{color:var(--text-3)}
/* Autocomplete preview. The typed part is invisible but occupies the same space
   (mono, same tracking), so the remainder sits exactly after the caret. */
.term-ghost{position:absolute;left:0;top:0;bottom:0;display:flex;align-items:center;pointer-events:none;white-space:pre;font-size:13px;color:var(--text-3)}
.term-ghost .typed{visibility:hidden}
/* iOS zooms the page into any input under 16px. So on a phone the field is set
   at 16px and drawn at 13/16 scale: it looks like the rest of the terminal and
   iOS stays put. The ghost scales with it so the two stay letter-aligned. */
@media (max-width:640px){
  .term-field{overflow:hidden}
  .term-in,.term-ghost{font-size:16px;scale:.8125;transform-origin:0 50%}
  .term-in{flex:none;width:calc(100% / .8125)}
}

.term-chips{display:flex;flex-wrap:wrap;align-items:center;gap:6px;padding:0 14px 12px}
.term-chip{
  height:24px;padding:0 8px;border-radius:6px;font-size:12px;color:var(--text-2);
  box-shadow:inset 0 0 0 1px var(--line);transition:color .15s,box-shadow .15s,background .15s;
}
.term-chip:hover{color:var(--text);box-shadow:inset 0 0 0 1px var(--line-2);background:var(--hover)}
.term-chip:focus-visible{box-shadow:var(--ring)}

/* ── Footer ────────────────────────────────────────────────────────────── */

.foot{
  display:flex;align-items:center;gap:12px;height:52px;margin-top:72px;
  border-top:1px solid var(--line);color:var(--text-3);font-size:12px;
}
/* Breaks out of .page to run edge to edge; the footer sits on its waterline. */
.opera{position:relative;width:100vw;margin:48px 0 0 calc(50% - 50vw);height:clamp(260px,42vw,560px);overflow:hidden}
.opera + .foot{margin-top:0}
.clock{position:relative;display:inline-flex;align-items:center;gap:8px;font-size:12px;cursor:default;border-radius:4px;outline:none}
.clock:focus-visible{box-shadow:var(--ring)}
.clock-time{color:var(--text)}

/* The card pops up from the time. */
.clock-card{
  position:absolute;left:-14px;bottom:calc(100% + 10px);z-index:20;
  display:flex;flex-direction:column;align-items:center;gap:3px;width:max-content;min-width:150px;padding:9px 14px;text-align:center;
  border-radius:10px;background:var(--surface);box-shadow:0 0 0 1px var(--line),var(--sh-3);
  pointer-events:none;opacity:0;
  transform-origin:28px 100%;scale:.92;translate:0 4px;
  transition:scale .22s var(--ease),translate .22s var(--ease),opacity .14s var(--ease);
}
.clock-diff{color:var(--text);font-size:12px;white-space:nowrap}
.clock-mood{color:var(--text-3);font-size:11.5px;white-space:nowrap}
.clock-mood:empty{display:none}
.clock:focus-visible .clock-card,.clock[data-open] .clock-card{opacity:1;scale:1;translate:0 0;transition:scale .34s var(--spring),translate .34s var(--spring),opacity .12s var(--ease)}
@media (hover:hover){
  .clock:hover .clock-card{opacity:1;scale:1;translate:0 0;transition:scale .34s var(--spring),translate .34s var(--spring),opacity .12s var(--ease)}
}
.colon{display:inline-block;animation:tick 1s steps(1) infinite}
@keyframes tick{50%{opacity:.4}}
.hint{display:inline-flex;align-items:center;gap:6px}
.hint kbd + kbd{margin-left:-5px}   /* 6px flex gap − 5px = 1px between caps */
@media (hover:none){.hint{display:none}}

/* ── Palette and toast adjustments ─────────────────────────────────────── */

.palette-item .keys kbd{min-width:18px;text-align:center}
.palette-item .hint-sub{font-size:11.5px;color:var(--text-3);flex:none}
.secret-hint{opacity:0;transition:opacity .4s}
.palette-foot:hover .secret-hint{opacity:1}
.palette-group{font-weight:400;letter-spacing:.04em}

/* The search menu sits a step below body size: it's a list of commands, not
   reading. Group labels stay small (capitals at 14px shout). */
.palette{font-size:13px}
.palette-input,.palette-item .label,.palette-empty{font-size:13px}
.palette-foot{font-size:12px}
.palette-group{font-size:11px}
.palette kbd{letter-spacing:-.4px}

/* Key caps in a pair sit 1px apart, wherever the pair appears. */
.tip kbd + kbd{margin-left:-5px}          /* .tip gap is 6px */
.palette-foot kbd + kbd{margin-left:-4px} /* .palette-foot > span gap is 5px */
/* Same trick as the terminal: 16px keeps iOS from zooming on focus, drawn at
   13/16 so it matches the items. The input stretches to fill the space the
   scale gives back, so long queries aren't clipped early. */
@media (max-width:640px){
  .palette-input{font-size:16px;scale:.8125;transform-origin:0 50%;margin-right:calc(-23.08% + 10px)}
}

.toast{white-space:nowrap}
.toast #toast-text{display:inline-flex;align-items:center;gap:8px;padding:2px 7px 2px 0}

/* ── Blueprint mode (G) ────────────────────────────────────────────────── */

.blueprint{
  position:fixed;inset:0;z-index:150;pointer-events:none;opacity:0;transition:opacity .2s;
  background-image:linear-gradient(to bottom,var(--line) 1px,transparent 1px);
  background-size:100% 8px;
}
html[data-blueprint] .blueprint{opacity:1}
.bp-col{
  height:100%;margin:0 auto;width:100%;max-width:calc(var(--page) + var(--gutter) * 2);
  border-inline:1px dashed var(--line-2);
  background:
    linear-gradient(to right,rgba(128,128,128,.07) var(--gutter),transparent var(--gutter),transparent calc(100% - var(--gutter)),rgba(128,128,128,.07) calc(100% - var(--gutter)));
}
.bp-size{
  position:fixed;right:12px;bottom:12px;font-size:10.5px;padding:3px 6px;border-radius:5px;
  background:var(--inv);color:var(--inv-text);
}
html[data-blueprint] .page *{outline:1px solid rgba(128,128,128,.14)}

/* ── Easter egg: confetti (monochrome, obviously) ──────────────────────── */

.confetti{position:fixed;inset:0;z-index:700;pointer-events:none;width:100vw;height:100vh}

/* ── Print: the page as a one-pager CV ─────────────────────────────────── */

@media print{
  .head .icon-btn,.menu-btn,.foot,.skip,.anchor,.row-go,.toast,.palette,.overlay,.blueprint,.report,.opera,#timeline,#offscreen,.term,.view-sw,.fd-grid{display:none!important}
  .fdlg{display:block!important;position:static;width:auto;max-height:none;box-shadow:none;margin:0 0 24px}
  .fdlg [data-close]{display:none}
  .row-detail{grid-template-rows:1fr!important}
  .reveal{animation:none}
  .page{padding:0}
  body{background:#fff;color:#000}
}
