@charset "UTF-8";
:root {
  /* palette (light) */
  --bg: #fafaf8;
  --surface: #ffffff;
  --surface-2: #f3f3ef;
  --text: #16181d;
  --muted: #6b7078;
  --footer-dim: #9a9ea6;
  /* dimmer than muted — for the quiet footer line */
  --border: #e6e6e0;
  --border-hover:#d3d3ca;
  /* gentle neutral emphasis — not the loud accent */
  --accent: #3457e0;
  --accent-soft: #3457e01a;
  --shadow: 0 1px 2px rgba(20,22,28,.04), 0 8px 30px rgba(20,22,28,.06);
  /* type */
  --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
               Helvetica, Arial, sans-serif;
  --font-mono: ui-monospace, "SF Mono", "JetBrains Mono", "Cascadia Code",
               Menlo, Consolas, monospace;
  /* rhythm — left-anchored shell, no global centering */
  --gutter: clamp(1.25rem, 5vw, 4rem);
  /* the shell's left/right inset  */
  --measure: 44rem;
  /* readable text column          */
  --side-w: clamp(13rem, 19vw, 16rem);
  /* sidebar width                 */
  --topbar-h: 3.5rem;
  /* header height; sidebar__head
     matches it so borders align   */
  --radius: 14px;
  --radius-sm: 9px;
  --radius-btn: 8px;
  /* shared by theme toggle, menu button, search input */
  --btn: 35px;
  /* icon-button footprint (width & height)           */
  /* motion */
  --ease: cubic-bezier(.22, 1, .36, 1);
  --spring: cubic-bezier(.34, 1.56, .64, 1);
  /* gentle overshoot — playful bar motion */
  --dur: .55s;
  --dur-nav: .35s;
  /* sidebar push + hamburger morph share this clock   */
  color-scheme: light;
}

[data-theme="dark"] {
  --bg: #0d0f13;
  --surface: #15181e;
  --surface-2: #1c2027;
  --text: #e9ebf0;
  --muted: #8b919c;
  --footer-dim: #626873;
  /* dimmer than muted — for the quiet footer line */
  --border: #262b33;
  --border-hover:#39404b;
  /* a touch lighter than the resting border */
  --accent: #7aa0ff;
  --accent-soft: #7aa0ff1f;
  --shadow: 0 1px 2px rgba(0,0,0,.4), 0 10px 40px rgba(0,0,0,.5);
  color-scheme: dark;
}

/* ===========================================================================
   BASE
   =========================================================================== */
*, *::before, *::after {
  box-sizing: border-box;
}

html {
  -webkit-text-size-adjust: 100%;
  font-size: 104%;
  /* ~16.6px base — a touch larger everywhere */
}

@media (max-width: 699.98px) {
  html {
    font-size: 109%;
  }

  /* ~17.4px on phones, checked on-device */
}
body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: 1rem;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  transition: background 0.3s var(--ease), color 0.3s var(--ease);
}

a {
  color: inherit;
  text-decoration: none;
}

.mono {
  font-family: var(--font-mono);
  font-size: .82em;
  letter-spacing: .01em;
}

.muted {
  color: var(--muted);
}

.accent {
  color: var(--accent);
}

.eyebrow {
  text-transform: uppercase;
  letter-spacing: .14em;
  font-size: .72rem;
}

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

/* Region inset is a left/right gutter only — never `margin: 0 auto`. */
.inset {
  padding-inline: var(--gutter);
}

/* ===========================================================================
   SHELL + SIDEBAR PUSH
   ---------------------------------------------------------------------------
   body
   ├─ .sidebar   fixed, full viewport height (top:0 → bottom:0), so the
   │             sticky header can never overlap it
   └─ .shell     everything else; moves when the sidebar opens:
                   ≥700px  margin-left grows  → content RESIZES, no h-scroll
                   <700px  translateX          → content is PUSHED off-canvas
   =========================================================================== */
.shell {
  display: flex;
  flex-direction: column;
  min-height: 100dvh;
  transition: margin-left var(--dur-nav) var(--ease), transform var(--dur-nav) var(--ease);
}

.sidebar {
  position: fixed;
  inset: 0 auto 0 0;
  /* top:0 bottom:0 left:0 — full height */
  width: var(--side-w);
  display: flex;
  flex-direction: column;
  background: var(--surface);
  box-shadow: inset -15px 0 7px -15px var(--border-hover);
  transform: translateX(-100%);
  visibility: hidden;
  /* off the tab order when closed */
  transition: transform var(--dur-nav) var(--ease), visibility 0s linear var(--dur-nav);
  z-index: 50;
}

body[data-nav="open"] .sidebar {
  transform: none;
  visibility: visible;
  transition: transform var(--dur-nav) var(--ease);
}

/* desktop & tablet: shell resizes beside the sidebar — no horizontal scroll */
@media (min-width: 700px) {
  body[data-nav="open"] .shell {
    margin-left: var(--side-w);
  }
}
/* mobile: shell is pushed as one piece; clip the off-screen part */
@media (max-width: 699.98px) {
  body {
    --side-w: min(78vw, 17rem);
  }

  html, body {
    overflow-x: hidden;
  }

  body[data-nav="open"] .shell {
    transform: translateX(var(--side-w));
  }

  /* the header's own theme toggle has no room on phones; the sidebar head's
     toggle (always present) covers it instead. */
  .topbar .theme-toggle {
    display: none;
  }
}
/* sidebar internals */
.sidebar__head {
  height: var(--topbar-h);
  flex-shrink: 0;
  display: flex;
  align-items: center;
  gap: .5rem;
  padding-inline: 1.1rem;
}

.sidebar__head .brand {
  flex: 1;
  min-width: 0;
}

/* brand takes column 1 */
.sidebar__body {
  flex: 1;
  overflow-y: auto;
  padding: 1rem .8rem 1.5rem;
}

/* icon-only theme toggle sitting in the head beside the brand (column 2) */
.theme-toggle--icon {
  flex-shrink: 0;
  width: 2rem;
  height: 2rem;
  display: grid;
  place-items: center;
  border-radius: var(--radius-sm);
  background: transparent;
  /* ghost — override the base bordered box */
  border: 1px solid transparent;
  color: var(--muted);
  transition: background .15s, color .15s, border-color .15s;
}

.theme-toggle--icon:hover {
  background: var(--surface-2);
  color: var(--text);
  border-color: var(--border);
}

.theme-toggle--icon .ico {
  width: 18px;
  height: 18px;
}

.sidebar__foot {
  flex-shrink: 0;
}

.sidebar__foot[hidden] {
  display: none;
}

.sidebar__title {
  padding: .2rem .6rem .55rem;
  margin: 0 0 .45rem;
  border-bottom: 1px solid var(--border);
}

.sidebar__nav {
  display: grid;
  gap: .15rem;
}

.sidebar__link {
  display: flex;
  align-items: baseline;
  gap: .55rem;
  padding: .45rem .6rem;
  border-radius: var(--radius-sm);
  color: var(--muted);
  transition: background .15s, color .15s;
}

.sidebar__link:hover {
  background: var(--surface-2);
  color: var(--text);
}

.sidebar__link.is-active {
  color: var(--text);
  background: var(--accent-soft);
}

.sidebar__prefix {
  color: var(--accent);
}

/* --- home sidebar: collapsible section groups (native <details>) --------- */
.sidebar__title + .sidebar__sections {
  margin-bottom: 1.4rem;
}

.sidebar__sections {
  display: grid;
  gap: .1rem;
}

.sidebar-sect {
  border-radius: var(--radius-sm);
}

.sidebar-sect__summary {
  display: flex;
  align-items: center;
  gap: .45rem;
  padding: .45rem .55rem;
  border-radius: var(--radius-sm);
  cursor: pointer;
  list-style: none;
  transition: background .15s;
}

.sidebar-sect__summary::-webkit-details-marker {
  display: none;
}

/* drop the default triangle */
.sidebar-sect__summary:hover {
  background: var(--surface-2);
}

/* chevron leads the row (file-tree style), rotates down when open */
.sidebar-sect__chevron {
  width: 15px;
  height: 15px;
  flex-shrink: 0;
  color: var(--muted);
  transition: transform 0.2s var(--ease);
}

.sidebar-sect[open] > .sidebar-sect__summary .sidebar-sect__chevron {
  transform: rotate(90deg);
}

.sidebar-sect__text {
  display: flex;
  flex-direction: column;
  gap: .05rem;
  min-width: 0;
  flex: 1;
}

.sidebar-sect__label {
  color: var(--text);
  line-height: 1.2;
  font-weight: 550;
}

.sidebar-sect__blurb {
  font-size: .72rem;
  color: var(--muted);
  line-height: 1.2;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* page previews as a file tree: a vertical rail drops from beneath the chevron
   and each item gets a short horizontal connector, so the sub-items visually
   hang off the parent's arrow. The rail sits at the chevron's optical centre
   (summary padding .55rem + half the 15px chevron ≈ 1rem from the edge). */
.sidebar-sect__pages {
  display: grid;
  gap: .05rem;
  margin-left: 1rem;
  /* rail position = chevron centre */
  padding: .1rem 0 .35rem;
  border-left: 1.5px solid var(--border);
}

.sidebar-sect__page,
.sidebar-sect__more {
  position: relative;
  margin-left: .55rem;
  /* clear the connector tick */
  padding: .3rem .5rem;
  border-radius: var(--radius-sm);
  font-size: .88rem;
  color: var(--muted);
  transition: background .15s, color .15s;
}

/* the horizontal connector from the rail to each item */
.sidebar-sect__page::before,
.sidebar-sect__more::before {
  content: "";
  position: absolute;
  left: -.55rem;
  top: 50%;
  width: .5rem;
  height: 1.5px;
  background: var(--border);
}

.sidebar-sect__page:hover,
.sidebar-sect__more:hover {
  background: var(--surface-2);
  color: var(--text);
}

.sidebar-sect__page.is-active {
  color: var(--text);
  background: var(--accent-soft);
}

/* the active item's connector + a segment of rail glow in the accent */
.sidebar-sect__page.is-active::before {
  background: var(--accent);
}

.sidebar-sect__more {
  color: var(--accent);
}

/* --- home sidebar: fixed overview items (Graph View, Documentation) ------ */
.sidebar__fixed {
  display: grid;
  gap: .1rem;
  margin-bottom: .5rem;
}

.sidebar__fixed-item {
  display: flex;
  align-items: center;
  gap: .6rem;
  padding: .5rem .55rem;
  border-radius: var(--radius-sm);
  color: var(--text);
  transition: background .15s;
}

.sidebar__fixed-item:hover {
  background: var(--surface-2);
}

.sidebar__fixed-icon {
  display: grid;
  place-items: center;
  flex-shrink: 0;
  width: 1.6rem;
  height: 1.6rem;
  color: var(--muted);
}

.sidebar__fixed-icon svg {
  width: 17px;
  height: 17px;
}

.sidebar__fixed-item:hover .sidebar__fixed-icon {
  color: var(--accent);
}

.sidebar__fixed-label {
  font-weight: 550;
}

.sidebar__divider {
  height: 1px;
  background: var(--border);
  margin: .35rem .3rem .8rem;
}

/* accent dot marking a highlighted section (replaces the old pinned group) */
.sidebar-sect__dot {
  display: inline-block;
  width: 6px;
  height: 6px;
  margin-left: .4rem;
  border-radius: 50%;
  background: var(--accent);
  vertical-align: middle;
}

/* ===========================================================================
   HEADER  (sticky inside the shell — the sidebar stands beside it, not under)
   =========================================================================== */
.topbar {
  position: sticky;
  top: 0;
  /* `position: sticky` creates its OWN stacking context unconditionally, in
     every major browser, regardless of whether z-index is set — unlike
     relative/absolute, which only do that when z-index isn't auto. That
     means anything nested inside .topbar (like .search) is permanently
     capped at .topbar's own rank when compared to anything OUTSIDE .topbar,
     no matter what z-index .search itself has. So to let the search box
     escape the dim overlay below, .topbar itself has to out-rank it — there
     is no way to do this from a descendant alone. This does mean topbar now
     also out-ranks .sidebar (z-index 50); the only visible cost is during
     the brief mobile open/close slide, where topbar may render over the
     sidebar's edge mid-transition instead of under it. */
  z-index: 65;
  height: var(--topbar-h);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding-inline: var(--gutter);
  background: color-mix(in srgb, var(--bg) 82%, transparent);
  backdrop-filter: saturate(1.4) blur(10px);
  border-bottom: 1px solid var(--border);
}

.topbar__cluster {
  display: flex;
  align-items: center;
  gap: clamp(0.45rem, 1.6vw, 0.9rem);
  min-width: 0;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  font-weight: 550;
  white-space: nowrap;
}

.brand-mark {
  font-family: var(--font-mono);
  color: var(--accent);
}

.brand-name {
  letter-spacing: -.01em;
}

.brand__icon {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  color: var(--muted);
}

.brand:hover .brand__icon {
  color: var(--accent);
}

/* --- icon buttons share one footprint ------------------------------------ */
.theme-toggle,
.navbtn {
  display: inline-grid;
  place-items: center;
  width: var(--btn);
  height: var(--btn);
  flex-shrink: 0;
  cursor: pointer;
  color: var(--muted);
  border-radius: var(--radius-btn);
  transition: color .2s, border-color .2s, background .2s;
}

/* on touch devices, grow the tap area to the 44px comfort minimum without
   enlarging the visible icon — the extra size is invisible padding around the
   same bars. Negative margin keeps it optically aligned in the cluster. */
@media (hover: none) and (pointer: coarse) {
  .navbtn {
    width: 44px;
    height: 44px;
    margin-left: -.7rem;
  }
}
.theme-toggle {
  background: var(--surface-2);
  border: 1px solid var(--border);
}

.theme-toggle:hover {
  color: var(--text);
  border-color: var(--border-hover);
}

.theme-toggle .ico {
  width: 18px;
  height: 18px;
  display: none;
}

:root[data-theme-pref="light"] .ico-light,
:root[data-theme-pref="dark"] .ico-dark,
:root[data-theme-pref="auto"] .ico-auto {
  display: block;
}

/* menu button — no background or border at all; it lives as part of the brand
   cluster and expresses everything through the bars themselves (colour + a
   characterful spring on hover). This is a button people will see constantly,
   so it's built to feel alive rather than boxed. */
.navbtn {
  background: transparent;
  border: none;
  margin-left: -.45rem;
  /* optically align icon with the gutter */
  -webkit-tap-highlight-color: transparent;
}

.navbtn:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: var(--radius-btn);
}

body[data-nav="open"] .navbtn {
  color: var(--accent);
}

/* hamburger ↔ X, driven by body[data-nav]. Three real bars in a flex column
   (even 14px box) so the stack is internally symmetric. Each bar has its own
   transform-origin and a spring easing, and hover adds a staggered flourish
   that differs by state — a gentle "reach" when closed, a tighter "settle"
   when open. */
.navbtn__icon {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  width: 18px;
  height: 14px;
  transition: transform var(--dur-nav) var(--spring);
}

.navbtn__bar {
  width: 18px;
  height: 2px;
  background: currentColor;
  border-radius: 2px;
  transform-origin: center;
  transition: transform var(--dur-nav) var(--spring), width 0.3s var(--ease), opacity calc(var(--dur-nav) / 2) var(--ease), background-color 0.25s var(--ease);
}

/* CLOSED + hover: bars warm to the accent and fan out with a slight stagger,
   the middle bar shortening — a small, friendly "reach toward you" gesture. */
.navbtn:hover .navbtn__bar {
  background: var(--accent);
}

body:not([data-nav="open"]) .navbtn:hover .navbtn__bar:nth-child(1) {
  transform: translateX(-2px);
}

body:not([data-nav="open"]) .navbtn:hover .navbtn__bar:nth-child(2) {
  width: 12px;
}

body:not([data-nav="open"]) .navbtn:hover .navbtn__bar:nth-child(3) {
  transform: translateX(2px);
}

/* OPEN: hamburger becomes an X. translateY(6px) meets centre from 0; -6px from 12. */
body[data-nav="open"] .navbtn__bar:nth-child(1) {
  transform: translateY(6px) rotate(45deg);
}

body[data-nav="open"] .navbtn__bar:nth-child(2) {
  opacity: 0;
  transform: scaleX(0.4);
}

body[data-nav="open"] .navbtn__bar:nth-child(3) {
  transform: translateY(-6px) rotate(-45deg);
}

/* OPEN + hover: the whole X spins 90° in place. Because a symmetric X looks
   identical at 0° and 90°, the rotation reads as a smooth, deliberate spin
   that lands back on a perfect X — far more satisfying than nudging the arms.
   Each arm rotates the extra 90° on top of its base 45°/-45°, and the icon
   scales up a touch so it feels responsive to the cursor. */
body[data-nav="open"] .navbtn:hover .navbtn__icon {
  transform: scale(1.08);
}

body[data-nav="open"] .navbtn:hover .navbtn__bar:nth-child(1) {
  transform: translateY(6px) rotate(135deg);
}

body[data-nav="open"] .navbtn:hover .navbtn__bar:nth-child(3) {
  transform: translateY(-6px) rotate(-135deg);
}

@media (prefers-reduced-motion: reduce) {
  .navbtn__bar {
    transition: opacity .2s, background-color .2s;
  }

  .navbtn__icon {
    transition: none;
  }

  .navbtn:hover .navbtn__bar {
    transform: none !important;
    width: 18px !important;
  }

  .navbtn:hover .navbtn__icon {
    transform: none !important;
  }

  /* keep the open X arms in place even under reduced-motion */
  body[data-nav="open"] .navbtn:hover .navbtn__bar:nth-child(1) {
    transform: translateY(6px) rotate(45deg) !important;
  }

  body[data-nav="open"] .navbtn:hover .navbtn__bar:nth-child(3) {
    transform: translateY(-6px) rotate(-45deg) !important;
  }
}
/* ===========================================================================
   PAGE SLOT
   =========================================================================== */
.page {
  flex: 1;
  width: 100%;
  min-width: 0;
  display: flex;
  flex-direction: column;
}

/* ===========================================================================
   HERO  (portfolio) — left-anchored
   =========================================================================== */
.hero {
  /* top padding: much tighter floor so phones don't open with a screenful of
     empty space before any content. It still breathes on desktop via the vh
     middle + rem ceiling. */
  padding-block: clamp(1.75rem, 9vh, 8rem) clamp(2rem, 7vh, 4.5rem);
  container-type: inline-size;
}

.hero__eyebrow {
  margin: 0 0 1.4rem;
  color: var(--muted);
}

/* --- 2-column split when a day has `art`: only art (left) + subject (right) */
.hero__media[hidden] {
  display: none;
}

.hero__subrow {
  margin-top: 1.6rem;
}

.hero__subrow.is-split {
  display: grid;
  grid-template-columns: minmax(0, auto) minmax(0, 1fr);
  gap: clamp(1.25rem, 3.5vw, 2.75rem);
  align-items: start;
  /* art top-aligns with the subject */
}

.hero__subject-col {
  min-width: 0;
}

/* art is decorative, so the hover is barely-there — a whisper of scale */
.hero__media {
  transition: transform 0.6s var(--ease);
}

.hero__media:hover {
  transform: scale(1.012);
}

.hero__art {
  display: block;
  width: -moz-fit-content;
  width: fit-content;
}

.hero__art img {
  display: block;
  width: auto;
  max-width: min(32vw, 180px);
  height: auto;
  border-radius: 16px;
}

.hero__art--symbolic {
  width: clamp(80px, 13vw, 150px);
  aspect-ratio: 1;
  display: grid;
  place-items: center;
  color: var(--text);
}

.hero__art--symbolic::before {
  content: "";
  width: 100%;
  height: 100%;
  background: currentColor;
  -webkit-mask: var(--icon) center/contain no-repeat;
  mask: var(--icon) center/contain no-repeat;
}

/* stack on small screens: art on top, subject beneath */
@media (max-width: 720px) {
  .hero__subrow.is-split {
    grid-template-columns: 1fr;
    gap: 1.25rem;
  }

  .hero__art img {
    max-width: 140px;
  }

  .hero__art--symbolic {
    width: 112px;
  }
}
.hero__caption {
  margin: 0;
  /* floor lowered to 1.9rem so narrow phones actually get a smaller headline
     — the old 2.4rem floor kept it oversized on ~390px screens, where it
     dominated and wrapped awkwardly. Fluid middle does the work from there. */
  font-size: clamp(1.9rem, 9vw, 4.6rem);
  line-height: 1.05;
  letter-spacing: -.035em;
  font-weight: 550;
  max-width: 18ch;
  text-wrap: balance;
  /* avoid a lone orphan word on the last line */
  color: var(--text);
  /* fallback if background-clip:text is unsupported */
}

/* a soft accent-tinted sheen drifts through the headline on a slow ~26s loop */
@supports (-webkit-background-clip: text) or (background-clip: text) {
  .hero__caption {
    background: linear-gradient(100deg, var(--text) 32%, color-mix(in srgb, var(--accent) 55%, var(--text)) 50%, var(--text) 68%);
    background-size: 220% auto;
    background-position: 0% center;
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
  }

  /* .reveal sets a single `animation`; re-declare both here so the entrance
     fade-up AND the looping sheen coexist (this selector outranks .reveal) */
  .hero__caption.reveal {
    animation: fade-up 0.7s var(--ease) both, caption-sheen 26s ease-in-out infinite;
    animation-delay: calc(var(--d, 0) * 90ms), 0s;
  }
}
@keyframes caption-sheen {
  0%, 100% {
    background-position: 0% center;
  }
  50% {
    background-position: 100% center;
  }
}
.hero__slogan {
  margin: .9rem 0 0;
  color: var(--muted);
  font-size: clamp(0.95rem, 2.5vw, 1.05rem);
}

.hero__subject {
  margin: 0;
  max-width: 34ch;
  font-size: clamp(1.05rem, 2.4vw, 1.3rem);
}

/* inline ↗ appended to the subject (single string link) */
.hero__arrow-link {
  display: inline-block;
  margin-left: .1em;
  color: var(--accent);
  text-decoration: none;
  transition: transform 0.25s var(--ease);
}

.hero__arrow-link[hidden] {
  display: none;
}

.hero__arrow-link:hover {
  transform: translate(3px, -3px);
}

/* labelled ↗ links row (list link) */
.hero__daylinks {
  list-style: none;
  margin: 1.4rem 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 1.1rem;
}

.hero__daylinks[hidden] {
  display: none;
}

.hero__daylinks a {
  display: inline-flex;
  align-items: center;
  gap: .3rem;
  color: var(--accent);
  text-decoration: none;
  font-weight: 500;
}

.hero__daylinks a:hover {
  text-decoration: underline;
  text-underline-offset: 3px;
}

.hero__daylinks .hero__arrow-icon {
  transition: transform 0.25s var(--ease);
}

.hero__daylinks a:hover .hero__arrow-icon {
  transform: translate(3px, -3px);
}

/* profile links — small hoverable cards at the foot of the page, echoing the
   same border/shadow/lift language as .card and .feature above */
.profile-links {
  margin-block: 2rem 1rem;
}

.profile-links ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: .8rem;
}

.profile-links a {
  display: inline-flex;
  align-items: center;
  gap: .75rem;
  padding: .65rem .9rem .65rem .65rem;
  color: var(--text);
  text-decoration: none;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  transition: border-color 0.2s, transform 0.2s var(--ease), box-shadow 0.2s;
}

.profile-links a:hover {
  border-color: var(--border-hover);
  transform: translateY(-2px);
  box-shadow: var(--shadow);
}

.profile-links__badge {
  display: grid;
  place-items: center;
  flex-shrink: 0;
  width: 2rem;
  height: 2rem;
  /* identical box for both the letter and the image variant */
  border-radius: 50%;
  background: var(--accent-soft);
  color: var(--accent);
  font-size: .8rem;
  text-transform: uppercase;
  overflow: hidden;
  /* clips a provided image to the same circle */
}

.profile-links__badge--img {
  background: var(--surface-2);
}

.profile-links__badge img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.profile-links__text {
  display: flex;
  flex-direction: column;
  gap: .1rem;
  min-width: 0;
}

.profile-links__label {
  font-weight: 550;
  line-height: 1.2;
}

.profile-links__url {
  font-size: .74rem;
  line-height: 1.2;
}

.profile-links__arrow {
  flex-shrink: 0;
  margin-left: .1rem;
  color: var(--muted);
  transition: transform 0.25s var(--ease), color 0.2s;
}

.profile-links a:hover .profile-links__arrow {
  color: var(--accent);
  transform: translate(2px, -2px);
}

@media (max-width: 560px) {
  /* stack as full-width rows, like the showcase cards, instead of a centered
     wrap — each item stretches to the container width */
  .profile-links ul {
    flex-direction: column;
    align-items: stretch;
  }

  .profile-links a {
    width: 100%;
  }
}
/* ===========================================================================
   ACTIVITY — live GitHub ticker between the hero and the first showcase
   =========================================================================== */
.activity {
  display: flex;
  align-items: center;
  gap: .7rem;
  min-height: 1.6rem;
  margin-block: 1.5rem;
  /* even rhythm above and below, not 0.5/2.5 */
  font-size: .92rem;
  opacity: 0;
  transition: opacity 0.5s var(--ease);
}

.activity.is-ready {
  opacity: 1;
}

.activity[hidden] {
  display: none;
}

.activity__status {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  flex-shrink: 0;
}

.activity__label {
  font-size: .78rem;
  color: var(--muted);
  letter-spacing: .01em;
}

.activity__label[hidden] {
  display: none;
}

.activity__dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--border-hover);
  flex-shrink: 0;
}

/* One keyframe, reused by every tier: each tier sets its own --pulse-rgb
   (glow colour) and --pulse-dur (speed) as custom properties, so the ring
   colour and pace both track that ring's own dot colour without needing a
   separate @keyframes block per tier. */
@keyframes activity-pulse {
  0% {
    box-shadow: 0 0 0 0 rgba(var(--pulse-rgb, 41, 167, 69), 0.45);
  }
  70% {
    box-shadow: 0 0 0 7px rgba(var(--pulse-rgb, 41, 167, 69), 0);
  }
  100% {
    box-shadow: 0 0 0 0 rgba(var(--pulse-rgb, 41, 167, 69), 0);
  }
}
/* The dot reflects the age of whichever event is CURRENTLY on screen — not a
   separate "last active overall" stat — so it can never disagree with the
   "x ago" text beside it: they always describe the same event. As an event
   ages from fresh to old, the dot cools from a fast vivid-green pulse down
   through calmer, paler greens, into a slow grey pulse, and finally a still
   grey with no pulse at all — six steps mapped in activity.js. */
.activity__status.tier-fresh .activity__dot {
  background: #22c55e;
  --pulse-rgb: 34,197,94;
  --pulse-dur: 1.1s;
}

.activity__status.tier-today .activity__dot {
  background: #29a745;
  --pulse-rgb: 41,167,69;
  --pulse-dur: 2.4s;
}

.activity__status.tier-week .activity__dot {
  background: #7bc596;
  --pulse-rgb: 123,197,150;
  --pulse-dur: 3.4s;
}

.activity__status.tier-fortnight .activity__dot {
  background: #9a9d97;
  --pulse-rgb: 154,157,151;
  --pulse-dur: 4.6s;
}

.activity__status.tier-month .activity__dot {
  background: #c3c5c0;
  --pulse-rgb: 195,197,192;
  --pulse-dur: 6s;
}

.activity__status.tier-quiet .activity__dot {
  background: var(--border-hover);
}

.activity__status.tier-fresh .activity__dot,
.activity__status.tier-today .activity__dot,
.activity__status.tier-week .activity__dot,
.activity__status.tier-fortnight .activity__dot,
.activity__status.tier-month .activity__dot {
  animation: activity-pulse var(--pulse-dur, 2.4s) ease-out infinite;
}

/* tier-quiet intentionally gets no animation — a still dot for old activity */
@media (prefers-reduced-motion: reduce) {
  .activity__dot {
    animation: none !important;
    box-shadow: none !important;
  }
}
/* ---- rotation reveal ---------------------------------------------------
   On each rotation the whole line re-enters as one orchestrated sweep: the
   dot gives a small "ping", then verb → prep → repo → time cascade in with a
   short stagger. Driven by the .is-in class on .activity__event; the parts
   share one keyframe and differ only by animation-delay, so they stay in sync
   no matter the tier or content. */
@keyframes activity-part-in {
  from {
    opacity: 0;
    transform: translateY(6px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}
@keyframes activity-dot-ping {
  0% {
    transform: scale(1);
  }
  35% {
    transform: scale(1.55);
  }
  100% {
    transform: scale(1);
  }
}
.activity__event {
  display: inline-flex;
  align-items: baseline;
  gap: .35rem;
  min-width: 0;
  color: var(--text);
  text-decoration: none;
}

/* parts start hidden; .is-in plays the cascade */
.activity__verb,
.activity__prep,
.activity__repo,
.activity__time {
  opacity: 0;
}

.activity__event.is-in .activity__verb,
.activity__event.is-in .activity__prep,
.activity__event.is-in .activity__repo,
.activity__event.is-in .activity__time {
  animation: activity-part-in 0.34s var(--ease) both;
}

.activity__event.is-in .activity__verb {
  animation-delay: .00s;
}

.activity__event.is-in .activity__prep {
  animation-delay: .06s;
}

.activity__event.is-in .activity__repo {
  animation-delay: .12s;
}

.activity__event.is-in .activity__time {
  animation-delay: .20s;
}

/* the dot also "pings" (a quick scale) on each rotation, in sync with the
   line cascade. That's driven from JS via the Web Animations API rather than a
   CSS class, so it composites independently of the continuous box-shadow pulse
   above instead of overriding it. Keyframe kept here for reference/reuse. */
.activity__verb {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* "to"/"in" is a connector, not part of the destination — it stays muted and
   never underlines, even though the whole row is one large click target */
.activity__prep {
  color: var(--muted);
}

.activity__prep:empty {
  display: none;
}

.activity__repo {
  color: var(--accent);
  white-space: nowrap;
}

.activity__event:hover .activity__repo {
  text-decoration: underline;
  text-underline-offset: 2px;
}

.activity__time {
  color: var(--muted);
  font-size: .78rem;
  flex-shrink: 0;
}

/* reduced-motion: no cascade or ping — parts are simply shown */
@media (prefers-reduced-motion: reduce) {
  .activity__verb, .activity__prep, .activity__repo, .activity__time {
    opacity: 1 !important;
    animation: none !important;
  }

  .activity__status.is-ping .activity__dot {
    animation: none !important;
  }
}
@media (max-width: 560px) {
  .activity {
    font-size: .86rem;
    gap: .55rem;
  }

  .activity__event {
    gap: .3rem;
  }
}
/* touch: pad the ticker link vertically so it's comfortably tappable, without
   shifting the visible baseline (negative margin absorbs the padding) */
@media (hover: none) and (pointer: coarse) {
  .activity__event {
    padding-block: .6rem;
    margin-block: -.6rem;
  }
}
.portfolio-body {
  margin-block: 2rem 4rem;
  max-width: var(--measure);
}

/* --- top highlight (optional, portfolio) --------------------------------- */
.feature {
  display: inline-flex;
  align-items: center;
  gap: .9rem;
  margin-bottom: 1.6rem;
  padding: 1.7rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 15px;
  font-size: .9rem;
  transition: border-color 0.2s, transform 0.2s var(--ease), box-shadow 0.2s;
}

.feature:hover {
  border-color: var(--border-hover);
  transform: translateY(-1px);
  box-shadow: var(--shadow);
}

.feature__link {
  display: inline-flex;
  align-items: center;
  gap: .7rem;
  color: inherit;
  text-decoration: none;
}

.feature__label {
  flex-shrink: 0;
  padding: .18rem .55rem;
  background: var(--accent-soft);
  color: var(--accent);
  border-radius: 7px;
  font-size: .72rem;
  letter-spacing: .02em;
}

.feature__body {
  color: var(--text);
}

.feature__body strong {
  font-weight: 550;
}

.feature__arrow {
  flex-shrink: 0;
  color: var(--muted);
  padding-right: .35rem;
}

/* a second, independent click target — opens the repo's star page. GitHub
   gives no way to star with one click from off-site, so this is honestly just
   a fast, clearly-labelled door to the real button, not a fake "star" action. */
.feature__star {
  display: inline-flex;
  align-items: center;
  gap: .3rem;
  flex-shrink: 0;
  padding-left: .9rem;
  margin-left: .1rem;
  border-left: 1px solid var(--border);
  color: var(--muted);
  text-decoration: none;
  transition: color .2s;
}

.feature__star[hidden] {
  display: none;
}

.feature__star-icon {
  width: 16px;
  height: 16px;
  transition: transform 0.3s var(--ease), fill 0.2s, color 0.2s;
}

.feature__star:hover {
  color: #d9922b;
}

.feature__star:hover .feature__star-icon,
.feature__star:focus-visible .feature__star-icon {
  fill: #d9922b;
  transform: scale(1.15) rotate(-8deg);
}

/* only applied when the count is re-checked on return and genuinely rose —
   never a guess, so this only ever fires on confirmed, real feedback */
.feature__star.just-rose .feature__star-icon {
  fill: #d9922b;
  color: #d9922b;
  animation: star-pop 0.5s var(--ease);
}

.feature__star.just-rose .feature__star-count {
  color: #d9922b;
}

@keyframes star-pop {
  0% {
    transform: scale(1);
  }
  40% {
    transform: scale(1.35) rotate(-10deg);
  }
  100% {
    transform: scale(1);
  }
}
.feature__star-count {
  font-size: .78rem;
  min-width: 1ch;
}

/* Small screens: the pill can't hold label + body on one line. Stack the
   feature into: the label pill on its own line, then a row holding the body
   and the star together — so the star sits WITH the content rather than
   stranded as a full-width bar at the bottom with dead space beside it. */
@media (max-width: 560px) {
  .feature {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: .7rem;
    width: 100%;
    padding: 1.3rem;
  }

  /* the link stacks too: label pill above, body beneath, with real breathing
     room between them (was cramped because the link stayed a centered row) */
  .feature__link {
    flex-direction: column;
    align-items: flex-start;
    gap: .55rem;
  }

  .feature__body {
    text-align: left;
    line-height: 1.45;
  }

  .feature__arrow {
    display: none;
  }

  /* star sits on its own line, but pulled to the start and compact — no
     full-width stretch, so there's no empty gutter to its right */
  .feature__star {
    align-self: flex-start;
    padding: .5rem .7rem;
    margin-left: 0;
    border-left: none;
    background: var(--surface-2);
    border-radius: 8px;
    color: var(--text);
  }
}
/* --- showcase: apps + tools share one card grid -------------------------- */
.showcase {
  padding-block: clamp(1.5rem, 4vh, 2.5rem);
}

.showcase__heading {
  margin: 0 0 1.4rem;
  text-transform: uppercase;
  letter-spacing: .14em;
  font-size: .72rem;
}

.showcase__grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
  max-width: 58rem;
}

.card {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-areas: "art name" "art blurb" "art links";
  align-content: start;
  column-gap: 1rem;
  row-gap: .3rem;
  padding: 1rem 1.1rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  transition: border-color 0.2s, transform 0.2s var(--ease), box-shadow 0.2s;
}

.card:hover {
  border-color: var(--border-hover);
  transform: translateY(-2px);
  box-shadow: var(--shadow);
}

.card__art {
  grid-area: art;
  align-self: start;
  flex-shrink: 0;
  width: 48px;
  height: 48px;
  display: grid;
  place-items: center;
  border-radius: 11px;
  overflow: hidden;
  background: var(--surface-2);
  color: var(--muted);
}

.card__art img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.card__art-mark {
  font-size: 1rem;
  color: var(--accent);
}

/* symbolic (GNOME) icons: monochrome glyphs painted with currentColor via a
   mask so they track the theme instead of rendering as flat black */
.card__art--symbolic {
  color: var(--text);
}

.card__art--symbolic::before {
  content: "";
  width: 62%;
  height: 62%;
  background: currentColor;
  -webkit-mask: var(--icon) center/contain no-repeat;
  mask: var(--icon) center/contain no-repeat;
}

.card__name {
  grid-area: name;
  align-self: center;
  min-width: 0;
  font-weight: 500;
  letter-spacing: -.01em;
}

.card__blurb {
  grid-area: blurb;
  margin: 0;
  font-size: .88rem;
  line-height: 1.5;
  color: var(--muted);
}

.card__links {
  grid-area: links;
  margin: .1rem 0 0;
  font-size: .82rem;
}

.card__links a {
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 2px;
  text-decoration-thickness: 1px;
  transition: opacity .15s;
}

.card__links a:hover {
  opacity: .7;
}

.card__sep {
  margin-inline: .5rem;
  color: var(--muted);
  opacity: .5;
}

.card__arrow {
  text-decoration: none;
}

/* Phones: the art no longer spans the whole card height (which left dead space
   beneath a short icon). Instead it pairs with the name on the top row, and the
   blurb + links run full width beneath — a tidy header-then-body layout. */
@media (max-width: 560px) {
  .card {
    grid-template-areas: "art name" "blurb blurb" "links links";
    align-items: center;
    row-gap: .55rem;
  }

  .card__art {
    align-self: center;
    width: 44px;
    height: 44px;
  }
}
/* ===========================================================================
   DOCS  — single column; the sidebar is global now
   =========================================================================== */
.docs {
  padding-block: clamp(1.5rem, 4vh, 2.5rem) 4rem;
}

.docs__content {
  min-width: 0;
  container-type: inline-size;
}

.docs__head {
  margin-bottom: 2rem;
}

.docs__head h1 {
  margin: .3rem 0 0;
  font-size: clamp(1.9rem, 5vw, 2.6rem);
  letter-spacing: -.025em;
}

.docs__lede {
  color: var(--muted);
  font-size: 1.1rem;
  margin: .8rem 0 0;
}

.is-graph .page,
.is-graph main {
  min-height: 0;
}

.is-graph .graph {
  display: flex;
  flex-direction: column;
  height: calc(100dvh - var(--topbar-h));
  padding: 1rem var(--gutter) 1rem;
  gap: .8rem;
}

.graph__stage {
  position: relative;
  flex: 1;
  min-height: 0;
  width: 100%;
  /* transparent — the graph floats on the page, no boxed white fill; a faint
     dotted texture gives depth and adapts to light/dark via the border token. */
  background: radial-gradient(circle at center, color-mix(in srgb, var(--border) 55%, transparent) 1px, transparent 1px);
  background-size: 22px 22px;
  border-radius: var(--radius);
  overflow: hidden;
  cursor: grab;
}

.graph__stage:active {
  cursor: grabbing;
}

.graph__canvas {
  display: block;
  width: 100%;
  height: 100%;
}

.graph__controls {
  position: absolute;
  right: .6rem;
  bottom: .6rem;
  display: flex;
  flex-direction: column;
  gap: .3rem;
  z-index: 2;
}

.graph__ctrl {
  width: 2rem;
  height: 2rem;
  display: grid;
  place-items: center;
  font-size: 1.05rem;
  line-height: 1;
  color: var(--text);
  background: color-mix(in srgb, var(--surface) 88%, transparent);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  cursor: pointer;
  backdrop-filter: blur(4px);
  transition: background .15s, border-color .15s;
}

.graph__ctrl:hover {
  background: var(--surface-2);
  border-color: var(--border-hover);
}

.graph__empty {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  color: var(--muted);
  font-size: .95rem;
  pointer-events: none;
  /* never intercept canvas interaction */
}

.graph__empty[hidden] {
  display: none;
}

/* [hidden] must beat display:grid */
.graph__tooltip {
  position: absolute;
  transform: translate(-50%, -100%);
  background: var(--text);
  color: var(--bg);
  padding: .25rem .55rem;
  border-radius: 7px;
  font-size: .8rem;
  white-space: nowrap;
  pointer-events: none;
  z-index: 2;
  box-shadow: var(--shadow);
}

.graph__tooltip[hidden] {
  display: none;
}

.graph__bar {
  flex-shrink: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: .5rem 1.5rem;
  padding: 0 .2rem;
}

.graph__info {
  display: flex;
  align-items: baseline;
  gap: .6rem;
  min-width: 0;
}

.graph__title {
  font-weight: 600;
}

.graph__hint {
  font-size: .8rem;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.graph__legend {
  display: flex;
  flex-wrap: wrap;
  gap: .4rem 1.1rem;
  font-size: .82rem;
  color: var(--muted);
}

.graph__legend-item {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  line-height: 1;
}

.graph__legend-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  flex-shrink: 0;
}

@media (max-width: 560px) {
  .graph__hint {
    display: none;
  }
}
/* ===========================================================================
   SEARCH — one design, identical on home and docs.
   ---------------------------------------------------------------------------
   Desktop : an always-visible box (leading icon + input); the results join it
             into one continuous, shadowed box. Fluid width — wider on big
             screens, never a fixed size that fights the layout on resize.
   Phones  : the box is a single icon that opens a full-screen overlay.
   =========================================================================== */
.search {
  position: relative;
  z-index: 60;
    /* lifts the WHOLE box (bar + panel) above the search-dim
overlay and the sidebar; a z-index on the panel alone
doesn't help since .search__bar itself has no stacking
elevation and would still be painted over */
  display: flex;
  align-items: center;
  min-width: 0;
}

/* --- the input box ------------------------------------------------------- */
.search__bar {
  display: flex;
  align-items: center;
  gap: .15rem;
  height: var(--btn);
  width: clamp(13rem, 26vw, 28rem);
  /* fluid: grows with the viewport */
  padding-left: .5rem;
  background: var(--surface);
  /* SAME colour as the results panel */
  border: 1px solid var(--border);
  border-radius: var(--radius-btn);
  transition: border-color .2s, box-shadow .2s;
}

.search__bar:hover {
  border-color: var(--border-hover);
}

.search__bar:focus-within {
  border-color: var(--border-hover);
  box-shadow: var(--shadow);
}

.search__toggle {
  display: inline-grid;
  place-items: center;
  flex-shrink: 0;
  width: 1.6rem;
  height: 1.6rem;
  color: var(--muted);
  background: none;
  border: none;
  cursor: text;
}

.search__toggle svg {
  width: 16px;
  height: 16px;
}

.search__field {
  display: flex;
  align-items: center;
  flex: 1;
  min-width: 0;
  padding-right: .4rem;
}

/* discoverable shortcut hint: "/" or Ctrl+K — hidden once the box has focus
   or a value. Two separate <kbd> chips (not one span of text) so each key
   reads as its own distinct, pressable-looking key, same convention as
   command palettes elsewhere (VS Code, Linear, GitHub's own search). */
/* The shortcut hint is only meaningful with a physical keyboard, which tracks
   desktop width — below the desktop breakpoint (iPad and down) it just looks
   like clutter, so hide it there. It's still additionally hidden on focus/typing
   at any size (below). */
.search__hint {
  display: none;
  user-select: none;
  /* decorative affordance — not text to select/copy */
  -webkit-user-select: none;
}

@media (min-width: 900px) {
  .search__hint {
    display: inline-flex;
    align-items: center;
    gap: .3rem;
    flex-shrink: 0;
    margin-left: .3rem;
    font-size: .72rem;
    color: var(--muted);
    white-space: nowrap;
  }
}
.search__hint kbd {
  padding: .08rem .4rem;
  font-family: var(--font-mono);
  line-height: 1.35;
  color: var(--muted);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: 4px;
}

.search__hint-sep {
  font-size: .68rem;
}

.search__hint-plus {
  font-size: .68rem;
  margin: 0 -.05rem;
}

.search__bar:focus-within .search__hint,
.search__input:not(:placeholder-shown) ~ .search__hint {
  display: none;
}

.search__back {
  display: none;
}

/* only appears in the phone overlay */
.search__input {
  flex: 1;
  min-width: 0;
  font: inherit;
  height: 100%;
  padding: 0 .7rem 0 .2rem;
  color: var(--text);
  background: var(--surface);
  border: none;
  border-radius: inherit;
}

.search__input:focus {
  outline: none;
}

.search__input::placeholder {
  color: var(--muted);
}

/* --- results panel: joins the bar into one box --------------------------- */
.search__panel {
  position: absolute;
  top: 100%;
  /* touches the bar — no gap, no overlap */
  left: 0;
  right: 0;
  /* exactly the bar's width */
  background: var(--surface);
  /* SAME colour as the bar */
  border: 1px solid var(--border-hover);
  border-top: none;
  /* the bar's bottom edge is the divider */
  border-radius: 0 0 var(--radius-btn) var(--radius-btn);
  box-shadow: var(--shadow);
  overflow: hidden;
  z-index: 60;
}

/* active: bar + panel read as a single continuous, shadowed box — the top of
   the panel and the bottom of the bar are flat where they meet, the outer
   corners stay round, and the shadow wraps the whole thing (the seam sits
   hidden between the two opaque surfaces). */
.search.has-results .search__bar {
  border-color: var(--border-hover);
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
  box-shadow: var(--shadow);
}

/* --- result rows --------------------------------------------------------- */
.search__results {
  list-style: none;
  margin: 0;
  padding: .35rem;
  max-height: 60vh;
  overflow-y: auto;
}

.search__results li {
  border-radius: var(--radius-sm);
}

.search__results a {
  display: block;
  padding: .6rem .7rem;
}

.search__results a:hover, .search__results a.is-focused {
  background: var(--surface-2);
}

.search__rtitle {
  font-weight: 550;
}

.search__rcat {
  font-family: var(--font-mono);
  font-size: .7rem;
  color: var(--accent);
}

.search__rsnip {
  color: var(--muted);
  font-size: .85rem;
  margin-top: .15rem;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.search__rsnip mark {
  background: var(--accent-soft);
  color: var(--text);
  border-radius: 3px;
  padding: 0 .1em;
}

.search__empty {
  padding: 1rem .8rem;
  color: var(--muted);
  font-size: .9rem;
}

/* --- phones: single icon → full-screen overlay --------------------------- */
@media (max-width: 699.98px) {
  .search__bar {
    width: auto;
    height: var(--btn);
    padding: 0;
    background: none;
    border: 1px solid transparent;
  }

  .search__field {
    display: none;
  }

  /* only the icon shows in the header */
  .search__toggle {
    width: var(--btn);
    height: var(--btn);
    border-radius: var(--radius-btn);
    cursor: pointer;
  }

  .search__toggle svg {
    width: 17px;
    height: 17px;
  }

  .search.is-open {
    position: fixed;
    inset: 0;
    height: 100dvh;
    z-index: 200;
    display: block;
    background: var(--bg);
  }

  .search.is-open .search__bar {
    display: flex;
    align-items: center;
    gap: .35rem;
    width: auto;
    height: var(--topbar-h);
    padding-inline: var(--gutter);
    border: none;
    border-bottom: 1px solid var(--border);
    border-radius: 0;
    box-shadow: none;
    background: var(--bg);
  }

  .search.is-open .search__toggle {
    display: none;
  }

  .search.is-open .search__field {
    display: flex;
    flex: 1;
  }

  .search.is-open .search__back {
    display: inline-grid;
    place-items: center;
    flex-shrink: 0;
    width: var(--btn);
    height: var(--btn);
    margin-left: -.4rem;
    color: var(--text);
    background: none;
    border: none;
    border-radius: var(--radius-btn);
    cursor: pointer;
  }

  .search.is-open .search__back:hover {
    background: var(--surface-2);
  }

  .search.is-open .search__back svg {
    width: 22px;
    height: 22px;
  }

  .search.is-open .search__input {
    height: 42px;
    padding: 0 .9rem;
    background: var(--surface);
    /* filled field, no border on phones */
    border: none;
    border-radius: var(--radius-btn);
  }

  .search.is-open .search__panel {
    position: static;
    display: block;
    width: 100%;
    height: calc(100dvh - var(--topbar-h));
    border: none;
    border-radius: 0;
    box-shadow: none;
    background: var(--bg);
    overflow-y: auto;
  }

  .search.is-open .search__results {
    max-height: none;
    padding: .5rem;
  }

  .search.is-open .search__results a {
    padding: .8rem .7rem;
  }
}
body.search-lock {
  overflow: hidden;
}

/* dims the sidebar and ordinary page content while results are open on
   desktop — a fixed, always-present, non-interactive layer so it can fade
   smoothly instead of popping in; phones skip this (their overlay is already
   an opaque full-screen surface). Because .topbar sits ABOVE this layer (see
   the comment on .topbar), its own chrome — brand, nav button, theme toggle —
   needs a second, separate rule below to dim in step with everything else. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 55;
  background: rgba(10, 12, 16, 0.4);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.25s var(--ease);
}

body.search-dim::before {
  opacity: 1;
}

/* the search box's own siblings within the topbar (brand, nav button, theme
   toggle) — everything in the header EXCEPT the search box itself */
body.search-dim .topbar__cluster > :not(.search) {
  opacity: .4;
  pointer-events: none;
  transition: opacity 0.25s var(--ease);
}

/* ===========================================================================
   PROSE
   =========================================================================== */
.prose {
  max-width: var(--measure);
}

.prose h1, .prose h2, .prose h3, .prose h4 {
  font-weight: 550;
}

.prose h2 {
  margin: 2.6rem 0 .8rem;
  font-size: 1.45rem;
  letter-spacing: -.02em;
}

.prose h3 {
  margin: 2rem 0 .6rem;
  font-size: 1.15rem;
}

.prose strong, .prose b {
  font-weight: 550;
}

.prose p, .prose ul, .prose ol {
  margin: 0 0 1.1rem;
}

.prose a {
  color: var(--accent);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
}

.prose a:hover {
  text-decoration-thickness: 2px;
}

.prose code {
  font-family: var(--font-mono);
  font-size: .85em;
  background: var(--surface-2);
  border: 1px solid var(--border);
  padding: .1em .35em;
  border-radius: 5px;
}

.prose pre {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1rem 1.1rem;
  overflow-x: auto;
  font-size: .85rem;
  line-height: 1.6;
}

.prose pre code {
  background: none;
  border: none;
  padding: 0;
}

.prose blockquote {
  margin: 1.4rem 0;
  padding: .2rem 1.1rem;
  border-left: 3px solid var(--accent);
  color: var(--muted);
}

.prose table {
  width: 100%;
  border-collapse: collapse;
  margin: 1.2rem 0;
  font-size: .92rem;
}

.prose th, .prose td {
  text-align: left;
  padding: .55rem .7rem;
  border-bottom: 1px solid var(--border);
}

.prose th {
  font-family: var(--font-mono);
  font-size: .8rem;
  color: var(--muted);
  font-weight: 550;
}

.prose hr {
  border: none;
  border-top: 1px solid var(--border);
  margin: 2.5rem 0;
}

/* wiki-links: internal [[links]] resolved at build time. Subtle accent with a
   dotted underline so they read as "connected notes" rather than plain links;
   broken ones (no matching page) are muted and dashed, like Obsidian. */
.wikilink {
  color: var(--accent);
  text-decoration: none;
  border-bottom: 1px dotted var(--accent);
}

.wikilink:hover {
  border-bottom-style: solid;
}

.wikilink--broken {
  color: var(--muted);
  border-bottom: 1px dashed var(--border-hover);
  cursor: help;
}

/* ===========================================================================
   FOOTERS
   =========================================================================== */
/* Footers sit quietly at the very bottom, so their text is dimmer than the
   usual muted colour — a dedicated --footer-dim token per theme (defined in
   _tokens.scss) rather than opacity, so the Jekyll link can still shift to the
   accent colour on hover without any parent-opacity compounding. */
.portfoot {
  padding-block: 2.5rem 2rem;
  color: var(--footer-dim);
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: .3rem .7rem;
}

.portfoot__updated[hidden] {
  display: none;
}

.portfoot__updated {
  font-size: .82rem;
}

.docfoot {
  margin-top: auto;
  border-top: 1px solid var(--border);
  padding-block: 1.5rem;
}

.docfoot__inner {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem 1.5rem;
  justify-content: space-between;
  color: var(--footer-dim);
}

.docfoot__built {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
}

.docfoot__jekyll {
  display: inline-flex;
  color: inherit;
  transition: color .15s;
}

.docfoot__jekyll:hover {
  color: var(--accent);
}

.docfoot__jekyll svg {
  display: block;
  width: 15px;
  height: 15px;
}

/* ===========================================================================
   MOTION
   =========================================================================== */
@keyframes fade-up {
  from {
    opacity: 0;
    transform: translateY(16px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}
.reveal {
  animation: fade-up 0.7s var(--ease) both;
  animation-delay: calc(var(--d, 0) * 90ms);
}

.js-reveal .reveal-on-scroll {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity 0.6s var(--ease), transform 0.6s var(--ease);
}

.js-reveal .reveal-on-scroll.is-visible {
  opacity: 1;
  transform: none;
}

/* profile-link items cascade in when the nav reveals — each <li> starts hidden
   and eases up with a stagger keyed to its --i index, so the list assembles
   itself the way the hero elements do, rather than appearing all at once. */
.js-reveal .profile-links.reveal-on-scroll .profile-links__item {
  opacity: 0;
  transform: translateY(12px);
  transition: opacity 0.5s var(--ease), transform 0.5s var(--ease);
}

.js-reveal .profile-links.reveal-on-scroll.is-visible .profile-links__item {
  opacity: 1;
  transform: none;
  transition-delay: calc(var(--i, 0) * 80ms);
}

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

  .reveal, .reveal-on-scroll, .profile-links__item {
    opacity: 1 !important;
    transform: none !important;
  }
}
