:root {
  color-scheme: dark;
  /* Ember-on-Teal — OKLCH lightness ramp. Every role has a defined lightness so
     contrast is structural, not accidental. Ground hue ~179 (deep teal, the silk
     bg); neutrals stay warm at h30 low-chroma. One signal hue in three steps:
     Deep (light-text fill), Live (hotter badge fill), Bright (text/icon on any
     surface incl. the silk ground). */
  --bg: #0a1718;            /* Deep Teal — page ground (dark teal-black) */
  --bg-grad: #0d1c1e;       /* Deep Teal² — vertical-gradient bottom */
  --bg-card: #301512;       /* Oxblood — card surface */
  --bg-card-hover: #7b2a25; /* Ember   — heated surface (hover/focus) */
  --bg-donate: #4c2f19;     /* Hearth  — donate surface, warm toward gold */
  --fg: #eee4e0;            /* Bone    — primary text */
  --fg-muted: #d6c9c5;      /* Ash     — muted text (clears 4.5:1 on every surface) */
  --accent: #d80e05;        /* Signal  — resting accent: fills, borders, rings, rails (avatar red) */
  /* Signal lifted to a readable tint: accent as TEXT/ICON anywhere, including on
     the heated Ember surface (clears 3:1 icons; 4.5:1 text on Deep Teal & Oxblood). */
  --accent-link: #ffb4a6;
  /* Deep signal red: high-contrast fill behind light text (New! badge; bone ≈ 5.2:1). */
  --accent-deep: #b3271e;
  /* Hotter signal red for the Live badge fill; white text clears ≈ 5.7:1. */
  --accent-live: #b9392f;
  /* Selection surface — tuned so Deep Teal text clears 4.5:1 on it. */
  --accent-sel: #ea5046;
  --gold: #fdd171;          /* Spotlight — donate block only */
  --on-live: #ffffff;       /* white on the Live fill — bone dips below AA at 12px; white clears 5.7 */
  /* Teal-tinted scrollbar — tonal with the Deep Teal ground, not warm gray. */
  --scrollbar: #448b8e;
  --scrollbar-hover: #5fb5b8;
  --max-width: 40rem;
  --radius: 2rem;
  /* Controls use a softer square than the card/pill surfaces. */
  --control-radius: 1rem;
  --control-radius-lg: 1.25rem;
  /* Link cards are softer than the outer article surfaces, but not pill-shaped. */
  --card-radius: 0.75rem;
  --card-radius-outer: 1.25rem;

  /* Type — three voices: Unbounded (display: logo, headings) + Rubik (body/UI)
     + JetBrains Mono (code). Fluid clamp scale; system stacks as load-time floor
     (fail-silent like live badges). No negative letter-spacing. */
  --text-display: clamp(2.25rem, 1.59rem + 2.88vw, 3.25rem);  /* .nick — focal hero; bolder, still below the 6rem ceiling */
  --text-headline: clamp(1.4rem, 1.19rem + 0.87vw, 1.75rem);  /* post titles, prose h1 */
  --text-title: 1rem;        /* section labels (uppercase, tracked) */
  --text-body: clamp(1rem, 0.76rem + 1.03vw, 1.25rem);        /* default body */
  --text-meta: 1rem;         /* muted secondary: dates, excerpts, descriptions, nav, footer */
  --text-tagline: clamp(0.95rem, 0.85rem + 0.43vw, 1.125rem); /* hero one-liner */
  --text-label: 0.75rem;     /* badges */
  /* Leading tuned to size: tight at display, generous for reading. The arc mirrors
     the Negative-Tracking Rule — tightness belongs to the big stuff, air to the small. */
  --leading-display: 1.1;
  --leading-headline: 1.2;
  --leading-body: 1.5;
  --leading-read: 1.7;
  /* ponytail: three webfont voices sourced (Unbounded + Rubik + JetBrains Mono);
     system stacks remain as the load-time floor so a blocked font request degrades
     to the incumbent look, not a broken one — same fail-silent discipline as the
     live badges. optical sizing + kerning on. */
  --font-sans: Rubik, system-ui, sans-serif;
  --font-display: Unbounded, "Arial Black", sans-serif;
  --font-mono: "JetBrains Mono", ui-monospace, monospace;
}

* { box-sizing: border-box; }
::selection { background: var(--accent-sel); color: var(--bg); }
html { scrollbar-color: var(--scrollbar) var(--bg); background: var(--bg); overflow-x: clip; }
/* scrollbar-gutter reserves layout space for the bar — only worth it where a real bar
   shows (fine pointer). Touch/overlay scrollbars are zero-width, so reserving space on
   phones just steals ~15px of right margin. */
@media (hover: hover) and (pointer: fine) { html { scrollbar-gutter: stable; } }
/* Old browsers that lack `overflow: clip` fall back to hidden on both roots — the
   decorative glow bleeds stay clipped, just as a scroll container instead. */
@supports not (overflow-x: clip) {
  html, body { overflow-x: hidden; }
}
/* Smooth scroll is motion — gated so reduced-motion users get instant anchors. */
@media (prefers-reduced-motion: no-preference) { html { scroll-behavior: smooth; } }
/* ponytail: @supports guard silences "bad selector" warnings in Firefox (no -webkit- pseudos) */
@supports selector(::-webkit-scrollbar) {
  html::-webkit-scrollbar { width: 1rem; }
  html::-webkit-scrollbar-track { background: var(--bg); }
  html::-webkit-scrollbar-button { width: 0; height: 0; display: none; }
  html::-webkit-scrollbar-thumb { background: var(--scrollbar); border-radius: 999px; border: 0.2rem solid transparent; background-clip: padding-box; }
  html::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-hover); }
}

body {
  margin: 0;
  position: relative;      /* containing block for the absolute .silk-bg — lets it stretch to the full document height */
  display: flex;
  flex-direction: column;
  min-height: 100vh;       /* fallback for pre-dvh browsers */
  min-height: 100dvh;      /* dynamic viewport — no jump when iOS Safari's bars collapse/expand */
  align-items: center;
  overflow-x: clip;   /* let the hero glow bleed full-width without a horizontal scrollbar (clip creates no scroll container) */
  font-family: var(--font-sans);
  font-optical-sizing: auto;
  font-kerning: normal;
  font-weight: 400;
  background: linear-gradient(180deg, var(--bg) 0%, var(--bg-grad) 100%);
  color: var(--fg);
  line-height: var(--leading-body);
  font-size: var(--text-body);
  /* ponytail: light-on-dark compensation — a hair of tracking aids legibility
     without breaking the Negative-Tracking Rule's spirit (reads ~0 at a glance). */
  letter-spacing: 0.005em;
  text-wrap: pretty;
  -webkit-font-smoothing: antialiased;
}

a { color: var(--accent-link); }
/* Screen-reader-only text (e.g. "(новая вкладка)" inside outbound cards). */
.visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0; }
strong, b { font-weight: 700; }

/* Skip link — first focusable, off-screen until keyboard focus reveals it top-left. */
.skip-link {
  position: fixed;
  top: max(.5rem, env(safe-area-inset-top)); left: max(.5rem, env(safe-area-inset-left));
  z-index: 100;
  padding: .5rem .9rem;
  background: var(--accent-link);
  color: var(--bg);
  font-weight: 800;
  border-radius: var(--control-radius);
  text-decoration: none;
  transform: translateY(-150%);
  transition: transform .15s ease-out;
}
.skip-link:focus { transform: translateY(0); }
/* Skip-link landing region: keep a visible focus boundary for focused sections,
   but the main page container is only a programmatic skip target and should not
   acquire a large outline when a pointer lands on its background. */
.section:focus,
.post-single:focus { outline: 2px solid var(--accent-link); outline-offset: -2px; }
.container:focus,
.container:focus-visible { outline: none; }

/* Focus — visible keyboard ring on every control. Ring counts as a state response
   (allowed glow); link cards also heat so the focused row is unambiguous. */
:where(a, button, input, [tabindex]):focus-visible {
  outline: 2px solid var(--accent-link);
  outline-offset: 2px;
}

.container {
  max-width: var(--max-width);
  margin: 0 auto;
  /* Safe-area aware (viewport-fit=cover): top clears the notch, sides clear rounded
     corners. max() falls back to the original insets where env() is unsupported/zero. */
  padding: max(2rem, env(safe-area-inset-top)) max(1.5rem, env(safe-area-inset-right)) 3rem max(1.5rem, env(safe-area-inset-left));
  position: relative;
  z-index: 1;
  flex: 1;
  width: 100%;
}

/* Single post reuses .container; padding-top clears the fixed close button.
   Safe-area bottom keeps the article foot clear of the home indicator in cover mode. */
.post-single { max-width: var(--max-width); margin: 0 auto; padding: 4.5rem max(1.5rem, env(safe-area-inset-right)) max(3rem, env(safe-area-inset-bottom)) max(1.5rem, env(safe-area-inset-left)); font-size: var(--text-body); flex: 1; width: 100%; position: relative; z-index: 1; }

/* Hero */
.hero {
  text-align: center;
  margin-bottom: 2rem;
  position: relative;
  /* Geometry vars the ember glow consumes: --hero-pad + half --avatar-size is the
     avatar's exact center. Media queries below change only these two values and the
     glow follows by construction — no per-breakpoint magic offset. */
  --hero-pad: 1rem;
  --avatar-size: 6rem;
  padding-top: var(--hero-pad);
}
.avatar {
  width: var(--avatar-size);
  height: var(--avatar-size);
  border-radius: 50%;
  display: block;
  margin: 0 auto 1rem;
  border: 0.125rem solid var(--accent);
}
.nick {
  font-family: var(--font-display);
  font-size: var(--text-display);
  line-height: var(--leading-display);
  margin: 0;
  font-weight: 900;
  text-wrap: balance;
}
.tagline {
  margin: 0.25rem 0 0;
  color: var(--fg-muted);
  font-size: var(--text-tagline);
}
/* Live avatar ring — badges.js adds .live on confirmed Twitch uptime.
   Sonar ping (1.6s, ease-out) fading outward, plus a persistent soft glow —
   both are live-activity state responses, so allowed under the Flat-At-Rest rule. */
.avatar.live { animation: avatar-live 1.6s ease-out infinite; }
@keyframes avatar-live {
  0%   { box-shadow: 0 0 0 0 rgba(216, 14, 5, 0.55), 0 0 0.5rem rgba(216, 14, 5, 0.45); }
  70%  { box-shadow: 0 0 0 0.6rem rgba(216, 14, 5, 0), 0 0 0.5rem rgba(216, 14, 5, 0.45); }
  100% { box-shadow: 0 0 0 0 rgba(216, 14, 5, 0), 0 0 0.5rem rgba(216, 14, 5, 0.45); }
}

/* ── Ember Stage ────────────────────────────────────────────────────────
   Ambient glow behind the hero: the stage at rest breathes (~6s), ignites
   when .avatar.live lands (badges.js). :has() lights the stage in pure CSS.
   ponytail: Heat-On-Touch targets surfaces (cards); this is stage-light —
   the Ember Stage north star literally calls for "a dark room, an ember-red
   glow." Ignition escalates intensity (scale/opacity/brightness), not hue —
   hotter = bigger + brighter, same fire. Tuning knob: the rgba alpha below.
   The glow bleeds past the hero to the viewport edges (no hard cutoff at the
   header bounds); body's overflow-x:clip keeps the bleed from causing scroll. */
.hero::before {
  content: "";
  position: absolute;
  /* Disc center == avatar center: hero pad + half avatar, minus half the disc.
     translate shifts with `top` so keyframe scale stays centered on the avatar. */
  top: calc(var(--hero-pad) + var(--avatar-size) / 2);
  left: 50%;
  /* Fit the hero column: full-width bleed on phones, 38rem cap on desktop. A fixed
     38rem disc is wider than any phone viewport — when overflow clipping didn't hold
     it left scrollable empty space to the right. Now the disc can never exceed the
     hero box, so there is nothing left to scroll into. */
  width: min(38rem, 100%);
  aspect-ratio: 1;
  translate: -50% -50%;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(216, 14, 5, 0.85) 0%, rgba(216, 14, 5, 0) 65%);
  filter: blur(48px);
  pointer-events: none;
  z-index: 0;
  animation: ember-wake 1s cubic-bezier(0.16, 1, 0.3, 1) both,
             ember-breath 7.5s ease-in-out infinite 1s;
  will-change: transform, opacity, filter;
}
/* Hero content sits above the glow. */
.hero > * { position: relative; z-index: 1; }
/* Rest: coal warming/cooling. Bolder: brighter coal floor (0.14 → 0.22 peak),
   bigger inhale (×1.12), longer cycle (7.5s). The room reads *alive* before any
   interaction instead of faintly warm. Opacity ceiling checked: Bone (#eee4e0,
   6.4:1 on Deep Teal) blended over Signal-link-tinted center at 22% peak stays ≥3:1
   for the nick (large-text floor); tagline sits in the faded 65% zone where the
   effective alpha is ~0.08, far above its own 4.5:1 floor. */
@keyframes ember-breath {
  0%, 100% { opacity: 0.14; transform: scale(1); }
  50%      { opacity: 0.22; transform: scale(1.12); }
}
/* Arrival: the glow swells from cold to its resting breath baseline once, then
   hands off — the dark room warming up. 100% matches breath's floor so the
   seam is invisible. Ignition (.avatar.live) overrides this later via :has(). */
@keyframes ember-wake {
  0%   { opacity: 0;    transform: scale(0.8); }
  100% { opacity: 0.14; transform: scale(1); }
}
/* Ignition: one-time catch flare (match-strike surge) hands off to a hotter breath.
   Bolder surge: ×1.35 scale, ×1.7 brightness at 35%; afterglow keeps a hotter
   breath (0.24 floor / 0.42 peak, scale 1.14→1.22) so live reads unmistakable.
   flare's 100% matches ignited's baseline so the seam is invisible. */
@keyframes ember-flare {
  0%   { opacity: 0.14; transform: scale(1);    filter: blur(48px) brightness(1); }
  35%  { opacity: 0.55; transform: scale(1.35); filter: blur(40px) brightness(1.7); }
  100% { opacity: 0.24; transform: scale(1.14); filter: blur(48px) brightness(1); }
}
@keyframes ember-ignited {
  0%, 100% { opacity: 0.24; transform: scale(1.14); filter: blur(48px); }
  50%      { opacity: 0.42; transform: scale(1.22); filter: blur(48px) brightness(1.18); }
}
.hero:has(.avatar.live)::before {
  animation: ember-flare 1s cubic-bezier(0.16, 1, 0.3, 1) both,
             ember-ignited 7s ease-in-out infinite 1s;
}

/* ── Delight: the match-strike ripples outward ───────────────────────────────
   When badges.js confirms Twitch live the hero catches (ember-flare above);
   .avatar.live is the single switch the whole page reads. The ripple is a set
   of secondary answers to that one event, in this order, staggered so the eye
   traces the fire from its source to the room around it. No new colors — the
   ripple reuses the Signal tint; it is light, not paint. */

/* 1. The avatar itself answers within the same 900 ms as its ring heats up:
      a 2px outward swell of the whole avatar, spring-settled. The ring's color
      move already says "hot"; this little puff says "alive". */
.avatar { transition: border-color 0.9s ease, transform 0.9s cubic-bezier(0.05, 0.7, 0.1, 1); }
.avatar.live { border-color: var(--accent-link); transform: scale(1.03); }

/* 2. Nick warms with the flare: an underline trace drawn by the heat. One 2px
      bar, Signal tint 40%, growing from 0 to the nick's full width over 0.8s.
      Settles and STAYS while live (no forever-pulse). */
.nick { position: relative; display: inline-block; padding-bottom: 0.35rem; }
.nick::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 2px;
  background: var(--accent);
  border-radius: 999px;
  transform: scaleX(0);
  opacity: 0.4;
}
.hero:has(.avatar.live) .nick::after {
  animation: nick-underline-draw 0.8s cubic-bezier(0.16, 1, 0.3, 1) both 0.9s;
}
@keyframes nick-underline-draw {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

/* 3. Only the LIVE card moves — Signal Bright icon pops full-sat (matches the
      avatar-ring tint so the two twitch states share a colour) and the card
      itself bobs one gentle settle over 0.7s. Donate and quiet cards do not
      ripple; one spark is the storyteller, the rest of the room stays dark. */
.container:has(.hero .avatar.live) .link-card:has(.badge-live:not([hidden])) svg:last-child,
.container:has(.hero .avatar.live) .post-card:has(.badge-live:not([hidden])) svg:last-child {
  color: var(--accent-link);
  opacity: 1;
}
.container:has(.hero .avatar.live) .link-card:has(.badge-live:not([hidden])) .external-link-icon,
.container:has(.hero .avatar.live) .post-card:has(.badge-live:not([hidden])) .external-link-icon {
  color: var(--accent-link);
  opacity: 1;
}
@media (prefers-reduced-motion: no-preference) {
  .container:has(.hero .avatar.live) .link-list li:has(.badge-live:not([hidden])) .link-card,
  .container:has(.hero .avatar.live) .link-list li:has(.badge-live:not([hidden])) .post-card {
    animation: live-card-settle 0.7s cubic-bezier(0.05, 0.7, 0.1, 1) 1.1s both;
  }
}
@keyframes live-card-settle {
  0%   { transform: translateY(0); }
  30%  { transform: translateY(-3px); }
  55%  { transform: translateY(1.5px); }
  100% { transform: translateY(0); }
}

/* 4. The donate card's gold spot tilts ever so slightly toward the sparks:
      a 4° skew on its ::before point so the spotlight leans toward the live
      card's pull for the flare second, then back. The one move the One
      Spotlight Rule can afford — a lean, not a tint change. */
@media (prefers-reduced-motion: no-preference) {
  .container:has(.hero .avatar.live) .donate-list .link-card::before {
    animation: donate-lean 1s ease-out 0.9s;
  }
}
@keyframes donate-lean {
  0%   { transform: translateX(0); opacity: 0; }
  35%  { transform: translateX(6px); opacity: 0.3; }
  100% { transform: translateX(0); opacity: 0; }
}

/* ── Delight: avatar warms under cursor ──────────────────────────────────────
   The avatar is the only face on the page — leaning into it should reward the
   visitor with warmth, not just the existing ring pulse. One move, hover-gated
   so touch gets nothing: the coal ring heats from Signal (rest) toward Signal
   Bright and the avatar image itself gains a whisper of glow around its edge,
   as if the stage light noticed you stepping up to it. */
@media (hover: hover) {
  .avatar {     transition: border-color 0.9s ease, transform 0.4s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.4s ease; }
  .avatar:hover {
    border-color: var(--accent-link);
    transform: scale(1.06);
    /* box-shadow is ring color at low alpha; the avatar's existing border
       still owns the strong ring. */
    box-shadow: 0 0 12px rgba(255, 180, 166, 0.4), 0 0 22px rgba(216, 14, 5, 0.2);
  }
  /* While live + hovered, hover wins over live's border so the warmth still
     answers directly even when the room's already hot. */
  .avatar.live:hover { border-color: var(--accent-link); transform: scale(1.06); }
}

/* Arrival: the stage warms into presence once. Content settles out of a soft blur
   as the room comes into focus — only the hero; cards stay instant so the action
   surface is ready at first paint. ponytail: blur-clearing is the material idea
   (not a generic fade-and-rise); one-shot, no layout props, reduced-motion-gated. */
@media (prefers-reduced-motion: no-preference) {
  .hero > * { animation: hero-settle 0.6s cubic-bezier(0.16, 1, 0.3, 1) both; }
  .hero > *:nth-child(2) { animation-delay: 0.1s; }
  .hero > *:nth-child(3) { animation-delay: 0.2s; }
}
@keyframes hero-settle {
  0%   { opacity: 0; transform: translateY(0.6rem); filter: blur(5px); }
  100% { opacity: 1; transform: translateY(0);    filter: blur(0); }
}

/* Sections */
.section { margin-bottom: 2rem; }
.section-title {
  font-family: var(--font-display);
  font-size: var(--text-title);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--fg-muted);
  margin: 0 0 1rem;
  font-weight: 700;
  text-wrap: balance;
}

.bio-content { text-align: center; }

/* Bio + post prose share styling. Kept flat for browsers without CSS nesting. */
.bio-content > :first-child,
.post-content > :first-child { margin-top: 0; }
.bio-content > :last-child,
.post-content > :last-child { margin-bottom: 0; }
.bio-content a,
.post-content a { color: var(--accent-link); text-decoration: underline; text-underline-offset: 0.15em; }
@media (hover: hover) {
  .bio-content a:hover,
  .post-content a:hover { color: var(--fg); text-decoration-thickness: 2px; }
}
.bio-content code,
.post-content code { font-family: var(--font-mono); background: var(--bg-card); padding: 0.5em 0.5em; border-radius: 0.25rem; font-size: 0.85em; }
.bio-content pre,
.post-content pre { font-family: var(--font-mono); background: var(--bg-card); padding: 1rem; border-radius: 1rem; overflow-x: auto; }
.bio-content pre:focus-visible,
.post-content pre:focus-visible { outline: 2px solid var(--accent-link); outline-offset: 2px; }
.bio-content pre code,
.post-content pre code { background: none; border: none; padding: 0; }
.bio-content blockquote,
.post-content blockquote { border-left: 0.25rem solid var(--accent); margin: 1rem 0; padding: 0.5rem 1rem; color: var(--fg-muted); font-style: italic; font-weight: 500; }
.bio-content ul, .bio-content ol,
.post-content ul, .post-content ol { padding-left: 1.5rem; }

/* Link list — cards read as one block via clipped container */
.link-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.link-list li { margin: 0; }

.link-card {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 1.25rem;
  touch-action: pan-y;
  background-color: var(--bg-card);
  border-radius: var(--card-radius);
  color: var(--fg);
  text-decoration: none;
  font-weight: 600;
  transition: background-color 0.3s ease, box-shadow 0.3s ease, color 0.3s ease, border-radius 0.3s ease, scale 0.3s ease, transform 0.3s ease-out;
  position: relative;
  overflow: hidden;
}
/* Heat-on-touch bloom: soft radial ember glow inside the card on hover/focus.
   Strengthens the Heat-On-Touch rule — cards literally heat, not just recolor.
   overflow:hidden clips the bloom to the card. Donate variant glows gold. */
.link-card::before {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(circle at center, rgba(255, 180, 166, 0.28), transparent 70%);
  opacity: 0;
  transition: opacity 0.3s ease;
  pointer-events: none;
}
.donate-list .link-card::before {
  background: transparent;
}
/* Card body: name + optional muted description line (social.yml `description:`) */
.link-card-body { min-width: 0; }
.link-desc {
  display: block;
  color: var(--fg-muted);
  font-size: var(--text-meta);
  font-weight: 600;
  margin-top: 0.125rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.external-link-icon {
  margin-left: auto;
  opacity: 0.4;
  transition: opacity 0.3s ease;
  flex-shrink: 0;
  width: 2rem;
  height: 2rem;
}

.link-list li:first-child .link-card { border-top-left-radius: var(--card-radius-outer); border-top-right-radius: var(--card-radius-outer); }
.link-list li:last-child .link-card { border-bottom-left-radius: var(--card-radius-outer); border-bottom-right-radius: var(--card-radius-outer); }
@media (hover: hover) {
.link-card:hover { background-color: var(--bg-card-hover); color: var(--fg); scale: 1.055; z-index: 1; }
.link-list li .link-card:hover { border-radius: var(--card-radius-outer); }
  .donate-list .link-card:hover { background-color: var(--gold); color: var(--bg); box-shadow: 0 0 1.2rem rgba(253, 209, 113, 0.3); }
/* Heat-On-Touch lifts muted secondary lines too — Ash on the heated Ember surface
   would dip below 4.5:1, so descriptions/excerpts/dates brighten with the card.
   Donate variant goes Deep Teal on gold (dark text on the spotlight fill). */
.link-card:hover .link-desc,
.link-card:hover .post-excerpt,
.link-card:hover .post-date { color: var(--fg); }
.donate-list .link-card:hover .link-desc { color: var(--bg); }
.link-card:hover::before { opacity: 1; }
.link-card:hover .external-link-icon { opacity: 1; }
}
/* Keyboard focus: not gated by hover capability — mirrors the hover heat + adds a
   double-ring (gap in --bg, ring in --accent-link) that reads on any surface. */
.link-card:focus-visible {
  background-color: var(--bg-card-hover);
  border-radius: var(--card-radius-outer);
  outline: none;
  box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--accent-link);
}
.link-card:focus-visible::before { opacity: 1; }
.donate-list .link-card:focus-visible { background-color: var(--gold); color: var(--bg); }
.link-card:focus-visible .link-desc,
.link-card:focus-visible .post-excerpt,
.link-card:focus-visible .post-date { color: var(--fg); }
.donate-list .link-card:focus-visible .link-desc { color: var(--bg); }
/* A post card is the morph source, not a layer above the dialog. */
.link-card.post-card,
.link-card.post-card:hover,
.link-card.post-card:focus-visible { z-index: 0; }
.link-card svg { flex-shrink: 0; width: 2rem; height: 2rem; }
/* ponytail: tap feedback for touch devices */
@media (hover: none) {
.link-card:active { scale: 0.96; transition: scale 0.15s; }
.post-close:active { scale: 0.96; transition: scale 0.15s; }
}

/* Badges — "Live"/"New!" chips on link cards. Rendered hidden, shown by badges.js.
   bob = a gentle float (translateY + soft scale) so the whole chip lifts like a
   bubble, not a side-pivot wobble; dot = recording-style pulse. */
.badge {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.2rem 0.6rem;
  border-radius: 999px;
  font-size: var(--text-label);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  line-height: 1.4;
  /* Reveal pop: one-shot scale-in when badges.js un-hides the chip — freshness
     arrival feels earned, not silent. Runs once (hidden→shown render), then the
     idle bob takes over. Overshoot lives in the 70% keyframe; ease stays exponential. */
  animation: badge-pop 0.35s cubic-bezier(0.16, 1, 0.3, 1) both,
             badge-bob 3s ease-in-out infinite 0.35s;
}
.badge[hidden] { display: none; }
.badge-new { background: var(--accent-deep); color: var(--fg); }
/* ponytail: text/dot are white, not bone — bone dips to 4.41 on the hotter Live fill (fails AA at 12px); white clears 5.51. New!'s Deep fill is dark enough for bone (5.12); Live's lighter fill isn't. */
.badge-live { background: var(--accent-live); color: var(--on-live); }
.badge-live::before {
  content: "";
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: var(--on-live);
  animation: badge-dot 1.2s ease-in-out infinite;
}
@keyframes badge-pop {
  0%   { transform: scale(0.5); opacity: 0; }
  70%  { transform: scale(1.12); opacity: 1; }
  100% { transform: scale(1); opacity: 1; }
}
@keyframes badge-bob {
  0%, 100% { transform: translateY(0) scale(1); }
  50% { transform: translateY(-0.3rem) scale(1.05); }
}
@keyframes badge-dot {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.25; }
}
/* Pin afterglow: badges.js adds .just-pinned to the live card's <li> on confirm.
   Brief ember flash (bg-card-hover = the heated surface) settles to oxblood —
   literally "this card just got hot." One-shot, ties the pin to the ignition theme. */
.link-list li.just-pinned .link-card {
  animation: pin-afterglow 1.4s ease-out;
}
@keyframes pin-afterglow {
  0%   { background-color: var(--bg-card-hover); }
  100% { background-color: var(--bg-card); }
}
@media (prefers-reduced-motion: reduce) {
  .badge, .badge-live::before,
  .link-list li.just-pinned .link-card,
  .footer-heart { animation: none; }
  /* Live avatar: stop the sonar ping but keep the persistent soft glow —
     it's the page's primary "on air" signal (ambient light, not motion). */
  .avatar.live { animation: none; box-shadow: 0 0 0.5rem rgba(216, 14, 5, 0.45); }
  /* Glow stays (it's ambient light, not motion) — just stop breathing. */
  .hero::before { animation: none; opacity: 0.12; }
  .hero:has(.avatar.live)::before { opacity: 0.20; }
  .skip-link,
  .link-card,
  .post-close,
  .sound-toggle,
  .not-found-home,
  .avatar,
  .footer-heart { transition-duration: 0s; }
  .link-card:hover,
  .link-card:active,
  .post-close:hover,
  .post-close:active,
  .sound-toggle:hover,
  .sound-toggle:active,
  .not-found-home:hover,
  .avatar:hover { transform: none; scale: 1; }
}

/* Donate block — the one gold spotlight. Hearth Brown lifted to a clear tonal step
   above Oxblood so the warm peak reads at rest; touch has no hover to reveal it. */
.donate-list .link-card {
  background-color: var(--bg-donate);
  color: var(--gold);
}
/* Glare hover (React Bits GlareHover adaptation, colors untouched): a soft warm
   champagne glare at -30° sweeps across the gold fill on pointer-enter. On leave the
   glare fades out fast (250ms) while the sweep eases back (800ms), so the closing
   pass reads near-transparent. background-position-driven, hover-gated,
   overflow:hidden on the card clips it to the pill. */
.donate-list .link-card::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    -30deg,
    hsla(0, 0%, 100%, 0) 60%,
    rgba(255, 236, 190, 0.55) 70%,
    hsla(0, 0%, 100%, 0) 100%
  );
  background-size: 300% 300%;
  background-repeat: no-repeat;
  background-position: -100% -100%;
  opacity: 0;
  transition: background-position 800ms ease, opacity 250ms ease;
  pointer-events: none;
}
@media (hover: hover) and (prefers-reduced-motion: no-preference) {
  .donate-list .link-card:hover::after {
    opacity: 1;
    background-position: 100% 100%;
  }
}
/* Donate title takes the spotlight: gold over the usual Ash Rose (One Spotlight Rule).
   ponytail: :has() targets the section owning the donate list; unsupported = title stays Ash Rose. */
.section:has(.donate-list) .section-title { color: var(--gold); }

/* Posts list — reuses the link-list card block; text remains selectable. */
.post-card-body { flex: 1; min-width: 0; }
.post-card-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem;
}
.post-title { font-weight: 600; }
.post-date { color: var(--fg-muted); font-size: var(--text-meta); font-weight: 600; flex-shrink: 0; font-variant-numeric: tabular-nums; }
.post-excerpt {
  display: block;
  color: var(--fg-muted);
  margin: 0.5rem 0 0;
  font-size: var(--text-meta);
  font-weight: 600;
}

/* Single post — article is a card (morph target matches .post-card visual) */
.post-article {
  background: var(--bg-card);
  border-radius: var(--radius);
  padding: 2rem;
}
.post-header { margin-bottom: 2rem; }
.post-header .post-title { margin: 0 0 0.5rem; font-family: var(--font-display); font-size: var(--text-headline); line-height: var(--leading-headline); font-weight: 800; text-wrap: balance; }
.post-header .post-date { font-size: var(--text-meta); color: var(--fg-muted); }
.post-content { line-height: var(--leading-read); max-width: 44rem; }
.post-content h1 { font-family: var(--font-display); font-size: var(--text-headline); font-weight: 800; }
.post-content h2 { font-family: var(--font-display); font-size: clamp(1.15rem, 1rem + 0.4vw, 1.4rem); font-weight: 700; }
.post-content h3 { font-family: var(--font-display); font-size: clamp(1.05rem, 0.92rem + 0.33vw, 1.25rem); font-weight: 700; }
.post-content h1, .post-content h2, .post-content h3 { margin: 2rem 0 0.75rem; line-height: var(--leading-headline); text-wrap: balance; }
.post-content img { max-width: 100%; height: auto; border-radius: var(--radius); }
/* Post nav — prev/next at the foot of the article. Lives inside .post-article so the
   index modal copies it; modal JS swaps content instead of navigating there. */
.post-nav {
  /* 2-column grid: each side owns its lane — a long title truncates itself,
     never the sibling (previously space-between + min-width:0 let one side starve the other). */
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 1rem;
  margin-top: 2.5rem;
  padding-top: 1.25rem;
  border-top: 1px solid rgba(238, 228, 224, 0.12);
  font-size: var(--text-meta);
}
.post-nav a {
  color: var(--fg-muted);
  text-decoration: none;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  transition: color 0.3s ease;
}
@media (hover: hover) { .post-nav a:hover { color: var(--accent-link); } }
.post-nav-prev { grid-column: 1; }
.post-nav-next { grid-column: 2; text-align: right; }

/* Close button — fixed top-right, soft-square control, same card language */
.post-close {
  position: fixed;
  top: max(1.5rem, env(safe-area-inset-top));
  right: max(1.5rem, env(safe-area-inset-right));
  z-index: 10;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 3rem;
  height: 3rem;
  background: var(--bg-card);
  border: none;
  border-radius: var(--control-radius);
  color: var(--fg);
  cursor: pointer;
  text-decoration: none;
  transition: background 0.3s ease, color 0.3s ease, scale 0.3s ease;
}
.post-close svg { width: 1.5rem; height: 1.5rem; }
@media (hover: hover) {
.post-close:hover {
  background: var(--bg-card-hover);
  /* ponytail: accent-link (Signal Bright) reads on the ember hover bg; plain --accent is ~1.5:1 red-on-red.
     Signal Bright on Ember ≈ 3.7:1 — clears the 3:1 non-text floor (resolved in DESIGN.md). */
  color: var(--accent-link);
  scale: 1.08;
}
}
.post-close:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--accent-link);
}

/* Sound toggle — fixed bottom-right companion of .post-close, same card language.
   Hidden until piano.js reveals it (no-JS pages have no audio to mute); aria-pressed
   swaps the two inline icons and piano.js owns the aria-label. */
.sound-toggle {
  position: fixed;
  bottom: max(1rem, env(safe-area-inset-bottom));
  right: max(1rem, env(safe-area-inset-right));
  z-index: 90;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  background: var(--bg-card);
  border: none;
  border-radius: var(--control-radius);
  color: var(--fg);
  cursor: pointer;
  transition: background 0.3s ease, color 0.3s ease, scale 0.3s ease;
}
.sound-toggle[hidden] { display: none; }
.sound-toggle span { display: inline-flex; }
.sound-toggle[aria-pressed="true"] .sound-on,
.sound-toggle[aria-pressed="false"] .sound-off { display: none; }
.sound-toggle svg { width: 1.25rem; height: 1.25rem; }
@media (hover: hover) {
.sound-toggle:hover {
  background: var(--bg-card-hover);
  color: var(--accent-link);
  scale: 1.08;
}
}
.sound-toggle:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--accent-link);
}
@media (hover: none) {
.sound-toggle:active { scale: 0.96; transition: scale 0.15s; }
}

/* Post modal — opens instantly over the page (no transition).
   Wider than the page column on desktop; standalone post page stays as no-JS/direct-link fallback. */
.post-modal {
  border: none;
  padding: 0;
  /* ponytail: UA dialog `margin:auto` centers against the layout scrollport, which
     drifts on mobile zoom/scroll. Pin to the *visual* viewport instead, and gate on
     dynamic vh so the modal never exceeds what the user can see. */
  position: fixed;
  inset: 50% auto auto 50%;
  transform: translate(-50%, -50%);
  margin: 0;
  width: min(56rem, calc(100vw - 2rem));
  max-height: 85vh;
  max-height: 85dvh;
  background: var(--bg-card);
  border-radius: var(--radius);
  color: var(--fg);
  overflow: hidden;
}
.post-modal[open] { display: flex; flex-direction: column; }
.post-modal::backdrop { background: rgba(10, 23, 24, 0.6); }
.post-modal-body { overflow-y: auto; padding: 4rem 2rem 4.5rem; scrollbar-color: var(--scrollbar) transparent; }
.post-modal-loading { position: absolute; inset: 4rem 2rem auto; color: var(--fg-muted); text-align: center; }
.post-modal-loading-fallback { margin: 2rem 0; color: var(--fg-muted); text-align: center; }
.post-modal .post-close { position: absolute; top: 1rem; right: 1rem; }
.post-modal .sound-toggle { position: absolute; right: 1rem; bottom: 1rem; z-index: 2; }
.post-modal-body:focus-visible { outline: 2px solid var(--accent-link); outline-offset: -2px; }

/* Post modal — a compact scale/fade entrance avoids a card snapshot covering the
   dialog; close may use the shared-element View Transition back to the card.
   Both paths fall back to instant behavior under reduced motion or where
   startViewTransition is unavailable. */
@media (prefers-reduced-motion: no-preference) {
  ::view-transition-group(post-morph) {
    animation-duration: 0.34s;
    animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
  }
}

/* Projects */
.project-name { font-weight: 600; }
.project-desc {
  display: block;
  color: var(--fg-muted);
  font-size: var(--text-meta);
  margin-top: 0.25rem;
  font-weight: 600;
}

/* 404 — centered hero, gradient code, card-style CTA matching link aesthetic */
.not-found {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: 1.25rem;
}
.not-found-code {
  font-family: var(--font-display);
  font-size: clamp(5rem, 22vw, 9rem);
  font-weight: 900;
  line-height: 1;
  margin: 0;
  color: var(--accent);
}
@supports (background-clip: text) or (-webkit-background-clip: text) {
  .not-found-code {
    background: var(--accent);
    background-clip: text;
    -webkit-background-clip: text;
    color: transparent;
  }
}
.not-found-text {
  color: var(--fg-muted);
  margin: 0;
  font-size: var(--text-meta);
}
.not-found-home {
  display: inline-flex;
  align-items: center;
  padding: 0.85rem 1.75rem;
  background: var(--bg-card);
  border-radius: var(--control-radius-lg);
  color: var(--fg);
  text-decoration: none;
  font-weight: 800;
  transition: background 0.3s ease, scale 0.3s ease, color 0.3s ease;
}
@media (hover: hover) {
.not-found-home:hover {
  background: var(--bg-card-hover);
  color: var(--accent-link);
  scale: 1.03;
}
}
.not-found-home:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--accent-link);
}

.site-footer {
  text-align: center;
  padding: 2rem max(1rem, env(safe-area-inset-right)) max(2rem, env(safe-area-inset-bottom)) max(1rem, env(safe-area-inset-left));
  color: var(--fg-muted);
  font-size: var(--text-meta);
  position: relative; /* stack above the fixed background layer */
  z-index: 1;

  /* Signal Bright like every other actionable accent — inherit (Ash Rose) read as plain text. */
  a { color: var(--accent-link); text-decoration: none; }
}
/* Inline-SVG heart (replaces emoji) — signal red, sized to the cap height.
   One-shot double-thump on load (lub-dub), delayed so it plays after settle. */
.footer-heart { display: inline-flex; vertical-align: -0.15em; color: var(--accent); animation: heart-beat 0.8s ease-in-out 0.4s 1; }
.footer-heart svg { width: 1em; height: 1em; }
@keyframes heart-beat {
  0%, 100% { transform: scale(1); }
  15% { transform: scale(1.15); }
  30% { transform: scale(1); }
  45% { transform: scale(1.12); }
  60% { transform: scale(1); }
}

/* Phone adaptation (< 768px). Desktop is the base; phones get denser spacing,
   stacked card head, smaller hero + close button. Tablet portrait folds in here. */
@media (max-width: 768px) {
  /* Type is fluid now (clamp scale above) — only layout/spacing adapt here. */
  .container { padding: max(1.5rem, env(safe-area-inset-top)) max(1rem, env(safe-area-inset-right)) 2rem max(1rem, env(safe-area-inset-left)); }
  .hero { margin-bottom: 1.5rem; --hero-pad: 0.5rem; --avatar-size: 5rem; }
  .avatar { margin-bottom: 0.75rem; }
  .section { margin-bottom: 1.5rem; }
  /* Phone cards keep the smaller radius and gain a little more breathing room. */
  .link-card { padding: 1rem; gap: 0.875rem; border-radius: var(--card-radius); }
  .link-list li:first-child .link-card { border-top-left-radius: 1rem; border-top-right-radius: 1rem; }
  .link-list li:last-child .link-card { border-bottom-left-radius: 1rem; border-bottom-right-radius: 1rem; }
  .link-card:focus-visible { border-radius: 1rem; }
  .link-card svg { width: 1.75rem; height: 1.75rem; }
  .post-card-head { flex-direction: column; align-items: flex-start; gap: 0.25rem; }
  .post-single { padding: 4rem max(1rem, env(safe-area-inset-right)) max(2rem, env(safe-area-inset-bottom)) max(1rem, env(safe-area-inset-left)); }
  .post-article { padding: 1.25rem; }
  .post-modal { max-height: 92vh; max-height: 92dvh; }
  .post-modal-body { padding: 4rem 1.25rem 4.5rem; }
  .post-modal .post-close { top: max(1rem, env(safe-area-inset-top)); right: max(1rem, env(safe-area-inset-right)); }
  .post-modal .sound-toggle { right: max(1rem, env(safe-area-inset-right)); bottom: max(1rem, env(safe-area-inset-bottom)); top: auto; }
  .sound-toggle { top: max(1rem, env(safe-area-inset-top)); bottom: auto; }
  body[data-page="post"] .sound-toggle { right: max(5.5rem, env(safe-area-inset-right)); }
  /* 44px touch-target floor (WCAG 2.5.8) — was 2.75rem (44px) nominal but padding shrink
     crowded the visual region; keep the hit area at 3rem everywhere. */
  .post-close { top: max(1rem, env(safe-area-inset-top)); right: max(1rem, env(safe-area-inset-right)); }
  .post-close svg { width: 1.35rem; height: 1.35rem; }
  .site-footer { padding: 1.5rem max(1rem, env(safe-area-inset-right)) max(1.5rem, env(safe-area-inset-bottom)) max(1rem, env(safe-area-inset-left)); }
}

/* Small phone refinement (< 380px): tighter density on tiny screens */
@media (max-width: 380px) {
  .link-card { padding: 0.875rem; }
}

/* Landscape phones (short, wide viewports). Stream viewers rotate to watch; the stacked
   hero would eat the whole short height and bury the links below the fold. Gate on BOTH
   orientation:landscape AND a short max-height (30rem = 480px) so tablets in landscape
   (tall) never match — only phones. Fires on top of the phone query where widths overlap
   (e.g. iPhone SE landscape < 768px); later source order wins, so the compact values apply. */
@media (orientation: landscape) and (max-height: 30rem) {
  :root { --text-display: 1.75rem; --text-tagline: 0.95rem; }
  .container { padding-top: 1rem; padding-bottom: 1.5rem; }
  .hero { margin-bottom: 1rem; --hero-pad: 0; --avatar-size: 3.5rem; }
  .avatar { margin-bottom: 0.35rem; }
  .section { margin-bottom: 1rem; }
  .post-single { padding-top: 4rem; }
  .post-modal { max-height: 96vh; max-height: 96dvh; }
  .post-modal-body { padding: 4rem 1.25rem 4rem; }
}
