/* ─────────────────────────────────────────────────────────────────
   Marco Luyckx hub (luyckx.dev)
   Catppuccin Mocha (dark) / Latte (light) · JetBrains Mono
   Designed to feel like the CV site's "home screen" at marco.luyckx.dev.
   ───────────────────────────────────────────────────────────────── */

/* Fonts ─────────────────────────────────────────────────────────── */

@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url('/fonts/JetBrainsMono-Latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url('/fonts/JetBrainsMono-LatinExt.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* Palette: Catppuccin Mocha ─────────────────────────────────── */

:root {
  --base: #1e1e2e;
  --mantle: #181825;
  --crust: #11111b;
  --surface0: #313244;
  --surface1: #45475a;
  --surface2: #585b70;
  --overlay0: #6c7086;
  --overlay1: #7f849c;
  --text: #cdd6f4;
  --subtext0: #a6adc8;
  --subtext1: #bac2de;
  --pink: #f5c2e7;
  --mauve: #cba6f7;
  --green: #a6e3a1;
  --blue: #89b4fa;
  --yellow: #f9e2af;
  --peach: #fab387;
  --red: #f38ba8;
  --teal: #94e2d5;
  --lavender: #b4befe;
  --maroon: #eba0ac;
  --sky: #89dceb;

  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
}

:root[data-theme="light"] {
  --base: #eff1f5;
  --mantle: #e6e9ef;
  --crust: #dce0e8;
  --surface0: #ccd0da;
  --surface1: #bcc0cc;
  --surface2: #acb0be;
  --overlay0: #6e7186;
  --overlay1: #626579;
  --text: #4c4f69;
  --subtext0: #5e6175;
  --subtext1: #5c5f77;
  --pink: #ea76cb;
  --mauve: #8839ef;
  --green: #40a02b;
  --blue: #1e66f5;
  --yellow: #df8e1d;
  --peach: #fe640b;
  --red: #d20f39;
  --teal: #179299;
  --lavender: #7287fd;
  --maroon: #e64553;
  --sky: #04a5e5;
}

/* Reset ────────────────────────────────────────────────────────── */

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

html {
  font-size: 16px;
  scroll-behavior: smooth;
  scroll-padding-top: 52px;
}

body {
  font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
  font-feature-settings: 'liga' 1, 'calt' 1;
  background: var(--base);
  color: var(--text);
  line-height: 1.7;
  min-height: 100svh;
  overflow-x: hidden;
  padding-top: 44px;      /* reserve space for the fixed tab-bar */
  transition: background-color 0.3s ease, color 0.3s ease;
}

::selection {
  background: var(--surface2);
  color: var(--pink);
}

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

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

.skip-link {
  position: absolute;
  top: -100%;
  left: 16px;
  z-index: 10001;
  padding: 8px 16px;
  background: var(--pink);
  color: var(--crust);
  font-size: 0.85rem;
  font-weight: 600;
  text-decoration: none;
  border-radius: 4px;
}
.skip-link:focus { top: 8px; }

/* Subtle scanline overlay, same trick as the CV site ─────────── */

body::after {
  content: '';
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 9999;
  background: repeating-linear-gradient(
    0deg,
    transparent,
    transparent 2px,
    rgba(0, 0, 0, 0.03) 2px,
    rgba(0, 0, 0, 0.03) 3px
  );
}

:root[data-theme="light"] body::after {
  background: repeating-linear-gradient(
    0deg,
    transparent,
    transparent 2px,
    rgba(0, 0, 0, 0.015) 2px,
    rgba(0, 0, 0, 0.015) 3px
  );
}

/* Tab bar, mirrors the CV site layout ─────────────────────── */

.tab-bar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 9998;
  background: var(--crust);
  border-bottom: 1px solid var(--surface0);
  display: flex;
  align-items: stretch;
  height: 44px;
  transition: background-color 0.3s ease, border-color 0.3s ease;
  overflow-x: auto;
  overflow-y: hidden;
  /* The action buttons are pinned over the right end of this scrollport with
     position:sticky, so they cover the last ~100px of it. scroll-padding
     shrinks the region browser-initiated scrolls (keyboard focus moving
     through the links, any future scrollIntoView) aim at, so a tab is never
     parked underneath the opaque buttons. The cv's tab bar does centre the
     active tab from JS and relies on the same declaration there. */
  scroll-padding-right: 100px;
  scrollbar-width: none;
}

.tab-bar::-webkit-scrollbar { display: none; }

.tab-bar a {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 0 16px;
  color: var(--overlay1);
  font-size: 0.78rem;
  text-decoration: none;
  white-space: nowrap;
  border-right: 1px solid var(--surface0);
  border-bottom: 2px solid transparent;
  transition: background 0.2s, color 0.2s, border-bottom-color 0.2s;
  flex-shrink: 0;
}

.tab-bar a:hover {
  background: var(--mantle);
  color: var(--text);
  text-decoration: none;
}

.tab-bar a.active {
  background: var(--base);
  color: var(--text);
  border-bottom: 2px solid var(--pink);
}

.tab-bar a .icon {
  width: 13px;
  height: 13px;
  flex-shrink: 0;
}

/* Pinned to the right edge of the scrolling bar. `margin-left: auto` resolves
   to zero once the bar overflows, which at 320px left the language toggle
   sitting at x=319 — one pixel outside the viewport, with no scrollbar drawn
   to hint that it was there. Same fix as cv/css/layout.css. */
.tab-actions {
  display: flex;
  align-items: center;
  margin-left: auto;
  flex-shrink: 0;
  border-left: 1px solid var(--surface0);
  position: sticky;
  right: 0;
  z-index: 1;
  background: var(--crust);
}

.tab-actions button {
  background: none;
  border: none;
  border-right: 1px solid var(--surface0);
  color: var(--overlay1);
  font-family: inherit;
  font-size: 0.75rem;
  font-weight: 500;
  padding: 0 12px;
  height: 44px;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 5px;
  transition: color 0.2s, background 0.2s;
  white-space: nowrap;
}

.tab-actions button:last-child { border-right: none; }
.tab-actions button:hover {
  color: var(--text);
  background: var(--mantle);
}
.tab-actions button .icon {
  width: 14px;
  height: 14px;
}

/* Terminal window ─────────────────────────────────────────────── */

/* 1100, not 900. At 1440 the old measure left 270px dead on each side and the
   page read as a narrow ribbon. Widening the FRAME is safe here only because
   the running text carries its own cap (see .about); the frame and the reading
   measure are two different jobs and must not share one number. */
.terminal-window {
  max-width: 1100px;
  margin: 0 auto;
  padding: 20px 24px 48px;
}

/* Section titles, copied from cv/css/layout.css ──────────── */

.section-title {
  font-size: 1.2rem;
  font-weight: 700;
  color: var(--text);
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 6px;
}

.section-title .icon {
  width: 24px;
  height: 24px;
  color: var(--pink);
  flex-shrink: 0;
}

.section-divider {
  color: var(--surface2);
  margin-bottom: 16px;
  user-select: none;
  overflow: hidden;
  white-space: nowrap;
}

/* Section reveal ──────────────────────────────────────────────── */

.section {
  margin-bottom: 48px;
}

/* The hero closes on a filled pink button, and a button carries far more visual
   weight than the line of text every other section ends on. At the shared 48px
   the gap under it read as a break in the page rather than as the join between
   "here is who I am" and "here is where to go next" -- the same measurement,
   but not the same perceived distance. 28px puts the Paths heading back in the
   button's orbit without crowding it.

   Scoped to #hero: the Paths-to-Contact gap is between two text lines and is
   correct at 48. */
#hero {
  margin-bottom: 28px;
}

.section .stagger {
  opacity: 0;
  transform: translateY(8px);
  animation: reveal 0.6s var(--ease-out) forwards;
}

@keyframes reveal {
  to { opacity: 1; transform: translateY(0); }
}

/* hero: whoami → role → cat about.txt → about → cta.
   Increments are tighter than before because the hero gained two children;
   the total reveal stays at ~1.7s rather than dragging on. */
#hero .stagger:nth-child(1) { animation-delay: 0.20s; }
#hero .stagger:nth-child(2) { animation-delay: 0.32s; }
#hero .stagger:nth-child(3) { animation-delay: 0.44s; }
#hero .stagger:nth-child(4) { animation-delay: 0.56s; }
#hero .stagger:nth-child(5) { animation-delay: 0.70s; }

/* paths: title → divider → prompt → grid → tail prompt → latest post.
   These are positional, so they must be extended whenever the section gains a
   child: .stagger sets opacity 0 with no delay by default, which means an
   unlisted nth-child does not merely arrive early -- it arrives FIRST, before
   the heading it belongs under. */
/* Seven, not six: .paths-also sits between the card grid and the latest-post
   prompt, so everything after it moved up one index. A .stagger with no delay
   rule animates at 0s and arrives BEFORE the hero, which is how this list
   silently breaks. Count the staggered children in #paths before editing. */
#paths .stagger:nth-child(1) { animation-delay: 0.88s; }
#paths .stagger:nth-child(2) { animation-delay: 0.96s; }
#paths .stagger:nth-child(3) { animation-delay: 1.08s; }
#paths .stagger:nth-child(4) { animation-delay: 1.20s; }
#paths .stagger:nth-child(5) { animation-delay: 1.28s; }
#paths .stagger:nth-child(6) { animation-delay: 1.38s; }
#paths .stagger:nth-child(7) { animation-delay: 1.48s; }

/* contact: title → divider → prompt → output */
#contact .stagger:nth-child(1) { animation-delay: 1.60s; }
#contact .stagger:nth-child(2) { animation-delay: 1.68s; }
#contact .stagger:nth-child(3) { animation-delay: 1.80s; }
#contact .stagger:nth-child(4) { animation-delay: 1.92s; }

/* ASCII art ─────────────────────────────────────────────────── */

.ascii-art {
  color: var(--pink);
  font-size: clamp(4px, 1.45vw, 14px);
  line-height: 1.2;
  white-space: pre;
  /* `overflow: hidden`, never `overflow-x: auto`. Setting one axis to a
     non-visible value forces the other to compute to `auto`, which turns this
     purely decorative banner into a scroll container the pointer can catch. */
  overflow: hidden;
  margin: 0 0 28px 0;
  opacity: 0;
  transform: translateY(8px);
  animation: reveal 0.6s var(--ease-out) forwards;
  animation-delay: 0.05s;
}

/* Prompt / terminal syntax ──────────────────────────────────── */

.prompt {
  color: var(--subtext0);
  overflow-wrap: break-word;
  word-break: break-word;
  margin-bottom: 4px;
  font-size: clamp(0.82rem, 1.1vw, 0.92rem);
}

.hero-lines .prompt { margin-bottom: 6px; }

.prompt .user   { color: var(--pink); }
.prompt .at     { color: var(--subtext0); }
.prompt .host   { color: var(--blue); }
.prompt .colon  { color: var(--subtext0); }
.prompt .path   { color: var(--mauve); }
.prompt .dollar { color: var(--text); padding: 0 4px 0 2px; }
.prompt .cmd    { color: var(--yellow); }
.prompt .arg    { color: var(--peach); }
.prompt .flag   { color: var(--teal); }

/* Output ──────────────────────────────────────────────────────── */

.output {
  color: var(--subtext1);
  margin: 4px 0 24px 0;
}

/* Inline colour for prose. Classes rather than inline styles, so the CSP can
   stay style-src 'self'. Kept to palette tokens so nothing here can drift from
   design/tokens.json. */
.c-pink { color: var(--pink); }
.c-mauve { color: var(--mauve); }
.c-green { color: var(--green); }
.c-blue { color: var(--blue); }
.c-yellow { color: var(--yellow); }
.c-peach { color: var(--peach); }
.c-red { color: var(--red); }
.c-teal { color: var(--teal); }
.c-lavender { color: var(--lavender); }

/* about.txt block, set in two columns since the frame went to 1100px.
   A single capped column left a third of the row empty; two columns use the
   full width AND bring each line down to about 60 characters, which is better
   reading than either the old 68ch single column or the 85ch one that replaced
   it. One paragraph lands in each column. */
.about {
  columns: 2;
  column-gap: 46px;
  margin: 4px 0 20px 0;
}

.about p {
  break-inside: avoid;
  margin-bottom: 10px;
  font-size: clamp(0.84rem, 1.1vw, 0.92rem);
  color: var(--subtext1);
}

.about p:last-child {
  margin-bottom: 0;
}

.role-line {
  color: var(--text);
  font-size: clamp(0.92rem, 1.2vw, 1rem);
  margin: 4px 0;
}

.role-sep {
  color: var(--overlay0);
  padding: 0 6px;
}

/* Hero CTA, ported from cv/css/terminal.css .hero-cta so both sites offer the
   same primary action with the same affordance. The lift lives on the anchor,
   not on the .stagger wrapper: the reveal animation is `forwards`, so an
   animated transform on the same element would outrank the :hover transform. */

.cta-line {
  margin: 10px 0 0 0;
  /* Centred, matching the CV hero's CTA. The surrounding hero text stays
     left-aligned on both sites, so the button reads as the one thing to act on. */
  text-align: center;
}

.hero-cta {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 10px 24px;
  background: var(--pink);
  color: var(--crust);
  font-size: 0.85rem;
  font-weight: 600;
  border-radius: 4px;
  text-decoration: none;
  transition: background 0.2s, transform 0.1s;
}

.hero-cta:hover {
  background: var(--mauve);
  color: var(--crust);
  transform: translateY(-1px);
}

.hero-cta .icon {
  width: 16px;
  height: 16px;
  color: var(--crust);
}

/* Path cards ───────────────────────────────────────────────── */

.prompt .prompt-hint {
  color: var(--overlay0);
  padding-left: 10px;
  font-style: italic;
  /* Wraps as a unit. Without this the line can break after the `#`, leaving it
     orphaned at the end of one line and "pick one" / "latest post" stranded on
     the next -- which reads as a stray character rather than a comment. */
  display: inline-block;
  white-space: nowrap;
}

.paths-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 16px;
  margin: 14px 0 8px;
}

/* The subordinate line under the two cards. Lighter type, no border, no card:
   the visual weight is the hierarchy argument. `$ ` is prepended rather than
   written into the markup so the link text stays exactly the path a reader
   would type, and so translation never has to carry a shell prompt. */
.paths-also {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 20px;
  /* Generous below, not above: this line belongs to the cards it follows, and
     the latest-post prompt underneath is a separate thought. Equal margins
     made it read as the heading of that block instead. */
  margin: 12px 0 26px;
  font-size: 0.82rem;
}

.path-inline {
  color: var(--subtext0);
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition:
    color         0.25s var(--ease-out),
    border-color  0.25s var(--ease-out);
}

.path-inline::before {
  content: '$ ';
  color: var(--overlay0);
}

@media (hover: hover) {
  .path-inline:hover {
    color: var(--pink);
    border-bottom-color: color-mix(in oklab, var(--pink) 40%, transparent);
  }
}

.path-card {
  position: relative;
  display: block;
  padding: 18px 20px;
  background: color-mix(in oklab, var(--mantle) 55%, transparent);
  border: 1px solid var(--surface0);
  border-radius: 6px;
  text-decoration: none;
  color: inherit;
  overflow: hidden;
  transition:
    border-color 0.25s var(--ease-out),
    background   0.25s var(--ease-out),
    transform    0.25s var(--ease-out),
    box-shadow   0.25s var(--ease-out);
}

/* Left accent, always visible so the card reads as actionable */
.path-card::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: 3px;
  background: var(--surface2);
  transition: background 0.25s var(--ease-out);
}

@media (hover: hover) {
  .path-card:hover {
    border-color: var(--pink);
    background: color-mix(in oklab, var(--mantle) 90%, transparent);
    transform: translateY(-2px);
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18);
  }
  .path-card:hover::before { background: var(--pink); }
  .path-card:hover .path-card-caret {
    color: var(--pink);
    transform: translateX(4px);
  }
}

.path-card:focus-visible {
  border-color: var(--pink);
  outline: none;
}
.path-card:focus-visible::before { background: var(--pink); }

.path-card-head {
  display: grid;
  grid-template-columns: min-content 1fr min-content;
  align-items: center;
  gap: 12px;
  margin-bottom: 8px;
}

.path-card-icon {
  width: 20px;
  height: 20px;
  flex-shrink: 0;
}
.path-card-icon.folder { color: var(--mauve); }
.path-card-icon.pen    { color: var(--teal); }

.path-card-label {
  font-size: clamp(1rem, 1.3vw, 1.12rem);
  font-weight: 600;
  letter-spacing: 0.01em;
  white-space: nowrap;
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
}
.path-card-label .dollar { color: var(--overlay1); font-weight: 500; }
.path-card-label .cmd    { color: var(--yellow); }
.path-card-label .arg    { color: var(--peach); }

.path-card-caret {
  width: 18px;
  height: 18px;
  color: var(--overlay1);
  transition: transform 0.25s var(--ease-out), color 0.25s var(--ease-out);
}

.path-card-desc {
  color: var(--subtext0);
  font-size: clamp(0.86rem, 1.05vw, 0.94rem);
  line-height: 1.55;
  padding-left: 32px;         /* align under the label, past the icon */
}

/* Latest post ────────────────────────────────────────────────
   Deliberately NOT a third card. Two path cards are the choice this section
   asks the reader to make; a third box below them, styled the same way, reads
   as a third destination of equal weight. This is one line of output under a
   `tail -n1` prompt -- which is exactly what it is.

   Everything between the LATEST-POST markers in index.html is written by
   scripts/sync-latest-post.py. */

.latest-post {
  display: flex;
  align-items: baseline;
  gap: 12px;
  padding: 8px 12px 8px 10px;
  margin: 2px 0 0 -10px;      /* pull the padding back to the text margin */
  text-decoration: none;
  color: inherit;
  border-radius: 4px;
  transition: background 0.25s var(--ease-out);
  /* Hugs its content rather than filling the frame. The row is one line of
     output, so a hover band running the full width of a 1100px page would be
     mostly empty highlight -- and it is what strands the caret a screen away
     from the headline it belongs to. */
  width: fit-content;
  max-width: 100%;
}

.latest-post-date {
  color: var(--overlay0);
  font-size: 0.8rem;
  flex-shrink: 0;
  /* Dates are compared down a column often enough to be worth the lining
     figures, even when today there is only one of them. */
  font-variant-numeric: tabular-nums;
}

.latest-post-title {
  color: var(--subtext1);
  font-size: clamp(0.88rem, 1.05vw, 0.96rem);
  line-height: 1.5;
  transition: color 0.25s var(--ease-out);
}

/* The caret is the affordance, so it stays out of the baseline flow of the
   text and tracks the title's colour instead. */
.latest-post-caret {
  width: 15px;
  height: 15px;
  flex-shrink: 0;
  align-self: center;
  color: var(--overlay1);
  transition: transform 0.25s var(--ease-out), color 0.25s var(--ease-out);
}

@media (hover: hover) {
  .latest-post:hover {
    background: color-mix(in oklab, var(--mantle) 70%, transparent);
  }
  /* Teal, matching the pen icon on the blog card above it: the two point at
     the same place and should say so. */
  .latest-post:hover .latest-post-title { color: var(--teal); }
  .latest-post:hover .latest-post-caret {
    color: var(--teal);
    transform: translateX(4px);
  }
}

.latest-post:focus-visible {
  outline: 2px solid var(--teal);
  outline-offset: 2px;
}

@media (max-width: 560px) {
  /* The caret is decoration once the row wraps; the date and title are not. */
  .latest-post { flex-wrap: wrap; gap: 4px 10px; }
  .latest-post-caret { display: none; }
}

/* Contact, mirrors cv/css/components.css exactly ─────────── */

.contact-row {
  display: flex;
  gap: 12px;
  margin: 6px 0;
  align-items: baseline;
}

.contact-label {
  color: var(--pink);
  min-width: 120px;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex-shrink: 0;
}

/* An address is one unbreakable token to the line breaker: neither `@` nor `.`
   is a break opportunity in UAX-14. At 320px, 120px of label plus the 173px
   min-content of `contact@luyckx.dev` came to 303px inside a 288px box, so the
   end of every address was pushed under the right gutter and clipped by
   `body { overflow-x: hidden }` — silently, with no scrollbar to reveal it.
   anywhere: break the token wherever it has to, rather than losing its tail. */
.contact-value {
  min-width: 0;
  overflow-wrap: anywhere;
}

.contact-label .icon {
  width: 14px;
  height: 14px;
  color: var(--pink);
  flex-shrink: 0;
}

.contact-value a {
  color: var(--blue);
  position: relative;
  text-decoration: none;
}

.contact-value a::after {
  content: '';
  position: absolute;
  bottom: -2px;
  left: 0;
  width: 0;
  height: 1px;
  background: var(--lavender);
  transition: width 0.3s ease;
}

.contact-value a:hover { text-decoration: none; }
.contact-value a:hover::after { width: 100%; }

/* Footer ─────────────────────────────────────────────────────
   The cv already carries a copyright notice; the hub had none, so the estate
   was inconsistent. Held to a 900px measure, narrower than the 1100px
   .terminal-window above it: the footer is an inscription, not content. */

.site-footer {
  max-width: 900px;
  margin: 0 auto;
  padding: 20px 24px 40px;
  border-top: 1px solid var(--surface0);
  text-align: center;
}

/* The unsigned line, and the motif under it. Mirrors blog/static/css/style.css,
   minus the prose face: this surface only ships JetBrains Mono, so the motif is
   letterspaced mono rather than letterspaced serif. Set as an inscription
   rather than an aside -- roman, not italic. */
.footer-quote {
  max-width: 46ch;
  margin: 0 auto 10px;
  font-size: 0.86rem;
  line-height: 1.7;
  color: var(--overlay1);
  text-wrap: balance;
}

.footer-motif {
  font-size: 0.95rem;
  font-weight: 600;
  color: var(--subtext0);
  letter-spacing: 0.13em;
  margin-bottom: 8px;
}

.footer-legal {
  font-size: 0.72rem;
  color: var(--overlay0);
}

@media (max-width: 760px) {
  .site-footer { padding: 20px 16px 32px; }
}

/* Responsive ────────────────────────────────────────────────── */

@media (max-width: 760px) {
  .terminal-window {
    padding: 36px 16px 40px;
  }

  /* Two columns of ~28 characters is not a layout, it is a squeeze. */
  .about {
    columns: 1;
  }

  .paths-grid {
    grid-template-columns: 1fr;
    gap: 12px;
  }

  .path-card {
    padding: 16px 18px;
  }

  .path-card-desc {
    padding-left: 0;
  }

  /* `grid-template-columns` was inert here — .contact-row is display:flex, not
     grid — so this block never actually did anything to the contact list. */
  .contact-row {
    gap: 10px;
    width: 100%;
  }
}

@media (max-width: 760px) {
  /* Standalone list rows, not links inside a sentence, so padding them out to
     a 44px target does not create overlapping hit areas. */
  .contact-value a {
    display: inline-block;
    padding: 11px 0;
  }

  .hero-cta {
    padding: 12px 22px;
  }
}

@media (max-width: 460px) {
  /* Below this the label and the address cannot share a line without the
     address wrapping mid-token, so the label goes above its value. Costs one
     line per row and buys the full content width for the address. */
  .contact-row {
    flex-direction: column;
    align-items: flex-start;
    gap: 1px;
    margin: 12px 0;
  }

  .contact-label {
    min-width: 0;
  }
}

/* Reduced motion ────────────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001s !important;
    animation-delay: 0s !important;
    transition-duration: 0.001s !important;
  }
  .section .stagger,
  .ascii-art {
    opacity: 1;
    transform: none;
  }
}

/* Icon sprite ───────────────────────────────────────────────── */

/* The inline <symbol> sprite must not occupy layout space. Set here rather
   than as a style attribute so the CSP can stay `style-src 'self'`. */
.icon-sprite {
  position: absolute;
  overflow: hidden;
}

/* Error page (404) ──────────────────────────────────────────── */

/* 404 and security-policy pages ─────────────────────────────
   These two pages use their own layout classes — .stage, .intro, .name,
   .role, .status — and NONE of them had a rule anywhere in this file. The
   result was a page with no horizontal padding at all: the prompt line and the
   404 glyphs started at x=0, hard against the screen edge and clipped on the
   left. `place-content: center` below was inert too, because .stage was never
   given a grid or flex display for it to apply to.

   The pages also inherit `body { padding-top: 44px }`, which reserves room for
   a tab bar that neither of them has, leaving a dead band at the top. */
.stage {
  /* Same 900px measure as .site-footer. --frame is a blog variable that was
     never defined on this surface, so the old var() fallback always applied;
     this is the value these pages have always rendered at. */
  max-width: 900px;
  margin: 0 auto;
  padding: 48px 24px 40px;
}

.stage .intro {
  max-width: 100%;
}

.stage .name {
  font-weight: 700;
  line-height: 1.05;
  margin: 12px 0 4px;
  /* An unbroken heading like "Security policy." is 16 characters of monospace;
     without this it is 307px wide inside a 288px box at 320px. */
  overflow-wrap: anywhere;
}

.stage .role {
  color: var(--subtext0);
}

.stage .status {
  color: var(--subtext1);
}

.name-dot {
  color: var(--lavender);
}

.stage.err-stage {
  /* Now a real grid, so place-content can centre the block in the space left
     over below the (unused) tab-bar padding. */
  display: grid;
  place-content: center start;
  text-align: left;
  min-height: calc(100vh - 44px);
  /* svh where supported: on a phone the toolbars make 100vh taller than what
     is actually on screen, which would push the "back to /" link below the
     fold on the one page whose entire job is to offer it. */
  min-height: calc(100svh - 44px);
}

.err {
  max-width: 560px;
}

.err .name {
  font-size: clamp(2.6rem, 12vw, 7rem);
}

.err-code {
  color: var(--peach);
}

.err-actions {
  margin-top: 24px;
}

.err-back {
  color: var(--pink);
  text-decoration: none;
  border-bottom: 1px dashed var(--pink);
}

/* Security policy page ──────────────────────────────────────── */

.policy {
  max-width: 720px;
}

.policy-h {
  color: var(--mauve);
  font-size: 1rem;
  font-weight: 600;
  margin: 28px 0 8px;
}

.policy-h::before {
  content: "## ";
  color: var(--surface2);
}

.policy-p {
  color: var(--subtext0);
  margin-bottom: 10px;
  line-height: 1.75;
}

.policy code {
  color: var(--teal);
  font-size: 0.92em;
}

/* Small screens: a readable name instead of an unreadable banner ────
   The figlet banner is 69 columns wide. Fitting that into ~288px of
   content forces a 4.6px glyph cell, which is not small text -- it is an
   illegible smear that still costs its full height. Below 560px it is hidden
   and the page's real <h1>, which until now was screen-reader-only, is shown
   in its place. Nothing is lost: the banner is aria-hidden decoration and the
   h1 carries the same words.

   The un-hiding has to undo every part of the .sr-only clip, not just the
   position, or the heading stays a 1px box. */
@media (max-width: 560px) {
  .ascii-art { display: none; }

  .banner-title {
    position: static;
    width: auto;
    height: auto;
    padding: 0;
    margin: 0 0 20px;
    overflow: visible;
    clip: auto;
    clip-path: none;
    white-space: normal;
    font-size: clamp(1.25rem, 6vw, 1.6rem);
    font-weight: 700;
    line-height: 1.25;
    color: var(--pink);
  }
}
