/* ============================================================
   main.css — the landing page

   Monochrome on purpose (branding deferred): every colour comes
   from tokens.css. The toggle at the foot of the page swings that
   file between a light paper and a dark one, and changes nothing
   else. This stylesheet is also what projects.html reads: it uses
   the same bar, cards, chips and buttons.

   Default (no JS) state is the final state: scripts only add
   motion and the toggle, never content.
   ============================================================ */

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

html {
  scroll-padding-bottom: var(--switch-room);
  background: var(--bg);
  scroll-behavior: smooth;
  scroll-padding-top: 6rem;
  /* the cards correct the scroll themselves when they open and close;
     the browser's own anchoring would apply a second correction on top
     (and Safari has none at all), so we are the single source here */
  overflow-anchor: none;
}
html.no-motion { scroll-behavior: auto; }

body {
  font-family: var(--font-body);
  font-size: var(--text-body);
  line-height: var(--leading-body);
  color: var(--text);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: clip;
}

::selection { background: var(--invert-bg); color: var(--invert-text); }
img, svg { display: block; max-width: 100%; }
a { color: inherit; }
ul, ol { list-style: none; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
a:not([href]) { cursor: default; }
/* author display rules must never beat the hidden attribute */
[hidden] { display: none !important; }
/* full-strength ink, not --line-strong at 40%: the ring is the one place
   on a monochrome page where restraint costs somebody the ability to use it */
:focus-visible { outline: 2px solid var(--text); outline-offset: 3px; border-radius: 2px; }

.container { max-width: var(--container); margin-inline: auto; padding-inline: var(--gutter); }
.container--narrow { max-width: var(--container-narrow); }

.ph { border-bottom: 1px dotted var(--dotted); }

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

.skip-link {
  position: fixed; top: -4rem; left: 1rem; z-index: 100;
  padding: 0.6rem 1rem;
  background: var(--invert-bg); color: var(--invert-text);
  border-radius: 0.5rem; text-decoration: none;
  transition: top 200ms ease;
}
.skip-link:focus { top: 1rem; }

/* ============================================================
   THE APPEARANCE RULE — there is no longer one.

   Two lines used to live here and hide every element that did not
   belong to the current mode. That is how the toggle came to hide
   entire roles from a career. It now swings tokens.css and nothing
   else; every word on the page is on the page in both appearances.
   ============================================================ */

/* ---------- floating capsule nav ---------------------------- */
.nav {
  position: fixed; top: 1rem; left: 50%;
  transform: translateX(-50%);
  z-index: 50;
  display: flex; align-items: center;
  gap: clamp(0.9rem, 2.5vw, 2rem);
  padding: 0.6rem 1.15rem;
  max-width: calc(100vw - 2rem);
  /* Opaque, and no blur behind it. The pill used to be 82% surface over a
     14px backdrop blur, which meant the browser had to filter whatever the
     nav was flying over on every frame of a scroll — the construction
     drawing included — and the drawing needed a punch that travelled with
     the window so a mark did not read through the glass. A solid surface
     hides what is behind it by itself, so the moving punch is gone and the
     per-frame backdrop filter with it. Shape, border, shadow and type are
     unchanged. */
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 999px;
  box-shadow: var(--shadow-float);
}
.nav__wordmark {
  font-family: var(--font-display);
  font-size: 1rem;
  font-weight: 560;
  font-variation-settings: "opsz" 24;
  text-decoration: none;
  white-space: nowrap;
}
.nav__name-short, .nav__name-mini { display: none; }
.nav__links { display: flex; align-items: center; gap: clamp(0.7rem, 1.8vw, 1.4rem); }

/* The one action among the destinations, so it looks like one.
   Selector carries .nav__links to outrank the underline rule above,
   which sets border-bottom-color: transparent — the pill was drawing
   three sides and leaving the fourth invisible. */
.nav__links a.nav__cv {
  display: inline-flex;
  align-items: center;
  gap: 0.28rem;
  padding: 0.26rem 0.62rem;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  color: var(--text-2);
  text-decoration: none;
  white-space: nowrap;
  transition: color var(--dur-breath) var(--ease-sand),
              background var(--dur-breath) var(--ease-sand),
              border-color var(--dur-breath) var(--ease-sand);
}
.nav__links a.nav__cv:hover,
.nav__links a.nav__cv:focus-visible {
  color: var(--invert-text);
  background: var(--invert-bg);
  border-color: var(--invert-bg);
}
.nav__cv::before { inset: -0.6rem -0.3rem; }
.nav__cv-icon { transition: translate var(--dur-breath) var(--ease-sand); }
.nav__cv:hover .nav__cv-icon { translate: 0 2px; }
html.no-motion .nav__cv, html.no-motion .nav__cv-icon { transition: none; }
.nav__links a {
  position: relative;
  font-size: var(--text-small);
  font-weight: 500;
  color: var(--text-2);
  text-decoration: none;
  padding-bottom: 2px;
  border-bottom: 1px solid transparent;
  transition: color var(--dur-breath) var(--ease-sand), border-color var(--dur-breath) var(--ease-sand);
}
.nav__links a:hover:not(.nav__cv), .nav__links a.is-active:not(.nav__cv) { color: var(--text); border-bottom-color: var(--text); }

/* The nav sits in a slim pill, so the links cannot grow to 44px without
   growing the pill with them. This extends the tap target past the box
   instead: same design, a target a thumb can hit. */
.nav__links a::before,
.nav__wordmark::before,
.term__row a::before,
.term__line a::before,
.hero__cue::before {
  content: "";
  position: absolute;
  inset: -0.75rem -0.45rem;
}
/* these set on a 16px mono line, so they need a little more to clear 44px.
   The lines in the contact panel used to be pending slots rather than links;
   now that they go somewhere, they need somewhere to be pressed. */
.term__row a::before,
.term__line a::before { inset: -0.95rem -0.5rem; }
.nav__wordmark, .term__row a, .term__line a, .hero__cue { position: relative; }
.term__line a { display: inline-block; }

/* ---------- I. hero ----------------------------------------- */
.hero {
  /* the name needs a wider stage than the rest of the page — shared
     with .hero__cue below so the cue lines up with the content edge
     instead of the viewport edge */
  --hero-stage: 78rem;
  min-height: 100svh;
  display: flex;
  align-items: center;
  position: relative;
  padding-top: 6rem;
  /* the hero is exactly one viewport tall, so its last line would sit
     under the toggle for as long as the hero is on screen */
  padding-bottom: 7rem;
}
.hero__grid {
  width: 100%;
  max-width: var(--hero-stage);
  display: grid;
  grid-template-columns: minmax(0, 1.5fr) minmax(0, 0.8fr);
  gap: clamp(2rem, 6vw, 4.5rem);
  align-items: center;
}
.hero__name {
  font-family: var(--font-display);
  font-size: var(--text-hero);
  font-weight: 560;
  font-variation-settings: "opsz" 144;
  line-height: 1.02;
  letter-spacing: -0.02em;
  margin-bottom: clamp(1.2rem, 3vh, 2rem);
}
.hero__role {
  font-family: var(--font-mono);
  font-size: var(--text-small);
  color: var(--text-2);
  margin-bottom: 0.9rem;
}
.hero__prompt { color: var(--text-3); margin-right: 0.35rem; }
.hero__at { color: var(--text-3); }
.hero__headline {
  font-family: var(--font-display);
  font-size: clamp(1.35rem, 2.3vw, 1.85rem);
  font-weight: 500;
  font-variation-settings: "opsz" 60;
  line-height: 1.28;
  max-width: 24ch;
  margin-bottom: 0.9rem;
}
.hero__lede {
  color: var(--text-2);
  max-width: 52ch;
  margin-bottom: clamp(0.9rem, 2vh, 1.2rem);
}

/* Where the work is going, kept apart from what it is now. It reads as a
   note against the lede rather than as another sentence of it, because a
   direction folded into a paragraph about the present job is exactly how a
   reader ends up thinking it is the present job. */
.hero__next {
  position: relative;
  padding-left: 0.9rem;
  max-width: 46ch;
  font-family: var(--font-mono);
  font-size: var(--text-small);
  line-height: 1.6;
  color: var(--text);
  margin-bottom: clamp(1.4rem, 3vh, 2rem);
}
.hero__next::before {
  content: "";
  position: absolute;
  left: 0; top: 0.3em; bottom: 0.3em;
  width: 2px;
  background: var(--line-strong);
}

/* the two destinations, before the proof numbers rather than after: they
   are the answer to "what do I do next", and that question arrives early */
.hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-top: clamp(1.6rem, 3.5vh, 2.4rem);
}


.pulse-dot {
  display: inline-block;
  width: 6px; height: 6px;
  margin-right: 0.45rem;
  border-radius: 50%;
  background: currentColor;
  animation: pulse 2.8s var(--ease-sand) infinite;
}
@keyframes pulse {
  0% { box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.4); }
  70% { box-shadow: 0 0 0 7px rgba(255, 255, 255, 0); }
  100% { box-shadow: 0 0 0 0 rgba(255, 255, 255, 0); }
}
html.no-motion .pulse-dot { animation: none; }

.hero__cue {
  position: absolute;
  /* Set OUTSIDE the stage, not on it, so it reads as furniture in
     the margin rather than as another line of the composition. This
     is the narrow fallback — nudged left of the content edge but
     floored at the gutter. See the min-width rule below for the
     wide case, where it clears the content entirely. */
  left: max(var(--gutter), (100% - var(--hero-stage)) / 2 + var(--gutter) - 2.75rem);
  /* Clear of the mode toggle, which is fixed at the foot of every
     screen: it sits clamp(1rem, 3vh, 1.75rem) up and stands about
     2.9rem tall, so its top edge is ~4.7rem off the bottom. This
     keeps roughly 20px of air above that at any viewport height. */
  bottom: clamp(5.25rem, 3vh + 4.25rem, 6.25rem);
  display: inline-flex; align-items: center; gap: 0.5rem;
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-3);
  text-decoration: none;
}
.hero__cue svg { animation: cue 1.9s var(--ease-sand) infinite; }
@keyframes cue { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(5px); } }

/* Once the margin is wide enough to hold the cue, it ENDS where the
   hero's content begins: left edge on the content edge, then pulled
   back by its own width. translateX rather than a `right` offset so
   nothing has to know how wide the word "scroll" renders.

   1400px is where that becomes possible and not a coincidence: the
   cue is ~76px, the stage is 78rem, so the margin only clears the
   gutter from (1248 + 2*76) = 1400px up. Below it, the base rule
   above keeps the cue on the page instead of sliding it off-screen.

   GSAP only ever touches autoAlpha on .hero__cue, so this transform
   is safe from the intro timeline and its clearProps sweep. */
@media (min-width: 1400px) {
  .hero__cue {
    left: calc((100% - var(--hero-stage)) / 2 + var(--gutter));
    transform: translateX(-100%);
  }
}
html.no-motion .hero__cue svg { animation: none; }

/* ---------- shared section furniture ------------------------ */
.section { padding-block: var(--band); position: relative; }

/* ---- section separators -----------------------------------
   The old rule was --line-soft (9% ink) and effectively invisible,
   so the page read as one continuous scroll with no sense of
   arriving anywhere. Now each section opens with a full-bleed
   hairline carrying its number, set at the container's left edge
   so it lines up with the `$ ls ~/work` beneath it.

   Writing and contact use .container--narrow, so the number has
   to follow THAT column, not always --container: otherwise 03
   and 04 hang in the left margin, unattached to their heading.

   The number is a CSS counter, not markup — sections can be
   reordered, added or removed and the sequence stays correct.
   .section only ever appears on index.html, so nothing else on
   the site is affected.
   ----------------------------------------------------------- */
main { counter-reset: section; }
.section {
  counter-increment: section;
  --section-col: var(--container);
}
.section:has(> .container--narrow) { --section-col: var(--container-narrow); }

.section::before {
  content: "";
  position: absolute;
  inset-inline: 0;
  top: 0;
  height: 1px;
  background: var(--line);
}

.section::after {
  content: counter(section, decimal-leading-zero);
  position: absolute;
  top: 0;
  /* the section's own column edge, by the same maths as .container */
  left: max(var(--gutter), (100% - var(--section-col)) / 2 + var(--gutter));
  transform: translateY(-50%);
  /* the page colour breaks the rule around the number */
  background: var(--bg);
  padding-inline: 0.85rem;
  margin-left: -0.85rem;
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  letter-spacing: 0.16em;
  color: var(--text-3);
  line-height: 1;
}

.head { margin-bottom: clamp(2.5rem, 6vh, 4rem); }
.head--center { text-align: center; }
.head--center .head__sub { margin-inline: auto; }
.head__cmd {
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  color: var(--text-3);
  margin-bottom: 0.6rem;
}
.head__title {
  font-family: var(--font-display);
  font-size: var(--text-h2);
  font-weight: 540;
  font-variation-settings: "opsz" 90;
  line-height: 1.1;
  letter-spacing: -0.015em;
  margin-bottom: 0.7rem;
  text-wrap: balance;
}
.head__sub { color: var(--text-3); max-width: 56ch; }

.btn {
  display: inline-flex; align-items: center; gap: 0.5rem;
  padding: 0.8rem 1.4rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  font-size: var(--text-small);
  font-weight: 500;
  text-decoration: none;
  transition: background var(--dur-breath) var(--ease-sand), color var(--dur-breath) var(--ease-sand), border-color var(--dur-breath) var(--ease-sand);
}
.btn:hover { border-color: var(--line-strong); }
.btn--solid { background: var(--invert-bg); border-color: var(--invert-bg); color: var(--invert-text); }
.btn--solid:hover { filter: brightness(1.25); }
.btn--ghost {
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  border-style: dashed;
  color: var(--text-2);
}
.btn--ghost:hover { color: var(--text); }

/* ---------- II. work cards ---------------------------------- */
/* Fixed 6-column spans could not tile. lg+md+sm is 4+3+2 = 9 of a
   6-track row, so SOMETHING always had to be left over: with
   auto-flow:dense a later card was pulled up into the hole (visual
   order stopped matching tab order, WCAG 2.4.3), and without dense
   the hole simply sat there beside card 01, pushing 02 and 03 under
   the fold on the jump to #work.

   auto-fit collapses the tracks nothing occupies, and 1fr makes the
   remaining cards stretch across the freed space — so a row cannot
   have a hole in it, at any project count, and the cards stay in
   document order. min() guards the very narrow case, where a 17rem
   minimum would otherwise overflow the screen.

   Size now reads through TYPE, not width: .card--lg keeps its larger
   title (below). The three tiers stopped being a width decision the
   moment the widths had to tile. */
.cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(17rem, 100%), 1fr));
  gap: clamp(0.9rem, 1.8vw, 1.4rem);
  counter-reset: card;
}

.card {
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 0.75rem;
  overflow: hidden;
  /* a resting shadow, so a card sits ON the paper rather than being
     a rectangle drawn level with it */
  box-shadow: var(--shadow-float);
  /* `translate`, NOT `transform`: motion.js runs rise("[data-card]",
     {y: 34}) and leaves an inline transform on every card, which
     would silently win over a transform declared here. The separate
     translate property composes with it instead of colliding. */
  translate: 0 0;
  transition: border-color var(--dur-breath) var(--ease-sand),
              box-shadow var(--dur-breath) var(--ease-sand),
              translate var(--dur-breath) var(--ease-sand);
}
@media (hover: hover) {
  .card:hover {
    border-color: var(--line-strong);
    box-shadow: var(--shadow-lift);
    translate: 0 -3px;
  }
}
.card.is-open {
  grid-column: 1 / -1;
  border-color: var(--line-strong);
  box-shadow: var(--shadow-lift);
  translate: 0 0;
}
/* the whole card is the target once JS is in play, so it says so */
.card--openable { cursor: pointer; }
.card--openable.is-open { cursor: default; }
.card--openable.is-open .card__toggle,
.card--openable.is-open a[href] { cursor: pointer; }
html.no-motion .card { transition: none; }
html.no-motion .card:hover { translate: 0 0; }

/* The header reads as a machine label: a fine diagonal hatch at
   rest, and it inverts to solid ink the moment the card is hovered
   or opened. That inversion is doing real work — it is the only
   thing on the page that tells you, at a glance across the whole
   grid, which card is currently open. */
.card__bar {
  display: flex; align-items: center; gap: 0.7rem;
  padding: 0.5rem 0.85rem;
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  color: var(--text-3);
  background-color: var(--surface-2);
  background-image: repeating-linear-gradient(
    45deg, transparent 0 4px, var(--hatch) 4px 5px);
  border-bottom: 1px solid var(--line-soft);
  transition: background-color var(--dur-breath) var(--ease-sand),
              color var(--dur-breath) var(--ease-sand);
}
/* The number is generated, so it counts what is actually on screen:
   a card hidden by the mode filter is display:none and does not
   increment. Baked into the HTML it read 01, 02, 05 in digital. */
.card { counter-increment: card; }
.card__index { color: var(--text); transition: color var(--dur-breath) var(--ease-sand); }
.card__index[data-count]::before { content: counter(card, decimal-leading-zero); }

.card.is-open .card__bar {
  background-color: var(--invert-bg);
  background-image: none;
  color: color-mix(in srgb, var(--invert-text) 62%, transparent);
  border-bottom-color: transparent;
}
.card.is-open .card__index { color: var(--invert-text); }

/* the same inversion on hover, but only where hover is real */
@media (hover: hover) {
  .card--openable:hover .card__bar {
    background-color: var(--invert-bg);
    background-image: none;
    color: color-mix(in srgb, var(--invert-text) 62%, transparent);
    border-bottom-color: transparent;
  }
  .card--openable:hover .card__index { color: var(--invert-text); }
}
.card__file { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.card__year { margin-left: auto; white-space: nowrap; }

/* A column, so the meta block can be pushed to the foot: with three
   cards abreast and summaries of different lengths, aligning role and
   chips across the row is what makes it read as composed rather than
   as three unrelated boxes. */
.card__face {
  padding: clamp(1.25rem, 2.2vw, 1.75rem);
  flex: 1;
  display: flex;
  flex-direction: column;
}
.card__title {
  font-family: var(--font-display);
  font-size: clamp(1.15rem, 1.8vw, 1.5rem);
  font-weight: 540;
  font-variation-settings: "opsz" 40;
  line-height: 1.2;
  margin-bottom: 0.6rem;
}
.card--lg .card__title { font-size: clamp(1.4rem, 2.4vw, 1.9rem); }
/* margin-bottom:auto eats the slack, so what follows sits at the foot
   of the card whether or not the project declares a role; the padding
   guarantees a gap when there is no slack to eat. */
.card__summary {
  color: var(--text-2);
  font-size: var(--text-small);
  line-height: 1.6;
  margin-bottom: auto;
  padding-bottom: 1.35rem;
}
.card__role {
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  color: var(--text-3);
  margin-bottom: 0.4rem;   /* tight to the chips: one meta group, not two */
  /* two-line slot, text on the last line: a short role then sits on
     the same baseline as a wrapping one in the card beside it.
     The build always emits this element, even empty, so a card
     without a role (reading-programmes) does not lift its chips. */
  min-height: calc(2 * 1.65em);
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
}
/* reading-programmes has no $ open strip. The grid stretches the row
   to one height, the face fills it, and the chips then sit where the
   toggle lives on the card beside it (~44px too low). Same min-height
   as .card__toggle, so they line up. */
.card:not(:has(.card__toggle)) {
  padding-bottom: 2.75rem;
}

/* Set as a plain mono line, not pills. Three bordered capsules inside
   a small card added three more shapes to a face that already carries
   a header strip, a title, a summary and a footer — the stack read as
   clutter. The dot separators do the same job with no chrome. */
.chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0;
  min-height: calc(2 * 1.65em);
  align-content: flex-end;
}
.chips li {
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  color: var(--text-3);
}
.chips li + li::before {
  content: "·";
  margin-inline: 0.4rem;
}

/* 2.75rem clears the 44px target size that the old 41px strip missed,
   and the chevron gives the control a shape rather than leaving "$ open"
   to carry the affordance on its own. */
.card__toggle {
  display: flex; align-items: center; justify-content: space-between;
  min-height: 2.75rem;
  padding: 0.6rem clamp(1.25rem, 2.2vw, 1.75rem);
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  color: var(--text-3);
  border-top: 1px dashed var(--line);
  text-align: left;
  transition: color var(--dur-breath) var(--ease-sand), background var(--dur-breath) var(--ease-sand);
}
.card__toggle:hover { color: var(--text); background: var(--surface-2); }
.card__toggle::after {
  content: "";
  width: 0.42rem; height: 0.42rem;
  margin-bottom: 2px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg);
  transition: transform var(--dur-breath) var(--ease-sand);
}
.card__toggle[aria-expanded="true"]::after { transform: rotate(225deg); }
html.no-motion .card__toggle::after { transition: none; }

.card__toggle-close { display: none; }
.card__toggle[aria-expanded="true"] .card__toggle-open { display: none; }
.card__toggle[aria-expanded="true"] .card__toggle-close { display: inline; }

.card__detail {
  border-top: 1px solid var(--line-soft);
  padding: clamp(1rem, 2vw, 1.6rem);
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: clamp(1rem, 2.5vw, 2rem);
  background: var(--surface-2);
}
.card__block h4 {
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  font-weight: 500;
  color: var(--text-3);
  margin-bottom: 0.45rem;
}
.card__block p, .card__block li { font-size: var(--text-small); color: var(--text-2); line-height: 1.7; }
.card__block ul li { position: relative; padding-left: 1rem; margin-bottom: 0.3rem; }
.card__block ul li::before {
  content: ""; position: absolute; left: 0; top: 0.68em;
  width: 0.5rem; height: 1px; background: var(--text-3);
}
.card__stack, .card__log li { font-family: var(--font-mono); font-size: var(--text-micro); }
.link-arrow { font-size: var(--text-small); color: var(--text); text-underline-offset: 3px; }

/* ---- the rest of the work, on request -----------------------
   Only the first row of each side is on show. The others are in
   the page from the start — folded away by a class JS adds, so
   with scripts off nothing is hidden and no control appears.
   -------------------------------------------------------------- */
/* The grid no longer folds. It used to hide the cards past the first row,
   because each side of the old toggle showed a different number of them; the
   home page now shows exactly the featured few and links to the rest. */

/* The row under the shortlist: one ordinary link to the whole directory,
   and a line saying what is behind it. */
.cards__more-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  margin-top: clamp(1.4rem, 3vw, 2rem);
  text-align: center;
}
.cards__more-note {
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  color: var(--text-3);
}

.cards__empty {
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  color: var(--text-3);
  text-align: center;
  padding: 3rem 0;
}

/* ---------- projects.html: every project, in groups ----------
   The same cards as the home page, with their detail already open
   and no toggle to press. A directory is read, not explored, and
   nine accordions is nine clicks between a reader and the list
   they came for.
   -------------------------------------------------------------- */
.index { padding: calc(var(--band) * 0.6) 0 var(--band); }
.index .head { margin-bottom: clamp(2rem, 5vh, 3rem); }
.index .head__title { font-size: var(--text-h2); }

.group { margin-bottom: clamp(3rem, 8vh, 5rem); }
.group__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.4rem 1rem;
  padding-bottom: 0.7rem;
  margin-bottom: clamp(1.2rem, 3vh, 1.8rem);
  border-bottom: 1px solid var(--line);
}
.group__title {
  font-family: var(--font-mono);
  font-size: var(--text-small);
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text);
}
.group__note { font-size: var(--text-small); color: var(--text-3); }

/* one column: these cards are open, so side by side would put two
   walls of prose next to each other */
.cards--all { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-4); }
.cards--all .card { width: 100%; }
.card--open { cursor: default; }
.card--open .card__detail { display: grid; }

.index__end {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-top: clamp(2rem, 5vh, 3rem);
}

/* ---------- III. experience: the git log -------------------- */
.log { position: relative; }
.log__entry, .log__tag {
  /* the graph gutter's width, in one place: the line below is centred
     on it by arithmetic rather than by inheriting a box */
  --log-gutter: 2.2rem;
  position: relative;
  display: grid; grid-template-columns: var(--log-gutter) minmax(0, 1fr); gap: 0.9rem;
}
.log__entry { padding-bottom: clamp(1.6rem, 4vh, 2.6rem); }
.log__tag { padding-bottom: clamp(1.2rem, 3vh, 2rem); align-items: center; }

/* Each row draws only its own segment, so long entries cannot extend
   the timeline past the root node into the record buttons. */
.log__graph { display: flex; justify-content: center; }
.log__entry::before, .log__tag::before {
  content: "";
  position: absolute;
  top: 0; bottom: 0;
  left: calc(var(--log-gutter) / 2);
  width: 1px;
  background: var(--line-strong);
  opacity: 0.5;
}
/* The final segment stops at the centre of the root node. */
.log__entry--root::before { bottom: auto; height: calc(0.45rem + 5.5px); }
.log__node, .log__ring {
  position: relative;
  margin-top: 0.45rem;
  width: 11px; height: 11px;
  border-radius: 50%;
  background: var(--text);
  outline: 4px solid var(--bg);
}
.log__node--root { background: var(--bg); border: 2px solid var(--text); }
.log__ring {
  width: 8px; height: 8px;
  margin-top: 0;
  background: var(--bg);
  border: 1px solid var(--line-strong);
}

.log__meta {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem;
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  color: var(--text-3);
  margin-bottom: 0.3rem;
}
.log__sha { color: var(--text); }
.log__head {
  padding: 0.05rem 0.45rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  color: var(--text);
}
.log__role {
  font-family: var(--font-display);
  font-size: clamp(1.2rem, 2vw, 1.55rem);
  font-weight: 540;
  font-variation-settings: "opsz" 40;
  line-height: 1.2;
  margin-bottom: 0.6rem;
}
.log__org { color: var(--text-3); font-weight: 400; }
.log__points { max-width: 68ch; margin-bottom: 0.7rem; }
.log__points li {
  position: relative;
  padding-left: 1.15rem;
  margin-bottom: 0.35rem;
  color: var(--text-2);
  font-size: var(--text-small);
}
.log__points li::before { content: "+"; position: absolute; left: 0; color: var(--text-3); font-family: var(--font-mono); }
.log__note { color: var(--text-2); font-size: var(--text-small); max-width: 68ch; }
.log__files {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.45rem;
  font-family: var(--font-mono);
  font-size: var(--text-micro);
}
.log__files > span { color: var(--text-3); margin-right: 0.2rem; }
.log__files b {
  font-weight: 400;
  padding: 0.1rem 0.45rem;
  border: 1px solid var(--line);
  border-radius: 0.3rem;
  color: var(--text-2);
}
.log__tag p {
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  color: var(--text-3);
}
.log__tagname {
  padding: 0.05rem 0.4rem;
  margin-right: 0.4rem;
  border: 1px dashed var(--line-strong);
  border-radius: 0.3rem;
  color: var(--text);
}

.xp__actions {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.8rem;
  margin-top: clamp(1.5rem, 4vh, 2.5rem);
}

/* ---------- terminals (man lucas, direct lines) -------------- */
.term-wrap { margin-top: 1.2rem; }
.term {
  background: var(--panel-bg);
  border: 1px solid var(--panel-line);
  border-radius: 0.7rem;
  overflow: hidden;
  font-family: var(--font-mono);
  font-size: var(--text-micro);
}
.term__bar {
  display: flex; gap: 0.8rem;
  padding: 0.5rem 0.85rem;
  background: var(--panel-shade);
  border-bottom: 1px solid var(--panel-line);
}
.term__host { color: var(--panel-text); }
.term__path { color: var(--panel-dim); }
.term__hint { margin-left: auto; color: var(--panel-text); }
.term__body { padding: 0.85rem; }
.term__line { color: var(--panel-text); line-height: 1.7; overflow-wrap: anywhere; }
.term__line--cmd { color: var(--panel-strong); }
.term__line--h { color: var(--panel-dim); margin-top: 0.6rem; letter-spacing: 0.08em; }
.term__line--h:first-child { margin-top: 0; }
.term__body--man .term__line:not(.term__line--h) { padding-left: 1.2rem; max-width: 80ch; }
.term__line--kv { display: grid; grid-template-columns: 5.5rem minmax(0, 1fr); gap: 0.8rem; }
.term__line--kv > span:first-child { color: var(--panel-dim); }
.term__line a { color: var(--panel-strong); text-underline-offset: 3px; }
.term .ph { border-bottom-color: rgba(255, 255, 255, 0.3); }

/* ---------- IV. writing shelf -------------------------------- */
.shelf__intro { color: var(--text-2); max-width: 60ch; }
.entries { position: relative; }
.entry {
  display: grid;
  grid-template-columns: 3.2rem minmax(0, 1fr);
  gap: 0.6rem;
  padding-bottom: clamp(1.2rem, 3vh, 2rem);
}
.entry__branch {
  position: relative;
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  color: var(--text-3);
  padding-top: 0.35rem;
}
/* the trunk: a continuous line down the gutter, so ├── and └── read as a tree */
.entry:not(:last-child) .entry__branch::before {
  content: "";
  position: absolute;
  left: 0.15rem; top: 1.35rem; bottom: calc(-1 * clamp(1.2rem, 3vh, 2rem));
  width: 1px;
  background: var(--line);
}
.entry__body {
  padding-bottom: clamp(1.2rem, 3vh, 2rem);
  border-bottom: 1px solid var(--line-soft);
}
.entry:last-child .entry__body { border-bottom: 0; }
.entry__meta {
  display: flex; flex-wrap: wrap; gap: 0.8rem;
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  color: var(--text-3);
  margin-bottom: 0.3rem;
}
.entry__tag { color: var(--text-2); }
.entry__title {
  font-family: var(--font-display);
  font-size: clamp(1.25rem, 2.2vw, 1.7rem);
  font-weight: 540;
  font-variation-settings: "opsz" 40;
  line-height: 1.22;
  margin-bottom: 0.35rem;
}
.entry__title a { text-decoration: none; background-image: linear-gradient(currentColor, currentColor); background-size: 0% 1px; background-repeat: no-repeat; background-position: 0 100%; transition: background-size var(--dur-breath) var(--ease-sand); }
.entry__title a:hover { background-size: 100% 1px; }
.entry__excerpt { color: var(--text-2); max-width: 60ch; line-height: var(--leading-slow); }
.entry__file {
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  color: var(--text-3);
  margin-top: 0.5rem;
}

.shelf__cta {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.5rem;
  margin-top: clamp(1rem, 3vh, 2rem);
}
.shelf__hint {
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  color: var(--text-3);
  /* keep the line in the left column so it cannot park under the
     sticky toggle, which sits in the middle of this same row */
  max-width: 36ch;
}

/* ---------- V. contact --------------------------------------- */
.contact__lines { margin-bottom: 1rem; }
.contact__cta { margin-top: 1.4rem; display: flex; justify-content: center; }

.console {
  background: var(--panel-bg);
  border: 1px solid var(--panel-line);
  border-radius: 0.7rem;
  overflow: hidden;
  font-family: var(--font-mono);
  font-size: var(--text-micro);
  text-align: left;
}
.console__bar {
  display: flex; align-items: center; gap: 0.7rem;
  padding: 0.5rem 0.85rem;
  background: var(--panel-shade);
  border-bottom: 1px solid var(--panel-line);
  color: var(--panel-dim);
}
.console__host { color: var(--panel-text); }
/* this line is the only thing telling anyone the shell takes input,
   so it reads at full panel text weight. `lucas@crossing` stays dim —
   that one is scenery and should look like scenery. */
.console__hint { margin-left: auto; color: var(--panel-text); }
.console__hint b { color: var(--panel-strong); font-weight: 500; }
.console__screen {
  height: clamp(9rem, 20vh, 13rem);
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 0.85rem 0.9rem 0.3rem;
  scrollbar-width: thin;
  scrollbar-color: rgba(255, 255, 255, 0.22) transparent;
}
.console__screen:focus-visible { outline: 2px solid var(--panel-strong); outline-offset: -2px; }
.cl { color: var(--panel-text); line-height: 1.62; white-space: pre-wrap; overflow-wrap: anywhere; }
.cl b { color: var(--panel-strong); font-weight: 500; }
.cl a { color: var(--panel-strong); text-underline-offset: 3px; }
.cl--cmd { color: var(--panel-strong); }
.cl--dim { color: var(--panel-dim); }
.cl--err::before { content: "! "; color: var(--panel-dim); }
.cl--gap { line-height: 0.7; }
.console .ph { border-bottom-color: rgba(255, 255, 255, 0.3); }
.console__form {
  display: flex; align-items: center; gap: 0.5rem;
  padding: 0.55rem 0.9rem 0.65rem;
  border-top: 1px solid var(--panel-line);
  background: var(--panel-shade);
}
.console__prompt { color: var(--panel-strong); }
.console__input {
  flex: 1; min-width: 0;
  /* a 21px-tall input is a poor thing to aim at on a phone; padding grows
     the target and the negative margin keeps the line where it was */
  padding-block: 0.75rem;
  margin-block: -0.75rem;
  font: inherit; color: var(--panel-strong);
  background: none; border: 0; outline: none;
  caret-color: var(--panel-strong);
}
.console__input::placeholder { color: rgba(255, 255, 255, 0.34); }

/* The footer lives in bar.css, shared with every other page. */

/* ---------- responsive --------------------------------------- */
@media (max-width: 1000px) {
  .hero__grid { grid-template-columns: minmax(0, 1fr); gap: 2rem; }
  .hero { padding-top: 7rem; }
}

/* On a phone the hero is the whole first screen, and the two links are the
   reason it exists. Tighten the space between the lines rather than the
   lines themselves, and let the sphere follow underneath instead of
   competing for the fold. */
@media (max-width: 700px) {
  .hero { min-height: auto; padding-top: 6rem; padding-bottom: 3rem; }
  .hero__grid { gap: 1.25rem; }
  .hero__name { margin-bottom: 0.9rem; }
  .hero__role { margin-bottom: 0.6rem; }
  .hero__headline { margin-bottom: 0.7rem; }
  .hero__lede { margin-bottom: 0.8rem; }
  .hero__next { margin-bottom: 1.1rem; }
  .hero__actions { margin-top: 0; gap: 0.7rem; }
  .hero__actions .btn { width: 100%; justify-content: center; }
}

@media (max-width: 760px) {
  .nav { gap: 0.75rem; padding: 0.5rem 0.9rem; }
  .nav__name-full { display: none; }
  .nav__name-short { display: inline; }
  .nav__links a { font-size: var(--text-micro); }
  /* the line follows the gutter, so only the gutter changes here */
  .log__entry, .log__tag { --log-gutter: 1.6rem; gap: 0.6rem; }
  .entry { grid-template-columns: 2.2rem minmax(0, 1fr); }
  .switch__opt { min-width: 6rem; }
}

@media (max-width: 460px) {
  .nav__name-short { display: none; }
  .nav__name-mini { display: inline; }
  .nav { gap: 0.6rem; }
  .nav__links { gap: 0.5rem; }
  .hero__cue { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}


/* ---------- the top bar on a phone ---------------------------
   Four destinations, the CV, and the name do not fit inside the
   pill at 375px and never did — the bar was already wider than
   its own max-width before the CV was added, and simply spilled
   past its rounded edge. Below this the name steps out: it is a
   link back to the top, which the page gives you anyway, and the
   CV is the one thing here worth keeping reachable.
   -------------------------------------------------------------- */
@media (max-width: 560px) {
  .nav { gap: 0.75rem; padding: 0.5rem 0.85rem; }
  .nav__links { gap: 0.7rem; }
  .nav__cv { padding: 0.24rem 0.5rem; gap: 0.2rem; }
}
@media (max-width: 460px) {
  .nav__wordmark { display: none; }
  .nav { gap: 0.6rem; padding: 0.5rem 0.75rem; }
  .nav__links { gap: 0.62rem; }
}

/* The form shares the site's paper and charcoal tokens. */
.contact__cta { flex-wrap: wrap; align-items: flex-start; gap: 1rem; }
.contact-form { width: 100%; max-width: 36rem; text-align: center; }
.contact-form summary { cursor: pointer; list-style: none; }
.contact-form summary::-webkit-details-marker { display: none; }
.contact-form__body { position: relative; display: grid; gap: .7rem; margin-top: 1.5rem; text-align: left; }
/* Explicit hiding keeps closed fields out of geometry used by the drawing. */
.contact-form:not([open]) .contact-form__body { display: none; }
.contact-form label { font-family: var(--font-mono); font-size: .85rem; }
.contact-form input, .contact-form textarea { width: 100%; min-height: 44px; padding: .8rem; color: var(--text); background: var(--surface); border: 1px solid var(--line); border-radius: 4px; font: inherit; }
.contact-form textarea { resize: vertical; }
.contact-form :is(input, textarea, summary):focus-visible { outline: 2px solid var(--text); outline-offset: 3px; }
.contact-form__note, .contact-form__status { font-size: .85rem; line-height: 1.6; }
.contact-form__status { min-height: 1.6em; }
.contact-form__body button { justify-self: start; min-height: 44px; }
.contact-form button:disabled { opacity: .55; cursor: wait; }
.contact-form__trap { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
