/* The site's one stylesheet. Every page links it and none carries CSS of its own.

   It was two — inline CSS on the homepage, prose.css on the detail pages — which
   worked only because a full page load replaced the <head> along with the body.
   Swup replaces the body and leaves the <head> alone, so two stylesheets would
   mean navigating to a detail page and getting the homepage's CSS. One file is
   what makes the swap safe.

   It also makes it cheaper: after the first load, a navigation fetches no CSS at
   all. The homepage pays one render-blocking request it did not pay before, and
   every page after that pays nothing. */

@font-face{
  font-family:'Geist';font-style:normal;font-weight:100 900;font-display:swap;
  src:url('/assets/geist-latin.woff2') format('woff2');
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face{
  font-family:'Geist';font-style:normal;font-weight:100 900;font-display:swap;
  src:url('/assets/geist-latin-ext.woff2') format('woff2');
  unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}

:root{
  --bg:#0a0a0a;
  --fg:#ededed;
  --fg-2:#9b9b9b;
  --surface:#171717;
  --line:#1e1e1e;
  --focus:#ededed;
  --ease:cubic-bezier(.32,.72,0,1);
}

*{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{
  margin:0;
  background:var(--bg);
  color:var(--fg);
  font-family:'Geist',system-ui,-apple-system,'Segoe UI',sans-serif;
  font-size:15px;
  line-height:1.6;
  -webkit-font-smoothing:antialiased;
  -moz-osx-font-smoothing:grayscale;
}
:focus-visible{outline:2px solid var(--focus);outline-offset:2px;border-radius:6px}

/* Announced, never drawn — the same span index.html uses on its outbound rows.
   Prose links carry no arrow, so this is the only cue a screen reader gets that
   the link leaves the site. */
.vh{
  position:absolute;width:1px;height:1px;margin:-1px;padding:0;
  overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0;
}

.page{max-width:640px;margin:0 auto;padding:clamp(32px,11vh,120px) 24px clamp(48px,8vh,96px)}

/* The gap between the header — which never moves — and whatever the page puts
   below it. Owned here rather than by each page's first element, so the two
   kinds of page cannot drift apart: the homepage's label used to open with
   clamp(28px,7vh,64px) and a prose heading with a flat 30px. */
#swup{display:block;margin-top:clamp(28px,7vh,64px)}

.skip{position:absolute;left:-9999px}
.skip:focus{left:20px;top:16px;z-index:9;background:var(--bg);color:var(--fg);padding:6px 10px;border-radius:6px}

/* ---- Head ---- */
/* The portrait is the only colour on the site, and it is allowed to be: it is a
   picture of a person, not chrome — the same exemption a product screenshot has
   on a prose page. Small enough that the page is still read as neutral, and the
   hairline is the same rule the footer uses. */
/* Centred, not top-aligned. Top-aligned put the portrait's top edge on the name's
   line box rather than on its letters — 2px above the visible text — and its
   bottom edge partway through the bio, which is nothing. Neither edge landed on
   anything the eye could see, so it read as unaligned. Centring gives it a
   symmetry instead, and holds when the bio wraps to a different number of lines. */
.head{display:flex;align-items:center;gap:16px}
.me{
  display:block;width:60px;height:60px;
  /* The row radius, not a fifth value invented for one element: the portrait is
     a surface-sized object, and it sits directly above a marker drawn with the
     same corner. */
  border-radius:10px;border:1px solid var(--line);
}
/* The name and the portrait are both links home, and on the homepage that link
   is a no-op the visitor never notices. They are two anchors rather than one
   wrapping both, because a single link would have to span two flex cells; the
   image's is hidden from assistive tech and taken out of the tab order so the
   same destination is not announced twice. */
.ident{display:block;flex:none;border-radius:10px;line-height:0}
.who{
  display:inline-block;
  font-size:15px;font-weight:500;letter-spacing:-.006em;
  color:var(--fg);text-decoration:none;
  text-underline-offset:3px;text-decoration-thickness:1px;
}
.who:hover{text-decoration:underline}

.bio{margin:8px 0 0;max-width:58ch;color:var(--fg-2);text-wrap:pretty}

/* ---- The list. One marker travels; the rows themselves never move. ---- */
/* The homepage's own <h1>, now that the name in the header is a link rather than
   a heading: identity belongs to the header, and each page's heading belongs to
   the page. It reads "What I've built" rather than "Tools" — the bio directly
   above already says "I build small tools", so a label repeating the word said
   nothing, and the list is broader than tools anyway: two Obsidian plugins and a
   maths site sit in it. Size and weight are restated because an h1 would
   otherwise arrive bold at 2em. */
.label{margin:0 0 10px;font-size:13px;font-weight:400;color:var(--fg-2)}
#swup:focus-visible{outline-offset:6px}

.list{position:relative;margin:0 -12px}
.rows{list-style:none;margin:0;padding:0}
.marker{
  position:absolute;left:0;right:0;top:0;z-index:0;
  height:var(--h,0px);
  border-radius:10px;
  background:var(--surface);
  transform:translate3d(0,var(--y,0px),0);
  opacity:0;
  pointer-events:none;
  /* Height is animated here on purpose, against the usual advice. This element is
     empty, aria-hidden and absolutely positioned, so it is out of flow and its
     height reflows nothing — the case the advice exists for does not apply. The
     alternative, scaleY, would squash the 10px radius into ellipses. Rows differ
     in height once a description wraps, so the marker has to resize to match. */
  transition:transform .3s var(--ease),height .3s var(--ease),opacity .18s linear;
}
.list[data-lit="1"] .marker{opacity:1}
/* Before the first hover there is nothing to slide from, so the first one fades in place. */
.list[data-warm="0"] .marker{transition:opacity .18s linear}

/* Two targets, one row. The name and description lead to this site's page about
   the tool; the host name leads to the tool itself. They are siblings rather
   than nested, because an <a> inside an <a> is not a thing — and siblings are
   what let the keyboard treat them differently: ↑ ↓ step between tools, Tab
   reaches the outbound links as well. */
.item{
  position:relative;z-index:1;
  display:flex;align-items:baseline;justify-content:space-between;gap:12px;
}

.row{
  flex:1 1 auto;min-width:0;
  padding:12px;
  color:inherit;text-decoration:none;border-radius:10px;
  -webkit-tap-highlight-color:transparent;
}
.name{display:block;font-weight:500;letter-spacing:-.006em}
.desc{display:block;margin-top:1px;font-size:13.5px;line-height:1.5;color:var(--fg-2)}

.host{
  flex:none;display:inline-flex;align-items:center;gap:5px;
  padding:12px 12px 12px 0;
  font-size:13px;color:var(--fg-2);text-decoration:none;border-radius:10px;
  -webkit-tap-highlight-color:transparent;
  transition:color .18s var(--ease);
}
/* The host lifts when either half is touched, so the row still reads as one
   object; it takes the underline only when it is the thing being pointed at. */
.item:hover .host,.item:focus-within .host{color:var(--fg)}
.host:hover{text-decoration:underline;text-underline-offset:3px;text-decoration-thickness:1px}

/* Outbound, so the arrow is back — the sighted half of the new-tab cue whose
   spoken half is the .vh span inside the same link. */
.arw{
  width:11px;height:11px;flex:none;
  opacity:0;transform:translate3d(-3px,2px,0);
  transition:opacity .18s var(--ease),transform .3s var(--ease);
}
.arw path{stroke:currentColor;stroke-width:1.6;fill:none;stroke-linecap:round;stroke-linejoin:round}
.item:hover .arw,.item:focus-within .arw{opacity:1;transform:translate3d(0,0,0)}

/* The hint only exists once the keyboard is actually in use. */
.hint{
  margin:14px 12px 0;font-size:12.5px;color:var(--fg-2);
  opacity:0;transform:translate3d(0,-2px,0);
  transition:opacity .2s var(--ease),transform .2s var(--ease);
}
:root[data-kbd="1"] .hint{opacity:1;transform:none}
kbd{
  font:inherit;font-size:11.5px;
  padding:1px 5px;border-radius:4px;background:var(--surface);color:var(--fg-2);
}

/* A phone's real first paint is ~700px, not the 844 of the large viewport, so the
   header stops scaling with vh and the rows tighten, starting the list as high as it
   can. Past six tools the list runs on below the fold by design — DESIGN.md → The
   Compact Header Rule. */
@media (max-width:560px){
  .page{padding-top:32px}
  .bio{margin-top:12px}
  .label{margin-top:28px}
  .row{padding:8px 12px}
  .desc{font-size:13px;line-height:1.45}
  /* The host stays on the right rather than stacking: it is a separate target
     now, and a phone needs the two apart from each other, not in a column where
     a thumb cannot tell which one it is about to hit. */
  .host{font-size:12.5px;padding:8px 12px 8px 0}
  .item{gap:8px}
}
/* A touch device never produces the hover that would reveal the arrow. */
@media (hover:none){
  .arw{opacity:1;transform:none}
}
@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{transition-duration:.01ms!important;animation-duration:.01ms!important}
}

/* ---- Return link and heading ---- */
.prose .back{
  display:inline-block;font-size:13px;color:var(--fg-2);text-decoration:none;
  text-underline-offset:3px;text-decoration-thickness:1px;
  transition:color .15s var(--ease);
}
.back:hover{color:var(--fg);text-decoration:underline}

/* ---- Prose tier (DESIGN.md → Prose) ----

   Five sizes: 27 / 18 / 17 / 16 / 13. The homepage is a link list and is right
   to hold everything at 15px; a 950-word article is a different job, and at one
   size it reads as an undifferentiated slab — a heading and the sentence under
   it were separated by weight alone. The steps are what make it scannable.
   Palette, typeface and restraint are unchanged; only the scale is new. */

.prose h1{
  margin:0;
  font-size:27px;font-weight:500;line-height:1.2;letter-spacing:-.021em;
  text-wrap:balance;
}


.prose p{margin:18px 0 0;max-width:68ch;font-size:16px;line-height:1.7;text-wrap:pretty}

/* More space above a heading than below it: the gap belongs to the section it
   opens, not to the paragraph it follows. */
.prose h2{
  margin:52px 0 0;
  font-size:18px;font-weight:500;line-height:1.35;letter-spacing:-.014em;
  text-wrap:balance;
}
.prose h2+p{margin-top:12px}

.prose .lede{
  margin-top:22px;
  font-size:17px;line-height:1.65;color:var(--fg-2);max-width:62ch;
}

.prose ul{margin:18px 0 0;padding-left:1.15em;max-width:68ch;font-size:16px;line-height:1.7}
.prose li+li{margin-top:10px}
.prose li::marker{color:var(--fg-2)}
.prose a{
  color:var(--fg);text-decoration:underline;
  text-underline-offset:3px;text-decoration-thickness:1px;
  transition:color .15s var(--ease);
}
.prose a:hover{color:var(--fg-2)}

/* Frameless and shadowless (DESIGN.md → Prose → The No-Second-Ground Rule). The
   hairline is the same rule the footer uses — it stops a dark screenshot from
   bleeding into a dark page without drawing a frame around it. */
.prose figure{margin:44px 0 0}
.prose figure img{display:block;width:100%;height:auto;border:1px solid var(--line);border-radius:6px}
.prose figcaption{margin-top:10px;font-size:13px;line-height:1.5;color:var(--fg-2);max-width:62ch}

/* ---- Footer, identical to the homepage's ---- */
.foot{
  margin-top:clamp(44px,8vh,80px);padding-top:20px;border-top:1px solid var(--line);
  display:flex;flex-wrap:wrap;gap:10px 20px;align-items:baseline;justify-content:space-between;
  font-size:13px;color:var(--fg-2);
}
.foot nav{display:flex;gap:18px}
.foot a{
  color:var(--fg-2);text-decoration:none;
  text-underline-offset:3px;text-decoration-thickness:1px;
  transition:color .15s var(--ease);
}
.foot a:hover{color:var(--fg);text-decoration:underline}
.foot p{margin:0;max-width:none}

@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{transition-duration:.01ms!important;animation-duration:.01ms!important}
}
