/* ===========================================================================
   AudioFlower — shared design system.

   The palette is the EQ plugin's own, taken from EQLookAndFeel's colour
   tokens rather than picked to match it by eye: ground #201B16, ink
   #CEC8BE, copper #C9825E. A visitor who downloads the plugin should find
   the same room on the other side.

   Every page links this file. Page-specific rules stay inline in that page.
   =========================================================================== */

@font-face {
  font-family: 'IBM Plex Mono'; src: url('IBMPlexMono-Regular.ttf') format('truetype');
  font-weight: 400; font-display: swap;
}
@font-face {
  font-family: 'Indie Flower'; src: url('IndieFlower-Regular.ttf') format('truetype');
  font-weight: 400; font-display: swap;
}

:root {
  --ground:   #201B16;
  --ink:      #CEC8BE;
  --copper:   #C9825E;
  --surf-top: #29241E;
  --surf-bot: #191511;
  --ink-90:   rgba(206,200,190,.90);
  --ink-75:   rgba(206,200,190,.75);
  /* Text roles bottom out at --ink-62: below that, 11-13px on this ground
     drops under WCAG AA (--ink-45 measures 3.14:1). Lighter tones are for
     borders and rules only, never for type. */
  --ink-62:   rgba(206,200,190,.62);
  --ink-25:   rgba(206,200,190,.25);
  --ink-16:   rgba(206,200,190,.16);
  --ink-10:   rgba(206,200,190,.10);
  /* The sticky nav's height, MEASURED (83px) rather than derived -- the
     wordmark is Indie Flower at 1.5rem and its line box is taller than the
     nominal size, so adding up the padding underestimates it. Anchor targets
     offset by this so they don't park underneath the bar. */
  --nav-h: 5.25rem;
  --mono: 'IBM Plex Mono', ui-monospace, 'SF Mono', Menlo, monospace;
  --hand: 'Indie Flower', cursive;
  --measure: 34rem;
  --gut: clamp(1.5rem, 5vw, 5rem);
}

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

/* ------------------------------------------------- in-page anchor scrolling */
/* Jumping to #install used to cut straight there. Smooth instead --
   the movement is what tells you you have travelled DOWN the same page rather
   than arrived at a new one.

   scroll-margin-top is the other half, and the reason the jump landed badly
   even before this: the nav is sticky, so an anchor target parks underneath
   it and the section's own eyebrow is hidden behind the bar. The offset is
   the nav's height plus a little air.

   The global reduced-motion block at the bottom of this file already forces
   scroll-behavior back to auto, so this needs no guard of its own. */
html { scroll-behavior: smooth; }
:target, section[id] { scroll-margin-top: calc(var(--nav-h) + 1.25rem); }

/* ----------------------------------------------------- view transitions */
/* Cross-document transitions: navigating between these pages cross-fades
   instead of hard-loading, so four separate HTML files behave like one
   site. Needs the rule present on BOTH the outgoing and incoming page --
   which it is, because every page links this stylesheet.

   Unsupported browsers simply navigate normally; nothing is conditional on
   it working. */
@view-transition { navigation: auto; }

/* Named elements are matched across the two pages and animate from one
   position to the other rather than fading out and back in. The wordmark
   and nav are in the same place on every page, so naming them holds them
   still while the content underneath changes -- which is what makes it
   read as one site rather than two documents.

   A name must be unique within a page; duplicates disable the transition
   entirely, so these are only used on elements that appear exactly once. */
.wordmark  { view-transition-name: wordmark; }
.sitenav   { view-transition-name: sitenav; }
.sitefoot  { view-transition-name: sitefoot; }

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: .28s;
  animation-timing-function: cubic-bezier(.4, 0, .2, 1);
}

/* The global reduced-motion rule below uses `*`, which does NOT match these
   pseudo-elements -- they need switching off explicitly. */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) { animation: none !important; }
}

body {
  margin: 0; background: var(--ground); color: var(--ink);
  font-family: var(--mono); font-size: 15px; line-height: 1.75;
  -webkit-font-smoothing: antialiased;
  /* A short page used to leave the footer stranded partway up the screen
     with bare ground beneath it, which reads as a page that stopped rather
     than one that ended. About is short enough to do it on any normal
     laptop. margin-top:auto on the footer eats the slack when there is any
     and does nothing when there is not, so long pages are untouched. */
  min-height: 100vh; display: flex; flex-direction: column;
}
.sitefoot { margin-top: auto; }
a { color: inherit; }

/* ------------------------------------------------------------------- nav */
.sitenav {
  display: flex; align-items: center; justify-content: space-between;
  padding: 1.25rem var(--gut); border-bottom: 1px solid var(--ink-10);
  position: sticky; top: 0; z-index: 20;
  background: rgba(32,27,22,.86); backdrop-filter: blur(14px);
}
.wordmark { font-family: var(--hand); font-size: 1.5rem; text-decoration: none; }
.navlinks { display: flex; gap: 1.75rem; font-size: 12.5px; letter-spacing: .06em; }
.navlinks a { color: var(--ink-62); text-decoration: none; transition: color .2s; }
.navlinks a:hover, .navlinks a[aria-current] { color: var(--ink); }

/* ---------------------------------------------------------------- layout */
section { padding: clamp(4rem, 11vw, 8.5rem) var(--gut); }
section + section { border-top: 1px solid var(--ink-10); }
.measure { max-width: var(--measure); }
.center { margin-inline: auto; text-align: center; }

/* ------------------------------------------------------------------ type */
.eyebrow {
  font-size: 11px; letter-spacing: .22em; text-transform: uppercase;
  color: var(--ink-62); margin: 0 0 1.5rem;
}
h1 {
  font-size: clamp(2.6rem, 7vw, 4.25rem); font-weight: 400;
  letter-spacing: -.035em; line-height: 1.05; margin: 0;
}
h2 {
  font-size: clamp(1.35rem, 3.2vw, 1.85rem); font-weight: 400;
  letter-spacing: -.02em; line-height: 1.3; margin: 0 0 1.75rem;
}
h3 {
  font-size: 14px; font-weight: 400; letter-spacing: .02em;
  margin: 0 0 .65rem;
}
p { margin: 0 0 1.35rem; color: var(--ink-90); }
p:last-child { margin-bottom: 0; }
.lede { font-size: 1.05rem; color: var(--ink); }
.meta { font-size: 11.5px; letter-spacing: .05em; color: var(--ink-62); }

/* --------------------------------------------------------------- buttons */
.btn {
  display: inline-block; padding: .85rem 2rem; font: inherit; font-size: 12.5px;
  letter-spacing: .14em; text-transform: uppercase; text-decoration: none;
  border: 1px solid var(--copper); background: var(--copper); color: #17120E;
  cursor: pointer; transition: background .2s, color .2s;
}
.btn:hover { background: transparent; color: var(--copper); }
/* A swell on hover, the web-side version of the plugin's own hover
   behaviour on its floating pills. transform doesn't reflow, so nothing
   around the button shifts as it grows. */
.btn, .play { transition: background .2s, color .2s, transform .25s cubic-bezier(.2,.8,.2,1); }
.btn:hover, .play:hover { transform: scale(1.03); }
.btn-ghost { background: transparent; color: var(--ink-75); border-color: var(--ink-25); }
.btn-ghost:hover { background: transparent; color: var(--ink); border-color: var(--ink); }
.cta { display: flex; gap: .75rem; justify-content: center; flex-wrap: wrap; margin: 2.5rem 0 1.25rem; }

/* ---------------------------------------------------------------- panels */
.panel {
  border: 1px solid var(--ink-16);
  background: linear-gradient(var(--surf-top), var(--surf-bot));
  padding: 1.75rem;
}
.cols {
  display: grid; gap: 1.5rem; max-width: 62rem; margin-inline: auto;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
}

figure { margin: 0; }
figure img, figure video { display: block; width: 100%; border: 1px solid var(--ink-16); }
figcaption { font-size: 11.5px; color: var(--ink-62); margin-top: .85rem; }

/* ---------------------------------------------------------------- footer */
.sitefoot {
  padding: 3rem var(--gut); border-top: 1px solid var(--ink-10);
  display: flex; justify-content: space-between; gap: 1.5rem; flex-wrap: wrap;
  font-size: 12px; color: var(--ink-62);
}
.sitefoot a { color: var(--ink-62); text-decoration: none; }
.sitefoot a:hover { color: var(--ink); }
.footlinks { display: flex; gap: 1.5rem; flex-wrap: wrap; }

/* ------------------------------------------------------------ foot banner */
/* A floral tile sits behind the footer, permanently mostly hidden. A
   soft-edged window travels across it on a loop and uncovers the part it is
   over -- the artwork itself never moves.

   The window and the art counter-translate by the same amount over the same
   duration, and that is what pins the artwork in place while the opening
   slides. It could equally be done by animating a mask's position, but that
   repaints a full-width band every frame; the home page runs a 60fps canvas
   with live DSP on it, and transforms stay on the compositor where they cost
   the analyzer nothing.

   Lives here rather than in a page's own <style> because the footer is a
   shared component -- it is the bottom of every page, so this is too. */
.sitefoot {
  /* Taller than it was. The tile is fitted to the band's height, so a short
     footer would render one repeat only a few hundred px wide and the
     flowers would come out tiny and repeat three times across. */
  padding: 5.5rem var(--gut) 5rem;
  position: relative;
  overflow: hidden;
}
.sitefoot > * { position: relative; z-index: 1; }

.foot-banner {
  /* The width of the opening, in one place. The keyframes read it, so
     changing it here moves the start and end of the travel with it -- set
     the two separately and the artwork drifts instead of holding still. */
  --band: 13vw;
  position: absolute; inset: 0;
  pointer-events: none;
  z-index: 0;
  /* Fades before it can touch the footer's own top rule or run off the bottom
     of the page. Eased the same way as the window's left and right edges, and
     for the same reason: a straight alpha ramp leaves a shoulder you can
     pick out. Asymmetric because the two ends are not the same problem --
     the top has a hairline rule just above it to stay clear of, the bottom
     just runs out of page. */
  --edge-y: linear-gradient(to bottom,
              rgba(0,0,0,0)    0%,  rgba(0,0,0,.16)  7%,
              rgba(0,0,0,.5)  14%,  rgba(0,0,0,.84) 21%,
              #000            28%,  #000            66%,
              rgba(0,0,0,.84) 74%,  rgba(0,0,0,.5)  82%,
              rgba(0,0,0,.16) 91%,  rgba(0,0,0,0)  100%);
  -webkit-mask-image: var(--edge-y);
          mask-image: var(--edge-y);
}

.banner-window {
  position: absolute; top: 0; bottom: 0; left: 0;
  width: var(--band);
  /* Static mask, so the soft edges cost one paint rather than one per frame.
     This is the shape of the opening.

     No flat section: the whole band is one smooth bell, alpha rising from
     nothing to full at the centre and back. Softening was previously limited
     by the plateau -- longer fades could only be bought by shortening the
     part at full strength -- and removing it lifts that ceiling entirely,
     because now every pixel of the band is fade. There is no edge to find
     because there is no edge, only a peak.

     The curve is a raised cosine, (1 - cos 2pi t) / 2, sampled every twelfth.
     That is a Hann window, which is also what the analyzer runs its FFT
     through, for the same reason: it goes to zero at both ends with no
     discontinuity in the slope.

     Full strength survives at the centre, so a flower crossing the middle
     still reaches --copper. That matters because opacity is pinned at .8 by
     the colour correction baked into the artwork -- the peak here cannot be
     traded away and bought back there. */
  --edge: linear-gradient(90deg,
            rgba(0,0,0,0)    0%,     rgba(0,0,0,.067) 8.33%,
            rgba(0,0,0,.25) 16.67%,  rgba(0,0,0,.5)     25%,
            rgba(0,0,0,.75) 33.33%,  rgba(0,0,0,.933) 41.67%,
            #000             50%,
            rgba(0,0,0,.933) 58.33%, rgba(0,0,0,.75)  66.67%,
            rgba(0,0,0,.5)     75%,  rgba(0,0,0,.25)  83.33%,
            rgba(0,0,0,.067) 91.67%, rgba(0,0,0,0)     100%);
  -webkit-mask-image: var(--edge);
          mask-image: var(--edge);
  animation: banner-sweep 7.5s linear infinite;
  will-change: transform;
}

.banner-art {
  position: absolute; top: 0; bottom: 0; left: 0;
  width: 100vw;
  background: url(assets/hero-banner.webp) repeat-x;
  /* Height-fit rather than cover: the tile is drawn at close to its native
     resolution and repeats sideways, so it stays crisp at any width instead
     of being scaled up to fill one. */
  background-size: auto 100%;
  /* The artwork is knocked out to transparency, so this is not dimming a
     sheet of colour -- it only ever paints flowers, with the page's own
     ground everywhere between them. That is what buys the strength: a
     full-bleed image had to sit near .15 to keep from washing the type out,
     and this runs four times that and still leaves it alone, because most of
     what sits behind any given word is nothing at all. */
  /* COUPLED TO build-banner.py's OPACITY -- change one and change the other.
     The artwork is pre-corrected for this exact blend so the flowers land on
     --copper and the foliage on --ink once composited over the ground. Below
     about .75 there is no correction that works: the red channel would have
     to exceed 255, which is why the flowers read brown rather than copper
     when this sat at .6. */
  opacity: .8;
  animation: banner-drift 7.5s linear infinite;
  will-change: transform;
}

/* Equal and opposite. Any change to one must be mirrored in the other or the
   artwork starts sliding along with the window.

   The travel takes 5.5s and then holds for 2s before repeating, which is why
   the duration is 7.5s and the hold begins at 73.33% -- that is 5.5/7.5.
   Three numbers that have to agree: change the duration and both keyframe
   percentages move with it.

   The pause is invisible rather than frozen. The band finishes at 100vw,
   entirely past the right edge, so the hold shows an empty footer and the
   jump back to off-screen-left at the end of the cycle happens where nobody
   can see it. */
@keyframes banner-sweep {
  from    { transform: translateX(calc(-1 * var(--band))); }
  73.33%  { transform: translateX(100vw); }
  to      { transform: translateX(100vw); }
}
@keyframes banner-drift {
  from    { transform: translateX(var(--band)); }
  73.33%  { transform: translateX(-100vw); }
  to      { transform: translateX(-100vw); }
}

/* On a phone the band is shorter, so one repeat of the tile is much wider
   relative to the screen -- the desktop opening would show a sliver of one
   leaf rather than anything that reads as a flower. */
@media (max-width: 700px) { .foot-banner { --band: 26vw; } }

/* Nothing moves, but the artwork is the point -- so it stays, spread the full
   width instead of being hidden along with the motion. */
@media (prefers-reduced-motion: reduce) {
  .banner-window { width: 100vw; animation: none; transform: none;
                   -webkit-mask-image: none; mask-image: none; }
  .banner-art    { animation: none; transform: none; }
}

/* --------------------------------------------------------------- icons */
/* The plugin's own glyphs (see assets/icons.svg for provenance). Stroke is
   currentColor and never scaled, so an icon inherits the colour of whatever
   it sits in and keeps a 1.5 stroke at any size -- which is why these hold
   up small, where downscaled raster icons fall apart. */
.icon {
  width: 1.5em; height: 1.5em;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-linejoin: round;
  vertical-align: -.3em;
  flex: none;
}
/* 1.5em is the floor -- these were tried at 1em and read too small on this
   ground, where a 1.5 stroke at that size loses its shape. */
.icon-lg { width: 2.25em; height: 2.25em; vertical-align: -.55em; }

/* ------------------------------------------------------------- pills */
/* Rebuilt from the plugin's own paint code, not eyeballed:
     radius  = height / 2            (full stadium)
     border  = ink @ 40%, 1px        (drawRoundedRectangle)
     fill    = surfaceAt() gradient  (its endpoints ARE --surf-top/--surf-bot)
     text    = 14px in the readout, 12px in the small pills
   The readout is frosted in the plugin by snapshotting the graph beneath
   it at quarter res and stretching it back up -- the resampling IS the
   blur. On the web that's backdrop-filter, over a 55% surface tint with a
   catch-light along the inner top edge at ink 10%. */
.pill {
  display: inline-flex; align-items: center; gap: .75rem;
  padding: .4rem .95rem;
  border: 1px solid rgba(206,200,190,.40);
  border-radius: 999px;
  background: linear-gradient(var(--surf-top), var(--surf-bot));
  font-size: 12px; color: var(--ink); white-space: nowrap;
}

.pill-readout {
  position: relative;
  flex-direction: column; gap: .3rem;
  padding: .6rem 2.25rem .5rem;
  font-size: 14px;
  background: rgba(41,36,30,.55);
  backdrop-filter: blur(7px);
  -webkit-backdrop-filter: blur(7px);
}
/* The one flourish that makes it read as glass rather than a translucent
   rectangle -- inset by radius*0.6 at each end, as in the plugin. */
.pill-readout::before {
  content: ""; position: absolute; left: 22%; right: 22%; top: 1.5px;
  height: 1px; background: rgba(206,200,190,.10);
}
.pill-row { display: flex; align-items: center; gap: 1.55rem; }
.pill-row.controls { gap: .8rem; color: var(--ink-75); }

.chip {
  border: 1px solid rgba(206,200,190,.40);
  border-radius: 999px; padding: .12rem .6rem;
  font-size: 12px; color: var(--ink-75);
}
.chip.on { border-color: var(--copper); color: var(--copper); }

/* ------------------------------------------------------------- shimmer */
/* PARKED -- nothing on the site uses this right now. It lived on the old home
   page's "completely free", which went when home and the EQ page merged, and
   he asked to drop it "for now". Kept rather than deleted because bringing it
   back is markup plus the splitter script, and because the note below is a
   real trap worth not rediscovering. */
/* A highlight that travels across a phrase, letter by letter: each glyph
   brightens and swells briefly, staggered by position (delays are set per
   letter in the page script, which splits the text into .sh-ch spans).

   Deliberately NOT done with a gradient and background-clip: text. That
   approach clips to the element's OWN text, so the moment the phrase is
   split into child spans the parent has no text left to clip to, the
   children inherit `color: transparent`, and the whole phrase renders
   invisible. Animating colour per letter has no such failure mode.

   Colours are literal rather than var() because keyframe interpolation of
   custom properties is unreliable across engines. They are --ink (#CEC8BE)
   and a warm near-white above it. */
.shimmer { color: var(--ink); }   /* if the script never runs, plain text */

.sh-ch {
  display: inline-block;
  color: #CEC8BE;
  animation: sh-char 3.4s linear infinite;
}

/* transform, not font-size: scaling by transform doesn't reflow, so letters
   grow over their neighbours instead of shoving them sideways -- which is
   what keeps the line from jittering as the wave passes. */
@keyframes sh-char {
  0%   { color: #CEC8BE; transform: scale(1); }
  4%   { color: #F6F2EB; transform: scale(1.13); }
  10%  { color: #CEC8BE; transform: scale(1); }
  100% { color: #CEC8BE; transform: scale(1); }
}

/* Keeps a phrase from being split across lines -- it wraps as one unit. */
.nb { white-space: nowrap; }

@media (prefers-reduced-motion: reduce) {
  * { animation: none !important; transition: none !important; scroll-behavior: auto !important; }
}
