/* ============================================================================
   Blog theme switch.
   site.css paints the site from six colour roles, so the light theme is mostly
   six new values: the ground goes white, the ink goes near black, and the pale
   rose accent has to become the deep crimson or it disappears on paper. The
   handful of rules that mix brand crimson at full strength into a background,
   or hard code a bone-on-crimson pair, are restated below; everything else
   follows the roles.

   Only pages that carry data-theme="light" on <html> read any of this, and
   only blog pages load the file, so nothing on the rest of the site moves.
   ========================================================================== */

:root[data-theme="light"] {
  color-scheme: light;

  --sc-canvas:      #FFFFFF;   /* white ground                           */
  --sc-surface:     #F2EFEA;   /* raised panels, a shade off the paper   */
  --sc-ink:         #12100F;   /* near black                             */
  --sc-ink-soft:    #575052;   /* secondary text, 7:1 on the ground      */
  --sc-accent:      #9D0034;   /* the brand crimson, 7.5:1 on white       */
  --sc-accent-ink:  #FFFFFF;   /* text that sits on the accent           */

  /* shadows stay dark but lose the blue cast; on white a blue shadow reads
     as dirt rather than as depth */
  --sc-shadow-color: 210 12% 22%;
  --shadow:          210 12% 22%;
}

/* The atmosphere is a tint on paper, not a wash. At the dark theme's strength
   the same gradients turn the top of the page pink. */
:root[data-theme="light"] body::before {
  background:
    radial-gradient(120% 85% at 15% 0%, color-mix(in oklab, var(--brand-crimson) 9%, transparent) 0%, transparent 55%),
    radial-gradient(90% 70% at 100% 100%, color-mix(in oklab, var(--brand-deep) 8%, transparent) 0%, transparent 60%);
}
/* The bar rides over white here, so the shadow behind the wordmark stops
   earning its keep and only fuzzes the type. */
:root[data-theme="light"] .site-nav__brand,
:root[data-theme="light"] .site-nav__link { text-shadow: none; }

/* White glass on a white page is no pill at all. The resting bar takes the
   surface tint instead, and a shallow shadow sketches the edge the dark theme
   got from the contrast alone. */
:root[data-theme="light"] .site-nav {
  background: color-mix(in oklab, var(--sc-surface) 72%, transparent);
  box-shadow: 0 12px 28px -20px hsl(var(--shadow) / 0.5);
}
:root[data-theme="light"] .site-nav[data-solid="true"] {
  background: color-mix(in oklab, var(--sc-surface) 94%, transparent);
  box-shadow: 0 16px 34px -18px hsl(var(--shadow) / 0.45);
}

/* The section comb is drawn in bone at very low alpha: a whisper on the dark
   ground and nothing at all on paper. The same alphas go on ink, so the rail
   keeps its rhythm instead of disappearing. */
:root[data-theme="light"] .melody-rail .rail-line {
  background: linear-gradient(
    color-mix(in oklab, var(--sc-ink) 4%, transparent),
    color-mix(in oklab, var(--sc-ink) 16%, transparent),
    color-mix(in oklab, var(--sc-ink) 4%, transparent));
}
:root[data-theme="light"] .melody-rail .tine::after {
  background: color-mix(in oklab, var(--sc-ink) 16%, transparent);
}
:root[data-theme="light"] .melody-rail .tine:hover::after {
  background: color-mix(in oklab, var(--sc-accent) 55%, transparent);
}
:root[data-theme="light"] .melody-rail .tine.is-passed::after {
  background: color-mix(in oklab, var(--sc-accent) 34%, transparent);
}
:root[data-theme="light"] .melody-rail .tine.is-current::after {
  background: var(--sc-accent);
  box-shadow: 0 0 10px color-mix(in oklab, var(--sc-accent) 32%, transparent);
}

/* Panels that pull crimson into their own gradient at dark strength. Left as
   they are they read as pink blocks on a white page. */
:root[data-theme="light"] .blog-feature {
  background:
    radial-gradient(120% 130% at 0% 0%, color-mix(in oklab, var(--brand-crimson) 11%, transparent) 0%, transparent 62%),
    color-mix(in oklab, var(--sc-surface) 88%, var(--sc-canvas));
}
:root[data-theme="light"] .cta-top {
  background: linear-gradient(100deg,
    color-mix(in oklab, var(--brand-crimson) 13%, var(--sc-canvas)) 0%,
    color-mix(in oklab, var(--sc-surface) 88%, var(--sc-canvas)) 88%);
}
:root[data-theme="light"] .cta-band {
  background:
    radial-gradient(130% 150% at 0% 0%, color-mix(in oklab, var(--brand-crimson) 16%, var(--sc-canvas)) 0%, transparent 64%),
    color-mix(in oklab, var(--sc-surface) 84%, var(--sc-canvas));
}

/* The play button's "playing" state was crimson under bone, which only worked
   because bone was the light colour. On paper the label has to be white. */
:root[data-theme="light"] .promo__play[data-state="playing"],
:root[data-theme="light"] .promo__play[data-state="playing"]:hover { color: #FFFFFF; }

/* The comb is painted on a canvas, so it cannot read the roles by itself: its
   three ramp stops are handed to assets/js/site.js as the custom properties
   below. Dark keeps the pale rose it was drawn with; on paper the tips have
   nothing to stand on, so the ramp drops to the band's crimson and coral and
   the canvas keeps the bar's own ground instead of a black screen. */
.player__viz {
  --viz-lo:  rgba(157, 0, 52, 0.95);
  --viz-mid: rgba(255, 197, 188, 0.75);
  --viz-hi:  rgba(255, 197, 188, 1);
}
:root[data-theme="light"] .player__viz {
  --viz-lo:  #8E002F;
  --viz-mid: #B4263F;
  --viz-hi:  #E0475F;
}

/* ------------------------------------------------------------------ toggle */
/* The switch borrows the back-to-top button's shape, because they are the same
   kind of object: a small fixed control on the right edge. The back-to-top
   button moves up one slot so the two never stack on each other, and both clear
   the preview player when it opens. The lift only happens on a page that has
   the switch: the hub does not, and it keeps the button in the corner. */
.theme-toggle {
  position: fixed; z-index: 60; right: var(--sc-gutter);
  bottom: calc(var(--sc-gutter) + env(safe-area-inset-bottom, 0px));
  width: 44px; height: 44px; display: grid; place-items: center;
  padding: 0; border-radius: var(--sc-r-pill);
  border: 1px solid var(--sc-hairline-strong);
  background: color-mix(in oklab, var(--sc-surface) 88%, transparent);
  backdrop-filter: blur(14px) saturate(140%);
  color: var(--sc-ink); cursor: pointer;
  transition: color 180ms var(--sc-ease-out), border-color 180ms var(--sc-ease-out);
}
.theme-toggle:hover { color: var(--sc-accent); border-color: var(--sc-accent); }
.theme-toggle:focus-visible { outline: 2px solid var(--sc-accent); outline-offset: 2px; }
.theme-toggle svg { width: 17px; height: 17px; }
/* the glyph shows the theme the click leads to: a sun while the page is dark,
   a moon while it is light */
.theme-toggle .i-moon { display: none; }
:root[data-theme="light"] .theme-toggle .i-sun { display: none; }
:root[data-theme="light"] .theme-toggle .i-moon { display: block; }

body:has(.theme-toggle) .totop { bottom: calc(var(--sc-gutter) + 3.5rem + env(safe-area-inset-bottom, 0px)); }
body:has(.player[data-open="true"]) .theme-toggle {
  bottom: calc(var(--sc-gutter) + 5.5rem + env(safe-area-inset-bottom, 0px));
}
body:has(.theme-toggle):has(.player[data-open="true"]) .totop {
  bottom: calc(var(--sc-gutter) + 9rem + env(safe-area-inset-bottom, 0px));
}

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

/* ============================================================== blog cover */
/* The hub opens on a band that runs to both window edges rather than on a
   framed panel: the drawing bleeds off the page and the copy is held inside
   the page's own column, the way a magazine lets its cover art run off the
   edge. The drawing is inline SVG (built in dev/_build_blog_i18n.py), so it is
   painted from the band's own colour variables below. That is the whole reason
   it is not a photograph: the light theme inverts the scene by repainting
   those variables, and there is only ever one file to load.

   The palette here is deliberately not the site's: coral and deep crimson on
   near black, which the brand's pale rose cannot express. One set of variables
   carries both themes, so the drawing never needs a second copy. */
.blog-cover__panel {
  --cover-ground:   #100608;
  --cover-ink:      #F6EDE9;
  --cover-soft:     #D8B6B1;
  --cover-line:     #E0475F;
  --cover-coral:    #FF6A3D;
  --cover-scrim:    rgb(12 4 6 / 0.62);

  /* Full bleed without losing the page's measure: the band cancels half the
     column it sits in to each side, which lands it exactly on the window,
     and then pads its own content back out to where the rest of the page
     starts — the gutter, plus whatever the window adds beyond the column's
     measure. `100vw` on the padding only overstates it by the width of the
     scrollbar, which the section clips; the alternative, escaping with 100vw
     as a width, would leave a horizontal scrollbar behind. */
  margin-inline: calc(50% - 50vw);
  padding-inline: max(var(--sc-gutter),
                      calc((100vw - var(--sc-maxw)) / 2 + var(--sc-gutter)));
  padding-block: clamp(1.75rem, 4vh, 2.75rem) clamp(2rem, 5vh, 3.25rem);

  position: relative; isolation: isolate; overflow: hidden;
  display: flex; flex-direction: column; justify-content: space-between;
  min-height: clamp(23rem, 60vh, 38rem);
  background: var(--cover-ground);
  color: var(--cover-ink);
}

/* The band is a few pixels wider than the section on a window with a visible
   scrollbar. Clip it rather than let it push the page sideways. */
.blog-cover { overflow-x: clip; }

/* On paper the scene inverts: crimson lines on off white, and a deeper coral
   for the light that travels along them. */
:root[data-theme="light"] .blog-cover__panel {
  --cover-ground:   #FBF8F3;
  --cover-ink:      #12100F;
  --cover-soft:     #4E4749;
  --cover-line:     #C2445B;
  --cover-coral:    #F2542D;
  --cover-scrim:    rgb(251 248 243 / 0.5);
}

/* Paper does not need the same scrim: dark type clears the grid on its own,
   so the wash only has to soften the lines behind the small print. */
:root[data-theme="light"] .blog-cover__svg .art-grid,
:root[data-theme="light"] .blog-cover__svg .art-ripple { stroke-opacity: 0.66; }
:root[data-theme="light"] .blog-cover__count { opacity: 1; }

.blog-cover__art {
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
}
.blog-cover__svg { width: 100%; height: 100%; }

/* The soft scrim under the copy, and the two edge fades above it. There is no
   disc for the headline to sit on any more, only lines crossing behind it, so
   the wash carries the legibility the bright centre used to: a wide ellipse,
   not a box, so it never shows an edge. The two flat layers are painted in the
   band's own ground, which dissolves the lattice into the page at the top and
   bottom instead of cutting it off on a straight line — the crop of the
   drawing changes with every window size, so the fade has to be part of the
   band, not part of the drawing. */
.blog-cover__panel::after {
  content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background:
    radial-gradient(58% 74% at 50% 50%, var(--cover-scrim) 0%, transparent 72%),
    linear-gradient(180deg, var(--cover-ground) 0%, transparent 13%,
                    transparent 85%, var(--cover-ground) 100%);
}

/* ---- the drawing's parts, in the band's colours ------------------------- */
/* Every stroke is drawn in screen pixels, so a line weighs the same on a
   phone as on a wide monitor — the drawing is scaled and cropped by the frame,
   and a stroke that scaled with it would thin out to hair on mobile. */
.blog-cover__svg .art-grid > *,
.blog-cover__svg .art-ripple > *,
.blog-cover__svg .art-plane > * { vector-effect: non-scaling-stroke; }
.blog-cover__svg .art-grid {
  fill: none; stroke: var(--cover-line); stroke-width: 1.2; stroke-opacity: 0.8;
}
/* The ripple breathes too, slowly, as if the floor were water. */
.blog-cover__svg .art-ripple {
  fill: none; stroke: var(--cover-line); stroke-width: 1.1; stroke-opacity: 0.62;
  animation: cover-bob 12s ease-in-out infinite alternate;
}
@keyframes cover-bob {
  from { transform: translate3d(0, 5px, 0); }
  to   { transform: translate3d(0, -9px, 0); }
}
/* The far wall sits behind the headline: it is there so the room has an end,
   not so it can be read. */
.blog-cover__svg .art-far {
  fill: none; stroke: var(--cover-line); stroke-opacity: 0.3;
}
/* And the folded plane drifts, the one element that is not part of the room. */
.blog-cover__svg .art-plane {
  fill: none; stroke: var(--cover-ink); stroke-width: 1.2; stroke-opacity: 0.62;
  animation: cover-drift 17s ease-in-out infinite alternate;
}
@keyframes cover-drift {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-18px, 13px, 0); }
}

/* The travelling light: a dashed second copy of the lattice, laid over the
   solid one. Its dashes run along the paths, which were drawn from the frame
   edge inward, so the glow reads as depth rather than as decoration — the
   room keeps receding. One path carries the whole lattice, so this costs one
   repaint per frame rather than forty. */
.blog-cover__svg .art-flow {
  fill: none; stroke: var(--cover-coral); stroke-width: 2.2; stroke-opacity: 0.9;
  stroke-dasharray: 26px 244px;
  animation: cover-flow 6.4s linear infinite;
}
@keyframes cover-flow { to { stroke-dashoffset: -270px; } }

/* The ripple gets its own sweep, across the rows rather than down them, so the
   surface reads as a sheet of water with something passing over it. */
.blog-cover__svg .art-flow--ripple {
  stroke-dasharray: 72px 328px;
  animation: cover-sweep 7.6s linear infinite;
}
@keyframes cover-sweep { to { stroke-dashoffset: -400px; } }

/* Static dashes over the solid lattice would read as a broken line, so the
   light layer goes entirely, and the room stays still. */
@media (prefers-reduced-motion: reduce) {
  .blog-cover__svg .art-flow { display: none; }
  .blog-cover__svg .art-ripple,
  .blog-cover__svg .art-plane { animation: none; }
}

/* ---- the masthead line, level with the crop of the drawing -------------- */
.blog-cover__bar {
  position: relative; z-index: 1;
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: var(--sc-3) var(--sc-4);
}
.blog-cover__mark {
  margin: 0;
  font-family: var(--sc-font-display); font-weight: 700;
  font-size: 11px; line-height: 1.32;
  letter-spacing: 0.22em; text-transform: uppercase;
  color: var(--cover-ink);
}
/* A thin outline over the artwork, the way the reference banner's subscribe
   pill sits on the frame. It fills with the page's ink on hover, so it is a
   real target with a clear state rather than a decorative outline. */
.blog-cover__cta {
  display: inline-flex; align-items: center; gap: 0.55rem;
  min-height: 40px; padding: 0 var(--sc-4);
  border: 1px solid color-mix(in oklab, var(--cover-ink) 45%, transparent);
  border-radius: var(--sc-r-pill);
  background: color-mix(in oklab, var(--cover-ground) 40%, transparent);
  color: var(--cover-ink);
  font-family: var(--sc-font-mono); font-size: var(--sc-t-xs);
  letter-spacing: var(--sc-track-wide); text-transform: uppercase;
  text-decoration: none; white-space: nowrap;
  backdrop-filter: blur(8px);
  transition: background 160ms var(--sc-ease-out), color 160ms var(--sc-ease-out),
              border-color 160ms var(--sc-ease-out);
}
.blog-cover__cta:hover, .blog-cover__cta:focus-visible {
  background: var(--cover-ink); color: var(--cover-ground); border-color: var(--cover-ink);
}
.blog-cover__cta svg { width: 14px; height: 14px; flex: none; }

/* ---- the copy, centred in the room ------------------------------------- */
.blog-cover__copy {
  position: relative; z-index: 1;
  display: grid; justify-items: center; text-align: center; gap: var(--sc-4);
  max-width: 46rem; margin-inline: auto;
  padding-block: clamp(1.5rem, 4vh, 2.75rem) clamp(1rem, 4vh, 2.25rem);
}
.blog-cover__title {
  margin: 0; max-width: 24ch;
  font-size: clamp(2.05rem, 5.2vw, 4.35rem);
  line-height: 1.02; letter-spacing: -0.022em; text-wrap: balance;
  color: var(--cover-ink);
}
.blog-cover__lede {
  margin: 0; max-width: 33rem;
  font-size: var(--sc-t-base); line-height: 1.55;
  color: var(--cover-soft);
}
.blog-cover__count { margin: 0; color: var(--cover-soft); opacity: 0.82; }

/* A phone crops the drawing sideways rather than top and bottom, so the room
   loses its far left and right and the ripple drifts in from the edge. The
   band keeps its height — the cropped frame needs it to show a run of the
   lattice rather than a few stray lines — and the scrim turns from a centre
   wash into a band, because the copy now covers most of the drawing instead
   of sitting in the middle of it. */
@media (max-width: 640px) {
  .blog-cover__panel { --cover-scrim: rgb(12 4 6 / 0.8); min-height: 30rem; }
  :root[data-theme="light"] .blog-cover__panel { --cover-scrim: rgb(251 248 243 / 0.68); }
  /* A phone squeezes the lattice into a handful of lines down the middle,
     where the copy has to live: the drawing steps back rather than the wash
     going opaque. */
  .blog-cover__svg .art-grid { stroke-opacity: 0.62; }
  .blog-cover__panel::after {
    background:
      linear-gradient(180deg, transparent 8%, var(--cover-scrim) 26%,
                      var(--cover-scrim) 96%, transparent 100%),
      linear-gradient(180deg, var(--cover-ground) 0%, transparent 8%,
                      transparent 92%, var(--cover-ground) 100%);
  }
  .blog-cover__title { max-width: 18ch; }
  .blog-cover__copy { gap: var(--sc-3); }
}

/* ---- the same band, opening a post -------------------------------------- */
/* A post's band is shallower than the hub's. The hub's headline is a title for
   a section and can afford to be a display line; a post's is a full sentence
   about a process, and at the hub's size it would fill the band and push the
   article itself below the fold. The article also has to start soon after it,
   so the band gives up its extra height before it gives up its drawing. */
.blog-cover--post .blog-cover__panel { min-height: clamp(19rem, 42vh, 27rem); justify-content: center; }
/* On a post the headline is the masthead, so the name and the date drop below
   it and step down to reading size. The trail leaves the band altogether and
   sits on the page between the crop and the article, where a section line
   belongs. */
.blog-cover--post .blog-cover__copy { margin-top: 0; }
.blog-cover--post .blog-cover__title {
  font-size: clamp(1.7rem, 3.4vw, 2.85rem); line-height: 1.08; max-width: 30ch;
}
.blog-cover--post .blog-cover__copy { gap: var(--sc-3); }
.blog-cover__byline {
  display: flex; flex-wrap: wrap; align-items: baseline; justify-content: center;
  gap: 0.15rem var(--sc-2);
  margin: var(--sc-2) 0 0;
  font-family: var(--sc-font-mono); font-size: var(--sc-t-xs);
  letter-spacing: var(--sc-track-wide); text-transform: uppercase;
  color: var(--cover-soft);
}
.blog-cover__byline .blog-cover__mark {
  font-family: inherit; font-size: inherit; font-weight: 500;
  letter-spacing: inherit; color: var(--cover-ink);
}
.post--cover > .sc-wrap > .crumbs { margin-top: var(--sc-6); }
/* The article carries the band itself, so its own top padding — which exists
   to clear the fixed bar — would only push the band down the page. */
.post--cover { padding-block: 0 var(--sc-8); }

/* ---- related posts, with the other sleeve behind the card ---------------- */
/* Each card carries the sibling post's cover art behind its own text. At rest
   it is a tint, no more than the card's background; on hover the art comes
   forward, loses its grey and drifts, so the card answers the pointer the way
   the rest of the blog does. The art is a child of the link rather than a
   background on it, which keeps it inside the card's radius and lets the
   browser scale it on the compositor instead of repainting the card. */
.related__item {
  position: relative; isolation: isolate; overflow: hidden;
  transition: border-color 220ms var(--sc-ease-out), transform 220ms var(--sc-ease-out);
}
.related__art {
  position: absolute; inset: 0; z-index: -1; display: block;
}
.related__art img {
  width: 100%; height: 100%; object-fit: cover;
  opacity: 0.3;
  transform: scale(1.04); transform-origin: 50% 38%;
  filter: saturate(0.3) contrast(0.85);
  transition: opacity 320ms var(--sc-ease-out), transform 700ms var(--sc-ease-out),
              filter 700ms var(--sc-ease-out);
}
/* A scrim in the page's own ground, so the small print stays readable over
   whatever the sibling's sleeve happens to look like, on either theme. */
.related__art::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(180deg,
    color-mix(in oklab, var(--sc-canvas) 86%, transparent) 0%,
    color-mix(in oklab, var(--sc-canvas) 72%, transparent) 52%,
    color-mix(in oklab, var(--sc-canvas) 90%, transparent) 100%);
}
.related__item:hover, .related__item:focus-visible {
  border-color: var(--sc-accent); transform: translate3d(0, -2px, 0);
}
.related__item:focus-visible { outline: 2px solid var(--sc-accent); outline-offset: 2px; }
.related__item:hover .related__art img,
.related__item:focus-visible .related__art img {
  opacity: 0.88;
  transform: scale(1.13) translate3d(-2%, -3%, 0); filter: none;
}

@media (prefers-reduced-motion: reduce) {
  .related__item, .related__art, .related__art img { transition: none; }
  .related__item:hover, .related__item:focus-visible { transform: none; }
}

/* ---- the chart in the side column ---------------------------------------- */
/* Ten rows, each a play key for the shared preview player. The rows stay tight
   — this card sits in a rail beside the text, not in the article — so a sleeve
   would crowd it: a small round key, the name, and the running time are enough
   to say what the thing is. */
.side-tracks { list-style: none; margin: var(--sc-3) 0 0; padding: 0; }
.side-tracks li {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center; gap: var(--sc-2) var(--sc-3);
  padding: var(--sc-2) 0;
  border-top: 1px solid var(--sc-hairline);
}
.side-tracks li:first-child { border-top: 0; padding-top: 0; }
.side-track__play {
  width: 30px; height: 30px; display: grid; place-items: center;
  padding: 0; cursor: pointer; border-radius: var(--sc-r-pill);
  border: 1px solid var(--sc-hairline-strong);
  background: transparent; color: var(--sc-ink);
  transition: background 160ms var(--sc-ease-out), border-color 160ms var(--sc-ease-out),
              color 160ms var(--sc-ease-out), transform 120ms var(--sc-ease-out);
}
.side-track__play svg { width: 12px; height: 12px; }
.side-track__play:hover { border-color: var(--sc-accent); color: var(--sc-accent); }
.side-track__play:active { transform: scale(0.94); }
.side-track__play:focus-visible { outline: 2px solid var(--sc-accent); outline-offset: 2px; }
.side-track__play[data-state="playing"] {
  background: var(--sc-accent); border-color: var(--sc-accent); color: var(--sc-accent-ink);
}
.side-track__name {
  min-width: 0; font-size: var(--sc-t-sm); font-weight: 600; line-height: 1.35;
  letter-spacing: var(--sc-track-snug);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.side-track__dur {
  font-family: var(--sc-font-mono); font-size: var(--sc-t-xs);
  color: var(--sc-ink-soft); font-variant-numeric: tabular-nums;
}
/* Below 960px the rail stacks under the article, where the pointer is a thumb. */
@media (max-width: 960px) {
  .side-track__play { width: 40px; height: 40px; }
  .side-track__play svg { width: 14px; height: 14px; }
}
@media (prefers-reduced-motion: reduce) {
  .side-track__play { transition: none; }
  .side-track__play:active { transform: none; }
}
