/* ===========================================================================
   The playable EQ — page-scoped styles.

   Loaded by every page that hosts one. The markup and the icon sprite are
   injected by eq-ui.js, so a host page only needs an empty
   <div class="eq-rig"></div> and these two files.
   =========================================================================== */

  /* The rig is empty in the HTML and everything inside it -- the transport,
     the graph, the caption -- is injected by eq-ui.js. So until the script
     runs the rig is 0px tall and the Download button below it sits at exactly
     the y the transport button will occupy: measured 838 both, gap of 0. The
     script then pushes it 593px down and drops the transport where it was.
     What that looks like is a button that says "Download free" for half a
     second and then says "Turn sound on".
     Reserving the height removes the shift. The canvas is sized purely in
     CSS, so the same clamp reproduces it exactly; 141px is everything around
     it -- the transport bar (51), the graph's 2rem top margin, its 10px
     padding either side and 1px border, and the caption line (20) with its
     1rem margin. Measured rather than added up, and constant at every
     viewport tested, from a 390px phone to 1440x1100.
     Scoped to :empty so it applies only before the injection and can never
     fight the real content -- if this number ever drifts, the cost is a small
     residual shift rather than a broken layout. */
  .eq-rig:empty { min-height: calc(clamp(380px, 68vh, 580px) + 141px); }

  .eq-rig {
    max-width: 60rem; margin-inline: auto; position: relative;
    /* The plugin insets its graph from the window edge -- resized() does
       r.reduced(8) in design space, and that space is drawn at 1.3x, so ~10px
       on screen. The chrome colour showing in that gap is what makes the
       graph read as a panel INSIDE a window rather than as the window. */
    --graph-inset: 10px;
  }

  /* The graph, built to the plugin's own proportions: the same vertical
     gradient (surfaceAt's two endpoints are --surf-top / --surf-bot), the
     same 20 Hz - 20 kHz log sweep, the same ten-mark frequency ladder. */
  .graph-wrap { position: relative; margin-top: 2rem; }
  /* No border and no caption: in the plugin the graph IS the surface, with
     grid, curve and labels running to all four edges and the pills floating
     on top. A bordered rectangle sitting in a page reads as a diagram of the
     plugin rather than as the plugin. */
  /* Height is set against the plugin's own proportions. Its default window is
     936x701; after the 1.3x UI scale, the 8px inset, the 20px top bar and the
     6px gap under it, the graph itself is 704x498 -- 1.41:1. Against the 960px
     measure here that would be 680px tall.
     The vh term is what actually binds on a laptop, not the pixel cap, so it
     had to move too: 52vh on an ~840px viewport was giving 438px (1.85:1 --
     wide and shallow next to the real thing). 68vh caps out at 580 (1.66:1).
     Go to 680px if you want the plugin's exact shape; it costs most of the
     fold. */
  canvas.graph {
    display: block; width: 100%; height: clamp(380px, 68vh, 580px);
    background: linear-gradient(var(--surf-top), var(--surf-bot));
    touch-action: none;              /* drags must not scroll the page */
    cursor: crosshair;
  }

  /* ------------------------------------------------------------ narrow */
  /* On a phone the clamp above inverts the graph. 68vh of an 812px screen is
     552px tall against a 327px measure -- 0.57:1, portrait, when the whole
     point of the sizing above is to match the plugin's 1.41:1. A frequency
     response is a landscape object: the octaves run across, and squeezing that
     axis while stretching the one that only carries dB is backwards.
     So here the height stops being a viewport fraction and becomes the
     plugin's own ratio applied to whatever width there is -- 704:498, the same
     numbers the desktop rule is chasing, just reached directly instead of
     approximated with vh.
     Everything in this block is additive and scoped: above 700px none of it is
     evaluated, so the desktop sizing cannot be affected by anything here. */
  @media (max-width: 700px) {
    canvas.graph {
      height: auto;
      aspect-ratio: 704 / 498;
    }
    /* The placeholder has to follow, or the layout shift this file exists to
       remove comes back on phones -- reserving 580px for a box about to be
       231px is the same bug in the other direction. Same 704:498, expressed
       against the rig's real width: the section's gutter is var(--gut) each
       side, measured 327px inside a 375px viewport. */
    .eq-rig:empty {
      min-height: calc((100vw - 2 * var(--gut)) * 498 / 704 + 141px);
    }

    /* The pill is sized for a 580px graph, where its 73px is an eighth of the
       surface. Against 216px it covers a third of the curve -- including the
       +9 label and whatever band you just grabbed, which is the one thing you
       need to see while dragging.
       The chrome shrinks harder than the text: horizontal padding is 36px a
       side here for a pill that has 305px to live in, and the row gap is
       25px. Those go first. Type only steps 14 -> 12.5, because the values are
       what the pill is FOR, and the controls have to stay big enough to hit
       with a thumb -- shrinking those to win pixels would trade a visual
       problem for an unusable one.
       Base rules live in site.css; this wins by load order, not specificity,
       so it stays a plain override rather than an escalation. */
    .pill-readout {
      padding: .4rem .9rem .35rem;
      font-size: 12.5px;
      gap: .2rem;
    }
    .pill-row { gap: .85rem; }

    /* Out of the graph and into the flow beneath it.
       On a 938x580 desktop graph the pill floats over a corner of the curve, as
       it does in the plugin. On 305x216 it covered 29% of the surface, and
       every way of shrinking it ran into the same floor: two rows of text and
       three tap targets do not compress below about 64px without becoming
       either unreadable or unhittable.
       So it stops floating. Below the graph it covers nothing, the curve is
       never obscured, and the controls are permanently reachable -- which is
       what deleting a band needs, the only other route being right-click.
       This replaces an auto-hide that showed the pill while a band was held and
       faded it after: that kept the curve clear but put every control behind a
       timed window, and left an invisible pill swallowing taps in the middle of
       the graph. Static and always visible is less clever and better.
       In flow, so overflow: hidden on .graph-wrap does not clip it -- the
       wrapper grows to contain it rather than the pill overflowing. */
    .eq-rig .readout-float {
      position: static;
      transform: none;
      display: flex;
      justify-content: center;
      margin-top: .55rem;
    }

    /* Fixed width, because the content's width is not fixed. Measured across
       the states it actually takes: 216px normally, 238px at the longest
       readout (20.00 kHz / -18.0 dB / Q 10.00), and 314px on a pass filter,
       where the slope button appears. A control that changes size as you drag
       reads as twitchy -- the same reason the hint OVERLAYS this pill instead
       of replacing its contents, rather than letting it grow.
       314px is also wider than the 305px graph it sits under, inside a wrapper
       with overflow: hidden, so the widest state was going to be clipped.
       100% pins it to the graph, which removes both problems at once: one
       width, always, and it can never exceed what contains it. The padding and
       gaps below buy back the room the slope button needs to fit inside that. */
    .eq-rig .pill-readout {
      width: 100%;
      box-sizing: border-box;
      padding: .4rem .55rem .35rem;
    }
    /* space-between is the plugin's arrangement -- shape left, dynamic centred,
       delete right -- and it only reads that way while the pill hugs its
       contents. Pinned to the graph's full width the same rule strands the two
       glyphs against opposite edges, 269px apart, with the values row centred
       above them. They stop looking like one control and start looking like
       two things that drifted apart.
       So the row centres as a group instead. The left/centre/right order is
       kept, just no longer stretched across space it does not need. */
    .eq-rig .pill-row.controls {
      justify-content: center;
      /* Scales with the viewport instead of a fixed 1.5rem: that was 24px of
         gap on a 375px phone, and the same 24px on a 320px one where there was
         no room for it. */
      gap: clamp(.35rem, 2.5vw, 1.5rem);
      /* The backstop. On a pass filter the slope button joins the row, and at
         320px the four controls need about 252px inside 232px of pill -- no gap
         value fixes that, because the CONTENT is already wider than the space.
         Measured clipping of 25px at 320 and 5px at 360, and the thing being
         cut off is the delete X at the right end: the one control with no
         other route on a touch device.
         Wrapping trades a taller pill for a reachable button on the narrowest
         phones, and costs nothing on the ones where it already fits. */
      flex-wrap: wrap;
      row-gap: .35rem;
    }
    .eq-rig .ctl-mid { gap: .5rem; }

    /* The pill's glyphs measure 26x23 -- a mouse target. Delete is the one that
       matters, because the alternative is right-click and a phone has none, so
       missing it means the band cannot be removed at all.
       Grown with a pseudo-element rather than padding: padding would push the
       row apart and make the pill taller, which is the thing this whole block
       is trying to avoid. inset: -10px leaves the drawn glyph exactly where it
       is and extends only what the finger has to find -- 26x23 becomes 46x43.
       Not quite the 44x44 guideline vertically, but the pill would have to grow
       to buy the last pixel. */
    .glyph-btn { position: relative; }
    .glyph-btn::after { content: ''; position: absolute; inset: -10px; }

    /* The watermark is a fixed 24px, so it does not shrink when the graph does.
       Measured: 117px wide either way, which is 12.5% of the 938px desktop
       graph and 38.5% of the 305px one here -- three times the presence, on the
       screen with the least room for it.
       Matching 12.5% exactly would mean about 8px, and Indie Flower at 8px is
       a smudge rather than a signature. 13px lands near 21%: no longer the
       first thing you see, still legibly the wordmark. The floor here is
       legibility, not arithmetic.
       Qualified with .eq-rig for specificity, not for meaning: this block sits
       above the base .watermark rule in the file, media queries add none of
       their own, and a bare .watermark here would tie and lose on order --
       silently, exactly as the readout rules did. */
    .eq-rig .watermark {
      font-size: 13px;
      bottom: calc(var(--graph-inset) + 14px);
    }

    /* touch-action: none is right with a cursor and wrong on a phone: it hands
       the browser's scrolling to JS, so a swipe that happens to start over the
       graph -- which is most of them, the graph being most of the screen --
       scrolls nothing and the page appears stuck.
       pan-y gives vertical scrolling back. The browser decides on the first
       real movement and then holds that decision for the rest of the gesture:
       swipe down and it scrolls, with JS never seeing it; move sideways first
       and it declines to scroll and hands the whole gesture over, vertical
       movement included. So a band still drags in two dimensions -- it just
       has to be started sideways. Pulling straight down on a handle scrolls
       the page instead, which is the price of the page scrolling at all. */
    canvas.graph { touch-action: pan-y; }
  }
  /* A thin dark edge, so the graph reads as a panel sitting ON the page
     rather than a region of it. Black rather than the ink hairline the old
     static shot used: on this ground the graph surface (#29241E at the top)
     is LIGHTER than the page (#201B16), so a dark line separates them where a
     light one only blended into the surface. */
  /* A slight swell on hover, the same language as the buttons elsewhere on
     the site. SCALED rather than re-rendered: the backing store stays put, so
     this costs nothing per frame -- the price is a 2% upscale of an already
     2x bitmap, which is why it stops at 1.02 and not further. Beyond about
     1.05 the softening starts to show.
     It also moves every band handle a little, most at the edges, which is why
     it is deliberately small: this is a surface you aim at, not a button. */
  /* The lift that comes with the swell: two dark shadows, a tight one to seat
     the panel and a wide one for the throw.
     There was a 1px ink ring here as well, and it did most of the work --
     black on #201B16 has barely any room left to darken, so shadow alone has
     little contrast to spend, where a hairline of light separates the panel
     from the ground at once. It read as a drawn line rather than as depth,
     so it is gone and the shadows are larger to make up what they can.
     Declared transparent at rest rather than omitted: box-shadow cannot
     interpolate from `none`, and the two lists must have matching lengths or
     the transition snaps instead of easing. */
  .graph-wrap {
    transition: transform .25s cubic-bezier(.2, .8, .2, 1),
                box-shadow .25s cubic-bezier(.2, .8, .2, 1);
    border: 1px solid #000;
    padding: var(--graph-inset);
    background: var(--ground);      /* the chrome the graph sits on */
    overflow: hidden;               /* clips children, never its own shadow */
    box-shadow: 0 0 0 rgba(0,0,0,0),
                0 0 0 rgba(0,0,0,0);
  }
  /* The filter-type and slope menus are siblings of the graph, not children
     of it -- they have to be, or .graph-wrap's overflow:hidden would clip
     them and the transform would soften their text. But that means moving the
     cursor onto one leaves .graph-wrap, :hover goes false, and the graph
     shrinks back mid-interaction. While a menu is open the swell is held
     regardless of where the cursor is: the menu IS the graph's interaction. */
  .graph-wrap:hover,
  .eq-rig:has(.menu.on) .graph-wrap {
    transform: scale(1.02);
    /* Bigger and darker than they were, now that they are on their own. The
       wide one carries most of it; the tight one keeps the panel from
       appearing to float free of the page.
       The scale stays at 1.02. It is the obvious lever for more lift and the
       wrong one -- the swell moves every band handle with it, most at the
       edges, and this is a surface you aim at. Shadows can grow; the geometry
       under the cursor should not. */
    box-shadow: 0 10px 26px  -6px rgba(0,0,0,.65),
                0 40px 72px -22px rgba(0,0,0,.9);
  }
  /* The swell is removed, not merely un-animated: the global reduced-motion
     rule kills the transition, which would leave the graph SNAPPING between
     two sizes. The shadow stays -- it is not motion, it snaps the way every
     other hover state on the site does, and without it the graph would be the
     one interactive surface here that gives no hover feedback at all. */
  @media (prefers-reduced-motion: reduce) { .graph-wrap:hover { transform: none; } }

  /* The readout floats over the graph, as it does in the plugin. */
  .readout-float {
    position: absolute; left: 50%; top: calc(var(--graph-inset) + 14px);
    transform: translateX(-50%);
    pointer-events: none; opacity: 0; transition: opacity .18s;
  }
  .readout-float.on { opacity: 1; }

  .watermark {
    position: absolute; left: 50%; bottom: calc(var(--graph-inset) + 24px);
    transform: translateX(-50%);
    font-family: var(--hand); font-size: 1.5rem;
    color: rgba(206,200,190,.10); pointer-events: none; user-select: none;
  }

  .bar { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; margin-top: 1.5rem; }
  .hint { font-size: 11.5px; color: var(--ink-62); }
  /* The track credit under the graph. Centred to sit with the plugin above it
     rather than hanging off the left edge, and empty until the loop loads --
     it names what you are hearing, so there is nothing to say before then.

     min-height reserves the line whether or not it has anything in it. An
     empty <p> has no line box at all, so without this the paragraph collapses
     to nothing on pause and everything below -- the download buttons -- jumps
     up by a line, then back down on play. 1.75em is one line box: the site's
     line-height against this element's own 11.5px. */
  .eq-rig > .hint { text-align: center; min-height: 1.75em; }
  .warn { color: var(--copper); }

  /* The hint OVERLAYS the pill rather than replacing its contents, so the
     pill never changes size -- a control that resizes as the cursor moves
     reads as twitchy, which is why the plugin shrinks its hint text to fit
     instead of growing the pill. The rows keep their space and go invisible
     underneath. Same 14px, same full ink as the values, so a hint reads as
     the same text swapped out rather than a different element borrowing the
     pill. */
  .pill-hint {
    position: absolute; inset: 0;
    display: none; place-items: center;
    font-size: 14px; color: var(--ink); white-space: nowrap;
  }
  .pill-readout.hinting > .pill-row { visibility: hidden; }
  .pill-readout.hinting > .pill-hint { display: grid; }

  /* The readout is pointer-transparent so it never swallows a drag on the
     graph behind it -- but the controls inside it have to be clickable, so
     they opt back in individually. */
  .readout-float { pointer-events: none; }
  .readout-float .pill { pointer-events: auto; }

  /* Shape at the left, the dynamic control centred, delete at the right --
     the pill's own layout in the plugin. */
  .pill-row.controls { width: 100%; justify-content: space-between; color: var(--ink-75); }
  .ctl-mid { display: flex; align-items: center; gap: .8rem; }
  /* Both glyphs share one alpha ladder, as they do in the plugin:
     0.75 at rest, 1.00 on hover, 0.25 when the control can't act. */
  .glyph-btn {
    background: none; border: 0; padding: .1rem .2rem; cursor: pointer;
    color: rgba(206,200,190,.75); line-height: 0; transition: color .15s;
  }
  .glyph-btn:hover:not(:disabled) { color: rgba(206,200,190,1); }
  .glyph-btn:disabled { color: rgba(206,200,190,.25); cursor: default; }
  .slope-btn {
    font: inherit; font-size: 11px; letter-spacing: .06em; cursor: pointer;
    background: none; color: var(--ink-75);
    border: 1px solid rgba(206,200,190,.40); border-radius: 999px;
    padding: .1rem .55rem; transition: color .15s, border-color .15s;
  }
  .slope-btn:hover { color: var(--copper); border-color: var(--copper); }
  .slope-btn[aria-pressed="true"] { color: var(--copper); border-color: var(--copper); }

  .menu {
    position: absolute; z-index: 30; display: none;
    border: 1px solid rgba(206,200,190,.40); border-radius: 5px;
    background: linear-gradient(var(--surf-top), var(--surf-bot));
    padding: 3px; pointer-events: auto;
  }
  .menu.on { display: block; }
  .menu button {
    display: flex; align-items: center; gap: .7rem; width: 100%;
    font: inherit; font-size: 12.5px; text-align: left; cursor: pointer;
    background: none; border: 0; color: var(--ink-75);
    padding: .3rem .7rem .3rem .5rem; border-radius: 3px; white-space: nowrap;
  }
  .menu button:hover { background: rgba(206,200,190,.10); color: var(--ink); }
  .menu button[aria-checked="true"] { color: var(--copper); }
