/* RAKS — home page
   Implemented from the "RAKS Home.dc.html" design canvas.
   The canvas held two artboards (1440 desktop / 390 mobile); they are the same
   page at two breakpoints, so they collapse into one responsive document here. */

:root {
  --ink:       #0B0B0B;   /* page ground */
  --paper:     #F2F0EA;   /* primary text */
  --accent:    #FF3B14;
  --accent-hi: #ff5a3a;
  /* The accent asleep: #FF3B14 at 12% over the ink ground, flattened to an
     opaque value so it can sit behind text without stacking up. Idle spectrum
     bars and the hovered/playing row band use it. */
  --accent-dim:  #28110C;
  /* The same at 30%, for the scrub preview. The preview only ever shows over
     the hovered row's band, which is already the 12% tint — drawn in the same
     value it vanished into it. */
  --accent-scrub: #54190E;
  --accent-glow: rgba(255, 59, 20, 0.06);   /* gradient stop only */
  --rule:      #1f1f24;   /* hairlines between mix rows */
  --bar:       #303038;   /* unplayed waveform */
  --muted:     #86868f;
  --faint:     #55555d;

  --gutter: 48px;
  --shell: 1440px;

  --font-display: 'Anton', 'Arial Narrow', Impact, sans-serif;
  --font-body: 'Darker Grotesque', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

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

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  min-height: 100vh;
  font-family: var(--font-body);
  color: var(--paper);
  background-color: var(--ink);
  background-attachment: fixed;
  overflow-x: hidden;
}

/* The canvas exposed `texture` as an editable prop — the three options are kept
   as body attributes so the choice survives into the build. */
body[data-texture="crosshatch"] {
  background-image:
    repeating-linear-gradient(45deg,  rgba(242,240,234,0.060) 0 0.8px, transparent 0.8px 22px),
    repeating-linear-gradient(-45deg, rgba(242,240,234,0.045) 0 0.8px, transparent 0.8px 22px);
}
body[data-texture="dots"] {
  background-image: radial-gradient(rgba(242,240,234,0.11) 1px, transparent 1.1px);
  background-size: 12px 12px;
}
body[data-texture="none"] { background-image: none; }

#stars {
  position: fixed;
  inset: 0;
  z-index: -1;
  width: 100%;
  height: 100%;
  display: block;
  pointer-events: none;
}

.page {
  max-width: var(--shell);
  margin-inline: auto;
  position: relative;
  isolation: isolate;
}

::selection { background: var(--accent-dim); color: var(--accent); }

a { color: var(--accent); text-decoration: none; }
a:hover { color: var(--accent-hi); }

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

/* ---------------------------------------------------------------- hero --- */

.hero {
  position: relative;
  height: 708px;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

.mark {
  position: relative;
  flex: none;
}

/* A breath of accent behind the mark while something is playing. It sits on
   ::before, which precedes the two <img> halves in paint order, so it lands
   behind them without a z-index and without leaking onto <body> — the hero's
   own overflow is what crops it.

   Scale rides the bass level JS writes onto the mark as --bass, so the glow
   pulses with the kick rather than on a timer of its own. */
.mark::before {
  content: '';
  position: absolute;
  inset: -55% -18%;
  background: radial-gradient(ellipse at center,
              var(--accent-glow) 0%, transparent 70%);
  opacity: 0;
  transform: scale(calc(1 + var(--bass, 0) * 0.15));
  transition: opacity 600ms ease, transform 120ms linear;
  pointer-events: none;
}

body[data-playing] .mark::before { opacity: 1; }

.mark img {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  /* A hair of skew keeps the two halves from hinting against each other as
     they slide apart. Carried over from the design. */
  transform: skewX(0.1deg);
}

.mark--wordmark {
  width: 68%;
  max-width: 980px;
  aspect-ratio: 1562 / 701;
  margin-bottom: 120px;
}

.mark--monogram {
  display: none;
  height: 250px;
  aspect-ratio: 814 / 1189;
}

/* -------------------------------------------------------- spectrum bars --- */

.spectrum {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 150px;
  padding-inline: var(--gutter);
  display: flex;
  align-items: flex-end;
  gap: 1px;
}

/* Width is set per bar in JS — see sizeBars(). Fractional flex widths bank
   sub-pixels until they have to hand a whole one back, which shows up as a
   wider gap every few bars. */
.spectrum span {
  flex: 0 0 auto;
  min-width: 0;
  display: block;
  height: var(--h);
  background: var(--accent-dim);
  transform-origin: bottom;

  /* On a fresh load the strip holds the design's canned silhouette at full
     height, in the sleeping tint — the shape of the readout is there, just
     unlit. Starting a mix turns the bars full accent and either the FFT or the
     canned keyframes take over the height.

     Stopping does not bring the bars back here. The mode — data-spectrum on
     <body>, "canned" or "live" — outlives playback, and only data-playing comes
     and goes: a paused spectrum holds the frame it stopped on and just goes
     back to the tint. */
  transform: scaleY(1);
  transition: transform 320ms cubic-bezier(0.3, 0, 0.2, 1),
              background-color 320ms linear;
}

body[data-playing] .spectrum span { background: var(--accent); }

/* No audio to analyse: the canvas design's canned animation. It is declared on
   the mode rather than on data-playing so that pausing can freeze it in place
   with play-state, instead of removing the animation and letting the bars
   settle back onto the resting silhouette. */
body[data-spectrum="canned"] .spectrum span {
  animation-name: specbar;
  animation-duration: var(--dur);
  animation-delay: var(--delay);
  animation-iteration-count: infinite;
  animation-direction: alternate;
  animation-timing-function: ease-in-out;
}

body[data-spectrum="canned"]:not([data-playing]) .spectrum span {
  animation-play-state: paused;
}

/* When there is real audio to analyse, JS drives scaleY off the FFT and the
   canned animation gets out of the way. The rule stays on while paused, so the
   inline transforms the last frame wrote keep holding the silhouette.

   Every bar also gets the same full-height base here, so scaleY alone decides
   the silhouette. Leaving the canned per-bar --h in place would multiply the
   FFT by the design's sine wave, and you would just see the old frozen shape
   breathing.

   No will-change on purpose: there are 256 of these, and hinting them all
   would ask for 256 compositor layers. scaleY updates composite fine without
   it, and the browser promotes what it needs once the frames start. */
body[data-spectrum="live"] .spectrum span {
  animation: none;
  height: 100%;
  /* The FFT writes transform every frame; easing it would smear the readout.
     The colour still fades, so lighting up is not a hard cut. */
  transition: background-color 320ms linear;
}

@keyframes specbar {
  from { transform: scaleY(0.18); }
  to   { transform: scaleY(1); }
}

/* ------------------------------------------------------------- the cut --- */

/* Both marks are split along a shallow diagonal; the type borrows it. Two
   copies of the same string sit on top of one another, each masked to one side
   of the cut, and slide apart ALONG it — the same move the mark makes.

   Only the first copy is real text. The second carries aria-hidden, so a screen
   reader still reads the string once.

   The edge is a hard stop in a linear-gradient mask rather than a clip-path
   polygon. A gradient's stop line is always perpendicular to the gradient's own
   angle and always through the box centre, so the cut holds at 4.562deg
   whatever width the string turns out to be — polygon points would have to be
   recomputed per element.

   Each stop is a one-pixel ramp rather than a hard stop, or the edge crawls as
   the copies move. The two ramps are offset by a pixel instead of meeting, so
   the bottom copy is already solid where the top one starts to fade: two
   half-transparent edges composited over each other come to 75%, and at rest
   that shows as a grey hairline across every letter. */
.cut {
  display: block;
  /* A mask covers the element's own box, and a display face at line-height 1
     paints a little outside it. A bleed of padding, pulled straight back out
     with margin, widens the mask without moving the text. */
  --bleed: 0.16em;
  padding-block: var(--bleed);
  margin-block: calc(var(--bleed) * -1);
  transition: transform 260ms cubic-bezier(0.45, 0, 0.55, 1);
}

/* 175.438deg = 180 - 4.562, i.e. perpendicular to the wordmark's cut. The
   angle is read off the clipPath in assets/RAKS-wordmark-top.svg; see
   ../raks-design/README.md for why the halves travel along it. */
.cut--top {
  position: absolute;
  left: 0;
  top: calc(var(--bleed) * -1);
  width: 100%;
  margin-block: 0;
  -webkit-mask-image: linear-gradient(175.438deg, #000 calc(50% - 0.5px), transparent calc(50% + 0.5px));
          mask-image: linear-gradient(175.438deg, #000 calc(50% - 0.5px), transparent calc(50% + 0.5px));
  transform: translate(calc(var(--split) * -0.9968), calc(var(--split) * 0.0795));
}

.cut--bot {
  -webkit-mask-image: linear-gradient(175.438deg, transparent calc(50% - 1.5px), #000 calc(50% - 0.5px));
          mask-image: linear-gradient(175.438deg, transparent calc(50% - 1.5px), #000 calc(50% - 0.5px));
  transform: translate(calc(var(--split) * 0.9968), calc(var(--split) * -0.0795));
}

/* ------------------------------------------------------------- mix list --- */

.mixes {
  padding: 0 var(--gutter) 44px;
  display: flex;
  flex-direction: column;
}

.mix {
  position: relative;
  border-top: 1px solid var(--rule);
  padding: 26px 0;
  display: grid;
  /* The last column is the clock. 156px rather than the 132 the m:ss form
     needs: an hour-long mix reads 1:02:14 / 1:02:14, and 17 characters of 13px
     mono tracked at 0.1em come to about 155px. The extra 24px come off the
     waveform, which is fluid. */
  grid-template-columns: 268px 62px 1fr 156px;
  align-items: center;
  column-gap: 32px;
  row-gap: 6px;
}

/* Full-bleed band behind a hovered or playing row. Absolutely positioned, so
   it is out of flow and never becomes a grid item; z-index puts it under the
   row's own content, and the negative inline inset carries it past the gutter
   to the page edges. The row's hairline is drawn on the border box, which this
   starts below, so the rule stays legible on top of the tint. */
.mix::before {
  content: '';
  position: absolute;
  inset: 0 calc(var(--gutter) * -1);
  z-index: -1;
  background: var(--accent-dim);
  opacity: 0;
  transition: opacity 200ms linear;
  pointer-events: none;
}

.mix:hover::before { opacity: 0.5; }
.mix[data-playing]::before { opacity: 1; }

/* The head/body wrappers exist for the mobile stack; on desktop they dissolve
   so their children land directly on the row grid. */
.mix__head, .mix__body { display: contents; }

.mix__title {
  grid-column: 1;
  grid-row: 1;
  /* Shrink-wrap to the string: the cut runs through the box centre, so a box
     wider than its text would put the diagonal somewhere different on every
     title. justify-self keeps it capped by the track rather than overflowing. */
  justify-self: start;
  position: relative;
  --split: 0px;
  margin: 0;
  font-family: var(--font-display);
  font-size: 34px;
  font-weight: 400;
  line-height: 1;
  color: var(--paper);
  text-transform: uppercase;
  white-space: nowrap;
}

/* Hover, keyboard focus anywhere in the row, and the playing row itself all
   open the cut. While playing the offset holds rather than animating, and rides
   the bass level JS writes onto the title as --bass when there is real audio to
   read; with none it simply sits at the resting split. */
.mix:hover .mix__title,
.mix:focus-within .mix__title { --split: 2px; }
.mix[data-playing] .mix__title { --split: calc(2px + var(--bass, 0) * 1px); }

/* The design's data carried a subtitle per mix that neither artboard drew.
   This is it — the one place Darker Grotesque is actually visible. */
.mix__meta {
  grid-column: 1;
  grid-row: 2;
  margin: 0;
  font: 600 15px/1.2 var(--font-body);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
}

.mix__clock {
  grid-column: 4;
  grid-row: 1 / -1;
  text-align: right;
  white-space: nowrap;
  font: 600 13px/1 var(--font-mono);
  letter-spacing: 0.1em;
  color: var(--paper);
  font-variant-numeric: tabular-nums;
}

.mix__play {
  grid-column: 2;
  grid-row: 1 / -1;
  width: 62px;
  height: 62px;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: var(--accent);
  color: var(--ink);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: background 120ms linear;
}
.mix__play:hover { background: var(--accent-hi); }

/* Pause glyph: two bars. Play glyph: a solid triangle from borders. */
.mix__play .pause { display: flex; gap: 5px; }
.mix__play .pause i { width: 6px; height: 22px; background: currentColor; display: block; }
.mix__play .play {
  width: 0;
  height: 0;
  border-left: 18px solid currentColor;
  border-top: 12px solid transparent;
  border-bottom: 12px solid transparent;
  margin-left: 4px;
  display: block;
}
.mix__play[data-state="playing"] .play,
.mix__play[data-state="paused"] .pause { display: none; }

/* ------------------------------------------------------------- waveform --- */

.mix__wave {
  grid-column: 3;
  grid-row: 1 / -1;
  position: relative;
  /* The preview layers stack by z-index; isolating keeps that ordering inside
     the strip rather than in the page's stacking context. */
  isolation: isolate;
  height: 62px;
  cursor: pointer;
  touch-action: none;
}

.wave__layer {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  gap: 1px;
}

/* Width per bar comes from JS — see sizeStrip(). A fractional gap plus
   fractional flex widths was the worst of both: the leftovers piled up into a
   wider space every few bars. */
.wave__layer span {
  flex: 0 0 auto;
  min-width: 0;
  display: block;
  height: var(--h);
}

.wave__layer--idle span  { background: var(--bar); }
.wave__layer--fill span  { background: var(--accent); }
.wave__layer--scrub span { background: var(--accent-scrub); }

/* Progress is a clip over a second, fully-drawn accent copy — so the played
   portion lights up without re-laying out any bars. */
.wave__clip {
  position: absolute;
  inset: 0;
  z-index: 1;
  clip-path: inset(0 100% 0 0);
}

/* Scrub preview: a third copy of the same bars, clipped to the stretch between
   the playhead and the cursor. Ahead of the playhead it sits under the played
   accent, which stays on top.

   Behind it, the two clips cover the same ground, and leaving the preview
   underneath would hide it completely — so a backwards hover raises it instead,
   dimming the stretch a click would hand back. Same colour either way: what it
   marks is the difference between here and there. */
.wave__scrub {
  position: absolute;
  inset: 0;
  z-index: 0;
  clip-path: inset(0 100% 0 100%);
  opacity: 0;
  transition: opacity 140ms linear;
}

.mix__wave[data-scrub] .wave__scrub { opacity: 1; }
.mix__wave[data-scrub-back] .wave__scrub { z-index: 2; }

/* Hairline at the cursor, and the time it lands on. Both are positioned in px
   from JS and neither takes the pointer, or moving onto the label would count
   as leaving the strip. */
.wave__cursor,
.wave__time {
  position: absolute;
  z-index: 3;
  pointer-events: none;
  opacity: 0;
  transition: opacity 140ms linear;
}

.mix__wave[data-scrub] .wave__cursor,
.mix__wave[data-scrub] .wave__time { opacity: 1; }

.wave__cursor {
  top: 0;
  bottom: 0;
  width: 1px;
  background: var(--paper);
}

.wave__time {
  bottom: calc(100% + 6px);
  transform: translateX(-50%);
  white-space: nowrap;
  font: 600 13px/1 var(--font-mono);
  letter-spacing: 0.1em;
  color: var(--paper);
  font-variant-numeric: tabular-nums;
}

/* --------------------------------------------------------------- footer --- */

.footer {
  padding: 0 var(--gutter) 40px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 40px;
}

.social {
  display: flex;
  gap: 40px;
  flex: none;
}

.social a {
  position: relative;
  --split: 0px;
  font-family: var(--font-display);
  font-size: 32px;
  font-weight: 400;
  line-height: 1;
  color: var(--paper);
  text-transform: uppercase;
  white-space: nowrap;
}
.social a:hover,
.social a:focus-visible {
  color: var(--accent);
  --split: 2px;
}

.copyright {
  font: 600 12px/1 var(--font-mono);
  letter-spacing: 0.16em;
  color: var(--faint);
  white-space: nowrap;
}

/* --------------------------------------------------------------- mobile --- */

@media (max-width: 899px) {
  :root { --gutter: 20px; }

  .hero { height: 456px; }

  .mark--wordmark { display: none; }
  .mark--monogram { display: block; }

  .spectrum { height: 84px; }
  .spectrum span { height: var(--hm); }

  .mixes { padding-top: 8px; padding-bottom: 32px; }

  .mix {
    padding: 18px 0;
    display: flex;
    flex-direction: column;
    /* reset the desktop grid's centring, or both rows shrink-wrap */
    align-items: stretch;
    gap: 12px;
  }

  /* A grid rather than a flex row, so the meta line can sit under the title
     while the clock stays pinned to the right of the first line. */
  .mix__head {
    display: grid;
    grid-template-columns: 1fr auto;
    align-items: baseline;
    column-gap: 12px;
    row-gap: 4px;
  }

  .mix__head .mix__title { grid-column: 1; grid-row: 1; }
  .mix__head .mix__clock { grid-column: 2; grid-row: 1; }
  .mix__head .mix__meta  { grid-column: 1; grid-row: 2; }

  .mix__body {
    display: flex;
    align-items: center;
    gap: 12px;
  }

  .mix__title { font-size: 24px; white-space: normal; }

  .mix__meta { font-size: 13px; letter-spacing: 0.1em; }

  .mix__clock { font-size: 11px; color: var(--muted); }

  .mix__play { width: 46px; height: 46px; }
  .mix__play .pause { gap: 4px; }
  .mix__play .pause i { width: 4px; height: 16px; }
  .mix__play .play {
    border-left-width: 13px;
    border-top-width: 9px;
    border-bottom-width: 9px;
    margin-left: 3px;
  }

  .mix__wave { flex: 1; height: 40px; }
  .wave__layer span { height: var(--hs); }

  /* Above the strip is where the title and its subtitle are on this layout, so
     the scrub label drops into the row's bottom padding instead. Only a narrow
     window with a mouse ever sees it — a touch has no hover to preview from. */
  .wave__time {
    bottom: auto;
    top: calc(100% + 4px);
    font-size: 11px;
  }

  /* Type is smaller here; the cut travels with it. */
  .mix:hover .mix__title,
  .mix:focus-within .mix__title { --split: 1.5px; }
  .mix[data-playing] .mix__title { --split: calc(1.5px + var(--bass, 0) * 1px); }

  .footer {
    padding-bottom: 32px;
    align-items: baseline;
    gap: 14px;
  }

  .social { flex: 1; gap: 14px; justify-content: space-between; }
  .social a { font-size: 20px; }

  .copyright { display: none; }
}

/* Narrow phones: let the three social links wrap rather than overflow. */
@media (max-width: 379px) {
  .social { flex-wrap: wrap; }
}

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

@media (prefers-reduced-motion: reduce) {
  /* Bars still light up while playing and go back to sleep when stopped — they
     just do it without animating, and never bounce. */
  .spectrum span { transition: none; }
  body[data-spectrum="canned"] .spectrum span { animation: none; transform: none; }
  .mix__play { transition: none; }

  /* The glow is a state, not a pulse: it is either on or off, and never rides
     the bass. Same for the cut in the type and the row band — the offsets and
     the tint still mark hover and playback, they just arrive instantly. */
  .mark::before { transition: none; transform: none; }
  .cut { transition: none; }
  .mix::before { transition: none; }
  .mix[data-playing] .mix__title { --split: 2px; }
  @media (max-width: 899px) {
    .mix[data-playing] .mix__title { --split: 1.5px; }
  }
  .wave__scrub, .wave__cursor, .wave__time { transition: none; }
}
