/* Michael Victory — site. Ink leads; the About section is the light exception.
   Every colour and every size comes from styles.css. */
*{box-sizing:border-box}
/* --veil lives on html, not body: the tuning panel sets it as an inline style on
   documentElement, and a declaration on body would shadow the inherited value
   and silently swallow every change. */
/* :root, NOT html — and that is load-bearing, not style. :root is a pseudo-class
   at specificity (0,1,0); html is a type selector at (0,0,1). styles.css declares
   these on :root, so an html{} block here LOSES to it no matter which stylesheet
   loads later. This block was written as html{} and the --t-hero override below
   therefore never applied: the hero rendered at the foundation's 110px, and only
   looked like 68px because the tuning panel wrote an inline style over the top.
   Delete site-tune.js before launch — as intended — and the hero silently jumped
   back to 110px. Matching the selector makes the site's own rung real without the
   panel present. */
/* --veil baked to 63% 2026-07-26 from Mike's saved panel values. Lower veil =
   LESS white over the footage, so more of the frame shows; it is paired with
   --fx-fade .78 below, which flattens that extra image so the type still wins.
   Declared on :root, not html — :root is (0,1,0) and beats html's (0,0,1), so a
   later html{} rule for this property would silently lose. */
:root{scroll-behavior:smooth;--veil:63%;
  /* The site's display rung is the SMALLER of the ladder's two. Declared as a
     token override so the h1 keeps governing off --t-hero and the type-scale
     presets have one variable to drive. */
  --t-hero:var(--t-step);
  /* One light weight for all display type. 300 rather than 100: at 26px a 100
     stem is 0.73px, under the ink floor, while 300 measures 1.43px. */
  --w-display-light:300}
body{margin:0;background:var(--ink);color:var(--on-ink);--edge:color-mix(in srgb,var(--on-ink) 22%,transparent)}
img{max-width:100%;display:block}
a{color:var(--signal);text-decoration:none;border-bottom:1px solid currentColor;padding-bottom:1px}
a:hover{color:var(--on-ink)}
.hair{display:block;width:100%;height:1px;border:0;margin:0;background:var(--edge)}
.mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;letter-spacing:-.02em}
.quiet{color:var(--on-ink-dim)}
.sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}

/* ---------- nav: absent until the hero is behind you ---------- */
/* Concealed means concealed to everyone: transform alone leaves these five
   links in the tab order, painted above the viewport with no visible focus. */
.nav{position:fixed;top:0;left:0;right:0;z-index:40;display:flex;align-items:center;justify-content:space-between;
  gap:26px;padding:14px var(--axis);background:color-mix(in srgb,var(--ink) 88%,transparent);
  backdrop-filter:blur(8px);box-shadow:0 1px 0 var(--edge);
  transform:translateY(-100%);visibility:hidden;pointer-events:none;
  transition:transform .45s cubic-bezier(.2,.7,.2,1),visibility .45s}
.nav.on{transform:none;visibility:visible;pointer-events:auto}
.nav img{height:22px;width:auto}
.nav ul{display:flex;gap:26px;list-style:none;margin:0;padding:0}
.nav a{font-size:var(--t-cap);font-weight:var(--w-bold);text-transform:uppercase;letter-spacing:.14em;
  color:var(--on-ink);border-bottom:1px solid transparent;padding-bottom:2px}
.nav a:hover{color:var(--signal);border-bottom-color:currentColor}

/* ---------- footage: one clip per panel ----------
   Every panel owns its own optimised video, sitting inside the panel rather than
   behind the whole document. It plays only while its panel is on screen, so the
   page is never decoding five clips at once, and each panel can be graded and cut
   for the content it carries.
   The veil is a child of the footage layer, not a background on the panel: a
   background paints BEHIND its own children, so a panel-level background would
   bury the video it is supposed to sit over. */
.vid{position:absolute;inset:0;z-index:0;overflow:hidden;
  background:radial-gradient(120% 90% at 50% 30%,#ffffff 0%,#f4f2ee 52%,#eceae4 100%)}
.vid-el{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;
  opacity:0;transition:opacity 1.1s linear;will-change:transform}
.vid.has .vid-el{opacity:1}
.vid-veil{position:absolute;inset:0;display:block;pointer-events:none;
  background:color-mix(in srgb,var(--ink) var(--veil),transparent)}
.page{position:relative;z-index:1}

/* the hero is a type lockup over the ground. The scrim's left column runs to
   effectively solid ink behind the text, because the foundation is explicit that
   body copy never sits over a moving frame — the footage stays visible above and
   to the right of the column, not under the paragraph. */
/* `gap` is load-bearing, not cosmetic. With space-between alone the clearance
   between the logo row and the lockup is LEFTOVER SPACE — so it is generous at
   the shipping rung (measured 71px at 68px type) and collapses to exactly 0 once
   the type grows enough to consume the remainder, at which point the headline
   butts into the row above and reads as clipped. text-box-trim compounds it by
   removing the half-leading that used to disguise the collision.
   gap sets a FLOOR that space-between then distributes extra space on top of, so
   the rhythm can grow but never vanish. At the largest rungs the hero simply
   becomes taller than one screen, which is honest — the type genuinely does not
   fit, and scrolling is the correct answer to that rather than a collision. */
.hero{position:relative;min-height:100svh;display:flex;flex-direction:column;justify-content:space-between;
  gap:var(--gap-block);
  padding:var(--axis);background:var(--ink);--on-ink-dim:var(--on-ink)}
/* the hero's footage carries no flat veil — this directional scrim is its veil,
   which is why it has to sit ABOVE the clip rather than behind it */
.hero > .vid{z-index:0}
.hero > .hero-top,.hero > .hero-mid,.hero > .hero-bot{position:relative;z-index:2}
.hero::before{content:'';position:absolute;inset:0;z-index:1;pointer-events:none;
  background:linear-gradient(96deg,color-mix(in srgb,var(--ink) 96%,transparent) 0%,
      color-mix(in srgb,var(--ink) 92%,transparent) 44%,
      color-mix(in srgb,var(--ink) 40%,transparent) 66%,transparent 82%),
    linear-gradient(180deg,color-mix(in srgb,var(--ink) 78%,transparent) 0%,transparent 26%),
    linear-gradient(0deg,color-mix(in srgb,var(--ink) 70%,transparent) 0%,transparent 20%)}
.hero-top{display:flex;flex-wrap:wrap;gap:26px var(--gap-block);align-items:flex-start;justify-content:space-between}
.hero-logo{width:min(16rem,30vw);height:auto}
/* The locator: map above the text, both bound to the text's width. The map is
   1.19:1, so this width is what keeps the lockup inside one viewport. */
/* THE STATION PLATE.
   White ink has nowhere to live on this site: the ground is white and the veils
   paint white, so the only place light type can sit is over unveiled footage —
   which is bright. Measured under this exact locator, white ran 1.09:1 at the
   brightest pixel. A halo patches that; a plate solves it, and a plate is what
   transit signage actually is. The type is not floating over a photograph, it is
   mounted on something.

   B Line #333333 is the only one of the three Vignelli greys that can carry white
   type: white measures 12.63:1 on it, against 3.25:1 on Stations #928E8B and
   2.94:1 on L Line #949797. Both of those fail small text outright, so the choice
   is the set's own ink rather than a preference. The pink marker holds too —
   #DA39AF on #333333 is 3.12:1, over the 3:1 bar a graphic needs.

   Square corners, no radius: Vignelli's system has no rounded plates. */
/* min-width:min-content is what stops "VANCOUVER BC" hanging off the plate.
   28vw is 77px of content box at phone width, and that token is unbreakable —
   the &nbsp; holding city to province is deliberate — so at 124.8px it simply
   overflowed the charcoal by 47.7px and sat on the footage.
   Sizing the floor off min-content rather than picking a bigger vw means the
   plate fits whatever the label says: change the city, or the type ladder, and
   it still fits. The vw cap keeps it from dominating a wide hero. */
.hero-loc{display:flex;flex-direction:column;align-items:stretch;gap:10px;
  width:min(10.5rem,28vw);min-width:min-content;padding:14px;background:var(--line-b);
  text-align:right;box-shadow:0 2px 14px -4px rgba(0,0,0,.45)}
/* Shading, now that there is a surface to shade against. Restrained — the plate
   is doing the contrast work, so this only has to give the white a little relief
   rather than rescue it. */
.hero-loc .label,.hero-loc .label.quiet{line-height:1.5;max-width:none;color:#fff;
  text-shadow:0 1px 2px rgba(0,0,0,.55)}
.hero-loc img{width:100%;height:auto;
  filter:drop-shadow(0 1px 2px rgba(0,0,0,.5))}

/* The lockup runs on the ladder's own rungs: the display, sub, heading and body
   classes each carry their gap, so no extra spacing is added here. No max-width
   on the group — the body measure belongs to the paragraph (styles.css gives p
   its own), and clamping the whole lockup to it wraps the display rungs.
   The name sits on --t-step (68px), the smaller of the ladder's two display
   rungs, not --t-hero (110px). */
.hero-mid{max-width:none}
.hero-mid h1{text-transform:uppercase}
/* Weight 300 is not one of the weight tokens (--w-body 400, --w-bold 700), but at
   40px+ the stem clears the ink floor comfortably. The presets still swap to bold
   if the rung resolves small. */
.hero-mid .display-sub{font-weight:var(--sub-weight,var(--w-display-light))}
.hero-mid > :last-child{margin-bottom:0}
.hero-bot{display:flex;flex-direction:column;gap:12px;align-items:flex-start}

/* ---------- sections: panels that pass OVER the ground, not erase it ----------
   A thin veil and quiet colour are mutually exclusive: over a bright graded frame
   there is no lightness left to be quiet with, and the goldenrod accent fails
   outright (4.43:1 at 78%, 2.85:1 at 60%). So inside these panels the neutral inks
   carry the text at full strength and subordinate themselves by case, tracking,
   measure and position — never by lightness — while anything that NEEDS the accent
   gets its own opaque fill instead of borrowing the panel's. Measured over the
   worst-case bright frame at the 60% floor: --on-ink 5.45:1, --on-paper 6.00:1. */
.sec{position:relative;padding:var(--gap-block) var(--axis);--on-ink-dim:var(--on-ink);background:none}
.sec > *:not(.vid){position:relative;z-index:1}
/* Links carry themselves with weight and a rule, not with the accent, which is
   unreadable at a thin veil. Goldenrod returns on hover, where it is a signal
   rather than the only thing holding the link up. Button-styled anchors are
   excluded: they own an opaque fill and their own colour pairing. */
.sec a:not(.buy){color:var(--signal);text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:3px;border-bottom:0}
.sec a:not(.buy):hover{color:var(--on-ink)}
.sec-head{display:flex;flex-direction:column;gap:var(--gap-sub);margin-bottom:var(--gap-block)}
.sec-top{display:flex;flex-wrap:wrap;gap:8px var(--gap-block);align-items:baseline;justify-content:space-between}
/* Section markers run large and light. On --t-sub, not a literal 40px: a fixed
   size sits outside the type-scale presets, so the bump would shrink every other
   rung and leave the eyebrow the biggest type on the page. */
.sec-top .label:first-child{font-size:var(--t-sub);font-weight:var(--w-display-light);line-height:1.1}
.sec-h{font-size:var(--t-sub);font-weight:var(--w-bold);line-height:1;letter-spacing:-.025em;margin:0}
.sec-p{font-size:var(--t-body);line-height:1.55;max-width:var(--measure);margin:0;text-wrap:pretty}

/* ---------- music ---------- */
/* Secondary text in the panels leans on case, tracking and measure rather than
   lightness, so nothing here needs a tint to sit back. */
.rel{display:grid;grid-template-columns:minmax(260px,380px) 1fr;gap:var(--gap-block)}
.rel-art{position:relative;aspect-ratio:1;background:var(--ink-2);box-shadow:inset 0 0 0 1px var(--edge);
  display:grid;place-items:center;overflow:hidden}
.rel-art img{width:100%;height:100%;object-fit:cover}
.rel-art .await{padding:26px;text-align:center;font-size:var(--t-cap);color:var(--on-ink-dim);text-wrap:pretty}
.rel-art .await b{display:block;font-weight:var(--w-bold);color:var(--signal);margin-bottom:6px}
.rel-art[data-has] .await{display:none}
.rel-body{display:flex;flex-direction:column;gap:var(--gap-sub);min-width:0}
.rel-title{font-size:var(--t-sub);font-weight:var(--w-bold);line-height:1;letter-spacing:-.025em;margin:0}
.rel-facts{display:grid;grid-template-columns:auto 1fr;gap:6px 26px;font-size:var(--t-cap);margin:0}
.rel-facts dt{font-weight:var(--w-bold);text-transform:uppercase;letter-spacing:.14em;white-space:nowrap}
.rel-facts dd{margin:0;color:var(--on-ink-dim)}
.buy{align-self:flex-start;display:inline-flex;align-items:center;gap:12px;font-size:var(--t-cap);font-weight:var(--w-bold);
  text-transform:uppercase;letter-spacing:.14em;padding:13px 24px;border:0;cursor:pointer;font-family:inherit;
  /* A button label is a single unit — it never breaks. Space Grotesk runs wider than Barlow
     wider than Barlow at the same rung, which pushed "Buy the CD — $23.23 CAD"
     onto two lines the moment the face changed. */
  white-space:nowrap;
  color:var(--on-ink);
  /* THE FILL SWEEP — replaces the magnetic pull as the CTA's attention move.
     Magnetic separated the label from the shell (that lag was the whole effect)
     and at any real strength it reads as the button coming apart. This animates
     one background instead: nothing inside the button moves relative to anything
     else, so there is nothing to pull apart. Charcoal wipes across the cyan from
     the left — the same direction a route travels — and the button reads as
     arming rather than wobbling. */
  background-image:linear-gradient(90deg,var(--on-ink) 0 50%,var(--signal-fill) 50% 100%);
  background-size:200% 100%;background-position:100% 0;
  transition:background-position .34s cubic-bezier(.22,.8,.28,1),
             color .34s cubic-bezier(.22,.8,.28,1),
             transform .34s cubic-bezier(.22,.8,.28,1)}
/* the lift moves the WHOLE element, so it cannot separate from its own label */
.buy:hover,.buy:focus-visible{background-position:0 0;color:var(--ink);transform:translateY(-1px)}
@media (prefers-reduced-motion:reduce){
  .buy{transition:none}
  .buy:hover,.buy:focus-visible{transform:none}
}
.buy[data-nolink]{background:none;color:var(--on-ink-dim);box-shadow:inset 0 0 0 1px var(--on-ink-dim);cursor:not-allowed}

/* tracklist: number, title, running time */
.tracks{list-style:none;margin:0;padding:0;display:flex;flex-direction:column}
.tracks li{display:grid;grid-template-columns:3ch 1fr auto;gap:16px;align-items:baseline;padding:11px 0;
  box-shadow:inset 0 -1px 0 var(--edge)}
.tracks li:first-child{box-shadow:inset 0 1px 0 var(--edge),inset 0 -1px 0 var(--edge)}
.t-n{font-size:var(--t-cap);font-weight:var(--w-bold);color:var(--on-ink-dim);font-variant-numeric:tabular-nums}
.t-name{font-size:var(--t-body);min-width:0}
.t-dur{font-size:var(--t-cap);color:var(--on-ink-dim);font-variant-numeric:tabular-nums}
/* Holes must look like holes. Lightness is unavailable at a thin veil, so an
   unfilled slot borrows the page's existing "needed" idiom: signal colour, upper
   case and tracking — the same language as the disabled Bandcamp button. */
/* The chips bring their own opaque ground, so the accent never has to survive
   the panel alpha — goldenrod on solid ink is 6.9:1 at any veil setting. */
.tracks li[data-await] .t-name{display:inline-block;justify-self:start;color:var(--signal);
  font-size:var(--t-cap);font-weight:var(--w-bold);text-transform:uppercase;letter-spacing:.14em;
  line-height:1;padding:6px 10px;background:var(--ink);box-shadow:inset 0 0 0 1px var(--signal)}
/* An outlined chip, not another goldenrod label: .earlier b directly above owns
   that treatment, and as adjacent siblings they would merge into one block. */
.earlier .t[data-await]{align-self:flex-start;color:var(--signal);font-size:var(--t-cap);
  font-weight:var(--w-bold);text-transform:uppercase;letter-spacing:.14em;line-height:1;
  padding:7px 11px;background:var(--ink);box-shadow:inset 0 0 0 1px var(--signal)}
/* more than one release, so they need air between them */
.rel + .rel{margin-top:var(--gap-block);padding-top:var(--gap-block);box-shadow:inset 0 1px 0 var(--edge)}

/* earlier work, as Sect */
.earlier{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:26px 42px;margin:0;padding:0}
.earlier div{display:flex;flex-direction:column;gap:6px;padding-top:16px;box-shadow:inset 0 1px 0 var(--edge)}
.earlier b{font-size:var(--t-cap);font-weight:var(--w-bold);text-transform:uppercase;letter-spacing:.14em;color:var(--on-ink)}
.earlier .t{font-size:var(--t-head);font-weight:var(--w-bold);line-height:1.15;letter-spacing:-.005em}
.earlier p{font-size:var(--t-cap);line-height:1.55;color:var(--on-ink-dim);margin:0;text-wrap:pretty}

/* ---------- about: the light exception ----------
   Uses var(--veil) like its siblings so the slider reaches it too. Secondary
   text here is full strength for the same reason it is in the ink panels: over a
   dark frame, muted paper ink fails at 2.4:1 by 60% veil, whereas full-strength
   paper ink holds to roughly 50%. Subordination comes from case and tracking. */
.about{color:var(--on-paper);
  --on-ink-dim:var(--on-paper);
  --edge:color-mix(in srgb,var(--on-paper) 24%,transparent)}
.about a:not(.buy){color:var(--signal);text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:3px}
.about a:not(.buy):hover{color:var(--on-paper)}
.about > .vid{background:var(--chalk-2)}
.about .vid-veil{background:color-mix(in srgb,var(--chalk) var(--veil),transparent)}
.about .sec-p,.about .quiet{color:var(--on-paper)}
.about .sec-top .label{color:var(--on-paper)}
/* the bio sits tighter under its heading than the other sections — --gap-hero
   rather than --gap-block, which is the 42px you pulled it up by */
.about .sec-head{margin-bottom:var(--gap-hero)}
.bio{display:grid;grid-template-columns:1fr minmax(0,42ch);gap:var(--gap-block)}
/* The lead sits on --t-head with the shared light weight — not a 20px literal and
   not a second light weight. */
.bio p.bio-lead{font-size:var(--t-head);font-weight:var(--w-display-light);line-height:1.35;margin:0 0 var(--gap-sub);text-wrap:pretty}
.bio p{font-size:var(--t-body);line-height:1.6;margin:0 0 1em;max-width:var(--measure);text-wrap:pretty}
.bio p:last-child{margin-bottom:0}
/* margin:0 is load-bearing: the UA gives blockquote 1em 40px, which pushes the
   hairline off the page's single left axis and crushes the measure. */
.pull{align-self:start;margin:0;padding-top:16px;box-shadow:inset 0 1px 0 var(--edge)}
/* Weight 100 is only safe at 42px — below that the stem drops under the 1px ink
   floor — so the quote steps DOWN the ladder rather than thinning at a size the
   foundation does not allow. The narrow band is 901–1100px, where the grid is
   still two columns but the second track is too tight for 42px to rag well. */
/* One treatment at two sizes, rather than light-at-42 and bold-at-26. The narrow
   band is 901–1100px, where the grid is still two columns but the second track is
   too tight for 42px to rag well. */
.pull p{font-size:var(--t-sub);font-weight:var(--w-display-light);line-height:1.1;letter-spacing:-.02em;margin:0;text-wrap:balance}
@media (min-width:901px) and (max-width:1100px){
  .pull p{font-size:var(--t-head);line-height:1.25;letter-spacing:-.005em}
}
.pull cite{display:block;font-size:var(--t-cap);font-weight:var(--w-bold);text-transform:uppercase;
  letter-spacing:.14em;color:var(--on-paper);font-style:normal;margin-top:16px}

/* ---------- list + contact ---------- */
.two{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:var(--gap-block)}
.form{display:flex;flex-direction:column;gap:16px;max-width:42ch}
.form label{font-size:var(--t-cap);font-weight:var(--w-bold);text-transform:uppercase;letter-spacing:.14em}
.form input{font:inherit;font-size:var(--t-body);color:var(--on-ink);background:var(--ink-2);
  border:0;box-shadow:inset 0 0 0 1px var(--edge);padding:14px 16px;width:100%}
.form input:focus-visible{outline:1px solid var(--signal);outline-offset:2px}
.form .fine{font-size:var(--t-cap);color:var(--on-ink-dim);margin:0;text-wrap:pretty}
.contact-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:12px;font-size:var(--t-body)}
.contact-list b{display:block;font-size:var(--t-cap);font-weight:var(--w-bold);text-transform:uppercase;
  letter-spacing:.14em;color:var(--on-ink-dim);margin-bottom:2px}

/* ---------- footer ----------
   The only region that always sits directly on moving footage, so it takes the
   same treatment as the panels: neutral inks at full strength, links carried by
   weight and a rule rather than the accent. */
.foot{position:relative;overflow:hidden;background:var(--ink);--on-ink-dim:var(--on-ink)}
.foot a:not(.buy){color:var(--signal);text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:3px;border-bottom:0}
.foot a:not(.buy):hover{color:var(--on-ink)}
.foot-stage{position:relative;min-height:420px;display:flex;flex-direction:column;justify-content:flex-end}
/* 0deg runs bottom-to-top. The strong end has to cover the whole content band,
   not just the last few pixels: the wordmark sits 103-199px off the bottom of a
   420px stage, and at the old 30%-at-58% stop it was standing on almost bare
   footage at 2.57:1. Strong ink now holds to 46%, past the top of the mark, and
   the loop reads in the clear upper half. Cream over the worst-case bright frame
   measures 9.8:1 anywhere content sits. */
.foot-scrim{position:absolute;inset:0;z-index:1;pointer-events:none;
  background:linear-gradient(0deg,color-mix(in srgb,var(--ink) 92%,transparent) 0%,
    color-mix(in srgb,var(--ink) 88%,transparent) 46%,
    color-mix(in srgb,var(--ink) 26%,transparent) 76%,transparent 100%)}
.foot-in{position:relative;z-index:3;padding:var(--gap-block) var(--axis) var(--axis);display:flex;flex-direction:column;gap:var(--gap-sub)}
.foot-mark{display:flex;flex-wrap:wrap;align-items:flex-end;gap:26px var(--gap-block);justify-content:space-between}
.foot-mark img{width:min(38vw,300px);height:auto}
.foot-meta{display:flex;flex-wrap:wrap;gap:8px 42px;font-size:var(--t-cap);color:var(--on-ink-dim)}
/* ---------- text transitions ----------
   The keyframes and the curves are the ones the Text reveal and Rules-as-grouping
   cards use, at the sizes they use them: a masked 110% rise at 880ms, a 0.5rem
   lift at 700ms, a clip wipe at 760ms, a hairline drawing from scaleX(0) at 850ms.
   Delays are folded in through --rv-d rather than declared separately, because the
   animation shorthand would wipe an animation-delay declaration.

   The hidden states only apply while html carries data-rvon — set by the reveal
   script alone — so with no JS, or under reduced motion, every line is present. */
@keyframes rv-rise{from{transform:translate3d(0,110%,0);opacity:0}to{transform:translate3d(0,0,0);opacity:1}}
@keyframes rv-fade{from{transform:translate3d(0,.5rem,0);opacity:0}to{transform:translate3d(0,0,0);opacity:1}}
@keyframes rv-wipe{from{clip-path:inset(-30% 100% -30% -30%)}to{clip-path:inset(-30%)}}
@keyframes rv-draw{from{transform:scaleX(0)}to{transform:scaleX(1)}}
/* Two additions to the card set, both chosen because they suit type over film.
   BLUR resolves out of defocus — the same gesture as a lens finding focus, which
   is the one piece of camera language this site can borrow without pretending the
   type is a physical object. SETTLE opens tracked-out and tightens to its real
   letter-spacing; it reads as the word composing itself rather than moving, so it
   works where a translation would fight the footage drifting behind it. */
@keyframes rv-blur{from{filter:blur(12px);opacity:0}to{filter:blur(0);opacity:1}}
@keyframes rv-settle{from{letter-spacing:.34em;opacity:0}to{opacity:1}}
html[data-rvon] .rv{--rv-t:780ms;--rv-d:0ms}
html[data-rvon] .rv[data-m=fade],html[data-rvon] .rv[data-m=rise],html[data-rvon] .rv[data-m=lines],
html[data-rvon] .rv[data-m=blur],html[data-rvon] .rv[data-m=settle]{opacity:0}
html[data-rvon] .rv[data-m=blur].in{animation:rv-blur calc(var(--rv-t) * 1.05) cubic-bezier(.22,.8,.28,1) var(--rv-d) both}
/* Tracking animates the box width, so this is only ever applied to display-level
   type where the line has room to breathe; on a paragraph it would reflow. The
   role map is what keeps it there. */
html[data-rvon] .rv[data-m=settle].in{animation:rv-settle calc(var(--rv-t) * 1.2) cubic-bezier(.16,.84,.28,1) var(--rv-d) both}
html[data-rvon] .rv[data-m=wipe]{clip-path:inset(-30% 100% -30% -30%)}
html[data-rvon] .rv[data-m=draw]{transform:scaleX(0);transform-origin:0 50%}
html[data-rvon] .rv[data-m=fade].in{animation:rv-fade var(--rv-t) cubic-bezier(.22,.8,.28,1) var(--rv-d) both}
html[data-rvon] .rv[data-m=wipe].in{animation:rv-wipe calc(var(--rv-t) * .975) cubic-bezier(.22,.8,.28,1) var(--rv-d) both}
html[data-rvon] .rv[data-m=draw].in{animation:rv-draw calc(var(--rv-t) * 1.09) cubic-bezier(.16,.84,.28,1) var(--rv-d) both}
/* the block rise needs its own mask, and gives it back the moment it lands — a
   permanent overflow:hidden would clip a focus ring or a hover shadow forever */
html[data-rvon] .rv[data-m=rise]{overflow:hidden}
html[data-rvon] .rv[data-m=rise].in{animation:rv-rise calc(var(--rv-t) * 1.13) cubic-bezier(.16,.86,.24,1) var(--rv-d) both}
/* per-line masks: the line clips, the inner span rises inside it. The
   padding/negative-margin pair keeps descenders off the clip edge at rest. */
html[data-rvon] .rv[data-m=lines]{opacity:1}
html[data-rvon] .rv[data-m=lines] .rv-line{display:block;overflow:hidden;padding-bottom:.14em;margin-bottom:-.14em}
html[data-rvon] .rv[data-m=lines] .rv-line > i{display:block;font-style:inherit;opacity:0}
html[data-rvon] .rv[data-m=lines].in .rv-line > i{font-style:inherit;
  animation:rv-rise calc(var(--rv-t) * 1.13) cubic-bezier(.16,.86,.24,1) calc(var(--rv-d) + var(--rv-ld,0ms)) both}
/* .done is the finished state and has to outrank the .in animation shorthand that
   put the element there — hence the extra [data-m] on the selector: without it the
   `both` fill on a paused animation clock keeps painting the FROM frame. */
html[data-rvon] .rv.done[data-m]{animation:none;opacity:1;transform:none;clip-path:none;overflow:visible}
html[data-rvon] .rv.done[data-m] .rv-line > i{animation:none;opacity:1;transform:none}

@media (max-width:900px){
  .rel{grid-template-columns:1fr}
  .bio{grid-template-columns:1fr}
  .nav ul{gap:16px}
}
@media (max-width:560px){
  .nav ul{display:none}
}
@media (prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
}

/* ==========================================================================
   ATMOSPHERE — grain, vignette, and the footage grade
   From the web-effects library (08 · Texture · film-grain-overlay), adapted.

   These live HERE, in the site, not in the tuning panel. The panel only writes
   the custom properties below; delete site-tune.js before launch and every one
   of these keeps rendering at whatever value was baked in. An effect that only
   exists while its dial is loaded is not an effect, it is a demo.

   Everything here is off or neutral by default. Film grain used to ship at .10
   and has been removed entirely — see the note on the tokens below.
   ========================================================================== */
html{
  /* FILM GRAIN REMOVED 2026-07-26. It existed to stop large flat near-black
     fields from banding and to give the footage a projected finish — both
     arguments belong to the dark film direction. On a white transit ground it
     does the opposite: it fights the "crisp, high-definition" requirement and
     muddies the hairlines the diagrams are made of. The whole layer, its
     keyframes and its three controls are gone rather than defaulted to zero, so
     nothing can quietly switch it back on. */
  /* GRADE BAKED 2026-07-26 from Mike's saved panel values on staging
     (mv-site-tune-v3: veil 63, fade 78, sat 70, soft 5, progress 1).
     These are now the authored look, not a default waiting to be dialled — the
     tuning panel writes the same properties, so it can still move them, and
     deleting site-tune.js changes nothing. */
  --fx-vignette:0;       /* 0 → .7 edge darkening                                */
  --fx-fade:.78;         /* 0 → 1 — narrows the footage's dynamic range          */
  --fx-sat:.7;           /* footage saturation                                   */
  --fx-soft:5px;         /* footage blur — pushes the frame back behind the type */
  --fx-magnet:0;         /* 0 → 1 pointer lean on buttons (desktop only)         */
  --fx-progress:1px;     /* 0 → 4px scroll-progress hairline                     */
}

/* ---------- scroll progress — web-effects 01 · Scroll · css-scroll-driven ----
   animation-timeline: scroll() means the browser drives this off the scroller
   itself: no listener, no rAF, nothing to throttle, and it cannot fall out of
   sync with the page. Where it is unsupported the bar simply keeps its scaleX(0)
   and is invisible — which is why height, not opacity, is the off switch. */
/* F Line magenta, NOT the cyan accent. Two reasons, and the second is the good
   one. First: a progress rule and a call to action are different jobs, and giving
   them the same colour teaches the reader that cyan means both "act" and "you are
   here". Second: F Line is one of the two TRAP colours — nothing is legible on it,
   so it can never be a button or a chip. A hairline is pure fill with nothing on
   it, which makes it the one place a trap colour costs nothing to spend. Magenta
   also sits opposite cyan on the wheel, so the two never read as a set. */
.progress{position:fixed;inset:0 0 auto 0;height:var(--fx-progress);z-index:70;
  background:var(--line-f);transform:scaleX(0);transform-origin:0 50%;pointer-events:none}
@supports (animation-timeline:scroll()){
  .progress{animation:progress-grow linear;animation-timeline:scroll(root block)}
}
@keyframes progress-grow{from{transform:scaleX(0)}to{transform:scaleX(1)}}

/* One element, two fixed layers: vignette below, grain above. pointer-events is
   load-bearing — this sits over the nav, and without it the whole page is dead
   to the mouse. */
.atmos{position:fixed;inset:0;z-index:60;pointer-events:none}

/* Vignette. A radial darkening that also buys back contrast at the margins,
   which is exactly where the hero's directional scrim is weakest. */
.atmos::before{content:"";position:absolute;inset:0;display:block;
  background:radial-gradient(120% 85% at 50% 45%,transparent 42%,
    color-mix(in srgb,#000 calc(var(--fx-vignette) * 100%),transparent) 100%)}

/* Atmosphere is a screen finish; it muddies print and wastes toner. */
@media print{.atmos{display:none}}

/* The footage grade. Fade narrows the dynamic range — the "full bleed faded /
   narrow dynamic range" look — by pulling contrast down and lifting the blacks
   slightly, rather than by dropping opacity, which would just show more ink.

   --fx-vscale is the blur's scale compensation, and it belongs HERE rather than
   in the drift code: a filtered element fades out over roughly 1.5x its blur
   radius, so a softened frame shows a feathered edge unless it is grown past its
   panel. Drift could do that arithmetic, but drift never runs under reduced
   motion, which would leave exactly the visitors who disabled animation looking
   at a broken edge. 3r of cover against a conservative 600px panel — over-
   scaling a tall panel only crops a few more pixels, under-scaling shows the
   seam, so the constant errs the safe way. When drift IS running it composes
   the same base into its inline transform. */
.vid-el{filter:saturate(var(--fx-sat))
              contrast(calc(1 - var(--fx-fade) * .45))
              brightness(calc(1 + var(--fx-fade) * .10))
              blur(var(--fx-soft));
        transform:scale(var(--fx-vscale,1.025))}


/* A sleeve can be faded back when it is louder than its place in the hierarchy.
   Opacity rather than a filter: on a white ground the page itself is what shows
   through, so the art recedes toward the paper instead of turning grey. */
.rel-art--fade img{opacity:var(--art-fade,.62)}

/* The hero's second heading in F Line pink. It sits at --t-head (22.8px) and
   bold, which clears WCAG's large-text bar at 3:1 — the authored #DA39AF measures
   4.05:1 on white and is legitimate here. It would NOT be at label size, which is
   why the locator below uses the darkened cut instead. */
.heading--pink{color:var(--pink)}
/* the divider is structure, not speech — it should not carry the same weight as
   the two counts it separates */
.heading--pink .bar{color:var(--on-ink-dim);font-weight:var(--w-body);
  padding:0 .18em;opacity:.55}

/* With the list hidden for launch, .two has one visible child and auto-fit hands
   it the whole row — a two-line contact list stretched across 1336px. Cap it at
   the body measure so it keeps the same column width it had beside the form. */
.two:has([data-launch-hidden]) #contact{max-width:var(--measure)}

/* ---------- Sect: records visible, appearances folded ----------
   <details> rather than a scripted toggle. It is keyboard-operable, announced as
   expandable by screen readers, and browsers open it automatically when the user
   runs find-in-page — none of which a div and a click handler give you for free. */
.appears{margin-top:var(--gap-sub);border-top:1px solid var(--edge)}
.appears summary{cursor:pointer;list-style:none;padding:14px 0;display:flex;
  align-items:baseline;justify-content:space-between;gap:16px;
  font-size:var(--t-cap);font-weight:var(--w-bold);text-transform:uppercase;letter-spacing:.14em}
.appears summary::-webkit-details-marker{display:none}
/* the disclosure mark is drawn, not a glyph — a rotating rule reads as transit
   signage where a triangle reads as an operating system */
.appears summary::after{content:"";width:11px;height:1px;background:currentColor;
  flex:0 0 auto;position:relative;transition:transform .3s cubic-bezier(.22,.8,.28,1)}
.appears summary::before{content:"";width:1px;height:11px;background:currentColor;
  flex:0 0 auto;margin-right:-6px;transform-origin:50% 50%;
  transition:transform .3s cubic-bezier(.22,.8,.28,1)}
.appears[open] summary::before{transform:scaleY(0)}
.appears summary:hover{color:var(--signal)}
.appears-yr{font-weight:var(--w-body);color:var(--on-ink-dim);margin-left:auto;padding-right:10px}

.appears-list{list-style:none;margin:0 0 var(--gap-sub);padding:0;
  display:flex;flex-direction:column}
.appears-list li{display:grid;grid-template-columns:4.5ch minmax(0,1.4fr) minmax(0,1fr) minmax(0,1.6fr);
  gap:8px 16px;align-items:baseline;padding:9px 0;font-size:var(--t-cap);
  box-shadow:inset 0 -1px 0 var(--edge)}
.a-y{font-variant-numeric:tabular-nums;font-weight:var(--w-bold)}
.a-t{font-weight:var(--w-bold)}
.a-l{color:var(--on-ink-dim)}
.a-k{color:var(--on-ink-dim);font-style:italic}
.a-k[data-unknown]{opacity:.5;font-style:normal}
@media (max-width:700px){
  .appears-list li{grid-template-columns:4.5ch 1fr;gap:2px 12px}
  .a-l,.a-k{grid-column:2}
}

/* ---------- upcoming: the three EPs ----------
   The masters are 720x1280 portrait. Every tile here is a DOWNSCALE of that and
   is capped so it stays one: max-width holds the tile at 190px however much room
   the row has, because a 720px-wide phone video stretched across a desktop column
   is the one thing this section must never do. The tiles shrink on narrow screens
   and never grow past the cap. */
/* Three EPs across on a desktop, folding to two and then one as the room runs
   out. auto-fit rather than a hard 3-column rule with a breakpoint: the track
   floor is the real constraint (an EP is two tiles wide and stops being legible
   below ~290px), so the layout changes when the CONTENT needs it to rather than
   at a width someone guessed. The 396px ceiling is 2 x 190px tiles + the row
   gap, so a column never grows wider than the artwork it holds and the three
   groups stay evenly spread instead of drifting apart on a wide screen. */
.eps-wrap{display:grid;grid-template-columns:repeat(auto-fit,minmax(290px,1fr));
  gap:var(--gap-block) var(--gap-sub);align-items:start;
  /* 3 x 396px of artwork + 2 gaps. A fixed 396px TRACK cannot be used — three of
     them plus the gaps overflow a 1280 viewport's content box by 27px, so
     auto-fit drops to two columns. Flexible tracks with the grid itself capped
     gets three across at 1280 and stops the columns sprawling on a wide screen. */
  max-width:calc(396px * 3 + var(--gap-sub) * 2)}
.ep{display:flex;flex-direction:column;gap:var(--gap-sub);position:relative}
/* Twelve tiles in a 3x4 grid is a lot to hold at once, so EP 1 and EP 3 take a
   wash and EP 2 stays on the page ground. The rhythm works either way round the
   layout folds: side by side on a desktop it reads as three columns, stacked on
   a phone it reads as three bands.

   Painted by a pseudo-element bled outside the box, NOT by padding on the odd
   ones — padding would shrink those columns' content and the tiles inside them
   would come out smaller than EP 2's, which is the opposite of the alignment
   this is meant to help.

   The tint is the L Line grey at 8%, which lands near #f6f7f7 over white: the
   lightest step that still separates from the ground. Body text over it measures
   16.1:1, so nothing gets harder to read. */
.ep:nth-child(odd)::before{content:'';position:absolute;
  inset:calc(var(--gap-sub) * -1) calc(var(--gap-sub) * -.7);
  background:color-mix(in srgb,var(--line-l) 8%,transparent);z-index:-1}

.ep-head{display:flex;align-items:center;gap:14px}
.ep-n{font-size:var(--t-sub);font-weight:var(--w-bold);line-height:1;letter-spacing:-.025em;margin:0}
/* The EP whose cut is audible names itself in the F Line magenta. At --t-sub
   bold this is large text, where 3:1 is the bar — measured 4.05:1 on white. The
   same magenta is NOT used for the small button text below, where the bar is
   4.5:1 and it would fail. */
.ep.is-live .ep-n{color:var(--pink)}

.ep-side{display:flex;flex-direction:column;gap:10px}
.ep-side-l{font-weight:var(--w-bold);color:var(--on-ink-dim)}
.ep-row{display:flex;gap:var(--gap-sub);align-items:flex-start}

.cut{margin:0;display:flex;flex-direction:column;gap:6px;flex:1 1 0;min-width:0;max-width:190px}
.cut-cap{display:flex;flex-direction:column;gap:1px}
/* Slot label and its spectrum on one line. The label is sized by content and the
   meter takes whatever is left, so a two-character slot and the bars always fill
   the tile width exactly however the type ladder is rescaled. */
/* baseline, not flex-end. A canvas takes its bottom margin edge as its baseline,
   so aligning on the baseline sits the foot of the bars exactly on the foot of
   the "A1" — flex-end would line it up with the bottom of the text's LINE BOX,
   which is the descender depth lower and reads as a misalignment. */
.cut-slot{display:flex;align-items:baseline;gap:9px}
/* the slot IS the title until the tracks are named — so it carries display weight */
.cut-n{font-size:var(--t-sub);font-weight:var(--w-bold);line-height:.95;letter-spacing:-.03em;
  display:block;flex:none;font-variant-numeric:tabular-nums}
/* The two ends of the frequency ramp, read by site-eps.js. Low frequencies take
   the F Line magenta, high frequencies the site's link blue — the blue a visitor
   has already met on every anchor. Both are read from here rather than hard-coded
   in the script, so the palette stays in one place.
   --viz-high is --signal (#007a93, 5.0:1 on white), NOT the brighter CTA fill
   #00add0: that measures 2.66:1 and these are 1-2px bars. */
/* flex-basis 0, not auto: a <canvas> carries an intrinsic 300px default width,
   and an auto basis takes that as its starting size — so the row wants to be
   wider than the tile and the bars run past the right edge of the artwork.
   From zero it takes exactly the space left over and no more. */
.cut-viz{--viz-low:var(--pink);--viz-high:var(--signal);
  flex:1 1 0;width:0;min-width:0;height:22px;display:block}
.cut-t{font-size:var(--t-body);line-height:1.4}
.cut-note{font-size:var(--t-cap);line-height:1.35;color:var(--on-ink-dim);text-wrap:pretty}

.cut-media{position:relative;aspect-ratio:9/16;overflow:hidden;background:var(--ink-2);
  box-shadow:inset 0 0 0 1px var(--edge)}
.cut-el{width:100%;height:100%;object-fit:cover;display:block}
/* an <audio> element has no picture to give; the plate beside it is the picture */
audio.cut-el{display:none}
.cut-media--plate{background:var(--line-b)}
/* The mark stands in for the frame that was never shot, turned on its side and
   run nearly the full height of the plate.
   The 153% is not a guess. The tile is locked to 9:16, so its height is 16/9 of
   its width; filling 86% of that height with a mark rotated a quarter turn means
   a PRE-rotation width of 0.86 x 16/9 = 1.529 of the tile width. Percentages
   resolve against width, so this needs no container queries — but it is only
   correct while .cut-media stays 9:16.
   The logo ships with #1a1612 baked into its paths, so it is used as a MASK and
   the colour comes from the background — an <img> could not be recoloured. */
.cut-logo{position:absolute;top:50%;left:50%;
  transform:translate(-50%,-50%) rotate(-90deg);
  width:153%;aspect-ratio:1133.99/359.97;
  background:var(--station);
  -webkit-mask:url(assets/logo.svg) center/contain no-repeat;
          mask:url(assets/logo.svg) center/contain no-repeat}

/* Anything qualifying the tile sits under it, so one annotated cut cannot shove
   its artwork out of line with the three beside it. */
.cut-below{display:flex;flex-direction:column;gap:2px;margin-top:2px}
.cut-kind{font-weight:var(--w-bold);color:var(--on-ink)}

/* The live tile is outlined rather than tinted: a tint over footage changes what
   the footage looks like, which is the one thing a picker must not do.
   outline, not an inset box-shadow. The tile's width is fractional (flex of a
   190px cap), and an inset shadow is rasterised inside that fractional box, so
   its edges land on different subpixels and read as 1px on two sides and 3px on
   the others. An outline is drawn on the border box and stays even; the negative
   offset pulls it inside so it does not shift the layout. It also replaces the
   1px edge rather than sitting on top of it, so the live tile does not gain a
   second hairline. */
.cut.is-live .cut-media{outline:1px solid var(--pink);outline-offset:-1px;box-shadow:none}

/* Charcoal fill, white text — 12.63:1 — in every state. The pink lives in the dot
   and the outline, never under the words: white on this magenta measures 4.05:1
   and the label is small text, which needs 4.5:1. */
.cut-sound{position:absolute;left:8px;bottom:8px;display:inline-flex;align-items:center;gap:7px;
  padding:6px 10px;border:0;cursor:pointer;background:var(--line-b);color:#fff;
  font-family:inherit;font-size:var(--t-cap);font-weight:var(--w-bold);
  letter-spacing:.08em;text-transform:uppercase;line-height:1}
.cut-sound:hover{background:#000}
.cut-sound:focus-visible{outline:2px solid var(--pink);outline-offset:2px}
.cut-ico{width:9px;height:9px;border-radius:50%;background:#fff;flex:none;display:block}
/* 3.12:1 against the charcoal fill — above the 3:1 bar for a graphical object */
.cut-sound[aria-pressed="true"] .cut-ico{background:var(--pink)}

/* THE STATION PLATE ON A PHONE.
   At 375px the plate was 152.8 x 184.6 — 41% of the screen's width and the
   loudest thing above the name. Everything here scales together, because the
   plate has no width of its own: min-width:min-content means the LABEL sets it,
   so shrinking the type is what shrinks the plate, and the map follows at 100%.

   How small it can honestly go: "VANCOUVER BC" is one unbreakable token, and at
   14px it is 124.8px. Even at 9px with the tracking pulled in it is 71.6px,
   which with padding still makes an 85.6px plate — so a literal half-width 76px
   plate is not reachable without type below 9px. 10px with .06em lands the plate
   at ~95px and ~116px tall: 62% of the width and 39% of the AREA, which is the
   "half the size" you actually see. Going further means shortening the label,
   which is Mike's copy, not mine to cut. */
@media (max-width:640px){
  .hero-loc{width:min(6rem,26vw);padding:8px;gap:6px}
  .hero-loc .label,.hero-loc .label.quiet{font-size:10px;letter-spacing:.06em;line-height:1.35}
}

@media (max-width:640px){
  .ep-head{gap:10px}
  .ep-row{gap:12px}
  .cut-n{font-size:var(--t-head)}
  .cut-slot{gap:7px}
  .cut-viz{height:17px}
}
/* Reduced motion is handled in site-eps.js, not here: the tile has to keep
   showing its poster frame, and a <video> only paints its poster while it is
   NOT playing. So the fix is to never start it — display:none on the element
   would leave an empty box, since nothing sets that poster as a background. */
