/* jpholecka.com
   Dark is the default register; light comes from the OS preference or an
   explicit data-theme on the root.

   Two faces, both from the Typekit kit:
     bite-stencil   display, weight 900 only — the name
     bd-terminal-vf everything else, variable 100–800, carrying all hierarchy */

/* Palette: orange and black, nothing else.
   Dark  — black ground, orange type and UI.
   Light — orange ground, black type and UI.

   The orange carries every word in dark mode, so it was chosen for contrast in
   both directions: #FF6B1A on #121011 is 6.65:1, and so is black on orange.
   That is a far lower base than the old bone-on-ink (17:1), so the secondary
   tints are much less transparent than they used to be — .88 and .80 rather
   than .72 and .50 — or the small tracked labels would have failed. */

:root {
  --orange: #FF6B1A;
  --black: #121011;

  --bg: var(--black);
  --ink: var(--orange);
  --soft: rgba(255, 107, 26, .88);
  --muted: rgba(255, 107, 26, .8);
  --rule: rgba(255, 107, 26, .34);
  --rule2: rgba(255, 107, 26, .6);
  --grain: .11;
  /* unused: kept as the hook for reinstating a third accent hue if wanted */
  --accent: #FD2E90;
  --ease: cubic-bezier(.23, 1, .32, 1);

  --font-display: "bite-stencil", "Hanken Grotesk", Impact, sans-serif;
  --font-text: "bd-terminal-vf", ui-monospace, SFMono-Regular, Menlo, monospace;
  /* bd-terminal-vf carries a width axis from 100 (normal) to 200. Extended is
     the point of it, so the page sits wide and goes wider on the labels, where
     the face reads most like a terminal. Weight stays on font-weight. */
  --vf-wdth: 130;
  --vf-wdth-wide: 150;

  /* Backdrop video, dark mode: bright artifacts added to a black ground and
     tinted orange, so the film burns read as projector light rather than
     introducing a third neutral into a two-colour palette. */
  --vid-blend: screen;
  --vid-filter: grayscale(1) sepia(1) saturate(2.6) hue-rotate(-14deg);
  --vid-opacity: .16;

  --portrait: url("/media/portrait-dark.webp");
  /* on a dark ground the toggle offers the sun */
  --icon-sun: block;
  --icon-moon: none;

  color-scheme: dark;
}

/* Light mode is the inversion: orange ground, black type.
   The video has to flip with it. On black it ADDS light (screen); on orange it
   has to SUBTRACT, so it is inverted and multiplied — the artifacts darken the
   orange towards black instead of blowing it out to white. A saturated
   mid-tone ground shows a multiply far more readily than a near-white one did,
   so it also sits back further: .09 against .16. */

@media (prefers-color-scheme: light) {
  html:not([data-theme]) {
    --bg: var(--orange);
    --ink: var(--black);
    --soft: rgba(18, 16, 17, .88);
    --muted: rgba(18, 16, 17, .8);
    --rule: rgba(18, 16, 17, .34);
    --rule2: rgba(18, 16, 17, .6);
    --grain: .07;
    --vid-blend: multiply;
    --vid-filter: grayscale(1) invert(1);
    --vid-opacity: .09;
    --portrait: url("/media/portrait-light.webp");
    --icon-sun: none;
    --icon-moon: block;
    color-scheme: light;
  }
}

html[data-theme="light"] {
  --bg: var(--orange);
  --ink: var(--black);
  --soft: rgba(18, 16, 17, .88);
  --muted: rgba(18, 16, 17, .8);
  --rule: rgba(18, 16, 17, .34);
  --rule2: rgba(18, 16, 17, .6);
  --grain: .07;
  --vid-blend: multiply;
  --vid-filter: grayscale(1) invert(1);
  --vid-opacity: .09;
  --portrait: url("/media/portrait-light.webp");
  --icon-sun: none;
  --icon-moon: block;
  color-scheme: light;
}

html[data-theme="dark"] {
  --bg: var(--black);
  --ink: var(--orange);
  --soft: rgba(255, 107, 26, .88);
  --muted: rgba(255, 107, 26, .8);
  --rule: rgba(255, 107, 26, .34);
  --rule2: rgba(255, 107, 26, .6);
  --grain: .11;
  --vid-blend: screen;
  --vid-filter: grayscale(1) sepia(1) saturate(2.6) hue-rotate(-14deg);
  --vid-opacity: .16;
  --portrait: url("/media/portrait-dark.webp");
  --icon-sun: block;
  --icon-moon: none;
  color-scheme: dark;
}

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

html, body { margin: 0; background: var(--bg); }

body {
  color: var(--ink);
  font-family: var(--font-text);
  font-variation-settings: "wdth" var(--vf-wdth);
  font-weight: 300;
  font-size: 17px;
  line-height: 1.55;
  font-variant-numeric: tabular-nums;
  -webkit-font-smoothing: antialiased;
  text-wrap: pretty;
}

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

@media (hover: hover) and (pointer: fine) {
  a:hover {
    text-decoration: underline;
    text-underline-offset: .16em;
    text-decoration-thickness: 1px;
  }
}

button { font: inherit; color: inherit; }

/* the tracked uppercase labels run wider still */
.dot-label, .ident-meta, .panel-head, .label,
.copy, .door .meta, .entry .meta, .topics .idx, .contact {
  font-variation-settings: "wdth" var(--vf-wdth-wide);
}

/* Selection and focus are straight inversions rather than a third hue. The
   handoff reserved magenta for both, but magenta sits at 1.12:1 on orange —
   invisible — and a two-colour palette has no room for it. The bg-coloured
   inner ring is what separates the outline from the element it is marking.
   To put magenta back on dark, set --accent and use it here. */
::selection { background: var(--ink); color: var(--bg); }

:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
  box-shadow: 0 0 0 2px var(--bg);
}

/* ---------- backdrop ---------- */

.backdrop {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  background: var(--bg);
  /* keep the blend inside this box rather than against the whole page */
  isolation: isolate;
  pointer-events: none;
}

.backdrop video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  mix-blend-mode: var(--vid-blend);
  filter: var(--vid-filter);
  transition: opacity 450ms ease-out;
  will-change: opacity;
}

.backdrop video.is-live { opacity: var(--vid-opacity); }

/* holds the artifacts off the type: darker at the edges and along the bottom,
   where the claim and the contact line sit */
.backdrop-scrim {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(120% 90% at 50% 38%, transparent 0%, transparent 42%, color-mix(in srgb, var(--bg) 78%, transparent) 100%),
    linear-gradient(to top, var(--bg) 0%, color-mix(in srgb, var(--bg) 55%, transparent) 26%, transparent 52%);
}

/* ---------- constellation ---------- */

.constellation {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  /* fade out over the bottom bar so the claim and contact line stay clean */
  -webkit-mask-image: linear-gradient(to top, transparent 0%, rgba(0,0,0,.35) 9%, #000 24%);
          mask-image: linear-gradient(to top, transparent 0%, rgba(0,0,0,.35) 9%, #000 24%);
}

.constellation svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  overflow: visible;
}

/* ---------- muzak ---------- */

/* Absolute inside the stage rather than fixed, so it sits in the corner and
   scrolls away with the page instead of covering a panel you are reading.

   The offsets repeat .stage's own padding-top and padding-left, so the block
   lines up with the claim and the fact line below. They cannot be 0: absolute
   positioning resolves against the padding box, whose edge is the outside of
   that padding, which would sit the player flush against the viewport. Keep
   these two values in step with .stage's padding. */
/* Stacking inside .stage, lowest first:
     0  the player       — a panel opening over this corner must be readable
     3  the leader line
     4  the panels
   The player is also first in the DOM here, so it would sit underneath anyway;
   the numbers are written down so a later reorder cannot quietly undo it. */
.muzak {
  position: absolute;
  top: clamp(18px, 3vh, 32px);
  left: clamp(20px, 4vw, 64px);
  z-index: 0;
  width: min(228px, 46vw);
  padding: 10px 12px 11px;
  border: 1px solid var(--rule);
  background: var(--bg);
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.muzak p { margin: 0; }

.muzak-name {
  font-size: 12px;
  font-weight: 500;
  letter-spacing: .12em;
  text-transform: uppercase;
  font-variation-settings: "wdth" var(--vf-wdth-wide);
}

.muzak-note,
.muzak-title {
  font-size: 11px;
  font-weight: 400;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--muted);
  font-variation-settings: "wdth" var(--vf-wdth-wide);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* --- transport, added by site.js only when there are tracks --- */

.muzak-transport {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 7px;
}

/* The waveform gets its own full-width row so it can breathe; the transport
   sits under it. Peaks are baked into tracks.json, so this draws on first
   paint without the audio having been fetched. */
.muzak-wave {
  display: block;
  width: 100%;
  height: 30px;
  margin-top: 8px;
  padding: 0;
  background: none;
  border: 0;
  cursor: pointer;
}

.muzak-wave svg { display: block; width: 100%; height: 100%; }

/* non-scaling-stroke keeps every bar the same weight however wide the box gets,
   while the spacing between them stretches */
.muzak-wave line {
  stroke-width: 1.5;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}

.muzak-wave .wave-base { stroke: var(--ink); stroke-opacity: .3; }
.muzak-wave .wave-played { stroke: var(--ink); }

.muzak-btn {
  flex: none;
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  padding: 0;
  background: none;
  border: 1px solid var(--rule2);
  color: var(--ink);
  cursor: pointer;
  transition: transform 150ms ease, background 150ms ease;
}

.muzak-btn svg {
  width: 12px;
  height: 12px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.muzak-step svg { width: 10px; height: 10px; }

.muzak-btn:active { transform: scale(.9); }

@media (hover: hover) and (pointer: fine) {
  .muzak-btn:hover { background: var(--rule); }
}

.muzak-step { width: 22px; height: 22px; border-color: var(--rule); }

/* a hairline that fills, scaled on the GPU rather than animating width */
.muzak-bar {
  position: relative;
  flex: 1 1 auto;
  height: 12px;
  cursor: pointer;
  background: none;
  border: 0;
  padding: 0;
}

.muzak-bar::before,
.muzak-fill {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 1px;
  margin-top: -.5px;
}

.muzak-bar::before { background: var(--rule); }

.muzak-fill {
  background: var(--ink);
  transform: scaleX(0);
  transform-origin: left center;
}

/* --- volume: a hairline like the waveform, with a square thumb (radius 0
   everywhere on this site). The filled part is a gradient driven by --v, set on
   the input itself so its own pseudo-elements inherit it. --- */

.muzak-vol {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 8px;
}

.muzak-vol-label {
  flex: none;
  font-size: 10px;
  font-weight: 400;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--muted);
  font-variation-settings: "wdth" var(--vf-wdth-wide);
}

.muzak-vol input {
  --v: 40%;
  -webkit-appearance: none;
  appearance: none;
  flex: 1 1 auto;
  min-width: 0;
  height: 16px;
  margin: 0;
  background: none;
  cursor: pointer;
}

.muzak-vol input::-webkit-slider-runnable-track {
  height: 1px;
  background: linear-gradient(to right, var(--ink) 0 var(--v), var(--rule2) var(--v) 100%);
}

.muzak-vol input::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 9px;
  height: 9px;
  margin-top: -4px;
  border: 0;
  border-radius: 0;
  background: var(--ink);
}

.muzak-vol input::-moz-range-track { height: 1px; border: 0; background: var(--rule2); }
.muzak-vol input::-moz-range-progress { height: 1px; background: var(--ink); }
.muzak-vol input::-moz-range-thumb {
  width: 9px;
  height: 9px;
  border: 0;
  border-radius: 0;
  background: var(--ink);
}

/* the whole thing is decoration until it has something to play */
.muzak[hidden] { display: none; }

/* On a phone there is no corner to float in — the ring is 88vw — so the player
   comes out of the corner, goes into the flow above the ring, and spreads
   sideways instead of stacking. It is the first child of .stage, so switching
   to static is all it takes to put it above the ring area.

   Laid out as a grid so the same markup reads as a wide bar rather than a
   column: the name and the links share the top line, the title runs under
   them, and the transport sits beside the waveform. */
@media (max-width: 719px) {
  .muzak {
    position: static;
    width: auto;
    display: grid;
    /* Column 1 is the transport, column 2 everything it sits beside. The
       transport spans both lower rows so the title tucks in above the waveform
       rather than costing a row of its own.

       The name and the links share row one and BOTH span the full width — one
       left, one right. If the name sat in column 1 it would size that column,
       and with an empty manifest (no transport) the title below would end up
       indented past it. */
    grid-template-columns: auto minmax(0, 1fr);
    align-items: center;
    column-gap: 0;
    row-gap: 4px;
    padding: 8px 11px 9px;
    /* 4px, not 10: the volume row cost ~28px, and at 375x780 the page had
       fitted one screen exactly — this and the slider height below win it back */
    margin-bottom: 4px;
  }

  .muzak-name {
    grid-row: 1;
    grid-column: 1 / -1;
    font-size: 11px;
    line-height: 1.25;
  }

  .muzak-links {
    grid-row: 1;
    grid-column: 1 / -1;
    justify-self: end;
    margin-top: 0;
  }

  .muzak-note,
  .muzak-title {
    grid-row: 2;
    grid-column: 2;
    font-size: 10px;
    line-height: 1.25;
  }

  .muzak-transport {
    grid-row: 2 / 4;
    grid-column: 1;
    margin-top: 0;
    margin-right: 12px;
  }

  .muzak-wave {
    grid-row: 3;
    grid-column: 2;
    margin-top: 0;
    height: 22px;
  }

  /* volume runs under the waveform, aligned with it; a bigger thumb for a finger */
  .muzak-vol {
    grid-row: 4;
    grid-column: 2;
    margin-top: 0;
  }
  .muzak-vol input { height: 20px; }
  .muzak-vol input::-webkit-slider-thumb { width: 11px; height: 11px; margin-top: -5px; }
  .muzak-vol input::-moz-range-thumb { width: 11px; height: 11px; }

  /* finger-sized rather than cursor-sized */
  .muzak-btn { width: 34px; height: 34px; }
  .muzak-step { width: 32px; height: 32px; }
  .muzak-btn svg { width: 13px; height: 13px; }
  .muzak-step svg { width: 11px; height: 11px; }
}

.muzak-links {
  display: flex;
  flex-wrap: wrap;
  gap: 2px 14px;
  margin-top: 7px;
  font-size: 10px;
  font-weight: 400;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--soft);
  font-variation-settings: "wdth" var(--vf-wdth-wide);
}

.muzak-links a { padding: 3px 0; transition: transform 120ms ease-out; }
.muzak-links a:active { transform: scale(.98); }

@media (prefers-reduced-motion: reduce) {
  .muzak-btn, .muzak-btn:active, .muzak-links a:active { transform: none; }
}

/* ---------- grain ---------- */

.grain {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  opacity: var(--grain);
  z-index: 50;
}

/* ---------- stage ---------- */

.stage {
  position: relative;
  z-index: 1;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: clamp(18px, 3vh, 32px) clamp(20px, 4vw, 64px) clamp(20px, 3.5vh, 36px);
}

.ring-area {
  flex: 1 1 auto;
  display: grid;
  place-items: center;
  padding: clamp(8px, 2vh, 28px) 0;
}

.ring {
  position: relative;
  width: min(68svh, 88vw, 860px, var(--ring-max, 860px));
  aspect-ratio: 1;
  container-type: inline-size;
  transition: transform 250ms var(--ease);
}

.ring-svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  display: block;
}

/* ---------- the constellation ---------- */

.track {
  fill: none;
  stroke: var(--ink);
  stroke-opacity: .6;
  stroke-width: 1.25;
}

/* Field and chords are decoration. Everything readable is real DOM text. */
.field { pointer-events: none; }

.chords {
  fill: none;
  stroke: var(--ink);
  stroke-opacity: .09;
  stroke-width: .6;
}

.specks { fill: var(--ink); }

/* One slow revolution. Imperceptible frame to frame, but the field is never
   quite where you left it. Only the outer group turns: those points clear the
   name block at every angle, so they can spin without ever crossing the type.
   The handful of specks inside the ring stay where they were placed. */
@media (prefers-reduced-motion: no-preference) {
  .ring-svg .field-drift {
    transform-origin: 140px 140px;
    animation: drift 480s linear infinite;
  }

  .constellation .field-drift {
    /* the parent group is already translated to the ring centre */
    transform-origin: 0 0;
    animation: drift 480s linear infinite;
    will-change: transform;
  }
}

@keyframes drift {
  from { transform: rotate(0deg); }
  to   { transform: rotate(360deg); }
}

/* ---------- section dots ---------- */

.dot {
  cursor: pointer;
  transform-box: fill-box;
  transform-origin: center;
  transition: transform 150ms ease, opacity 250ms var(--ease);
  outline-offset: 4px;
}

.dot .mark {
  fill: var(--bg);
  stroke: var(--ink);
  stroke-width: 1.25;
  transition: stroke-width 150ms ease, fill 150ms ease;
}

.dot.is-open .mark { fill: var(--ink); }

.stage[data-open] .dot:not(.is-open) { opacity: .3; }

/* These five dots are the whole navigation, and they drift. Without a hover
   response you cannot tell you have acquired one until you have clicked it.
   Touch fires a false hover on tap, so every hover rule is gated. */
@media (hover: hover) and (pointer: fine) {
  .dot:hover { transform: scale(1.15); }
  .dot:hover .mark { stroke-width: 1.9; fill: var(--rule); }
  .dot.is-open:hover .mark { fill: var(--ink); }
  /* a dimmed dot is still live — say so, or the other sections read as disabled */
  .stage[data-open] .dot:not(.is-open):hover { opacity: .7; }
}

/* The browser's own focus ring is drawn round the SVG link's bounding box — a
   pale-blue rounded square in Safari and Chrome that ignores the palette and
   boxes a circle. It goes, in every focus state, including after a mouse click.
   Keyboard focus still gets a clear, on-brand indicator instead: a dashed
   hairline ring on the dot's hit circle, like a callout on a drawing, plus the
   same grow/stroke/tint as hover. Visible focus is what accessibility asks
   for; it does not require the browser's default ring. */
.dot,
.dot:focus,
.dot:focus-visible {
  outline: none;
  box-shadow: none;
  -webkit-tap-highlight-color: transparent;
}

.dot circle:not(.mark) {
  stroke: transparent;
  stroke-width: .9;
  stroke-dasharray: 2 2.4;
  transition: stroke 150ms ease;
}

.dot:focus-visible circle:not(.mark) { stroke: var(--ink); }

/* keyboard parity */
.dot:focus-visible { transform: scale(1.15); }
.dot:focus-visible .mark { stroke-width: 1.9; fill: var(--rule); }
.stage[data-open] .dot:not(.is-open):focus-visible { opacity: .7; }

/* press beats hover — last at equal specificity */
.dot:active { transform: scale(.9); }

.dot-label.is-hot { fill: var(--ink); }

.dot-label {
  /* size lives on --fs per label so a container query can scale them all */
  font-size: calc(var(--fs, 8) * var(--label-scale, 1) * 1px);
  transition: fill 150ms ease;
  font-family: var(--font-text);
  font-weight: 400;
  letter-spacing: .1em;
  text-transform: uppercase;
  fill: var(--soft);
  pointer-events: none;
}

/* The labels stay at every width. In narrow mode the closed panels collapse,
   so these five labels are the only thing telling you what the dots are.

   They hang outside the ring, so on a phone the longest of them ("02 Speaking",
   anchored start at the far right) ran off the screen edge once the ring grew.
   Scaling them down on a small ring keeps all five on screen. */
@container (max-width: 420px) {
  .dot-label { --label-scale: .82; }
}

/* ---------- inside the ring ---------- */

.ring-center {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  pointer-events: none;
}

.ident {
  width: 86%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  text-align: center;
}

/* The portrait ships in both registers and is swapped by --portrait, so only
   the one in use is ever fetched. Its own ground already matches the page, so
   it masks the track it sits on and reads as a node rather than a disc pasted
   over the ring.

   Placed on the track at 310°: viewBox (63.4, 75.72) of 280, which is
   22.64% / 27.04%. Recompute both if the angle ever changes. */
.portrait {
  position: absolute;
  left: 22.64%;
  top: 27.04%;
  transform: translate(-50%, -50%);
  width: clamp(64px, 17cqw, 124px);
  aspect-ratio: 1;
  border-radius: 50%;
  border: 1px solid var(--rule2);
  background-color: var(--bg);
  background-image: var(--portrait);
  background-size: cover;
  background-position: 50% 50%;
  background-repeat: no-repeat;
  pointer-events: none;
}

/* The stencil is the one display moment on the page. Two lines now that the
   portrait has moved out to the ring: HOLECKA is the wider line at 6.14× its
   own font-size, so this lands it near 70% of the ring. nowrap keeps each line
   intact — the break is the <br>, never the box. */
.ident h1 {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 900;
  font-size: 9cqw;
  line-height: .88;
  letter-spacing: -.01em;
  text-transform: uppercase;
  white-space: nowrap;
}

.ident-meta {
  margin: 0;
  font-family: var(--font-text);
  font-weight: 400;
  font-size: max(11px, 2.1cqw);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--soft);
  line-height: 1.6;
  white-space: nowrap;
}

/* Below a ~300px ring the 11px floor makes this extended line wider than the
   circle it sits in. Real phones start at 320px and stay clear; this only
   catches anything narrower. Must follow the rule above to win the cascade. */
@container (max-width: 300px) {
  .ident-meta { font-size: max(9px, 2.1cqw); letter-spacing: .06em; }
}

/* On a small ring the meta's 11px floor is proportionally much larger than the
   2.1cqw it would otherwise be, which shoves the name upwards into the
   portrait. On a phone the two cleared by well under a pixel. Easing the name
   and the portrait here buys the gap back. */
@container (max-width: 380px) {
  .ident h1 { font-size: 8.2cqw; }
  .portrait { width: clamp(56px, 17cqw, 124px); }
}

/* ---------- leader ---------- */

.leader {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  overflow: visible;
  z-index: 3;
}

.leader line {
  stroke: var(--ink);
  stroke-width: 1;
  opacity: 0;
}

/* ---------- panels ---------- */

.panels {
  display: flex;
  flex-direction: column;
  gap: 28px;
  margin: 0 0 40px;
}

.panel {
  border: 1px solid var(--rule2);
  /* opaque, so the artifacts never run behind reading text */
  background: var(--bg);
  padding: 20px 22px 24px;
}

.panel-head {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 18px;
  font-size: 12px;
  font-weight: 500;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--muted);
}

.panel-head h2 {
  margin: 0;
  font: inherit;
  letter-spacing: inherit;
  color: var(--soft);
}

.panel-close {
  background: none;
  border: 0;
  padding: 0 0 0 8px;
  font: inherit;
  letter-spacing: inherit;
  color: var(--muted);
  cursor: pointer;
  transition: transform 120ms ease-out;
}

.panel-close:active { transform: scale(.98); }

/* Without JavaScript there is nothing to close and nothing to copy. */
html:not(.js) .panel-close,
html:not(.js) .copy { display: none; }

/* --- JS mode --- */

html.js .panels {
  position: relative;
  display: grid;
  gap: 0;
  margin: 0;
  z-index: 4;
}

html.js .panel {
  grid-area: 1 / 1;
  opacity: 0;
  pointer-events: none;
  clip-path: inset(40%);
  transform: scale(.9);
  transform-origin: 50% 0%;
  /* All five panels share one grid cell, so in narrow mode a section-to-section
     swap cross-fades two different texts in the same place. The blur masks it. */
  filter: blur(3px);
  transition: opacity 180ms var(--ease), transform 180ms var(--ease),
              clip-path 180ms var(--ease), filter 140ms var(--ease);
}

html.js .panel.is-open {
  opacity: 1;
  pointer-events: auto;
  clip-path: inset(0);
  transform: scale(1);
  filter: blur(0);
  /* the leader arrives first, then the box */
  transition: opacity 210ms var(--ease) 120ms, transform 210ms var(--ease) 120ms,
              clip-path 210ms var(--ease) 120ms, filter 170ms var(--ease) 120ms;
}

/* Contents rise in behind the box, starting just after it has begun opening so
   they follow the container rather than racing it. Decorative only: pointer
   events are never withheld, so a link works the moment the panel is live. */
html.js .panel > * {
  opacity: 0;
  transform: translateY(6px);
  transition: opacity 200ms var(--ease), transform 200ms var(--ease);
}

html.js .panel.is-open > * {
  opacity: 1;
  transform: translateY(0);
}

html.js .panel.is-open > :nth-child(1) { transition-delay: 190ms; }
html.js .panel.is-open > :nth-child(2) { transition-delay: 230ms; }
html.js .panel.is-open > :nth-child(3) { transition-delay: 270ms; }
html.js .panel.is-open > :nth-child(4) { transition-delay: 310ms; }
html.js .panel.is-open > :nth-child(n+5) { transition-delay: 350ms; }

/* narrow: the open panel sits in flow beneath the ring, closed ones take no space */
html.js .stage[data-mode="narrow"] .panel:not(.is-open) {
  position: absolute;
  inset: 0 0 auto 0;
  height: 0;
  overflow: hidden;
  padding: 0;
  border: 0;
}

html.js .stage[data-mode="narrow"][data-open] .panels { margin: 0 0 28px; }

/* wide: the wrapper is an overlay and JS places each panel beside the ring */
html.js .stage[data-mode="wide"] .panels {
  position: absolute;
  inset: 0;
  pointer-events: none;
  display: block;
}

/* ---------- panel content ---------- */

.lede {
  margin: 0;
  font-size: clamp(20px, 1.7vw, 24px);
  font-weight: 400;
  line-height: 1.35;
  letter-spacing: -.005em;
}

.prose {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.prose p { margin: 0; color: var(--soft); }
.prose .lede { color: var(--ink); }

.ruled {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  border-top: 1px solid var(--rule);
}

.ruled > li { border-bottom: 1px solid var(--rule); }

.topics { margin: 0 0 28px; }

.topics > li {
  display: flex;
  gap: 14px;
  align-items: baseline;
  padding: 12px 0;
}

.topics .idx {
  font-size: 12px;
  font-weight: 400;
  color: var(--muted);
  flex: 0 0 2em;
}

.topics h3 {
  margin: 0;
  font-weight: 600;
  font-size: 19px;
  line-height: 1.25;
  letter-spacing: -.01em;
}

.block {
  display: flex;
  flex-direction: column;
  gap: 12px;
  margin-bottom: 28px;
}

.block:last-child { margin-bottom: 0; }

.block p { margin: 0; color: var(--soft); }

.label {
  font-size: 12px;
  font-weight: 500;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--muted);
}

.copy {
  align-self: flex-start;
  background: none;
  border: 1px solid var(--rule2);
  padding: 9px 14px;
  min-height: 44px;
  font-size: 12px;
  font-weight: 500;
  letter-spacing: .12em;
  text-transform: uppercase;
  cursor: pointer;
  transition: transform 160ms ease-out, background 160ms ease-out;
}

.copy-strong { border-color: var(--ink); padding: 11px 16px; }

.copy:active { transform: scale(.97); background: var(--rule); }

.copy span {
  display: inline-block;
  transition: filter 150ms ease-out;
}

.copy.is-swapping span { filter: blur(2px); }

.lead-in { margin: 0 0 18px; color: var(--soft); }

.door {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 14px 0;
  transition: transform 120ms ease-out, background 120ms ease-out;
}

/* Same shape as a door, but these lead nowhere: a board seat is a statement of
   fact, not a link, and dressing one up as a link to keep the list tidy would
   be a lie the cursor tells. */
.entry {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 14px 0;
}

.door:active { transform: scale(.98); background: var(--rule); }

.door .title, .entry .title {
  font-weight: 600;
  font-size: 21px;
  line-height: 1.2;
  letter-spacing: -.01em;
}

.door .meta, .entry .meta {
  font-size: 12px;
  font-weight: 400;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--muted);
}

.row {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  padding: 15px 0;
  min-height: 44px;
  font-size: 19px;
  font-weight: 600;
  letter-spacing: -.005em;
  transition: transform 120ms ease-out, background 120ms ease-out;
}

.row:active { transform: scale(.98); background: var(--rule); }

/* ---------- theme toggle ---------- */

.theme-toggle {
  display: inline-grid;
  place-items: center;
  width: 44px;
  height: 44px;
  padding: 0;
  margin: -8px 0 -8px 2px;
  background: none;
  border: 0;
  color: var(--soft);
  cursor: pointer;
  transition: transform 120ms ease-out, color 120ms ease-out;
}

.theme-toggle:active { transform: scale(.9); }

@media (hover: hover) and (pointer: fine) {
  .theme-toggle:hover { color: var(--ink); }
}

.theme-toggle .icon {
  width: 17px;
  height: 17px;
  fill: none;
  stroke: currentColor;
  stroke-width: 2.4;
  stroke-linecap: round;
  stroke-linejoin: round;
  grid-area: 1 / 1;
}

.icon-sun  { display: var(--icon-sun); }
.icon-moon { display: var(--icon-moon); }

/* nothing it could do without JS */
html:not(.js) .theme-toggle { display: none; }

/* ---------- bottom bar ---------- */

.bar {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: flex-end;
  gap: 18px 40px;
}

/* ---------- typewriter ---------- */

/* The caret is a real element positioned over the copy; the copy itself is
   revealed by clipping, never by inserting text. See site.js for why. */
.bar-claim { position: relative; }

.caret {
  position: absolute;
  left: 0;
  top: 0;
  width: 1px;
  height: 0;
  background: var(--ink);
  pointer-events: none;
  opacity: 0;
  will-change: transform;
}

.caret.is-live {
  opacity: 1;
  animation: caret-blink 1.06s steps(1, end) infinite;
}

/* fades out once the line has finished, so nothing blinks at you forever */
.caret.is-done {
  animation: none;
  opacity: 0;
  transition: opacity 600ms var(--ease);
}

@keyframes caret-blink {
  0%, 50%      { opacity: 1; }
  50.01%, 100% { opacity: 0; }
}

.bar-claim {
  flex: 1 1 320px;
  /* At the larger claim size a 46ch column wrapped to four lines, which grew
     the bottom bar by ~60px and pushed the ring off a 900px screen. Three
     lines keeps the whole page on one screen. */
  max-width: 58ch;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.claim {
  margin: 0;
  font-weight: 700;
  font-size: clamp(21px, 2.1vw, 30px);
  line-height: 1.2;
  letter-spacing: -.015em;
}

.fact {
  margin: 0;
  font-size: 15px;
  font-weight: 300;
  line-height: 1.55;
  color: var(--soft);
}

.contact {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 22px;
  font-size: 13px;
  font-weight: 400;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--soft);
}

.contact a { padding: 8px 0; transition: transform 120ms ease-out; }
.contact a:active { transform: scale(.98); }

/* ---------- reduced motion ---------- */

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

  html.js .panel,
  html.js .panel.is-open {
    transform: none;
    clip-path: none;
    filter: none;
    transition: opacity 180ms var(--ease);
  }

  /* transition:none alone would not clear a delay set by an nth-child rule */
  html.js .panel > *,
  html.js .panel.is-open > * {
    opacity: 1;
    transform: none;
    transition: none;
    transition-delay: 0s;
  }

  .dot, .dot:active, .dot:hover, .dot:focus-visible,
  .copy:active, .row:active, .door:active,
  .panel-close:active, .contact a:active { transform: none; }

  .dot { transition: opacity 180ms var(--ease); }

  .leader line { transition: none !important; }

  /* site.js does not build the backdrop at all under reduced motion */
  .backdrop video { display: none; }

  /* site.js skips the typewriter entirely; belt and braces in case it does not */
  .caret { display: none; }
  .claim, .fact { clip-path: none !important; }
}
