/* mushreeves.com - personal site, brand: mush.
   Self-hosted variable fonts, no build step, no third-party requests.

   Two colours carry the whole page: a deep navy (mush.'s "Deep") for text, the
   mark and every dark surface, and one blue accent for links, rules and label
   markers. Bright blue is reserved for accent-on-dark only - it is the only
   thing that reaches AA against Deep, which the contrast pass below exists to
   prove rather than assume. Space Grotesk carries headings and interface text;
   JetBrains Mono carries labels, metadata and tags - the two-typeface system
   the brand pack specifies, not a serif/sans pairing.

   Cards and bands are flat with hairline rules. No drop shadows. */

@font-face {
  font-family: "Space Grotesk";
  src: url("../fonts/space-grotesk-latin-var.woff2") format("woff2-variations");
  font-weight: 300 700;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "JetBrains Mono";
  src: url("../fonts/jetbrains-mono-latin-var.woff2") format("woff2-variations");
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}

:root {
  --paper: #eef2f8;
  --paper-alt: #e3eaf4;
  --card: #ffffff;
  --ink: #0b1b34;
  --ink-soft: #46586f;
  --ink-mute: #55647e;
  --rule: #d6e0ee;
  --rule-strong: #b8c8de;

  --banner: #0b1b34;
  --navy-solid: #0b1b34;
  --accent-line: #2563c9;
  --on-banner: #eef2f8;
  --on-banner-soft: #8fa4c2;
  --on-banner-bright: #5b9bff;

  /* fixed Blue, never swapped to Bright - for the couple of spots that fill
     a shape with the accent and put white text on top. Bright only clears
     AA as a text colour on a dark surface, not as a filled background
     (white on Bright is 2.77:1, white on Blue is 5.67:1, checked in both
     themes) - so anything doing that fill has to stay off --accent-line,
     which swaps to Bright in dark theme, and read this token instead */
  --blue-fixed: #2563c9;

  /* one radius family, everywhere - the mark's own tile is a rounded
     square at roughly 22% of its own size (see .mark), so every other
     rounded-rect container on the page - cards, buttons, the terminal,
     chips - uses a radius from this same small scale rather than each
     component having picked its own value independently. --radius-sm for
     small controls and chips, --radius for anything card-sized. Circular
     elements (the mark itself, the hero photo) stay circular - this is
     about the rounded-*rectangle* family, not forcing one shape everywhere. */
  --radius: 0.875rem;
  --radius-sm: 0.5rem;

  /* a soft, layered shadow for elevation - two stops so it doesn't read as
     one hard-edged smudge. Applied the same in both themes rather than
     conditionally, deliberately: the colour is Deep, which in dark theme
     is close to the page background it would fall on, so it recedes to
     near-invisible on its own without needing an explicit override to
     remove it. The hairline border already carries the separation job
     there regardless. */
  --shadow: 0 1px 2px rgba(11, 27, 52, 0.06), 0 10px 28px -8px rgba(11, 27, 52, 0.14);

  --measure: 62ch;
  --gutter: clamp(1.25rem, 5vw, 4rem);

  --t-mega: clamp(2.5rem, 8vw, 5.5rem);
  --t-loud: clamp(1.75rem, 4.2vw, 2.875rem);
  --t-mid: clamp(1.1875rem, 2vw, 1.5rem);
  --t-lede: clamp(1.125rem, 1.6vw, 1.375rem);
  --t-body: 1.0625rem;
  --t-small: 0.9375rem;
  --t-micro: 0.75rem;

  color-scheme: light;

  /* --- Bootstrap, themed before it's fought ---
     Bootstrap 5.3 reads most of its own palette from these custom properties
     at runtime, so redefining them reskins buttons, the navbar, focus rings
     and links without touching bootstrap.min.css or writing a single
     !important. Two traps the `bootstrap` skill flags, handled here: the
     `-rgb` triplets Bootstrap's opacity utilities read (colour alone does
     nothing to them), and `.btn-primary` itself further down - it compiles
     to a literal hex in Bootstrap's own build, so a :root variable is never
     read there and it needs its own --bs-btn-* set. */
  --bs-primary: var(--accent-line);
  --bs-primary-rgb: 37, 99, 201;
  --bs-link-color: var(--accent-line);
  --bs-link-color-rgb: 37, 99, 201;
  --bs-link-hover-color: var(--ink);
  --bs-link-hover-color-rgb: 11, 27, 52;
  --bs-body-bg: var(--paper);
  --bs-body-color: var(--ink);
  --bs-border-color: var(--rule);
  --bs-font-sans-serif: "Space Grotesk", Helvetica, Arial, sans-serif;
  --bs-font-monospace: "JetBrains Mono", ui-monospace, monospace;
  --bs-border-radius: var(--radius);
  --bs-focus-ring-color: rgba(37, 99, 201, 0.35);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-bs-theme="light"]) {
    --paper: #0a1220;
    --paper-alt: #0f1a2c;
    --card: #131f34;
    --ink: #eef2f8;
    --ink-soft: #c3d4ea;
    --ink-mute: #8fa4c2;
    --rule: #1f2e45;
    --rule-strong: #33455f;

    --banner: #0d1f3b;
    /* not its own dark-theme value any more - see the comment on the
       [data-bs-theme="dark"] block below, which this must match */
    --navy-solid: var(--banner);
    --accent-line: #5b9bff;

    --bs-primary-rgb: 91, 155, 255;
    --bs-link-color-rgb: 91, 155, 255;
    --bs-link-hover-color-rgb: 238, 242, 248;
    --bs-focus-ring-color: rgba(91, 155, 255, 0.4);

    color-scheme: dark;
  }
}

:root[data-bs-theme="dark"] {
  --paper: #0a1220;
  --paper-alt: #0f1a2c;
  --card: #131f34;
  --ink: #eef2f8;
  --ink-soft: #c3d4ea;
  --ink-mute: #8fa4c2;
  --rule: #1f2e45;
  --rule-strong: #33455f;

  --banner: #0d1f3b;
  /* was its own lighter value (#21406f) - a full-page contrast audit
     (7 September 2026) found the Contact block's on-banner-soft body text
     failing AA at 4.07:1 against it, because that token is calibrated
     against --banner, not against a second, lighter "dark surface" colour
     nobody else on the page uses. Light theme already has navy-solid
     tracking banner exactly (both #0b1b34); this restores the same
     relationship in dark theme rather than patching the text colour, so
     every other var(--navy-solid) consumer (the skip-link, the hover text
     on outline buttons) gets the same fix for free */
  --navy-solid: var(--banner);
  --accent-line: #5b9bff;

  --bs-primary-rgb: 91, 155, 255;
  --bs-link-color-rgb: 91, 155, 255;
  --bs-link-hover-color-rgb: 238, 242, 248;
  --bs-focus-ring-color: rgba(91, 155, 255, 0.4);

  color-scheme: dark;
}

/* Bootstrap's link colour is only half themed by --bs-link-color-rgb above -
   .btn-primary and the form focus ring are compiled to literal hex, so they
   get their own --bs-btn-*/--bs-* overrides here rather than a :root value
   that would never be read (see the `bootstrap` skill). Deliberately the
   same Blue in both themes, not swapped to Bright - see --blue-fixed above
   for why. This rule used to reference var(--accent-line) here, which
   swaps to Bright in dark theme and would have quietly reintroduced the
   exact 2.77:1 failure the comment already warned against, on the first
   page to actually use .btn-primary - caught by a full-page contrast audit
   even though nothing currently renders this class. */
.btn-primary {
  --bs-btn-bg: var(--blue-fixed);
  --bs-btn-border-color: var(--blue-fixed);
  --bs-btn-color: #fff;
  --bs-btn-hover-bg: var(--ink);
  --bs-btn-hover-border-color: var(--ink);
  --bs-btn-hover-color: #fff;
  --bs-btn-active-bg: var(--ink);
  --bs-btn-active-border-color: var(--ink);
  --bs-btn-active-color: #fff;
  --bs-btn-focus-shadow-rgb: var(--bs-primary-rgb);
}

.btn-outline-light {
  --bs-btn-color: var(--ink);
  --bs-btn-border-color: var(--rule-strong);
  --bs-btn-hover-bg: var(--ink);
  --bs-btn-hover-border-color: var(--ink);
  --bs-btn-hover-color: var(--paper);
  --bs-btn-active-bg: var(--ink);
  --bs-btn-active-border-color: var(--ink);
  --bs-btn-active-color: var(--paper);
}

/* buttons sitting on the dark hero/contact block need light text and a
   light hover fill, the same "light ring on dark surfaces" swap the focus
   styles already make below */
.hero .btn-outline-light,
.contact .btn-outline-light {
  --bs-btn-color: var(--on-banner);
  --bs-btn-border-color: var(--on-banner);
  --bs-btn-hover-bg: var(--on-banner);
  --bs-btn-hover-border-color: var(--on-banner);
  --bs-btn-hover-color: var(--navy-solid);
  --bs-btn-active-bg: var(--on-banner);
  --bs-btn-active-border-color: var(--on-banner);
  --bs-btn-active-color: var(--navy-solid);
}

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: "Space Grotesk", Helvetica, Arial, sans-serif;
  font-size: var(--t-body);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

img,
svg {
  max-width: 100%;
}

h1,
h2,
h3,
h4 {
  font-family: "Space Grotesk", "Helvetica Neue", Arial, sans-serif;
  text-wrap: balance;
  margin: 0;
  font-weight: 700;
  letter-spacing: -0.035em;
}

h1 {
  font-size: var(--t-mega);
  line-height: 0.95;
  letter-spacing: -0.045em;
}

h2 {
  font-size: var(--t-loud);
  line-height: 1.02;
  letter-spacing: -0.04em;
}

h3 {
  font-size: var(--t-mid);
  line-height: 1.15;
  letter-spacing: -0.025em;
}

h4 {
  font-size: 1.0625rem;
  line-height: 1.25;
  letter-spacing: -0.015em;
}

p {
  margin: 0;
}

a {
  color: inherit;
  text-decoration-color: var(--accent-line);
  text-decoration-thickness: 2px;
  text-underline-offset: 4px;
}

a:hover {
  text-decoration-color: currentColor;
}

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

/* The blue accent only reaches ~3:1 against Deep - brand-documented as
   "accent on dark only" territory for Bright, not for Blue - so it fails
   WCAG 2.2 SC 1.4.11 as a focus ring. Anything focusable on a dark surface
   gets Page instead, which clears 15:1 against Deep. */
.contact .block :focus-visible,
.hero :focus-visible {
  outline-color: var(--on-banner);
}

/* the masthead is theme-following, not fixed dark like hero/contact above -
   so its focus ring only needs the light-on-dark swap in dark theme, where
   var(--accent-line) resolves to Bright and is safe anyway (6.21:1 on
   Deep) - this is belt-and-braces consistency with the rest of the dark
   surfaces rather than a contrast fix in its own right */
:root[data-bs-theme="dark"] .masthead :focus-visible {
  outline-color: var(--on-banner);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-bs-theme="light"]) .masthead :focus-visible {
    outline-color: var(--on-banner);
  }
}

.prose {
  max-width: var(--measure);
  color: var(--ink-soft);
}

/* an intro sitting above a full-width grid can run wider without hurting the
   read, because it is one or two lines rather than a body of text */
.prose--wide {
  max-width: 88ch;
}

.lede {
  max-width: 38ch;
  font-size: var(--t-lede);
  line-height: 1.4;
  color: var(--ink-soft);
}

.label {
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: var(--t-micro);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--ink-soft);
  display: flex;
  align-items: center;
  gap: 0.6rem;
}

.label::before {
  content: "";
  width: 1.75rem;
  height: 3px;
  background: var(--accent-line);
  flex: none;
}

.skip-link {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 20;
  clip-path: inset(50%);
  width: 1px;
  height: 1px;
  overflow: hidden;
  white-space: nowrap;
  background: var(--navy-solid);
  color: #fff;
  padding: 0.75rem 1.25rem;
  font-family: "Space Grotesk", sans-serif;
  font-weight: 700;
}

.skip-link:focus {
  clip-path: none;
  width: auto;
  height: auto;
}

.shell {
  max-width: 82rem;
  margin: 0 auto;
  padding-inline: var(--gutter);
}

/* Alternating grounds give the page rhythm without a rule under every band.
   The classes are explicit rather than :nth-of-type so reordering a section
   cannot silently flip the whole sequence. */
/* --- section grounds ---
   Flat, not textured (8 September 2026 - was a low-opacity tile pattern
   reused on every band, the same one the hero draws). Reordering sections
   this session broke the light/alt alternation in three places at once
   (two plain bands touching, two dark bands touching with no seam, two
   alt bands touching) - alternating fill colour turns out to be a fragile
   way to mark a section boundary, because it silently breaks every time a
   section moves, and it did. A hairline border-top does the same job
   without depending on what colour happens to be next to what, so it
   can't break again the same way. The texture itself is kept, but only on
   the two bookend dark moments - the hero and the Contact block - see
   those rules for it rather than here. */
.band {
  background-color: var(--paper);
  border-top: 1px solid var(--rule);
}

/* restores real alternation on top of the flat/bordered base above - see
   the 8 September fix note for why fill colour alone was dropped as the
   *only* seam, but going all the way to one flat tone for every content
   band read as monotonous ("grey, grey, grey") once seen live. Kept
   deliberately simple: one modifier, applied by hand to alternate
   sections in document order - the hairline border from .band still
   marks every boundary regardless, so a future reorder can dull the
   alternation again without ever erasing the seam the way it did before. */
.band--alt {
  background-color: var(--paper-alt);
}

.band > .shell {
  display: grid;
  grid-template-columns: 9rem minmax(0, 1fr);
  gap: 0 clamp(1.5rem, 4vw, 4rem);
  padding-block: clamp(3rem, 6vw, 5.5rem);
}

.band .label {
  position: sticky;
  top: 1.5rem;
  align-self: start;
}

/* Set the gutter label on its side. It is a 9rem column that otherwise holds
   two words, and turning it gives the page an edge you do not see often. */
@media (min-width: 60rem) {
  .band .label {
    writing-mode: vertical-rl;
    rotate: 180deg;
    gap: 0.9rem;
    height: max-content;
    letter-spacing: 0.1em;
  }

  .band .label::before {
    width: 3px;
    height: 2.25rem;
  }
}

.band .col {
  display: flex;
  flex-direction: column;
  gap: clamp(1.25rem, 2.5vw, 2rem);
  min-width: 0;
}

/* --- section-toggle ---
   Every top-level section collapses, native <details>/<summary> the same
   way the Experience roles and the tool-status groups already do - a
   section's own label stays put (it's the sticky sidebar identifier, not
   part of what collapses), but the heading and everything below it live
   inside a <details> so the whole section can be tucked away. Applied
   alongside .col where a section already uses that flex-column class, or
   on its own where a section (Highlights, Beliefs) has no .col wrapper. */
.section-toggle {
  display: flex;
  flex-direction: column;
  gap: clamp(1.25rem, 2.5vw, 2rem);
}

.section-toggle > summary {
  display: flex;
  align-items: flex-start;
  gap: 0.875rem;
  cursor: pointer;
  list-style: none;
}

.section-toggle > summary::-webkit-details-marker {
  display: none;
}

.section-toggle > summary h2 {
  margin: 0;
}

.section-toggle > summary:hover h2 {
  color: var(--accent-line);
}

.section-toggle > summary:focus-visible {
  outline: 3px solid var(--accent-line);
  outline-offset: 3px;
}

.section-toggle > summary .chev {
  margin-top: 0.5em;
}

.section-toggle .section-body {
  display: flex;
  flex-direction: column;
  gap: clamp(1.25rem, 2.5vw, 2rem);
}

/* the always-dark surfaces (Highlights, the Beliefs band, the Contact
   block) can't use the default accent-line chevron/hover colour - Blue
   only reaches ~3:1 against Deep, the same reason .contact .block
   :focus-visible already swaps to Page elsewhere in this file. Bright is
   the one that holds up there. */
.highlights .chev,
.beliefs-band .chev,
.contact .chev {
  border-color: var(--on-banner-soft);
}

.highlights .chev::before,
.highlights .chev::after,
.beliefs-band .chev::before,
.beliefs-band .chev::after,
.contact .chev::before,
.contact .chev::after {
  background: var(--on-banner-bright);
}

.highlights details[open] .chev,
.beliefs-band details[open] .chev,
.contact details[open] .chev {
  border-color: var(--on-banner-bright);
}

.highlights .section-toggle > summary:hover h2,
.beliefs-band .section-toggle > summary:hover h2,
.contact .section-toggle > summary:hover h2 {
  color: var(--on-banner-bright);
}

/* Prose wants a 60-ish character measure to stay readable, which leaves nearly
   half the content column empty. Rather than stretching the lines, the space
   carries a summary card that people actually skim first. */
.split {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 17rem);
  gap: clamp(1.5rem, 3vw, 3.5rem);
  align-items: start;
}

.split .flow {
  display: flex;
  flex-direction: column;
  gap: 1.125rem;
}

.split .flow .prose {
  max-width: 60ch;
}

@media (max-width: 60rem) {
  .split {
    grid-template-columns: 1fr;
  }
}

.glance {
  position: sticky;
  top: 2rem;
  background: var(--card);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 1.25rem 1.375rem;
  display: flex;
  flex-direction: column;
  gap: 0.875rem;
}

.glance h3 {
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--accent-line);
  font-weight: 500;
}

.glance dl {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.glance dt {
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: 0.6875rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--ink-mute);
}

.glance dd {
  margin: 0.1rem 0 0;
  font-size: var(--t-small);
  line-height: 1.35;
  color: var(--ink);
}

@media (max-width: 60rem) {
  .glance {
    position: static;
  }
}

@media (max-width: 60rem) {
  .band > .shell {
    grid-template-columns: 1fr;
    gap: 1.25rem;
  }

  .band .label {
    position: static;
  }
}

/* --- masthead ---
   A Bootstrap navbar (navbar-expand-lg + navbar-toggler/collapse for the
   mobile hamburger), reskinned via --bs-navbar-* rather than fought - see
   the `bootstrap` skill on why a :root value alone would not reach it.

   Theme-following, not fixed dark - light bar with the mark's normal dark
   tile in light mode, dark bar in dark mode. That means the mark's fixed
   Deep tile briefly stops being safe to read: on a light masthead it's
   fine (a dark square always reads on a light ground), but on a DARK
   masthead the fixed dark tile would blend into it, the same problem the
   hero's own dark ground never has because it's always dark. So the mark
   (and only the mark) still inverts to a light tile, but conditionally -
   in dark theme only, further down - rather than unconditionally the way
   an earlier pass had it. */

.masthead {
  background: var(--paper);
  border-bottom: 1px solid var(--rule);
}

.masthead .navbar {
  --bs-navbar-padding-y: 1.25rem;
  --bs-navbar-color: var(--ink-soft);
  --bs-navbar-hover-color: var(--ink);
  --bs-navbar-active-color: var(--ink);
  --bs-navbar-brand-color: var(--ink);
  --bs-navbar-toggler-border-color: var(--rule-strong);
  --bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='%230b1b34' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 0.875rem;
  font-family: "Space Grotesk", sans-serif;
  font-weight: 700;
  letter-spacing: -0.04em;
  font-size: 1.5rem;
  text-decoration: none;
  color: var(--ink);
}

.brand .dot {
  color: var(--accent-line);
}

/* dark theme only: the masthead itself is now dark, so the toggler icon
   and the mark both need their light-on-dark treatment - see the comment
   above for why the mark's fixed tile can't stay fixed here specifically */
:root[data-bs-theme="dark"] .masthead .navbar {
  --bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='%23eef2f8' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
}

:root[data-bs-theme="dark"] .masthead .mark {
  background: #eef2f8;
  color: #0b1b34;
}

:root[data-bs-theme="dark"] .masthead .mark .dot {
  color: #2563c9;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-bs-theme="light"]) .masthead .navbar {
    --bs-navbar-toggler-icon-bg: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='%23eef2f8' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
  }

  :root:not([data-bs-theme="light"]) .masthead .mark {
    background: #eef2f8;
    color: #0b1b34;
  }

  :root:not([data-bs-theme="light"]) .masthead .mark .dot {
    color: #2563c9;
  }
}

.navbar-nav .nav-link {
  font-family: "Space Grotesk", sans-serif;
  font-size: var(--t-small);
  font-weight: 600;
  letter-spacing: -0.01em;
  padding-block: 0.25rem;
  border-bottom: 3px solid transparent;
}

.navbar-nav .nav-link:hover,
.navbar-nav .nav-link:focus-visible {
  border-bottom-color: var(--accent-line);
}

@media (min-width: 992px) {
  .navbar-nav .nav-link {
    margin-inline-start: 1.5rem;
  }
}

.theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  margin-inline-start: 1rem;
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--ink-soft);
  cursor: pointer;
}

.theme-toggle:hover,
.theme-toggle:focus-visible {
  color: var(--ink);
  border-color: var(--ink-soft);
}

.theme-toggle svg {
  width: 1.125rem;
  height: 1.125rem;
}

/* --- mobile nav, opened --- the collapsed menu inherited the desktop
   nav-link's styling wholesale (a 3px hover-underline sized for a link
   sitting in a horizontal row), which on a stacked list read as five
   nearly-touching lines of text with no visible structure - technically
   fine, tight and flat to look at. Below the breakpoint the collapse
   becomes its own panel: real dividers between rows, a proper tap target
   on each, and the theme toggle folded into the same list instead of
   floating alone underneath it. */
@media (max-width: 991.98px) {
  .navbar-collapse {
    margin-top: 1rem;
    padding-top: 0.25rem;
    border-top: 1px solid var(--rule);
  }

  .navbar-nav {
    gap: 0;
  }

  .navbar-nav .nav-link {
    display: block;
    padding-block: 0.9375rem;
    border-bottom: 1px solid var(--rule);
    color: var(--ink-soft);
  }

  .navbar-nav .nav-link:hover,
  .navbar-nav .nav-link:focus-visible {
    border-bottom-color: var(--rule);
    background: var(--paper-alt);
    color: var(--ink);
    padding-inline: 0.5rem;
    margin-inline: -0.5rem;
  }

  .navbar-nav li:last-child .nav-link {
    border-bottom: none;
  }

  /* the toggle moves into the list's own flow rather than sitting apart
     from it - a labelled row like every other control here, not an
     unlabelled icon that reads as an afterthought on its own line */
  .navbar-nav li:has(.theme-toggle) {
    padding-block: 0.5rem 0.125rem;
  }

  .theme-toggle {
    width: auto;
    height: auto;
    gap: 0.625rem;
    padding: 0.625rem 0.875rem;
    margin-inline-start: 0;
  }

  .theme-toggle::after {
    content: "Switch theme";
    font-family: "Space Grotesk", sans-serif;
    font-size: var(--t-small);
    font-weight: 600;
  }
}

/* --- the banner ---
   A deep blue block carrying a tile pattern built from the logo's own rounded
   box, plus two soft washes of a brighter blue. All of it is drawn in SVG
   rather than loaded, so the banner costs nothing and themes with the page. */

.hero {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  background: var(--banner);
  color: var(--on-banner);
}

.hero-art {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: -1;
  pointer-events: none;
}

.hero .shell {
  position: relative;
  padding-block: clamp(3.5rem, 10vw, 7rem);
}

.hero-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
  gap: clamp(1.75rem, 5vw, 4.5rem);
  align-items: center;
}

@media (max-width: 60rem) {
  .hero-grid {
    grid-template-columns: 1fr;
    align-items: start;
  }
}

.hero-visual {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1.5rem;
}

/* the one photo on the site (see the README's "before this goes live" note)
   - circular, centred above the terminal, with a soft glow behind it rather
   than a hard edge. The glow is a blurred radial gradient in the brand's
   own Bright accent (already used for the terminal's own highlights), not
   a new colour - "like a sun" without inventing a second palette for it.
   The photo itself is soft-masked at its own edge (radial-gradient alpha,
   not a hard circular crop with a border) so it fades into that glow
   rather than sitting on top of it like a sticker. */
.hero-photo {
  position: relative;
  width: clamp(9.5rem, 19vw, 13rem);
  aspect-ratio: 1 / 1;
}

.hero-photo::before {
  content: "";
  position: absolute;
  inset: -35%;
  border-radius: 50%;
  background: radial-gradient(circle, var(--on-banner-bright) 0%, transparent 68%);
  opacity: 0.55;
  filter: blur(18px);
  z-index: 0;
}

.hero-photo img {
  position: relative;
  z-index: 1;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 50%;
  mask-image: radial-gradient(circle, #000 62%, transparent 100%);
  -webkit-mask-image: radial-gradient(circle, #000 62%, transparent 100%);
}

.hero h1 {
  color: var(--on-banner);
}

.hero .strap {
  font-family: "Space Grotesk", sans-serif;
  font-weight: 500;
  font-size: var(--t-mid);
  letter-spacing: -0.02em;
  color: var(--on-banner-bright);
  margin-top: 1rem;
}

/* the one highlighter-block use on the page - the brand name itself, in the
   line that says it out loud. Same accent hue as everything else, just as a
   fill instead of a line, so "one accent blue per screen" still holds.
   --blue-fixed, not --accent-line: a full-page contrast audit (7 September
   2026) found this failing AA in dark theme at 2.77:1 - --accent-line
   swaps to Bright there, and white text on a Bright fill is exactly the
   pairing the .btn-primary comment already documented as broken. Fixed to
   the same always-Blue token; now 5.67:1 in both themes. */
.brand-mark {
  background: var(--blue-fixed);
  color: #fff;
  padding: 0.05em 0.3em;
  border-radius: 0.25em;
  font-weight: 700;
}

.hero .lede {
  color: var(--on-banner);
  max-width: 40ch;
  margin-top: 1.5rem;
}

/* the mission sits a size down from the three-beat opener, so the hero has a
   hierarchy rather than one long paragraph */
.hero .lede-sub {
  margin-top: 0.875rem;
  max-width: 42ch;
  font-size: var(--t-body);
  line-height: 1.5;
  color: var(--on-banner-soft);
  padding-bottom: 0.4rem;
}

/* the closing line's three-beat "still, still, still" is the one place in
   the hero copy with real rhythm - marking each "still" picks that out on
   the page the way it already reads on the tongue, using the same bright
   blue as .strap and the terminal's [TAG] labels rather than a new colour,
   so it reads as "the one accent, used again" and not a second device */
.hero .beat {
  color: var(--on-banner-bright);
  font-weight: 600;
}

/* --- the terminal ---
   A fake boot log in the hero, inspired by the "system status" widgets on
   other developer portfolios but built from what's already on the page: the
   same JetBrains Mono the eyebrow labels use, and no colour beyond the one
   accent blue - deliberately not a green "OK", which would be a second
   accent hue the brand pack's own voice-and-tone rule doesn't allow. It's
   aria-hidden because everything it says (the two roles, the tool count) is
   already on the page in real markup - this is a decorative restatement,
   not unique content, so hiding it stops a screen reader announcing the
   same facts twice. */

.terminal {
  background: color-mix(in srgb, var(--on-banner) 6%, var(--banner));
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: 0 20px 60px -20px rgba(0, 0, 0, 0.55);
}

.terminal-bar {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.75rem 1rem;
  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}

.terminal-dot {
  width: 0.65rem;
  height: 0.65rem;
  border-radius: 50%;
  flex: none;
}

/* the only non-brand colour on the page - the three traffic-light dots are
   a universally recognised window-chrome convention, not a design accent,
   the same way a favicon glyph isn't held to the page's contrast rules */
.terminal-dot--red {
  background: #ff5f56;
}

.terminal-dot--amber {
  background: #ffbd2e;
}

.terminal-dot--green {
  background: #27c93f;
}

.terminal-title {
  margin-inline-start: 0.25rem;
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: 0.75rem;
  color: var(--on-banner-soft);
}

.terminal-body {
  padding: 1.25rem 1.25rem 1.5rem;
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: 0.8125rem;
  color: var(--on-banner-soft);
  overflow-wrap: break-word;
}

/* each line fades and rises in on load, staggered by JS adding
   .t-line--in one at a time - the "boot log" effect chirag.codes and
   friends use, built from what's already here rather than a new library.
   Skips straight to the end state under reduced motion; JS also checks
   the same preference and adds every .t-line--in class at once rather
   than staggering, so nothing is ever gated behind an animation that
   isn't going to play. */
.t-line {
  margin: 0 0 0.55rem;
  opacity: 0;
  transform: translateY(4px);
  transition: opacity 0.35s ease, transform 0.35s ease;
}

.t-line:last-child {
  margin-bottom: 0;
}

.t-line.t-line--in {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  .t-line {
    transition: none;
  }
}

.t-row {
  display: flex;
  gap: 0.75rem;
}

.t-tag {
  flex: 0 0 5.75rem;
  color: var(--on-banner-bright);
  font-weight: 500;
}

.t-prompt {
  color: var(--on-banner-bright);
}

.t-ok {
  color: var(--on-banner);
  font-weight: 700;
}

.terminal-cursor {
  display: inline-block;
  width: 0.5em;
  height: 1em;
  margin-left: 0.15em;
  vertical-align: text-bottom;
  background: var(--on-banner-bright);
  animation: terminal-blink 1s steps(1) infinite;
}

@media (prefers-reduced-motion: reduce) {
  .terminal-cursor {
    animation: none;
  }
}

@keyframes terminal-blink {
  50% {
    opacity: 0;
  }
}

@media (max-width: 40rem) {
  .terminal-body {
    font-size: 0.75rem;
  }
}

/* --- scroll-reveal ---
   A small IntersectionObserver-driven fade+rise on the highlight items and
   tool cards - the two grids on the page where a stagger reads as motion
   rather than as sluggish. Nowhere near everything: the prose sections
   appear immediately, because holding body text behind an animation makes
   the page feel slower to read, not more polished.

   .reveal is NOT hidden by default - only once `.reveal-ready` lands on
   <html> (the first line the reveal script runs, before it does anything
   else) does it start hiding elements pending `.is-visible`. If that
   script fails to run at all - blocked, a JS error earlier on the page,
   anything - real content (tool cards, highlights) stays visible rather
   than disappearing permanently behind an animation that was never going
   to fire. This is the actual reason it's two rules, not one. */
.reveal-ready .reveal {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 0.5s ease, transform 0.5s ease;
}

.reveal-ready .reveal.is-visible {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  .reveal-ready .reveal {
    transition: none;
  }
}

/* --- highlights: the only other dark band on the page, so the page reads
       light, dark, light rather than one long pale scroll --- */

.highlights {
  background-color: var(--banner);
  border-top: 1px solid rgba(255, 255, 255, 0.14);
  color: var(--on-banner);
}

.highlights .shell {
  padding-block: clamp(2.5rem, 5vw, 4rem);
}

.highlights h2 {
  color: var(--on-banner);
  max-width: 22ch;
  margin-bottom: 1.75rem;
}

.highlights .label {
  color: var(--on-banner-soft);
  margin-bottom: 0.875rem;
}

.highlights .label::before {
  background: var(--on-banner-bright);
}

.hl {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
  gap: 0;
}

.hl li {
  padding: 0 clamp(1rem, 2vw, 1.75rem);
  border-left: 1px solid rgb(255 255 255 / 16%);
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}

.hl li:first-child {
  padding-left: 0;
  border-left: 0;
}

.hl b {
  font-family: "Space Grotesk", sans-serif;
  font-weight: 700;
  font-size: clamp(1.0625rem, 1.5vw, 1.25rem);
  letter-spacing: -0.02em;
  line-height: 1.15;
  color: var(--on-banner);
}

.hl span {
  font-size: var(--t-small);
  line-height: 1.45;
  color: var(--on-banner-soft);
}

@media (max-width: 52rem) {
  .hl li {
    padding: 0.875rem 0;
    border-left: 0;
    border-top: 1px solid rgb(255 255 255 / 16%);
  }

  .hl li:first-child {
    padding-top: 0;
    border-top: 0;
  }
}

/* --- tools --- */

.tools-toolbar {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem 2rem;
  flex-wrap: wrap;
}

.tools-toolbar .prose {
  flex: 1 1 30rem;
  margin: 0;
}

.tools-toolbar .view-toggle {
  flex: 0 0 auto;
}

.view-toggle {
  display: inline-flex;
  gap: 0.25rem;
  padding: 0.25rem;
  border: 1px solid var(--rule);
  border-radius: var(--radius-sm);
  background: var(--card);
  box-shadow: var(--shadow);
}

.view-toggle button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  height: 2rem;
  border: 0;
  border-radius: calc(var(--radius-sm) - 0.25rem);
  background: transparent;
  color: var(--ink-mute);
  cursor: pointer;
}

.view-toggle button svg {
  width: 1rem;
  height: 1rem;
}

.view-toggle button[aria-pressed="true"] {
  background: var(--accent-line);
  color: #fff;
}

/* Windows High Contrast (forced-colors) strips background-color and text
   colour to a handful of system values, so the pressed state above -
   currently the *only* thing telling these icon-only buttons apart -
   would flatten to identical, indistinguishable buttons: no border ever
   separated them, and there's no text label to fall back on the way the
   tool-status chips have. Highlight/HighlightText are the system colours
   meant exactly for "this is the selected one" in this mode. */
@media (forced-colors: active) {
  .view-toggle button[aria-pressed="true"] {
    border: 1px solid Highlight;
    background: Highlight;
    color: HighlightText;
    forced-color-adjust: none;
  }
}

.view-toggle button:not([aria-pressed="true"]):hover,
.view-toggle button:not([aria-pressed="true"]):focus-visible {
  color: var(--ink);
}

/* --- tools filter ---
   Filters by the same data-state values the status chips already use
   (plus "idea", which no card carries yet - see #tools-empty). One
   vocabulary for status, not two - the filter pill and the card's own
   chip always say the same thing about the same tool. */
.tools-filter {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.tools-filter button {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.4rem 0.875rem;
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-sm);
  background: var(--card);
  font-family: "Space Grotesk", sans-serif;
  font-size: var(--t-small);
  font-weight: 600;
  color: var(--ink-soft);
  cursor: pointer;
}

.tools-filter button .count {
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: 0.6875rem;
  font-weight: 500;
  color: var(--ink-mute);
}

.tools-filter button[aria-pressed="true"] {
  background: var(--accent-line);
  border-color: var(--accent-line);
  color: #fff;
}

.tools-filter button[aria-pressed="true"] .count {
  color: rgb(255 255 255 / 75%);
}

.tools-filter button:not([aria-pressed="true"]):hover,
.tools-filter button:not([aria-pressed="true"]):focus-visible {
  border-color: var(--ink-soft);
  color: var(--ink);
}

/* same reasoning as the view-toggle's own forced-colors rule just above -
   the pressed state here is otherwise colour-only too */
@media (forced-colors: active) {
  .tools-filter button[aria-pressed="true"] {
    border: 2px solid Highlight;
    background: Highlight;
    color: HighlightText;
    forced-color-adjust: none;
  }
}

/* --- tools pagination --- */
.tools-pagination {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.tools-pagination button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 2.25rem;
  height: 2.25rem;
  padding-inline: 0.5rem;
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-sm);
  background: var(--card);
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: var(--t-small);
  font-weight: 500;
  color: var(--ink-soft);
  cursor: pointer;
}

.tools-pagination button[aria-current="page"] {
  background: var(--accent-line);
  border-color: var(--accent-line);
  color: #fff;
}

.tools-pagination button:not([aria-current="page"]):hover,
.tools-pagination button:not([aria-current="page"]):focus-visible {
  border-color: var(--ink-soft);
  color: var(--ink);
}

@media (forced-colors: active) {
  .tools-pagination button[aria-current="page"] {
    border: 2px solid Highlight;
    background: Highlight;
    color: HighlightText;
    forced-color-adjust: none;
  }
}

/* Tile is the default now - two columns, each carrying a photo (see
   .tool-shot below), rather than the old three-up grid with the image
   hidden unless "showcase" was picked. One good view earns its place
   better than three mediocre ones. */
.tools {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(1rem, 2.5vw, 1.75rem);
}

@media (max-width: 40rem) {
  .tools {
    grid-template-columns: 1fr;
  }
}

/* the list view: one column of flat, full-width rows instead of a card
   grid - the same content, in the same order, just wider and without the
   box - divided by hairlines the way the experience accordion already is,
   rather than a second "list" pattern with its own rules. Deliberately not
   a row-flex re-layout of the same fields: that reads as denser in theory
   but a title, a status chip and two paragraphs of real sentences don't
   compress into one line without either truncating the copy or wrapping
   unpredictably, and this site doesn't truncate the copy that's the actual
   point of the card (see the `Replaces` line in "Design", above) */
.tools.tools--list {
  grid-template-columns: 1fr;
  gap: 0;
  border-top: 1px solid var(--rule);
}

.tools--list .tool {
  --bs-card-border-radius: 0;
  --bs-card-bg: var(--card);
  border-left: 0;
  border-right: 0;
  border-top: 0;
  border-bottom: 1px solid var(--rule);
  box-shadow: none;
}

.tools--list .card-body {
  --bs-card-spacer-y: clamp(1rem, 2vw, 1.375rem);
}

.tools--list .tool.tool--linked:hover,
.tools--list .tool.tool--linked:focus-within {
  --bs-card-border-color: var(--rule);
  background: var(--paper-alt);
}

.tools--list .tool-shot {
  display: none;
}

/* --- the tile photo ---
   Every tile carries an image now, not just an opt-in "showcase" mode -
   a real screenshot where one exists, the same dashed "not real yet"
   placeholder used everywhere else on the site (the showcase view used to
   own this pattern alone) where it doesn't. Bled to the card's own edges
   via a negative margin matching .tool's own padding variables, rounded
   only on the top corners, rather than padded like the rest of the card -
   that's what actually makes it read as a photo rather than a boxed
   diagram sitting inside the card. */
.tool-shot {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  gap: 0.5rem;
  aspect-ratio: 16 / 9;
  margin: calc(-1 * var(--bs-card-spacer-y)) calc(-1 * var(--bs-card-spacer-x)) 0;
  border-bottom: 1px solid var(--rule);
  border-radius: var(--radius) var(--radius) 0 0;
  background: var(--paper-alt);
  color: var(--ink-mute);
  overflow: hidden;
}

/* only the placeholder state gets the dashed "not real yet" border - a
   real screenshot is bordered by the hairline above instead, the same
   way a real photo doesn't get a dashed frame drawn around it */
.tool-shot:not(:has(img)) {
  border: 1px dashed var(--rule-strong);
  border-top: 0;
}

.tool-shot img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: left top;
}

.tool-shot svg {
  width: 1.75rem;
  height: 1.75rem;
  opacity: 0.6;
}

.tool-shot span {
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: 0.6875rem;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  text-align: center;
  padding: 0 0.5rem;
}

/* A handful of tools got more than one real screenshot this pass, so
   .tool-shot grows a carousel variant rather than a second component -
   same bleed-to-the-edges box, just tall enough for a control strip below
   the image. WCAG 2.2.2 (Pause, Stop, Hide) is why this isn't a plain
   auto-looping GIF: the strip always carries a working pause/play control,
   autoplay never starts at all under prefers-reduced-motion, and hovering,
   focusing or clicking anything in the carousel pauses it too. See the
   "Tool screenshots" entry in README for the full reasoning. */
.tool-shot.shot-carousel {
  aspect-ratio: auto;
  flex-direction: column;
  align-items: stretch;
  justify-content: flex-start;
  gap: 0;
}

.shot-frame {
  position: relative;
  aspect-ratio: 16 / 9;
  overflow: hidden;
}

.shot-frame .shot-slide {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: left top;
}

.shot-frame .shot-slide[hidden] {
  display: none;
}

.shot-controls {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.25rem 0.5rem;
  border-top: 1px solid var(--rule);
  background: var(--paper-alt);
}

.shot-playpause {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  min-height: 1.5rem;
  padding: 0.25rem 0.5rem;
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--ink-mute);
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: 0.625rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  cursor: pointer;
}

.shot-playpause:hover,
.shot-playpause:focus-visible {
  color: var(--ink);
  border-color: var(--ink-mute);
}

/* margin-inline-start: auto rather than the parent's justify-content, so
   the dots stay pinned to the far edge even when the play/pause button is
   hidden (prefers-reduced-motion) and only the dots remain in the strip */
.shot-dots {
  display: flex;
  gap: 0.125rem;
  margin-inline-start: auto;
}

/* Each dot's visible mark is a small circle, but the clickable button is
   a full 24x24 CSS px (2.5.8 Target Size) - the dot is drawn with ::before
   so the hit area can stay generous without the mark itself looking oversized. */
.shot-dots button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  border: 0;
  border-radius: 50%;
  background: transparent;
  padding: 0;
  cursor: pointer;
}

.shot-dots button::before {
  content: "";
  display: block;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: var(--rule-strong);
}

.shot-dots button[aria-current="true"]::before {
  background: var(--ink);
}

.shot-dots button:hover::before,
.shot-dots button:focus-visible::before {
  background: var(--ink-mute);
}

/* Forced-colours strips the ::before background to nothing meaningful -
   the current dot needs a border, not a fill, to still read as selected. */
@media (forced-colors: active) {
  .shot-dots button::before {
    background: CanvasText;
  }

  .shot-dots button[aria-current="true"]::before {
    background: Highlight;
    forced-color-adjust: none;
  }
}

/* .tool is also a Bootstrap .card - themed via --bs-card-* (the variables
   .card itself reads, per the `bootstrap` skill) rather than plain
   background/border, which a :root override would never reach */
.tool {
  --bs-card-bg: var(--card);
  --bs-card-border-color: var(--rule);
  --bs-card-border-radius: var(--radius);
  --bs-card-spacer-y: clamp(1.125rem, 2vw, 1.5rem);
  --bs-card-spacer-x: clamp(1.125rem, 2vw, 1.5rem);
  box-shadow: var(--shadow);
}

.tool .card-body {
  display: flex;
  flex-direction: column;
  gap: 0.625rem;
}

.tool h3 {
  color: var(--accent-line);
}

.tool-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.75rem;
}

.tool .what {
  color: var(--ink-soft);
  font-size: var(--t-small);
  line-height: 1.5;
}

/* only a tool with a real link (live, or an early beta with one) gets the
   hover/focus response - a card that leads nowhere shouldn't look
   interactive, that's the whole reason status and "has a url" are two
   separate things rather than one */
.tool.tool--linked {
  transition: border-color 0.15s ease;
}

.tool.tool--linked:hover,
.tool.tool--linked:focus-within {
  --bs-card-border-color: var(--accent-line);
}

/* An actual button, not a text link - a live tool is the one thing on this
   page making an unqualified claim ("this exists, go look"), and it earns
   a visible target rather than a small mono line easy to miss. Beta links,
   if one ever exists, stay the plainer text-link treatment further down -
   a caveat feels wrong dressed as a confident button. */
/* Wraps the Architecture link and the (conditional) project link into one
   row, rather than each managing its own top margin - added once a card
   could carry two links instead of at most one. */
.tool-links {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin-top: 0.25rem;
}

/* Quiet on purpose - it points at the one build standard every tool
   shares (architecture.html), not at this tool specifically, so it
   shouldn't compete with the project link for attention. Needs its own
   stacking context once a project link is also present, because that
   link's ::after overlay (z-index: 1, see .tool-link.stretched-link::after
   below) would otherwise swallow the click - same fix the `bootstrap`
   skill names for anything that must stay clickable inside a stretched
   card. */
.tool-link--arch {
  position: relative;
  z-index: 2;
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: var(--t-micro);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--ink-mute);
  text-decoration: none;
  border-bottom: 1px solid transparent;
}

.tool-link--arch:hover,
.tool-link--arch:focus-visible {
  color: var(--accent-line);
  border-bottom-color: currentColor;
}

.tool-link {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.4rem 0.9rem;
  border-radius: var(--radius-sm);
  background: var(--accent-line);
  font-family: "Space Grotesk", sans-serif;
  font-size: var(--t-small);
  font-weight: 700;
  color: #fff;
  text-decoration: none;
  transition: background-color 0.15s ease;
}

.tool-link svg {
  width: 0.875rem;
  height: 0.875rem;
  transition: transform 0.15s ease;
}

.tool.tool--linked:hover .tool-link,
.tool.tool--linked:focus-within .tool-link {
  background: var(--ink);
}

.tool.tool--linked:hover .tool-link svg,
.tool.tool--linked:focus-within .tool-link svg {
  transform: translateX(2px);
}

/* the plainer treatment for a beta link - a caveated "here it is, but" */
.tool-link.tool-link--beta {
  background: transparent;
  padding: 0;
  border-radius: 0;
  color: var(--accent-line);
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: var(--t-micro);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.14em;
}

.tool.tool--linked:hover .tool-link--beta,
.tool.tool--linked:focus-within .tool-link--beta {
  background: transparent;
  text-decoration: underline;
}

/* the whole card is the click target - .card already sets position:relative,
   which is exactly what .stretched-link needs (see the `bootstrap` skill) */
.tool-link.stretched-link::after {
  z-index: 1;
}

/* Pinned to the card foot so the block lines up across a row whatever length
   the description runs to. */
.tool .replaces {
  margin-top: auto;
  padding-top: 0.875rem;
  border-top: 1px solid var(--rule);
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  font-size: var(--t-small);
  line-height: 1.4;
  color: var(--ink-soft);
}

.tool .rl {
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: var(--t-micro);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--accent-line);
}

.tool .tags {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
}

/* the one card without build history yet (an Idea-stage tool) gets a
   plain note instead of a "Build notes" disclosure - a disclosure titled
   "Build notes" implies something was actually built, which would be the
   one dishonest claim on the whole page for a tool that's still just a
   PDD and a phased plan */
.tool .idea-note {
  font-size: var(--t-small);
  font-style: italic;
  color: var(--ink-mute);
  line-height: 1.4;
}

.chip {
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: 0.6875rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  padding: 0.125rem 0.5rem;
  border: 1px solid var(--rule-strong);
  border-radius: var(--radius-sm);
  color: var(--ink-soft);
}

/* Five honest stages, each its own colour, verified to at least 3.85:1
   (comfortably clears the 3:1 non-text floor - these are outlined chips,
   not filled text blocks) against both --card and --paper in both themes.
   This is a deliberate, scoped exception to the site's "one accent"
   discipline elsewhere: a maturity indicator is semantic state, not
   decoration, the same distinction the artifact/brand guidance draws
   between a status colour and a brand accent. Idea reuses --ink-mute
   rather than inventing a sixth colour, since "nothing built yet" is
   genuinely the absence of a state, not a hue of its own. Live is filled,
   the one visibly different treatment, because it's the only stage making
   an unqualified claim. */
.chip[data-state="idea"] {
  border-color: var(--ink-mute);
  color: var(--ink-mute);
}

.chip[data-state="prototype"] {
  border-color: #6d28d9;
  color: #6d28d9;
}

.chip[data-state="in-progress"] {
  border-color: var(--accent-line);
  color: var(--accent-line);
}

.chip[data-state="beta"] {
  border-color: #92400e;
  color: #92400e;
}

.chip[data-state="live"] {
  background: #166534;
  border-color: #166534;
  color: #fff;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-bs-theme="light"]) .chip[data-state="prototype"] {
    border-color: #c4b5fd;
    color: #c4b5fd;
  }

  :root:not([data-bs-theme="light"]) .chip[data-state="beta"] {
    border-color: #fcd34d;
    color: #fcd34d;
  }

  :root:not([data-bs-theme="light"]) .chip[data-state="live"] {
    background: #86efac;
    border-color: #86efac;
    color: #0b1b0f;
  }
}

:root[data-bs-theme="dark"] .chip[data-state="prototype"] {
  border-color: #c4b5fd;
  color: #c4b5fd;
}

:root[data-bs-theme="dark"] .chip[data-state="beta"] {
  border-color: #fcd34d;
  color: #fcd34d;
}

:root[data-bs-theme="dark"] .chip[data-state="live"] {
  background: #86efac;
  border-color: #86efac;
  color: #0b1b0f;
}

/* forced-colours strips every one of the hex values above to nothing -
   the five stages collapse to identical outlined chips with only their
   text ("Idea"/"Beta"/etc.) telling them apart, which is fine: the text
   was always the real information, the colour was the scan-speed layer */
@media (forced-colors: active) {
  .chip[data-state] {
    border-color: CanvasText;
    color: CanvasText;
    forced-color-adjust: none;
  }

  .chip[data-state="live"] {
    background: Highlight;
    border-color: Highlight;
    color: HighlightText;
  }
}

/* --- category tags ---
   13 categories is too many for a full-colour chip each without turning
   the grid into confetti, so the colour is a small dot ahead of the text
   (decorative, 3:1 floor per 1.4.11) rather than a filled or coloured-text
   chip - enough to let a scanning eye cluster "the two Finance ones"
   without the page reading as a rainbow. Hues are evenly spaced (360/13),
   the same spacing method the brand register uses for sibling apps, at a
   fixed saturation/lightness chosen to clear 3:1 on both --card and
   --paper in both themes - computed, not eyeballed; see README. */
.chip.tag-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
}

.chip.tag-chip::before {
  content: "";
  display: block;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: var(--tag-color, var(--ink-mute));
  flex: none;
}

@media (forced-colors: active) {
  .chip.tag-chip::before {
    background: CanvasText;
    forced-color-adjust: none;
  }
}

:root {
  --tag-operations: #8c2121;
  --tag-finance: #8c5321;
  --tag-clinical: #8c8421;
  --tag-learning: #638c21;
  --tag-scheduling: #318c21;
  --tag-communications: #218c42;
  --tag-household: #218c74;
  --tag-legal: #21748c;
  --tag-compliance: #21428c;
  --tag-recruitment: #31218c;
  --tag-public-safety: #63218c;
  --tag-community: #8c2184;
  --tag-time-tracking: #8c2153;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-bs-theme="light"]) {
    --tag-operations: #e68989;
    --tag-finance: #e6b489;
    --tag-clinical: #e6df89;
    --tag-learning: #c2e689;
    --tag-scheduling: #97e689;
    --tag-communications: #89e6a6;
    --tag-household: #89e6d1;
    --tag-legal: #89d1e6;
    --tag-compliance: #89a6e6;
    --tag-recruitment: #9789e6;
    --tag-public-safety: #c289e6;
    --tag-community: #e689df;
    --tag-time-tracking: #e689b4;
  }
}

:root[data-bs-theme="dark"] {
  --tag-operations: #e68989;
  --tag-finance: #e6b489;
  --tag-clinical: #e6df89;
  --tag-learning: #c2e689;
  --tag-scheduling: #97e689;
  --tag-communications: #89e6a6;
  --tag-household: #89e6d1;
  --tag-legal: #89d1e6;
  --tag-compliance: #89a6e6;
  --tag-recruitment: #9789e6;
  --tag-public-safety: #c289e6;
  --tag-community: #e689df;
  --tag-time-tracking: #e689b4;
}

.chip.tag-chip[data-tag="operations"] { --tag-color: var(--tag-operations); }
.chip.tag-chip[data-tag="finance"] { --tag-color: var(--tag-finance); }
.chip.tag-chip[data-tag="clinical"] { --tag-color: var(--tag-clinical); }
.chip.tag-chip[data-tag="learning"] { --tag-color: var(--tag-learning); }
.chip.tag-chip[data-tag="scheduling"] { --tag-color: var(--tag-scheduling); }
.chip.tag-chip[data-tag="communications"] { --tag-color: var(--tag-communications); }
.chip.tag-chip[data-tag="household"] { --tag-color: var(--tag-household); }
.chip.tag-chip[data-tag="legal"] { --tag-color: var(--tag-legal); }
.chip.tag-chip[data-tag="compliance"] { --tag-color: var(--tag-compliance); }
.chip.tag-chip[data-tag="recruitment"] { --tag-color: var(--tag-recruitment); }
.chip.tag-chip[data-tag="public-safety"] { --tag-color: var(--tag-public-safety); }
.chip.tag-chip[data-tag="community"] { --tag-color: var(--tag-community); }
.chip.tag-chip[data-tag="time-tracking"] { --tag-color: var(--tag-time-tracking); }

/* --- experience: a native accordion ---
   <details>/<summary> rather than a scripted one: it is keyboard-operable,
   announced as expandable, findable by in-page search in current browsers, and
   works with JavaScript off. */

.roles {
  list-style: none;
  margin: 0;
  padding: 0;
  border-top: 1px solid var(--rule);
}

.role {
  border-bottom: 1px solid var(--rule);
}

.role > details > summary {
  display: grid;
  grid-template-columns: 1.5rem minmax(0, 1fr);
  gap: 0.15rem 0.875rem;
  align-items: baseline;
  padding-block: 1.125rem;
  cursor: pointer;
  list-style: none;
}

.role > details > summary::-webkit-details-marker {
  display: none;
}

.role > details > summary:hover .role-title {
  color: var(--accent-line);
}

.role > details > summary:focus-visible {
  outline: 3px solid var(--accent-line);
  outline-offset: -3px;
}

/* the chevron is the only moving part on the page */
.chev {
  grid-row: span 3;
  align-self: center;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: var(--radius-sm);
  border: 2px solid var(--rule-strong);
  position: relative;
  transition: transform 0.18s ease, border-color 0.18s ease;
}

.chev::before,
.chev::after {
  content: "";
  position: absolute;
  background: var(--accent-line);
  border-radius: 1px;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

.chev::before {
  width: 0.625rem;
  height: 2px;
}

.chev::after {
  width: 2px;
  height: 0.625rem;
  transition: opacity 0.18s ease;
}

details[open] .chev {
  border-color: var(--accent-line);
}

details[open] .chev::after {
  opacity: 0;
}

@media (prefers-reduced-motion: reduce) {
  .chev,
  .chev::after {
    transition: none;
  }
}

.role .when {
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: var(--t-micro);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--ink-mute);
  grid-column: 2;
}

.role .role-title {
  font-family: "Space Grotesk", sans-serif;
  font-weight: 700;
  font-size: var(--t-mid);
  letter-spacing: -0.025em;
  line-height: 1.15;
  color: var(--accent-line);
  grid-column: 2;
  margin: 0;
}

.role .org {
  font-family: "Space Grotesk", sans-serif;
  font-weight: 600;
  font-size: var(--t-small);
  color: var(--ink-soft);
  letter-spacing: -0.01em;
  grid-column: 2;
}

/* a solid surface under the role's own text, not the band's textured
   background directly - achievement bullets and the "what I learned"
   mini-grid are small print, and small print sitting right on the tile
   pattern was flagged as genuinely harder to read even though it still
   cleared AA on paper. Same fix as .learned-body already used one level
   in; .learned-body itself moved to --card so the two don't flatten into
   one indistinguishable box now that role-body has a background of its
   own. */
.role-body {
  margin-top: 0.75rem;
  padding: 1.125rem 1.375rem;
  background: var(--paper-alt);
  border: 1px solid var(--rule);
  border-radius: var(--radius-sm);
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
  gap: 0.75rem clamp(1.5rem, 3vw, 3rem);
  align-items: start;
}

/* a role with no achievement list gets the whole width back - :not(:has(ul))
   rather than :only-child, because a role can now also carry a trailing
   "what I learned here" .learned disclosure as a third child, and that
   shouldn't stop the single paragraph claiming the full row the way a
   plain :only-child check would */
.role-body:not(:has(ul)) > p {
  grid-column: 1 / -1;
  max-width: 74ch;
}

@media (max-width: 56rem) {
  .role-body {
    grid-template-columns: 1fr;
  }
}

.role-body p {
  color: var(--ink-soft);
  font-size: var(--t-small);
  line-height: 1.55;
}

.role-body ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}

.role-body ul li {
  position: relative;
  padding-left: 1rem;
  font-size: var(--t-small);
  line-height: 1.5;
  color: var(--ink-soft);
}

.role-body ul li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.62em;
  width: 7px;
  height: 2px;
  background: var(--accent-line);
}

.role-body ul li b {
  color: var(--ink-soft);
  font-weight: 600;
}

@media (max-width: 40rem) {
  .role-body {
    padding: 1rem;
  }
}

/* --- skills --- */

.skillsets {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(17rem, 100%), 1fr));
  gap: clamp(1.25rem, 3vw, 2.25rem);
}

.skillset h3 {
  color: var(--accent-line);
  margin-bottom: 0.875rem;
}

.skillset ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}

.skillset li {
  font-family: "Space Grotesk", sans-serif;
  font-size: var(--t-small);
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--ink-soft);
  background: var(--card);
  border: 1px solid var(--rule);
  border-radius: var(--radius-sm);
  padding: 0.3rem 0.625rem;
}

/* --- contact --- */

.contact {
  background: var(--paper);
  border-top: 1px solid var(--rule);
}

.contact .shell {
  padding-block: clamp(2.5rem, 6vw, 4.5rem);
}

.contact .block {
  background-color: var(--navy-solid);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='76' height='76'%3E%3Crect x='14' y='14' width='48' height='48' rx='11' fill='none' stroke='%23eef2f8' stroke-opacity='0.045' stroke-width='2'/%3E%3C/svg%3E");
  color: var(--on-banner);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: clamp(1.75rem, 5vw, 3.25rem);
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
  align-items: flex-start;
}

.contact .block h2 {
  max-width: 26ch;
}

.contact .block p {
  max-width: 54ch;
  color: var(--on-banner-soft);
}

.contact .block .label {
  color: var(--on-banner-soft);
}

.contact .block .label::before {
  background: var(--on-banner-bright);
}

.actions {
  display: flex;
  gap: 0.75rem;
  flex-wrap: wrap;
  align-items: center;
}

/* rounded-rect, not the pill shape an earlier pass used - "the same radius
   as the logo for all buttons and boxes" was explicit, and a pill button
   next to a --radius card read as two different systems rather than one */
.btn {
  --bs-btn-font-family: "Space Grotesk", sans-serif;
  --bs-btn-font-weight: 700;
  --bs-btn-padding-y: 0.75rem;
  --bs-btn-padding-x: 1.5rem;
  --bs-btn-border-width: 2px;
  --bs-btn-border-radius: var(--radius);
  letter-spacing: -0.015em;
}

/* the filled button on a dark surface (hero/contact): light fill, dark
   text, inverting to an outline on hover/focus - the same swap the old
   hand-rolled .button/.button--ghost pair made, now themed onto Bootstrap's
   own light/outline-light variants instead of two bespoke classes */
.hero .btn-light,
.contact .btn-light {
  --bs-btn-bg: var(--on-banner);
  --bs-btn-border-color: var(--on-banner);
  --bs-btn-color: var(--navy-solid);
  --bs-btn-hover-bg: transparent;
  --bs-btn-hover-border-color: var(--on-banner);
  --bs-btn-hover-color: var(--on-banner);
  --bs-btn-active-bg: transparent;
  --bs-btn-active-border-color: var(--on-banner);
  --bs-btn-active-color: var(--on-banner);
}

/* --- footer --- */

.site-foot {
  background: var(--paper-alt);
  border-top: 1px solid var(--rule);
}

.site-foot .shell {
  padding-block: 2rem 3.5rem;
  display: flex;
  gap: 1rem 2.5rem;
  flex-wrap: wrap;
  align-items: center;
}

.site-foot .brand {
  font-size: 1.1875rem;
}

.site-foot p {
  font-size: var(--t-small);
  color: var(--ink-mute);
}

.site-foot .right {
  margin-left: auto;
}

/* --- the mark ---
   This is the brand pack's own "Avatar / favicon" swatch, reproduced exactly
   - same markup technique (a flex-centred box holding real text, not drawn
   SVG shapes), same ratios (border-radius 16/72, font-size 28/72 of the box),
   same colours. --mark-size is the one thing each usage sets; everything
   else is a fixed ratio of it, the way the brand pack itself scales the mark
   at 56/32/16px. Deep/Page/Bright stay constant across the page theme, same
   as any logo - it is a fixed asset, not a themed component. */

.mark {
  --mark-size: 34px;

  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: var(--mark-size);
  height: var(--mark-size);
  border-radius: calc(var(--mark-size) * 0.2222);
  background: #0b1b34;
  font-family: "Space Grotesk", sans-serif;
  font-weight: 700;
  font-size: calc(var(--mark-size) * 0.3889);
  letter-spacing: -0.04em;
  color: #eef2f8;
  line-height: 1;
}

.mark .dot {
  color: #5b9bff;
}

/* The nav wordmark's mark is bigger than the footer's - was two inline
   `style="--mark-size: …"` attributes per page (8 across the site), which is
   the one thing standing between the CSP below and a `style-src` with no
   `'unsafe-inline'` at all. `.mark` itself already carries the footer's
   34px as its default, so only the nav size needed a real modifier class. */
.mark--lg {
  --mark-size: 46px;
}

/* --- panel: the shared container for the two new card grids below
   (problems, currently exploring) - same border/radius/shadow/padding scale
   .tool already uses, so a "problems" card and a "what I build" card read
   as the same design system rather than two competing ones */
.panel {
  background: var(--card);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: clamp(1.125rem, 2vw, 1.5rem);
  display: flex;
  flex-direction: column;
  gap: 0.625rem;
}

/* --- problems I enjoy solving ---
   Same card shell as .tool, deliberately no status chip and no tags - these
   aren't shipped tools, they're the pattern-matching that comes before one. */
.problems,
.exploring-grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(19rem, 100%), 1fr));
  gap: clamp(0.875rem, 1.5vw, 1.25rem);
}

.problem h3 {
  color: var(--accent-line);
}

.problem .tell {
  color: var(--ink-soft);
  font-size: var(--t-small);
  line-height: 1.5;
}

.problem .why {
  margin-top: auto;
  padding-top: 0.875rem;
  border-top: 1px solid var(--rule);
  font-size: var(--t-small);
  line-height: 1.45;
  color: var(--ink-mute);
}

.why-label {
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: var(--t-micro);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--accent-line);
}

/* --- technology beliefs ---
   Styled as the site's other manifesto-plaque band (same treatment as
   .highlights: dark ground, on-banner tokens) rather than another light
   card grid - a list of opinions earns the one place the page raises its
   voice, and it keeps the light/dark rhythm going between "what I build"
   and "experience", both of which are light bands either side of it. */
.beliefs-band {
  background-color: var(--banner);
  border-top: 1px solid rgba(255, 255, 255, 0.14);
  color: var(--on-banner);
}

.beliefs-band .shell {
  padding-block: clamp(2.5rem, 5vw, 4rem);
}

.beliefs-band h2 {
  color: var(--on-banner);
  max-width: 34ch;
  margin-bottom: 1.75rem;
}

.beliefs-band .label {
  color: var(--on-banner-soft);
  margin-bottom: 0.875rem;
}

.beliefs-band .label::before {
  background: var(--on-banner-bright);
}

/* the belief list sitting right on the band's own tile texture was flagged
   as hard to read - same "solid surface under small print" fix as
   .role-body, reusing the terminal's own recipe for a solid card on a dark
   band (color-mix against --on-banner, a soft white border) rather than
   inventing a second one */
.beliefs-panel {
  background: color-mix(in srgb, var(--on-banner) 6%, var(--banner));
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: var(--radius);
  padding: clamp(1.25rem, 3vw, 2rem);
}

.beliefs {
  list-style: none;
  margin: 0;
  padding: 0;
  columns: 2;
  column-gap: clamp(1.5rem, 3vw, 3rem);
}

.beliefs li {
  break-inside: avoid;
  position: relative;
  padding: 0.75rem 0 0.75rem 1rem;
  border-bottom: 1px solid rgb(255 255 255 / 16%);
  font-size: var(--t-small);
  line-height: 1.5;
  color: var(--on-banner-soft);
}

.beliefs li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 1.15em;
  width: 7px;
  height: 2px;
  background: var(--on-banner-bright);
}

@media (max-width: 40rem) {
  .beliefs {
    columns: 1;
  }
}

/* --- CTO notes ---
   Short asides scattered next to the content they comment on, not a section
   of their own - a rule on one side, mono type, quieter than body text,
   because these are observations rather than claims. */
.cto-note {
  position: relative;
  padding-left: 1rem;
  border-left: 2px solid var(--accent-line);
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: var(--t-small);
  line-height: 1.5;
  color: var(--ink-mute);
}

.highlights .cto-note {
  border-left-color: var(--on-banner-bright);
  color: var(--on-banner-soft);
  margin-top: 1.75rem;
}

/* --- mini-toggle ---
   A small "+" disclosure trigger shared by the build journal (inside tool
   cards) and "what I learned here" (inside roles) - same native
   <details>/<summary> pattern the experience accordion already uses one
   level up, just smaller. */
/* padding-block, not just the text's own line-height, is what gets this to
   the WCAG 2.5.8 24px target-size floor - at font-size alone this measured
   19.2px tall. Not relying on the "inline in a sentence" or "spacing"
   exceptions here: both plausibly applied, but a genuine 24px box holds
   regardless of what sits near it or how the copy around it changes later. */
.mini-toggle > summary {
  cursor: pointer;
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding-block: 0.3125rem;
  margin-block: -0.3125rem;
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: var(--t-micro);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--accent-line);
}

.mini-toggle > summary::-webkit-details-marker {
  display: none;
}

.mini-toggle > summary::before {
  content: "+";
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1rem;
  height: 1rem;
  border: 1px solid var(--accent-line);
  border-radius: 3px;
  font-size: 0.75rem;
  line-height: 1;
  flex: none;
}

.mini-toggle[open] > summary::before {
  content: "\2212";
}

/* build journal: sits inside a .tool card, which may also carry a
   .stretched-link overlay once a tool goes live (z-index: 1 - see
   .tool-link.stretched-link::after) - z-index: 2 here keeps the toggle
   clickable above that overlay rather than being swallowed by the card's
   own link the moment a tool ships (see the `bootstrap` skill on
   .stretched-link) */
.journal {
  position: relative;
  z-index: 2;
  margin-top: 0.375rem;
}

.journal-body {
  margin: 0.75rem 0 0;
  padding-top: 0.75rem;
  border-top: 1px solid var(--rule);
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

.journal-body dt {
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: 0.6875rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--ink-mute);
  margin-bottom: 0.15rem;
}

.journal-body dd {
  margin: 0;
  font-size: var(--t-small);
  line-height: 1.5;
  color: var(--ink-soft);
}

/* what I learned here: nested inside a role's own <details>, so it reads as
   an optional extra layer rather than replacing the CV-style summary */
.learned {
  grid-column: 1 / -1;
  margin-top: 0.25rem;
}

/* --card, not --paper-alt - .role-body itself sits on --paper-alt now (see
   above), so this stayed --paper-alt it would flatten into its own parent
   with no visible edge between them. One shade lighter keeps it reading as
   a nested panel rather than disappearing into the box around it. */
.learned-body {
  margin-top: 0.75rem;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: 0.5rem 1.5rem;
  padding: 0.875rem 1rem;
  background: var(--card);
  border: 1px solid var(--rule);
  border-radius: var(--radius-sm);
}

.learned-body dt {
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: 0.6875rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink-mute);
  margin-bottom: 0.15rem;
}

.learned-body dd {
  margin: 0;
  font-size: var(--t-small);
  line-height: 1.5;
  color: var(--ink-soft);
}

/* --- currently exploring --- */

.explore-card h3 {
  color: var(--accent-line);
}

.explore-card .chip {
  align-self: flex-start;
}

.explore-card p {
  color: var(--ink-soft);
  font-size: var(--t-small);
  line-height: 1.5;
}

/* "Still mush" / "Beta" / "Live" empty states, and any future "nothing
   matches this filter" message - dashed, no shadow, same "not real yet"
   language the tool-shot placeholder uses, rather than a new visual
   vocabulary for the same idea. Content is set by the filter script. */
.panel--ghost {
  border: 1px dashed var(--rule-strong);
  box-shadow: none;
  color: var(--ink-mute);
}

.panel--ghost p {
  margin: 0;
  font-size: var(--t-small);
  line-height: 1.55;
}

.panel--ghost a {
  color: var(--accent-line);
}

.explore-card .field b {
  display: block;
  font-family: "JetBrains Mono", ui-monospace, monospace;
  font-size: 0.6875rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink-mute);
  margin-bottom: 0.15rem;
}

/* --- accessibility.html's own reading layout ---
   Was a page-scoped inline <style> block. Moved here so the CSP below can
   drop 'unsafe-inline' from style-src entirely - .doc only exists on this
   one page, but there is nothing wrong with a single-use class living in
   the shared sheet, and it means every page now draws only from this file. */
.doc .shell { max-width: 52rem; padding-block: clamp(2.5rem, 6vw, 4.5rem); }
.doc h1 { font-size: clamp(2rem, 5vw, 3.25rem); margin-bottom: 1.5rem; }
.doc h2 { font-size: clamp(1.375rem, 2.6vw, 1.75rem); margin-top: 2.5rem; color: var(--accent-line); }
.doc h3 { font-size: 1.125rem; margin-top: 1.75rem; }
.doc p, .doc li { color: var(--ink-soft); }
.doc p + p, .doc ul, .doc ol { margin-top: 1rem; }
.doc ul, .doc ol { padding-left: 1.25rem; display: flex; flex-direction: column; gap: .5rem; }
.doc .dates { border-top: 1px solid var(--rule); margin-top: 2.5rem; padding-top: 1.25rem; }
.doc .dates p { font-size: var(--t-small); }
