/* IBM Plex Mono + IBM Plex Sans, SIL Open Font License 1.1.
   Subset to the glyphs this site uses. Files live in fonts/. */
@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(fonts/ibm-plex-mono-regular.woff2) format("woff2");
}
@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url(fonts/ibm-plex-mono-medium.woff2) format("woff2");
}
@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(fonts/ibm-plex-mono-semibold.woff2) format("woff2");
}
@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url(fonts/ibm-plex-mono-bold.woff2) format("woff2");
}
@font-face {
  font-family: "IBM Plex Sans";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url(fonts/ibm-plex-sans-regular.woff2) format("woff2");
}
@font-face {
  font-family: "IBM Plex Sans";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url(fonts/ibm-plex-sans-semibold.woff2) format("woff2");
}

/* ==========================================================================
   Krishnaraj — personal portfolio
   Palette: Gruvbox Material (dark medium / light medium)
   Type: IBM Plex Mono + IBM Plex Sans, subset and embedded.
   JavaScript is used for exactly one thing: remembering the theme choice
   across pages. Everything else is CSS.
   ========================================================================== */

/* -------------------------------------------------------------- theme --- */

:root {
  --is-dark: 1;
  /* Scrollbars, focus rings and form widgets are drawn by the browser, not by
     anything here. Naming a single scheme keeps them dark with the page even
     when the OS is set to light; the light theme names its own below. */
  color-scheme: dark;

  --bg:        #282828;
  --bg-hard:   #1d2021;
  --surface:   #32302f;
  --surface-2: #3c3836;
  --line:      #45403d;
  --header:    rgba(40, 40, 40, .92);

  --fg:        #d4be98;
  --fg-dim:    #a89984;
  --muted:     #928374;

  --red:    #ea6962;
  --orange: #e78a4e;
  --yellow: #d8a657;
  --green:  #a9b665;
  --aqua:   #89b482;
  --blue:   #7daea3;
  --purple: #d3869b;
}

/* Light is opt-in, never inherited from the OS. The site opens dark for
   everyone and only leaves it if the visitor asks, which is why there is no
   prefers-color-scheme rule here: the dark block above is simply the default,
   and the only thing that overrides it is [data-theme], set from the
   remembered choice before first paint. It also keeps the page in step with
   the diagrams in images/, which are dark-only and cannot follow a toggle. */
:root[data-theme="light"] {
  --is-dark: 0;
  color-scheme: light;
  --bg:        #f6f4f0;
  --bg-hard:   #fdfcfa;
  --surface:   #edeae4;
  --surface-2: #e4e0d8;
  --line:      #ccc6bb;
  --header:    rgba(246, 244, 240, .92);
  --fg:        #2f2b27;
  --fg-dim:    #4a443d;
  --muted:     #6f6962;
  --red:    #c14a4a;
  --orange: #c35e0a;
  --yellow: #b47109;
  --green:  #6c782e;
  --aqua:   #4c7a5d;
  --blue:   #45707a;
  --purple: #945e80;
}

:root {
  --mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
  --sans: "IBM Plex Sans", system-ui, sans-serif;
  --measure: 68rem;
  /* One reading column, shared by every run of body text so that a lede, a
     section blurb and a paragraph all break at the same place. ~72 characters
     of IBM Plex Sans at the body size. */
  --measure-text: 40rem;
  /* Footprint the fixed contents panel needs kept clear: its own max-width
     plus the largest right offset it can take, plus a little breathing room. */
  --toc-rail: 16.5rem;
  --gutter: clamp(1.25rem, 4vw, 3rem);
  --radius: 10px;
}

/* --------------------------------------------------------------- reset -- */

*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg-dim);
  font-family: var(--sans);
  font-size: 1.0625rem;
  line-height: 1.7;
  -webkit-font-smoothing: antialiased;
}

img, svg { max-width: 100%; }
a { color: var(--aqua); text-underline-offset: 3px; }
a:hover { color: var(--yellow); }

:focus-visible {
  outline: 2px solid var(--yellow);
  outline-offset: 3px;
  border-radius: 3px;
}

.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  padding: .6rem 1rem;
  background: var(--yellow);
  color: var(--bg-hard);
  font: 600 .85rem/1 var(--mono);
  z-index: 10;
}
.skip:focus { left: 0; }

/* ---------------------------------------------------------------- type -- */

h1, h2, h3 {
  font-family: var(--mono);
  color: var(--fg);
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.15;
  margin: 0;
}

.eyebrow {
  margin: 0 0 .9rem;
  font: 600 .74rem/1 var(--mono);
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--muted);
}

p { margin: 0 0 1.1rem; }
p:last-child { margin-bottom: 0; }
strong { color: var(--fg); font-weight: 600; }

code {
  font-family: var(--mono);
  font-size: .9em;
  color: var(--aqua);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 5px;
  padding: .1em .35em;
}

/* -------------------------------------------------------------- layout -- */

.wrap {
  width: 100%;
  max-width: var(--measure);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.section { padding-block: clamp(3rem, 7vw, 5.5rem); scroll-margin-top: 6rem; }
.section + .section { border-top: 1px solid var(--line); }

.section-head { margin-bottom: 2.5rem; }
.section-head h2 { font-size: clamp(1.35rem, 3vw, 1.75rem); }
/* Full width: a section blurb introduces the whole section, so it wraps on the
   same edge as the block under it rather than on the reading measure. */
.section-head p { margin: .75rem 0 0; color: var(--muted); }

/* -------------------------------------------------------------- header -- */

.site-header {
  position: sticky;
  top: 0;
  z-index: 5;
  background: var(--header);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--line);
}

.site-header .wrap {
  display: flex;
  align-items: center;
  gap: clamp(1rem, 3vw, 2rem);
  min-height: 4.25rem;
}

.brand {
  margin-right: auto;
  font: 700 1.05rem/1 var(--mono);
  color: var(--fg);
  text-decoration: none;
  letter-spacing: -.02em;
}
.brand::after { content: "_"; color: var(--yellow); }
.brand:hover { color: var(--fg); }

.nav { display: flex; gap: clamp(1rem, 3vw, 2rem); }
.nav a {
  position: relative;
  font: 500 .9rem/1 var(--mono);
  color: var(--fg-dim);
  text-decoration: none;
  padding-block: .4rem;
}
.nav a:hover { color: var(--fg); }
.nav a::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -.15rem;
  height: 2px;
  background: var(--yellow);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform .18s ease;
}
.nav a:hover::after,
.nav a[aria-current="page"]::after { transform: scaleX(1); }
.nav a[aria-current="page"] { color: var(--fg); }

.theme-toggle {
  display: grid;
  place-items: center;
  width: 2.1rem;
  height: 2.1rem;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--surface);
  color: var(--fg-dim);
  cursor: pointer;
  transition: color .16s ease, border-color .16s ease, background .16s ease;
}
.theme-toggle:hover {
  color: var(--yellow);
  border-color: var(--muted);
  background: var(--surface-2);
}
.theme-toggle svg { width: 1.05rem; height: 1.05rem; }

/* ---------------------------------------------------------------- hero -- */

.hero {
  display: grid;
  grid-template-columns: minmax(0, 340px) minmax(0, 1fr);
  gap: clamp(2rem, 6vw, 4.5rem);
  align-items: center;
  padding-block: clamp(2rem, 5vw, 3.5rem);
}

.portrait { position: relative; isolation: isolate; }
.portrait .shot {
  display: block;
  width: 100%;
  height: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
}
.portrait img {
  border-radius: var(--radius);
}
.portrait::before {
  content: "";
  position: absolute;
  inset: -14px -14px -14px -14px;
  border: 1px solid var(--aqua);
  border-radius: var(--radius);
  opacity: .5;
  z-index: -1;
}

.hero h1 {
  font-size: clamp(2.5rem, 7.5vw, 4.5rem);
  letter-spacing: -.045em;
  margin-bottom: 1.25rem;
}
.hero h1 .name { color: var(--yellow); }
.hero-copy { max-width: 48ch; font-size: 1.125rem; }
.hero-actions { display: flex; flex-wrap: wrap; gap: .75rem; margin-top: 2rem; }

.btn {
  display: inline-block;
  font: 600 .85rem/1 var(--mono);
  text-decoration: none;
  padding: .8rem 1.3rem;
  border-radius: 8px;
  border: 1px solid var(--line);
  color: var(--fg);
  background: var(--surface);
  transition: background .16s ease, border-color .16s ease, color .16s ease;
}
.btn:hover { background: var(--surface-2); border-color: var(--muted); color: var(--fg); }
.btn-primary { background: var(--yellow); border-color: var(--yellow); color: var(--bg-hard); }
.btn-primary:hover { background: var(--orange); border-color: var(--orange); color: var(--bg-hard); }

/* -------------------------------------------------------------- skills -- */

/* Ten rows in two columns, so every millimetre of row height is paid five
   times over. The spacing below is tuned to keep the whole grid on one
   screen at a typical laptop height -- if a row is ever added, this is the
   first place to take height back out of. */
.stack-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1rem 3rem;
}

.stack-row { padding-top: .6rem; border-top: 1px solid var(--line); }

.stack-label {
  display: flex;
  align-items: baseline;
  gap: .5rem;
  margin: 0 0 .55rem;
  font: 600 .72rem/1.4 var(--mono);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--accent, var(--fg-dim));
}
.stack-label span {
  margin-left: auto;
  color: var(--muted);
  font-weight: 400;
  letter-spacing: .06em;
}

.chips { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: .45rem; }

.chip {
  --brand: var(--brand-d);
  position: relative;
  width: 2.6rem;
  height: 2.6rem;
  display: grid;
  place-items: center;
  border: 1px solid var(--line);
  border-radius: 8px;
  background-color: var(--surface);
  color: var(--brand);
  transition: transform .16s ease, border-color .16s ease,
              color .16s ease, background .16s ease;
}
:root[data-theme="light"] .chip { --brand: var(--brand-l); }

/* Logos carry their real brand colour at rest. Each --brand value is nudged
   toward white (dark theme) or black (light theme) until it clears 3.4:1
   against the hover surface, which is the lower-contrast of the two states. */
.chip:hover,
.chip:focus-visible {
  transform: translateY(-2px);
  border-color: var(--brand);
  background-color: var(--surface-2);
}

/* Pointer-only, by design. The chips used to carry tabindex="0" purely so this
   label could be reached by keyboard, which put 35 tab stops in the middle of
   the about page -- the whole grid had to be tabbed through to get past it.
   The name and the rating now ride in each chip's aria-label instead, so
   nothing is announced any less; this is decoration for anyone using a mouse.
   The :focus-visible half stays so the label returns for free if a chip ever
   becomes focusable again (a link, say). */
.chip::after {
  content: attr(data-name);
  position: absolute;
  top: calc(100% + .5rem);
  left: 50%;
  transform: translateX(-50%) translateY(-4px);
  padding: .35rem .6rem;
  background: var(--bg-hard);
  color: var(--fg);
  border: 1px solid var(--line);
  border-radius: 6px;
  font: 500 .72rem/1 var(--mono);
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity .16s ease, transform .16s ease;
  z-index: 4;
}
.chip:hover::after,
.chip:focus-visible::after { opacity: 1; transform: translateX(-50%) translateY(0); }

/* Presentation properties here are inherited by the <use> shadow content;
   class selectors would not cross that boundary. */
.ico { width: 1.32rem; height: 1.32rem; fill: currentColor; stroke: none; }

/* A few source logos (C, Espressif, Raspberry Pi) draw right to the edge of
   their viewBox, so at full .ico size they read as clipped on hover. */
.ico-sm { width: 1.14rem; height: 1.14rem; }

/* ---------------------------------------------------------- confidence -- */

/* One class carries both halves of a rating, so the length of the bar and its
   position on the red-to-green ramp can never disagree. Each step is its
   palette accent pulled toward the neutral grey, which drops the chroma by
   about half so the bar stops competing with the brand-coloured logo while
   still clearing 2.5:1 against its own track in both themes. Muting toward
   the surface instead would wash the ramp out entirely in light mode, where
   the surface is lighter than the track. --rate-mute is the one knob. */
:root { --rate-mute: 45%; }

.r1 { --rate: 1; --rate-color: color-mix(in oklab, var(--red)    var(--rate-mute), var(--muted)); }
.r2 { --rate: 2; --rate-color: color-mix(in oklab, var(--orange) var(--rate-mute), var(--muted)); }
.r3 { --rate: 3; --rate-color: color-mix(in oklab, var(--yellow) var(--rate-mute), var(--muted)); }
.r4 { --rate: 4; --rate-color: color-mix(in oklab, var(--green)  var(--rate-mute), var(--muted)); }
.r5 { --rate: 5; --rate-color: color-mix(in oklab, var(--aqua)   var(--rate-mute), var(--muted)); }

.chip,
.rate-key {
  --fill: calc(var(--rate) / 5 * 100%);
  background-image: linear-gradient(to right,
    var(--rate-color) 0%  var(--fill),
    var(--line)           var(--fill) 100%);
  background-repeat: no-repeat;
}

/* The meter is a background layer rather than a pseudo-element so the corner
   radius trims its ends for free -- clipping a child would take overflow:
   hidden, which would swallow the hover label with it. Clipped to the padding
   box so it sits inside the border rather than under it. */
.chip {
  background-clip: padding-box;
  background-position: bottom;
  background-size: 100% 3px;
}

.scale { margin-top: 1.35rem; padding-top: .85rem; border-top: 1px solid var(--line); }

.scale-keys {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: .8rem 3rem;
}
.scale-keys li { font: 400 .8rem/1.5 var(--mono); color: var(--muted); }
.scale-keys b { margin-right: .35rem; font-weight: 600; color: var(--fg-dim); }

/* Same width as a .chip, so the key reads as a sample of the real thing. */
.rate-key { display: block; width: 2.4rem; height: 3px; margin-bottom: .55rem; border-radius: 2px; }

/* ------------------------------------------------------------ projects -- */

.project-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 1rem; }

.project-card {
  position: relative;
  display: grid;
  grid-template-columns: 6.5rem minmax(0, 1fr) auto;
  gap: 1.5rem;
  align-items: baseline;
  padding: 1.6rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  transition: border-color .16s ease, background .16s ease;
}
.project-card:hover { border-color: var(--muted); background: var(--surface-2); }
.project-card .year { font: 500 .8rem/1.6 var(--mono); color: var(--muted); }
.project-card h3 { font-size: 1.1rem; margin-bottom: .4rem; }
.project-card h3 a { color: var(--fg); text-decoration: none; }
.project-card h3 a::after { content: ""; position: absolute; inset: 0; border-radius: var(--radius); }
.project-card:hover h3 a { color: var(--yellow); }
.project-card p { margin: 0 0 .9rem; max-width: 58ch; font-size: .98rem; }

.tags { list-style: none; display: flex; flex-wrap: wrap; gap: .4rem; margin: 0; padding: 0; }
.tags li {
  font: 500 .72rem/1 var(--mono);
  color: var(--muted);
  border: 1px solid var(--line);
  border-radius: 5px;
  padding: .3rem .45rem;
}

.status {
  font: 600 .72rem/1 var(--mono);
  letter-spacing: .1em;
  text-transform: uppercase;
  padding: .35rem .55rem;
  border-radius: 5px;
  white-space: nowrap;
  color: var(--tone);
  background: color-mix(in srgb, var(--tone) 14%, transparent);
}
.status-active   { --tone: var(--green); }
.status-upkeep   { --tone: var(--blue); }
.status-archived { --tone: var(--muted); }

/* ----------------------------------------------------- project detail -- */

.crumb { font: 500 .8rem/1 var(--mono); color: var(--muted); margin: 0 0 1.25rem; }
.crumb a { color: var(--muted); text-decoration: none; }
.crumb a:hover { color: var(--yellow); }

.page-head { padding-block: clamp(2.5rem, 6vw, 4rem) 0; }
.page-head h1 { font-size: clamp(2rem, 5.5vw, 3rem); letter-spacing: -.04em; }
.lede { margin-top: 1.25rem; max-width: var(--measure-text); font-size: 1.125rem; }

/* A lede that introduces a block laid out to the full width -- the project
   list, the about columns -- rather than a single reading column, so it wraps
   on the same right edge as the thing it introduces. */
.lede-wide { max-width: none; }

/* The head and the section under it are one thought, so they sit closer than
   two unrelated sections do. */
.page-head + .section,
.page-head + .toc + .section { padding-top: clamp(2rem, 4vw, 3rem); }

/* Pinned to the viewport corner rather than the .wrap column -- docking it
   beside the reading measure only leaves room on very wide screens, and on
   anything short of that the panel just vanished. A corner overlay works at
   any width and stays put while the page scrolls under it. */
.toc {
  position: fixed;
  top: 5.5rem;
  right: clamp(.9rem, 3vw, 1.75rem);
  z-index: 6;
  display: flex;
  flex-direction: column;
  gap: .55rem;
  max-width: 13rem;
  padding: 1rem 1.2rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: 0 12px 28px -14px rgba(0, 0, 0, .45);
}
.toc-title {
  margin: 0 0 .2rem;
  font: 600 .7rem/1 var(--mono);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--muted);
}
.toc a {
  font: 500 .82rem/1.3 var(--mono);
  color: var(--fg-dim);
  text-decoration: none;
  transition: color .16s ease;
}
.toc a:hover, .toc a:focus-visible { color: var(--yellow); }

/* Too little viewport left for a corner overlay to sit without covering
   content -- it rejoins the flow instead, as a plain card right after the
   page head (where it lives in the markup already). Matches the breakpoint
   the rest of the site already treats as "narrow" (hero/about-grid stack
   here too), so this isn't a second, later mobile cutoff. */
@media (max-width: 60rem) {
  .toc {
    position: static;
    max-width: none;
    margin: 1.5rem var(--gutter) 0;
    box-shadow: none;
  }
}

/* While the panel floats it is out of flow, so nothing in the column knows it
   is there -- now that the prose runs the full width, it would slide straight
   under it. The centred .wrap already leaves (100vw - --measure) / 2 of empty
   page beside it; the column only gives up whatever the panel still needs on
   top of that, so past ~100rem of viewport the gutter is wide enough on its
   own and the reservation costs nothing. */
@media (min-width: 60.001rem) {
  body:has(.toc) main .wrap {
    padding-right: max(
      var(--gutter),
      var(--toc-rail) - max(0rem, (100vw - var(--measure)) / 2)
    );
  }
}

.meta {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 0 2.5rem;
  margin: 2rem 0 0;
  padding: 1.5rem 0 0;
  border-top: 1px solid var(--line);
}
.meta div { padding-block: .35rem; }
.meta dt {
  font: 600 .7rem/1 var(--mono);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: .45rem;
}
.meta dd { margin: 0; font: 500 .9rem/1.4 var(--mono); color: var(--fg); }

.figure { margin: 2.5rem 0; }
.figure figcaption {
  margin-top: .9rem;
  text-align: center;
  font: 400 .8rem/1.6 var(--mono);
  color: var(--muted);
}
.figure figcaption b { color: var(--fg-dim); font-weight: 600; }

.canvas {
  overflow-x: auto;
  /* Room on every side so a paragraph never sits against the frame. Adjacent
     margins collapse, so a canvas after a section head keeps the head's
     larger spacing rather than adding to it. */
  margin-block: 2rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--bg-hard);
  padding: 1.25rem;
}
/* Inside a figure the caption owns the spacing below. */
.figure .canvas { margin-block: 0; }
.canvas svg { display: block; width: 100%; min-width: 640px; height: auto; }

.canvas-embed {
  overflow: hidden;
  padding: 0;
  /* The frame marks the embed as live rather than a picture. Muting the accent
     toward the ordinary border keeps that hint without shouting; raise the
     percentage for more yellow, drop it for a plainer box. */
  border-color: color-mix(in oklab, var(--yellow) 35%, var(--line));
  background: var(--surface);
}
.canvas-embed tree-crawl-graph { display: block; }

.canvas-img {
  overflow: hidden;
  padding: 0;
  background: #16181c;
}
/* An <img> diagram carries its own (halved) size via width/height attributes,
   so the mat should hug it rather than stay stretched to the full column --
   otherwise the dark background just shows as bare space either side of a
   now-smaller picture. The inline-svg data-viz canvases (category/time per
   day) still want to fill the column, so this only kicks in when the canvas
   actually wraps an <img>. */
.canvas-img:has(img) {
  width: fit-content;
  max-width: 100%;
  margin-inline: auto;
}
/* img is a replaced element with default vertical-align: baseline, which
   leaves a few pixels of descender gap under it -- on this dark mat that gap
   read as a stray black bar under every diagram. display: block removes it. */
.canvas-img img {
  display: block;
  height: auto;
}

.prose { max-width: var(--measure-text); }
/* On a project page the prose is the section's only column -- there is nothing
   beside it for the reading measure to line up with, so capping it just left a
   dead band down the right of every section while the diagrams above ran the
   full width. It wraps on the same edge as they do instead. (About keeps the
   cap: its prose sits in a grid column next to the fact cards.) */
.section > .prose { max-width: none; }
.prose h3 { font-size: 1.05rem; margin: 2rem 0 .75rem; }
.prose ul { margin: 0 0 1.1rem; padding-left: 1.1rem; }
.prose li { margin-bottom: .45rem; }
.prose li::marker { color: var(--muted); }

/* ------------------------------------------------------------- callout -- */

/* A note that interrupts the prose: caveats, provisional content, asides.
   Set --accent per instance the way .stack-row does; one property drives the
   bar, the label and the tint together. Capped at the reading measure so it
   does not stretch on project pages, where .prose runs full width. */
.callout {
  --accent: var(--blue);
  max-width: var(--measure-text);
  margin: 1.6rem 0;
  padding: 1.1rem 1.3rem;
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--accent) 7%, var(--surface));
}
.callout p { margin-bottom: .8rem; }
.callout > :last-child { margin-bottom: 0; }

.callout-label {
  margin: 0 0 .5rem;
  font: 600 .74rem/1 var(--mono);
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--accent);
}

.callout-note { --accent: var(--blue); }
.callout-warn { --accent: var(--yellow); }
.callout-todo { --accent: var(--purple); }

.d-box  { fill: var(--surface); stroke: var(--line); }
.d-line { stroke: var(--muted); fill: none; }
.d-title { font: 600 13px var(--mono); fill: var(--fg); }
.d-sub { font: 400 11px var(--mono); fill: var(--muted); }
.d-tick { font: 600 11px var(--mono); letter-spacing: .1em; fill: var(--fg-dim); }
.d-dash { stroke: var(--line); fill: none; stroke-dasharray: 3 5; }
.d-axis { stroke: var(--line); fill: none; stroke-width: 2; }

/* --------------------------------------------------------------- about -- */

/* The two fact cards sit side by side under the page head rather than stacked
   down a sidebar: the facts are the shortest thing on the page and the least
   worth scrolling for, and taking them out of a column beside the prose lets
   everything below run the full width. */
/* No align-items here: the cards hold different numbers of rows, and letting
   the shorter one stretch to the taller keeps the pair reading as one band
   rather than two loose boxes. */
.about-facts {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1rem;
  margin-top: 2rem;
}
/* .card + .card stacks two cards in a column; in this grid they are siblings
   side by side, so that margin would only push the second one down. */
.about-facts .card + .card { margin-top: 0; }

/* Two short runs of prose, side by side, so neither reads as a wall. Each is
   already inside the reading measure at this width. */
.about-cols {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(2rem, 6vw, 4rem);
  align-items: start;
}
.about-cols h2 { font-size: clamp(1.35rem, 3vw, 1.75rem); margin-bottom: 1rem; }

.card {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  padding: 1.6rem;
}
.card h3 {
  font-size: .78rem;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 1rem;
}
.card + .card { margin-top: 1rem; }

.kv {
  margin: 0;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: .55rem 1.25rem;
  font: 500 .88rem/1.5 var(--mono);
}
.kv dt { color: var(--muted); }
/* The values are URLs -- one long token each, in a column that narrows with
   the viewport. Without this they would spill past the card. */
.kv dd { margin: 0; color: var(--fg); overflow-wrap: anywhere; }

/* --------------------------------------------------------------- focus -- */

/* The "passionate about" list. Same accent-per-row idea as .stack-row: one
   custom property drives the icon colour and its tint together. Cards rather
   than bullets so five multi-sentence items read as five things and not as
   one block of text. */
.focus-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1rem;
}

.focus-card {
  --accent: var(--blue);
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: .35rem 1.1rem;
  align-items: center;
  padding: 1.5rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
}
/* Odd item out: five cards in two columns leave a hole, so the last one takes
   the whole row and closes the block off. */
.focus-list > li:last-child { grid-column: 1 / -1; }

.focus-card h3 { font-size: 1.02rem; }
.focus-card p { grid-column: 1 / -1; margin: .35rem 0 0; font-size: .98rem; }

.focus-ico {
  display: grid;
  place-items: center;
  width: 2.4rem;
  height: 2.4rem;
  border-radius: 8px;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, var(--surface-2));
}

/* Drawn rather than filled -- these are line icons, not brand logos, so they
   are the one place on the site that strokes instead of fills. Sized to sit
   between the .ico logos and the type around them. */
.ico-line {
  width: 1.35rem;
  height: 1.35rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* -------------------------------------------------------------- footer -- */

.site-footer {
  margin-top: clamp(2rem, 4vw, 3rem);
  border-top: 1px solid var(--line);
  padding-block: 1.6rem;
  font: 400 .85rem/1.6 var(--mono);
  color: var(--muted);
}
.site-footer .wrap {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: .35rem 2rem;
}
.site-footer p { margin: 0; }
.site-footer a { color: var(--muted); text-decoration: none; }
.site-footer a:hover { color: var(--yellow); }

/* ---------------------------------------------------------- responsive -- */

@media (max-width: 60rem) {
  .hero { grid-template-columns: minmax(0, 1fr); gap: 2.5rem; }
  .portrait { max-width: 17rem; }
  .about-facts,
  .about-cols,
  .focus-list { grid-template-columns: minmax(0, 1fr); }
  .about-cols { gap: 2.5rem; }
  .stack-list { grid-template-columns: minmax(0, 1fr); gap: 1.1rem 0; }
}

@media (max-width: 40rem) {
  .project-card { grid-template-columns: minmax(0, 1fr); gap: .75rem; }
  .project-card .year { order: -1; }
  .meta { gap: 0 1.75rem; }
  .scale-keys { grid-template-columns: minmax(0, 1fr); gap: .6rem; }
}

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