/* ============================================================
   trainer.css — Poly Polyrhythm Trainer
   Shell, transport, buttons, fields and the status line come from
   shared/components.css; this file is the stage, the stats and the
   two pads — the parts only the trainer has.
   ============================================================ */

/* Poly's two apps share every layout decision on purpose — the same shell,
   the same transport, the same status line — so what tells you which one you
   are in is HUE, not shape. The metronome is blue; the trainer is violet, and
   that runs all the way through: the brand dot, the focus rings, the slider
   thumb, the transport. See --app in shared/tokens.css.

   Deliberately restrained. An earlier pass gave this screen its own gradient
   background, glowing text and blooming pads, and it read as a different
   product rather than the other half of the same one. */
:root {
  /* the trainer's own hue — the brand dot, and every shared control */
  --app: var(--app-trainer);
  --accent: #9b8cff;
  --accent-2: #8878ff;
  --accent-soft: rgba(155, 140, 255, .16);

  /* The two voices: Poly's blue and the trainer's own violet. Far enough
     apart to tell two SIMULTANEOUS streams apart at a glance, which is the
     whole job here — and voice B sharing the page accent is the same
     relationship the metronome has, where its accent IS its layer colour. */
  --va: #6ea8ff;
  --vb: #9b8cff;
  --va-soft: rgba(110, 168, 255, .16);
  --vb-soft: rgba(155, 140, 255, .16);
  /* Unison is white — in Poly, white is already the accented articulation,
     and both voices landing together is the strongest moment in the bar.
     It also keeps unison clear of the three grade colours. */
  --unison: var(--text);
}

/* The trainer never scrolls: the stage and the pads split whatever height
   the device has. A scrollbar here would mean a pad you have to hunt for. */
.trainer-shell { max-width: 820px; }
/* Everything vertical on this screen is fluid in dvh, and the two big
   surfaces carry NO minimum height. A min-height on a flex item is a floor
   flex cannot shrink past, so on a short window the stage and the pads kept
   their floors, overflowed #main, and got clipped by the overflow: hidden
   below — the pads ended up half visible with no way to scroll to the rest.
   With min-height: 0 they always divide whatever is left, so the screen
   always fits and never scrolls, which is the whole contract here. */
#main {
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: clamp(6px, 1.6dvh, 12px);
  overflow: hidden;
}

/* ---------- Control strip ----------
   Ratio · tempo · settings, on one line. Same band the metronome puts its
   tempo strip in, in the same place on the page. */
.ctrl-bar {
  display: flex; align-items: center; gap: clamp(6px, 1.7dvh, 16px);
  padding: clamp(4px, 1dvh, 8px) var(--s3);
  flex-wrap: wrap;
}

/* The ratio is the headline of this screen, so it gets the weight the
   metronome gives its BPM: one big mono number per voice, in that voice's
   colour, with its own −/+ either side. The A/B tag under each number is
   what stops a bare "3 : 4" from being ambiguous about which is which. */
.ratio { display: flex; align-items: center; gap: var(--s1); flex: 0 0 auto; }
.ratio-cell { display: flex; align-items: center; gap: var(--s1); }
.ratio-step {
  width: clamp(24px, 5dvh, 30px); height: clamp(24px, 5dvh, 30px); flex: 0 0 auto;
  background: transparent; border: 1px solid var(--line-soft); color: var(--muted);
  border-radius: var(--radius-xs); font-size: 16px; line-height: 1; cursor: pointer;
  transition: color .15s, border-color .15s, background .15s;
}
.ratio-cell:has(.ra) .ratio-step { --rc: var(--va); }
.ratio-cell:has(.rb) .ratio-step { --rc: var(--vb); }
.ratio-step:hover { color: var(--rc, var(--accent)); border-color: var(--rc, var(--accent)); }
.ratio-step:active { background: var(--accent-soft); }
.ratio-val {
  display: flex; flex-direction: column; align-items: center;
  min-width: 30px; line-height: 1;
}
.ratio-num { font-family: var(--mono); font-size: clamp(18px, 4.2dvh, 25px); font-weight: 700; }
/* each number carries its own voice's colour — that, and the tag under it,
   is what stops a bare "3 : 4" being ambiguous about which is which */
.ratio-num.ra { color: var(--va); }
.ratio-num.rb { color: var(--vb); }
.ratio-tag {
  font-size: 8.5px; font-weight: 600; letter-spacing: .8px;
  text-transform: uppercase; color: var(--muted-2); margin-top: 2px;
}
.rx { font-family: var(--mono); font-size: 20px; color: var(--muted-2); padding: 0 var(--s1) 12px; }

.tempo-slide { display: flex; align-items: center; gap: var(--s3); flex: 1 1 200px; min-width: 160px; }
.bpm-step {
  width: clamp(28px, 5.7dvh, 34px); height: clamp(28px, 5.7dvh, 34px); flex: 0 0 auto; border-radius: 50%;
  background: var(--card-2); border: 1px solid var(--line); color: var(--text);
  font-size: 18px; line-height: 1; cursor: pointer;
  transition: border-color .15s, background .15s, transform .08s, color .15s;
}
.bpm-step:hover { border-color: var(--accent); color: var(--accent); }
.bpm-step:active { transform: scale(.92); background: var(--accent-soft); }

.gear { width: 17px; height: 17px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }

/* ---------- Stage ----------
   Notes flow right to left toward a fixed playhead. This is the only part of
   the screen you read while playing, so it gets the height the pads can spare.
   The canvas paints itself; everything here is the frame around it. */
.stage {
  position: relative; overflow: hidden;
  border-radius: var(--radius);
  background: linear-gradient(180deg, var(--card), var(--bg-soft));
  border: 1px solid var(--line-soft);
  box-shadow: var(--shadow-sm);
  flex: 1.3 1 0; min-height: 0; max-height: 420px;
  display: flex; flex-direction: column;
}
#cv { display: block; width: 100%; flex: 1 1 0; min-height: 0; }

.stage-foot {
  display: flex; align-items: center; justify-content: space-between; gap: var(--s3);
  flex: 0 0 auto;
  padding: var(--s2) var(--s3);
  border-top: 1px solid var(--line-soft);
  background: rgba(0, 0, 0, .18);
}
.legend { display: flex; flex-wrap: wrap; gap: var(--s1) var(--s3); font-size: 11.5px; color: var(--muted); }
.leg-item { display: flex; align-items: center; gap: var(--s2); }
.leg-sw { width: 11px; height: 11px; border-radius: 4px; border: 1px solid rgba(255, 255, 255, .12); }
.sw-a { background: var(--va); } .sw-b { background: var(--vb); } .sw-u { background: var(--unison); }
.tol-note { font-family: var(--mono); font-size: 11px; color: var(--muted-2); white-space: nowrap; }

.countin {
  position: absolute; inset: 0; display: grid; place-items: center;
  pointer-events: none; background: rgba(13, 15, 20, .72);
  opacity: 0; transition: opacity .2s;
}
.countin.on { opacity: 1; }
.countin b {
  font-family: var(--mono); font-size: 62px; font-weight: 700;
  letter-spacing: -.03em; color: var(--text);
}
.countin.on b { animation: pop .35s cubic-bezier(.2, .8, .25, 1); }
@keyframes pop { from { transform: scale(.7); opacity: .2 } to { transform: scale(1); opacity: 1 } }

/* ---------- Stats ---------- */
.stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--s2); flex: 0 0 auto; }
.stat {
  display: flex; flex-direction: column; align-items: center; gap: clamp(0px, .3dvh, 2px);
  padding: clamp(3px, .9dvh, 8px) var(--s1);
  background: var(--bg-soft); border: 1px solid var(--line-soft);
  border-radius: var(--radius-sm);
  transition: border-color .2s;
}
.stat .k {
  font-size: clamp(8px, 1.3dvh, 9.5px); font-weight: 600; letter-spacing: .7px;
  text-transform: uppercase; color: var(--muted-2);
}
.stat .v {
  font-family: var(--mono); font-size: clamp(14px, 2.4dvh, 21px); font-weight: 700;
  transition: color .2s;
}
.stat .v.sm { font-size: clamp(11px, 1.9dvh, 16px); }
/* a streak worth protecting announces itself */
.stat.hot { border-color: var(--good); }
.stat.hot .v { color: var(--good); }

/* every change of the streak number kicks it, so a run you are on is
   something you feel at the edge of vision rather than have to read */
.stat .v.bump { animation: bump .28s cubic-bezier(.2, .9, .3, 1); }
@keyframes bump {
  0%   { transform: scale(1); }
  38%  { transform: scale(1.28); }
  100% { transform: scale(1); }
}

/* ---------- Pads ----------
   Two targets, one per hand. Everything about a pad is feedback: it flashes
   on its own beat, fills a bar as the next one approaches, takes a grade
   colour when you hit, and keeps the last nine results along the bottom. */
.pads { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s3); flex: 1 1 0; min-height: 0; }
.pad {
  --c: var(--va);
  --c-soft: var(--va-soft);
  position: relative; overflow: hidden;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: clamp(1px, .5dvh, 4px);
  /* fluid, because the pad's own box is the last thing that can overflow:
     the row fits the window, but the label + count + hint inside it still
     has to fit the row, and .pad clips what does not */
  padding: clamp(4px, 1.3dvh, 12px);
  border-radius: var(--radius);
  border: 1px solid var(--line-soft);
  background: linear-gradient(180deg, var(--card), var(--bg-soft));
  box-shadow: var(--shadow-sm);
  color: inherit; cursor: pointer;
  touch-action: none; -webkit-user-select: none; user-select: none;
  transition: transform .1s ease, border-color .18s, box-shadow .18s;
}
.pad-b { --c: var(--vb); --c-soft: var(--vb-soft); }

/* the beat glow — a pool of the voice's colour, lit on that voice's pulse */
.pad::before {
  content: ''; position: absolute; inset: -40% -10% auto; height: 120%;
  background: radial-gradient(50% 50% at 50% 50%, var(--c), transparent 70%);
  opacity: 0; pointer-events: none;
  transition: opacity .32s ease;
}
.pad.beat::before { opacity: .22; transition: opacity .04s; }
.pad:hover { border-color: var(--line); }
.pad.pressed { transform: scale(.976); }

.pad-lbl {
  font-size: 10.5px; font-weight: 600; letter-spacing: 1.1px;
  text-transform: uppercase; color: var(--muted);
}
.pad-cnt {
  font-family: var(--mono); font-size: clamp(22px, 5.4dvh, 42px); font-weight: 700;
  line-height: 1; color: var(--c);
}
.pad-key { font-family: var(--mono); font-size: 10px; color: var(--muted-2); letter-spacing: .5px; }

/* grades: the border says it, so the pad's own colour stays readable */
.pad.good { border-color: var(--good); box-shadow: inset 0 0 0 1px var(--good), var(--shadow-sm); }
.pad.ok   { border-color: var(--warn); box-shadow: inset 0 0 0 1px var(--warn), var(--shadow-sm); }
.pad.bad  { border-color: var(--danger); box-shadow: inset 0 0 0 1px var(--danger), var(--shadow-sm); }

/* ---------- Timing readouts ----------
   One per pad, in a reserved row directly ABOVE the pads. Inside the pad they
   were hidden under the hand that had just hit it and, on a phone, printed
   straight over the pad's own label. Up here they sit in the same sightline
   as the stage, the row never changes height, and the two columns line up
   with the two pads so which reading belongs to which hand is never a
   question. */
.readouts {
  display: grid; grid-template-columns: 1fr 1fr; gap: var(--s3);
  flex: 0 0 auto; min-height: clamp(16px, 3.2dvh, 24px);
}
.err {
  display: flex; align-items: center; justify-content: center;
  align-self: center; justify-self: center;
  padding: 3px var(--s3); border-radius: 999px;
  font-family: var(--mono); font-size: 11.5px; font-weight: 700; white-space: nowrap;
  border: 1px solid currentColor;
  background: var(--bg-soft);
  color: var(--muted-2);
  opacity: 0; transform: translateY(5px) scale(.92);
  transition: opacity .16s ease, transform .16s cubic-bezier(.2, .9, .3, 1);
}
.err.show { opacity: 1; transform: none; }
.err.good { color: var(--good); }
.err.ok { color: var(--warn); }
.err.bad { color: var(--danger); }

.hist {
  position: absolute; bottom: var(--s3); left: 0; right: 0;
  display: flex; justify-content: center; gap: 3px;
}
.hist i { width: 5px; height: 5px; border-radius: 2px; background: var(--line); }
.hist i { transition: background .15s; }
.hist i.good { background: var(--good); }
.hist i.ok { background: var(--warn); }
.hist i.bad { background: var(--danger); }

/* how far into the current beat this voice is — a bar that empties into the hit */
.approach { position: absolute; left: 0; right: 0; bottom: 0; height: 4px; background: rgba(255, 255, 255, .05); }
.approach i { display: block; height: 100%; width: 0; background: var(--c); }

#statusHint { color: var(--muted-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* ---------- Settings sheet ----------
   A bottom sheet on phones, a centred card on desktop — the same shape the
   metronome's beat menu takes, for the same reason: a panel pinned to the
   bottom is reachable one-handed. */
.scrim {
  position: fixed; inset: 0; z-index: 40;
  background: rgba(5, 6, 9, .62); backdrop-filter: blur(3px);
  opacity: 0; pointer-events: none; transition: opacity .25s;
}
.scrim.on { opacity: 1; pointer-events: auto; }

.sheet {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 50;
  max-height: 88dvh; overflow-y: auto; overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  background: linear-gradient(180deg, var(--card), var(--bg));
  border-top: 1px solid var(--line);
  border-radius: var(--radius) var(--radius) 0 0;
  padding: var(--s2) var(--s4) calc(var(--sab) + var(--s5));
  box-shadow: 0 -16px 44px -20px rgba(0, 0, 0, .8);
  transform: translateY(calc(100% + 40px));
  visibility: hidden;
  transition: transform .32s cubic-bezier(.2, .8, .25, 1), visibility 0s linear .32s;
}
.sheet.on { transform: none; visibility: visible; transition: transform .32s cubic-bezier(.2, .8, .25, 1), visibility 0s; }
@media (min-width: 720px) {
  .sheet {
    left: 50%; right: auto; bottom: var(--s5); width: 560px;
    border-radius: var(--radius); border: 1px solid var(--line);
    max-height: 82dvh; padding-bottom: var(--s5);
    transform: translate(-50%, calc(100% + 64px));
  }
  .sheet.on { transform: translate(-50%, 0); }
}
.grab { width: 38px; height: 4px; border-radius: 2px; background: var(--line); margin: var(--s2) auto var(--s3); }
.sheet-close { position: absolute; top: var(--s3); right: var(--s3); }
.sheet-h {
  font-size: 10.5px; font-weight: 600; letter-spacing: .7px; text-transform: uppercase;
  color: var(--muted-2); margin: var(--s5) 0 var(--s3);
}
.sheet-h:first-of-type { margin-top: var(--s1); }

.presets { display: grid; grid-template-columns: repeat(auto-fill, minmax(78px, 1fr)); gap: var(--s2); }
.preset {
  padding: var(--s2) var(--s1);
  background: var(--bg-soft); border: 1px solid var(--line-soft);
  border-radius: var(--radius-xs); cursor: pointer; text-align: center;
  transition: border-color .15s, background .15s, transform .08s;
}
.preset:hover { border-color: var(--accent); }
.preset:active { transform: translateY(1px); }
.preset b { display: block; font-family: var(--mono); font-size: 15px; font-weight: 700; color: var(--text); }
.preset span {
  display: block; margin-top: 2px;
  font-size: 8.5px; font-weight: 600; letter-spacing: .6px; text-transform: uppercase; color: var(--muted-2);
}
.preset.active { background: var(--accent-soft); border-color: var(--accent); }
.preset.active span { color: var(--muted); }

.field-row { display: flex; gap: var(--s3); align-items: flex-end; margin-bottom: var(--s4); }
.field-label { display: flex; justify-content: space-between; align-items: baseline; gap: var(--s2); }
.field-label b { font-family: var(--mono); font-size: 11px; color: var(--text); text-transform: none; letter-spacing: 0; }
.field-note { font-size: 11px; color: var(--muted-2); margin-top: var(--s1); }

.toggles { display: flex; flex-direction: column; gap: 1px; border-radius: var(--radius-sm); overflow: hidden; border: 1px solid var(--line-soft); }
.toggle {
  display: flex; align-items: center; gap: var(--s3);
  padding: var(--s3); background: var(--bg-soft); cursor: pointer;
  transition: background .15s;
}
.toggle:hover { background: var(--card-2); }
.t-lbl { flex: 1 1 auto; min-width: 0; font-size: 13.5px; }
.t-lbl em { display: block; font-style: normal; font-size: 11.5px; color: var(--muted-2); margin-top: 1px; }

.danger-btn { width: 100%; color: var(--danger); border-color: rgba(255, 107, 129, .3); background: var(--danger-soft); }
.danger-btn:hover:not(:disabled) { border-color: var(--danger); color: var(--danger); background: rgba(255, 107, 129, .2); }

/* ---------- Toast ----------
   Pinned to the TOP of the screen and dropping in from above. It used to sit
   at the bottom, which put every "streak broken" message directly under the
   pads — over the two things being hit, at the moment of being hit.

   Up here it is clear of the pads and clear of the stage. It is not centred on
   the viewport but on the band that STOPS SHORT of the transport, so it can
   never land on the play button; --toast-l is the shell's left edge (the shell
   is 820px wide and centred), and the reserve on the right is the transport's
   own width. That keeps the one control up here always hittable. */
.toast {
  --toast-l: max(var(--s3), calc(50vw - 410px));
  position: fixed; z-index: 60;
  top: calc(var(--sat) + var(--s2));
  left: var(--toast-l);
  right: calc(var(--toast-l) + 124px);
  margin-inline: auto;
  width: max-content;
  max-width: min(420px, calc(100vw - var(--toast-l) * 2 - 124px));
  transform: translateY(-16px);
  padding: var(--s2) var(--s4); border-radius: 999px;
  font-size: 13px; font-weight: 600;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  text-align: center;
  background: var(--card);
  border: 1px solid var(--line);
  box-shadow: var(--shadow);
  opacity: 0; pointer-events: none;
  transition: opacity .22s, transform .22s cubic-bezier(.2, .9, .3, 1);
}
.toast.show { opacity: 1; transform: translateY(0); }
.toast.good { border-color: rgba(95, 227, 161, .5); color: var(--good); }
.toast.bad { border-color: rgba(255, 107, 129, .45); color: var(--danger); }

/* ---------- Responsive ---------- */
@media (max-width: 560px) {
  .ctrl-bar { gap: var(--s2) var(--s3); padding: var(--s2); }
  .ratio-num { font-size: 21px; }
  .ratio-step { width: 28px; height: 28px; }
  /* the tempo slider takes a line of its own rather than squeezing the ratio */
  .tempo-slide { order: 3; flex-basis: 100%; }
  .stat .k { font-size: 9px; letter-spacing: .5px; }
  .pads { gap: var(--s2); }
  .readouts { gap: var(--s2); }
  .err { font-size: 11px; padding: 2px var(--s2); }
  .pad { padding: var(--s2); }
  .pad-key { display: none; }
  /* "Voice A" shrinks to "A": the pad's colour and the readout above it
     already say which hand this is, and the row is short on width */
  .pad-lbl-full { display: none; }
  .pad-lbl { font-size: 11px; letter-spacing: 1.4px; color: var(--c); }
  #statusHint { display: none; }
  .toast { font-size: 12px; }
}

/* A phone in landscape has almost no height — the stage and the pads keep
   theirs by taking it from everything that is only labelling. */
@media (max-height: 480px) {
  .app-shell { gap: var(--s2); padding-top: calc(var(--sat) + var(--s2)); }
  .stage-foot { display: none; }
  .statusbar { display: none; }
  .stat { padding: var(--s1); }
  .pad-lbl { font-size: 9px; }
  /* no room for a hint the colour and the label already give */
  .pad-key { display: none; }
  .hist { bottom: var(--s1); }
  /* landscape has no vertical room to spare: the readout row keeps its place
     above the pads but gives back everything it can */
  .err { font-size: 10px; padding: 1px var(--s2); }
}

@media (hover: none) {
  .pad:hover { border-color: var(--line-soft); }
}
