$sloprun.dev

Metronome

demo sandbox: allow-scripts · csp: default-src 'none'

sandboxed and isolated in your browser · never enter a real password in a demo

▶ 9 ran · ✓ 0 worked
i ran it — no login needed:
share: preview embed ↗
post it anywhere:
email the card:

0 reported it broke · the ▶ count is distinct visitors who ran it, counted once each, anonymously

A metronome whose clicks are booked ahead of time on the sound card's own clock, so they land where they are supposed to even while the browser is busy repainting. A timer wakes up every 25 ms and schedules every click falling in the next 120 ms; the timer is allowed to be late, the clicks are not. To make the difference visible, a plain setInterval runs alongside doing nothing but counting, and its error is sampled at each tick — watch the millisecond count walk away from zero while the booked clicks stay exactly where they were put.

The arm is already swinging when the page opens — silent, because browsers only allow sound after a click. Press Start, then tap Tap tempo four times to lift the tempo out of your head, switch to 7/8 to hear the accents land on 1, 3 and 5, and turn on Triplets to subdivide the beat. The sliding weight rides up and down the scale as the tempo changes, exactly like the wind-up kind.

Embed this demo — put a runnable demo in your blog post

Paste this where you write. It renders a live, runnable demo inline anywhere raw HTML / iframes are allowed — Ghost, WordPress, Notion, Discourse, your own site.

<iframe src="https://sloprun.dev/embed/metronome" width="100%" height="640" loading="lazy" allow="" style="border:1px solid #E3E2DC;border-radius:10px" title="Metronome — a runnable demo on sloprun.dev"></iframe>
preview ↗

On Medium and dev.to the plain link becomes a rich preview card that links back here — they don't run third-party iframes, so paste the URL there and the reader clicks through to run it. https://sloprun.dev/p/metronome

Source

the code you see is the code that just ran raw ↗
Source — the code you see is the code that just ran 1034 lines 44.4 KB index.html
demos/metronome/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Metronome — sloprun.dev</title>
<link rel="icon" href="data:,">
<style>
/* ── sloprun design tokens (verbatim from demos/tokens.css) ───────────────── */
:root {
  --bg: #F6F6F3;          /* neutral paper, not cream */
  --surface: #FFFFFF;
  --ink: #1A1D21;
  --muted: #5A6068;
  --line: #E3E2DC;
  --accent: #C05600;      /* phosphor amber, darkened for light bg */
  --accent-soft: #FFF3E6;
  --run: #1A7F37;         /* earned green */
  --run-soft: #E7F4EA;
  --danger: #C93C37;
  --radius: 10px;
  --font-sans: system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono: ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas, monospace;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
    --line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
    --run: #3FB950; --run-soft: #16281B; --danger: #F47067;
  }
}
:root[data-theme="dark"] {
  --bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
  --line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
  --run: #3FB950; --run-soft: #16281B; --danger: #F47067;
}
body { background: var(--bg); color: var(--ink); font-family: var(--font-sans); }
/* ── end tokens ───────────────────────────────────────────────────────────── */

/* demo-local values the shared tokens don't carry */
:root {
  --well: #F1F0EC;
  --shadow: 0 1px 2px rgba(26,29,33,.05);
  --shadow-lift: 0 8px 22px rgba(26,29,33,.08);
  --glow: rgba(192,86,0,.28);
  --btn-ink: #FFFFFF;
  --rod: #3A3F47;
  --case: #EAE8E1;
  --slot: #FCFBF9;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --well: #16181D;
    --shadow: 0 1px 2px rgba(0,0,0,.35);
    --shadow-lift: 0 10px 26px rgba(0,0,0,.5);
    --glow: rgba(255,180,84,.3);
    --btn-ink: #14161A;
    --rod: #C6C3BC;
    --case: #242932;
    --slot: #12151A;
  }
}
:root[data-theme="dark"] {
  --well: #16181D;
  --shadow: 0 1px 2px rgba(0,0,0,.35);
  --shadow-lift: 0 10px 26px rgba(0,0,0,.5);
  --glow: rgba(255,180,84,.3);
  --btn-ink: #14161A;
  --rod: #C6C3BC;
  --case: #242932;
  --slot: #12151A;
}

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
html { -webkit-text-size-adjust: 100%; }
body { min-height: 100%; overflow-x: hidden; -webkit-tap-highlight-color: transparent; }

.wrap {
  max-width: 940px; margin: 0 auto;
  padding: 22px 16px 26px;
  display: flex; flex-direction: column; gap: 14px;
}
@media (max-width: 460px) { .wrap { padding: 16px 10px 20px; gap: 12px; } }

h1 { margin: 0 0 6px; font-size: clamp(21px, 3.6vw, 29px); letter-spacing: -.021em; line-height: 1.15; }
.lede { margin: 0; max-width: 68ch; color: var(--muted); font-size: 14.5px; line-height: 1.55; }
.lede b { color: var(--ink); font-weight: 600; }
.lede .m { font-family: var(--font-mono); font-size: 13px; color: var(--ink); }

.card {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow);
}

/* ── main split ───────────────────────────────────────────────────────────── */
.main { display: grid; grid-template-columns: minmax(0, 300px) minmax(0, 1fr); gap: 0; }
@media (max-width: 700px) { .main { grid-template-columns: 1fr; } }

.stage {
  padding: 16px 14px 18px; display: flex; flex-direction: column; align-items: center;
  border-right: 1px solid var(--line); background:
    radial-gradient(120% 60% at 50% 0%, var(--accent-soft) 0%, transparent 62%);
  border-radius: var(--radius) 0 0 var(--radius);
}
@media (max-width: 700px) {
  .stage { border-right: 0; border-bottom: 1px solid var(--line); border-radius: var(--radius) var(--radius) 0 0; }
}

.bigtempo { text-align: center; line-height: 1; }
.bpmline { font-family: var(--font-mono); font-weight: 600; letter-spacing: -.03em; display: flex; align-items: baseline; justify-content: center; gap: 6px; }
#bpmBig { font-size: clamp(40px, 11vw, 54px); font-variant-numeric: tabular-nums; }
.bpmline .unit { font-size: 12px; letter-spacing: .12em; color: var(--muted); }
.term { margin-top: 7px; font-size: 13px; color: var(--muted); font-style: italic; }

.metro { width: 100%; max-width: 252px; height: auto; display: block; margin: 4px 0 2px; }

.case { fill: var(--case); stroke: var(--line); stroke-width: 1.6; stroke-linejoin: round; }
.window { fill: var(--slot); stroke: var(--line); stroke-width: 1.2; }
.base { fill: var(--case); stroke: var(--line); stroke-width: 1.6; stroke-linejoin: round; }
.rod { fill: var(--rod); }
.bob { fill: var(--accent); stroke: rgba(0,0,0,.22); stroke-width: 1; }
.bobline { stroke: rgba(0,0,0,.25); stroke-width: 1; }
.pivot { fill: var(--surface); stroke: var(--rod); stroke-width: 2; }
.tick { stroke: var(--ink); opacity: .3; stroke-width: 1.5; stroke-linecap: round; }
.tick.maj { opacity: .55; }
.tlabel { fill: var(--ink); opacity: .6; font-family: var(--font-mono); font-size: 8px; letter-spacing: -.04em; }
.tip { fill: var(--accent); }
.gstop { stop-color: var(--accent); }

/* beat dots */
.dots { display: flex; gap: 8px; align-items: center; justify-content: center; min-height: 22px; margin-top: 8px; flex-wrap: wrap; }
.dot {
  width: 10px; height: 10px; border-radius: 50%;
  background: var(--line); border: 1px solid transparent;
  transition: background .12s linear, box-shadow .12s linear, border-color .12s linear;
}
.dot.acc { width: 14px; height: 14px; border-color: var(--line); background: transparent; }
.dot.mid { width: 12px; height: 12px; }
.dot.on { background: var(--accent); border-color: var(--accent); box-shadow: 0 0 0 4px var(--glow); }
@media (prefers-reduced-motion: reduce) {
  .dot, .btn, .chip, .step { transition: none; }
  .btn:active, .step:active { transform: none; }
}

.stagenote {
  margin: 12px 0 0; font-size: 11.5px; line-height: 1.45; color: var(--muted);
  text-align: center; max-width: 30ch;
}

/* ── control panel ────────────────────────────────────────────────────────── */
.panel { padding: 16px 16px 18px; display: flex; flex-direction: column; gap: 15px; min-width: 0; justify-content: space-between; }

.transport { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }

.btn {
  font: inherit; font-size: 13.5px; font-weight: 550;
  color: var(--ink); background: var(--surface);
  border: 1px solid var(--line); border-radius: 8px;
  padding: 9px 14px; cursor: pointer; box-shadow: var(--shadow);
  display: inline-flex; align-items: center; gap: 8px; line-height: 1;
  transition: transform .08s ease, background .15s ease, border-color .15s ease;
}
.btn:hover { border-color: var(--muted); }
.btn:active { transform: translateY(1px); }
.btn.primary { background: var(--accent); border-color: var(--accent); color: var(--btn-ink); min-width: 116px; justify-content: center; }
.btn.primary:hover { filter: brightness(1.06); }
.gl { font-size: 11px; line-height: 1; }

.status { display: inline-flex; align-items: center; gap: 7px; font-family: var(--font-mono); font-size: 11.5px; letter-spacing: .09em; color: var(--muted); }
.led { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); opacity: .5; }
.status.preview .led { background: var(--accent); opacity: 1; }
.status.preview { color: var(--accent); }
.status.run .led { background: var(--run); opacity: 1; box-shadow: 0 0 0 3px var(--run-soft); }
.status.run { color: var(--run); }

.readout {
  margin-left: auto; display: inline-flex; align-items: baseline; gap: 6px;
  font-family: var(--font-mono); font-size: 11px; color: var(--muted); letter-spacing: .08em;
}
.readout b { font-size: 13.5px; color: var(--ink); font-weight: 600; font-variant-numeric: tabular-nums; letter-spacing: 0; }

.ctl { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
.cl {
  font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .11em;
  color: var(--muted); display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
}
.cv { color: var(--ink); font-size: 12px; letter-spacing: .02em; font-variant-numeric: tabular-nums; }

.sliderrow { display: flex; align-items: center; gap: 8px; }
.step {
  font: inherit; font-family: var(--font-mono); font-size: 15px; line-height: 1;
  width: 32px; height: 32px; flex: 0 0 auto; border-radius: 8px;
  border: 1px solid var(--line); background: var(--surface); color: var(--ink);
  cursor: pointer; box-shadow: var(--shadow);
}
.step:hover { border-color: var(--muted); }
.step:active { transform: translateY(1px); }

input[type=range] {
  -webkit-appearance: none; appearance: none; width: 100%; min-width: 0;
  background: transparent; height: 26px; margin: 0; cursor: pointer;
}
input[type=range]::-webkit-slider-runnable-track { height: 6px; border-radius: 999px; background: var(--well); border: 1px solid var(--line); }
input[type=range]::-moz-range-track { height: 6px; border-radius: 999px; background: var(--well); border: 1px solid var(--line); }
input[type=range]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none; width: 18px; height: 18px; margin-top: -7px;
  border-radius: 50%; background: var(--accent); border: 2px solid var(--surface);
  box-shadow: 0 1px 3px rgba(0,0,0,.25);
}
input[type=range]::-moz-range-thumb {
  width: 16px; height: 16px; border-radius: 50%; background: var(--accent);
  border: 2px solid var(--surface); box-shadow: 0 1px 3px rgba(0,0,0,.25);
}

.taprow { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.btn.tap { position: relative; overflow: hidden; min-width: 118px; justify-content: center; }
.btn.tap.hit { background: var(--accent-soft); border-color: var(--accent); }
.tapout { font-family: var(--font-mono); font-size: 11.5px; color: var(--muted); letter-spacing: .02em; }
.tapout b { color: var(--ink); font-weight: 600; }

.chips { display: flex; flex-wrap: wrap; gap: 6px; }
.chip {
  font: inherit; font-family: var(--font-mono); font-size: 12px; letter-spacing: .02em;
  padding: 7px 11px; border-radius: 999px; cursor: pointer;
  border: 1px solid var(--line); background: var(--surface); color: var(--muted);
  transition: background .14s ease, color .14s ease, border-color .14s ease;
}
.chip:hover { border-color: var(--muted); color: var(--ink); }
.chip[aria-pressed="true"] { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); font-weight: 600; }
.chip .sub { font-size: 10px; opacity: .75; margin-left: 5px; letter-spacing: .04em; }
.cnote { margin: -1px 0 0; font-family: var(--font-mono); font-size: 11px; color: var(--muted); letter-spacing: .01em; }

/* ── under the hood ───────────────────────────────────────────────────────── */
.hood { padding: 14px 16px 16px; }
.hood h2 { margin: 0 0 3px; font-size: 14px; letter-spacing: -.01em; }
.hood .sub { margin: 0 0 12px; font-size: 13px; color: var(--muted); line-height: 1.5; max-width: 74ch; }
.rows { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
@media (max-width: 560px) { .rows { grid-template-columns: 1fr; } }
.hrow { background: var(--well); border-radius: 8px; padding: 9px 11px; display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.hk { font-family: var(--font-mono); font-size: 10px; letter-spacing: .12em; color: var(--muted); }
.hv { font-family: var(--font-mono); font-size: 12.5px; color: var(--ink); font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
.hrow.ok .hv { color: var(--run); }
.hrow.warn .hv { color: var(--accent); }
.hrow.bad .hv { color: var(--danger); }

.hint { margin: 0; font-size: 12.5px; line-height: 1.6; color: var(--muted); }
.hint b { color: var(--ink); font-weight: 600; }
kbd {
  font-family: var(--font-mono); font-size: 11px; padding: 2px 6px; border-radius: 5px;
  border: 1px solid var(--line); background: var(--surface); color: var(--ink); box-shadow: var(--shadow);
}
.note {
  margin: 0; font-size: 12.5px; color: var(--danger);
  border: 1px solid var(--line); border-left: 3px solid var(--danger);
  background: var(--surface); border-radius: 6px; padding: 8px 11px;
}
footer { font-family: var(--font-mono); font-size: 11px; color: var(--muted); letter-spacing: .06em; padding-top: 2px; }

:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 6px; }
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
</style>
</head>
<body>
<div class="wrap">

  <header>
    <h1>Metronome</h1>
    <p class="lede">
      A click track that keeps time on the <b>sound card's own clock</b> instead of a JavaScript timer.
      The arm is already swinging &mdash; it is silent until you press <b>Start</b>, because browsers
      only allow sound after a click.
    </p>
  </header>

  <section class="card main">

    <div class="stage">
      <div class="bigtempo">
        <div class="bpmline"><span id="bpmBig">108</span><span class="unit">BPM</span></div>
        <div class="term" id="term">Moderato</div>
      </div>

      <svg class="metro" id="metro" viewBox="0 0 200 232" role="img" aria-labelledby="metroTitle">
        <title id="metroTitle">A metronome arm swinging in time with the beat</title>
        <defs>
          <radialGradient id="mGlow" cx="50%" cy="50%" r="50%">
            <stop class="gstop" offset="0%" stop-opacity=".5"/>
            <stop class="gstop" offset="100%" stop-opacity="0"/>
          </radialGradient>
        </defs>

        <path class="case" d="M30 214 L72 54 L128 54 L170 214 Z"/>
        <rect class="window" x="71" y="64" width="57" height="140" rx="6"/>
        <g id="ticks"></g>
        <rect class="base" x="22" y="210" width="156" height="13" rx="5"/>

        <g id="arm">
          <circle id="glow" cx="100" cy="20" r="30" fill="url(#mGlow)" opacity="0"/>
          <rect class="rod" x="97.8" y="18" width="4.4" height="190" rx="2.2"/>
          <g id="bobG" transform="translate(0,140)">
            <rect class="bob" x="89" y="-8" width="22" height="16" rx="3"/>
            <line class="bobline" x1="91" y1="-3.5" x2="109" y2="-3.5"/>
          </g>
          <circle class="tip" id="tip" cx="100" cy="20" r="5"/>
        </g>

        <circle class="pivot" cx="100" cy="206" r="6.5"/>
      </svg>

      <div class="dots" id="dots" aria-hidden="true"></div>
      <p class="stagenote" id="stageNote">Silent preview &mdash; press Start to hear it.</p>
    </div>

    <div class="panel">
      <div class="transport">
        <button class="btn primary" id="play" type="button">
          <span class="gl" id="playGlyph" aria-hidden="true">&#9654;</span><span id="playLabel">Start</span>
        </button>
        <span class="status preview" id="status"><span class="led" aria-hidden="true"></span><span id="statusText">PREVIEW</span></span>
        <div class="readout" aria-hidden="true">
          <span>BAR</span><b id="roBar">001</b><span>BEAT</span><b id="roBeat">1/4</b>
        </div>
      </div>

      <div class="ctl">
        <label class="cl" for="tempo">TEMPO<span class="cv"><span id="tempoV">108</span> BPM</span></label>
        <div class="sliderrow">
          <button class="step" id="slower" type="button" aria-label="Slower by one BPM">&minus;</button>
          <input type="range" id="tempo" min="40" max="240" step="1" value="108" aria-label="Tempo in beats per minute">
          <button class="step" id="faster" type="button" aria-label="Faster by one BPM">+</button>
        </div>
      </div>

      <div class="ctl">
        <span class="cl">TAP IT OUT</span>
        <div class="taprow">
          <button class="btn tap" id="tap" type="button">Tap tempo</button>
          <span class="tapout" id="tapOut">tap 4 times on the beat</span>
        </div>
      </div>

      <div class="ctl">
        <span class="cl" id="sigLabel">TIME SIGNATURE</span>
        <div class="chips" id="sigChips" role="group" aria-labelledby="sigLabel"></div>
        <p class="cnote" id="sigNote"></p>
      </div>

      <div class="ctl">
        <span class="cl" id="divLabel">SUBDIVIDE THE BEAT</span>
        <div class="chips" id="divChips" role="group" aria-labelledby="divLabel"></div>
        <p class="cnote" id="divNote"></p>
      </div>

      <div class="ctl">
        <label class="cl" for="vol">VOLUME<span class="cv"><span id="volV">70</span>%</span></label>
        <input type="range" id="vol" min="0" max="100" step="1" value="70" aria-label="Volume">
      </div>
    </div>
  </section>

  <section class="card hood">
    <h2>Under the hood</h2>
    <p class="sub">
      A timer that fires every 25&nbsp;ms wakes up and books every click that falls inside the next
      120&nbsp;ms, stamped with an exact time on the audio clock. The timer is allowed to be late;
      the clicks are not. A plain <span style="font-family:var(--font-mono)">setInterval</span> runs
      alongside doing nothing but counting, and its error is read at each tick &mdash; that is the
      number you can watch slide away from zero.
    </p>
    <div class="rows">
      <div class="hrow"><span class="hk">SCHEDULER</span><span class="hv" id="hSched">wakes every 25 ms &middot; books 120 ms ahead</span></div>
      <div class="hrow" id="hRowJit"><span class="hk">TIMER LATENESS</span><span class="hv" id="hJit">&mdash;</span></div>
      <div class="hrow" id="hRowNote"><span class="hk">CLICK PLACEMENT</span><span class="hv" id="hNote">waiting for start</span></div>
      <div class="hrow" id="hRowDrift"><span class="hk">PLAIN setInterval</span><span class="hv" id="hDrift">&mdash;</span></div>
    </div>
  </section>

  <p class="hint">
    <kbd>Space</kbd> starts and stops, <kbd>T</kbd> taps the tempo, <kbd>&uarr;</kbd><kbd>&darr;</kbd> nudge
    it by 1 (hold <kbd>Shift</kbd> for 5). The <b>first beat of every bar</b> is the loud one; 4/4, 5/4
    and 6/8 add one softer accent inside the bar, and 7/8 adds two &mdash; it counts 2+2+3.
  </p>
  <p class="note" id="audioNote" hidden>This browser has no Web Audio support, so the arm swings without sound.</p>
  <p class="note" id="blockNote" hidden></p>

  <footer>demo &middot; sloprun.dev</footer>
</div>

<div class="sr" id="live" role="status" aria-live="polite"></div>

<script>
(function () {
  "use strict";

  var root = document.documentElement;
  var AC = window.AudioContext || window.webkitAudioContext;
  var audioDead = !AC;

  var mq = window.matchMedia ? window.matchMedia("(prefers-reduced-motion: reduce)") : null;
  var reduce = mq ? mq.matches : false;
  if (mq) {
    if (mq.addEventListener) mq.addEventListener("change", function (e) { reduce = e.matches; });
    else if (mq.addListener) mq.addListener(function (e) { reduce = e.matches; });
  }

  /* ── musical config ─────────────────────────────────────────────────────── */
  /* accent levels: 2 = downbeat, 1 = inner accent, 0 = plain, -1 = subdivision */
  var SIGS = [
    { id: "2/4", beats: 2, pat: [2, 0] },
    { id: "3/4", beats: 3, pat: [2, 0, 0] },
    { id: "4/4", beats: 4, pat: [2, 0, 1, 0] },
    { id: "5/4", beats: 5, pat: [2, 0, 0, 1, 0] },
    { id: "6/8", beats: 6, pat: [2, 0, 0, 1, 0, 0] },
    { id: "7/8", beats: 7, pat: [2, 0, 1, 0, 1, 0, 0] }
  ];
  var DIVS = [
    { n: 1, label: "Quarter", sub: "×1" },
    { n: 2, label: "Eighths", sub: "×2" },
    { n: 3, label: "Triplets", sub: "×3" },
    { n: 4, label: "Sixteenths", sub: "×4" }
  ];
  var TERMS = [
    [40, "Grave"], [46, "Largo"], [56, "Larghetto"], [66, "Adagio"], [76, "Andante"],
    [108, "Moderato"], [120, "Allegro moderato"], [156, "Allegro"], [176, "Vivace"],
    [200, "Presto"], [241, "Prestissimo"]
  ];

  var LOOKAHEAD_MS = 25;   /* how often the timer wakes up */
  var AHEAD = 0.12;        /* how far ahead of the audio clock we book clicks */
  var AMP = 24;            /* pendulum swing, degrees */

  var state = { bpm: 108, sig: 2, div: 1, vol: 70, mode: "preview" };
  function spb() { return 60 / state.bpm; }
  function sig() { return SIGS[state.sig]; }

  /* ── elements ───────────────────────────────────────────────────────────── */
  var $ = function (id) { return document.getElementById(id); };
  var elArm = $("arm"), elBob = $("bobG"), elTip = $("tip"), elGlow = $("glow");
  var elDots = $("dots"), elBpmBig = $("bpmBig"), elTerm = $("term");
  var elTempo = $("tempo"), elTempoV = $("tempoV"), elVol = $("vol"), elVolV = $("volV");
  var elPlay = $("play"), elPlayLabel = $("playLabel"), elPlayGlyph = $("playGlyph");
  var elStatus = $("status"), elStatusText = $("statusText");
  var elBar = $("roBar"), elBeat = $("roBeat"), elStageNote = $("stageNote");
  var elTap = $("tap"), elTapOut = $("tapOut"), elLive = $("live");
  var elJit = $("hJit"), elNote = $("hNote"), elDrift = $("hDrift");
  var rowNote = $("hRowNote"), rowDrift = $("hRowDrift"), rowJit = $("hRowJit");

  /* ── audio ──────────────────────────────────────────────────────────────── */
  var ctx = null, master = null, noiseBuf = null;

  function volGain() { var v = state.vol / 100; return v * v * 0.9; }

  function ensureAudio() {
    if (ctx || audioDead) return ctx;
    try { ctx = new AC(); } catch (e) { ctx = null; audioDead = true; $("audioNote").hidden = false; return null; }
    master = ctx.createGain();
    master.gain.value = volGain();
    master.connect(ctx.destination);
    var n = Math.max(1, Math.floor(ctx.sampleRate * 0.2));
    noiseBuf = ctx.createBuffer(1, n, ctx.sampleRate);
    var d = noiseBuf.getChannelData(0);
    for (var i = 0; i < n; i++) d[i] = Math.random() * 2 - 1;
    return ctx;
  }

  /* stalled = the context reports "running" but its clock never advances
     (no output device). Fall back to the wall clock so the arm keeps time. */
  var stalled = false;
  function audioUsable() { return !!ctx && !stalled; }
  function nowSec() { return audioUsable() ? ctx.currentTime : performance.now() / 1000; }

  var VOICE = {
    "2":  { f: 1660, g: 0.95, n: 0.32, d: 0.055 },
    "1":  { f: 1245, g: 0.66, n: 0.22, d: 0.050 },
    "0":  { f: 988,  g: 0.54, n: 0.17, d: 0.046 },
    "-1": { f: 1320, g: 0.17, n: 0.06, d: 0.026 }
  };

  function click(t, lvl) {
    if (!ctx) return;
    var s = VOICE[String(lvl)] || VOICE["0"];
    var o = ctx.createOscillator();
    o.type = "sine";
    o.frequency.setValueAtTime(s.f, t);
    o.frequency.exponentialRampToValueAtTime(s.f * 0.7, t + s.d);
    var g = ctx.createGain();
    g.gain.setValueAtTime(0.0001, t);
    g.gain.exponentialRampToValueAtTime(s.g, t + 0.0018);
    g.gain.exponentialRampToValueAtTime(0.0001, t + s.d);
    o.connect(g); g.connect(master);
    o.start(t); o.stop(t + s.d + 0.02);

    var src = ctx.createBufferSource();
    src.buffer = noiseBuf;
    var hp = ctx.createBiquadFilter();
    hp.type = "highpass"; hp.frequency.value = 2400;
    var ng = ctx.createGain();
    ng.gain.setValueAtTime(s.n, t);
    ng.gain.exponentialRampToValueAtTime(0.0001, t + 0.013);
    src.connect(hp); hp.connect(ng); ng.connect(master);
    src.start(t); src.stop(t + 0.04);
  }

  /* ── the lookahead scheduler ────────────────────────────────────────────── */
  var timer = null, nextTick = 0, tickInBeat = 0, sBeatInBar = 0, sBar = 1, sTotal = 0;
  var vq = [], placed = 0;
  var wakePrev = 0, jitPeak = 0, jitSum = 0, jitN = 0, jitAt = 0, dropped = 0;
  var shTimer = null, shTicks = 0, shStart = 0, shPeriod = 0, shDrift = 0;

  function scheduler() {
    if (!ctx) return;
    var t = performance.now();
    if (wakePrev) {
      var late = Math.abs(t - wakePrev - LOOKAHEAD_MS);
      if (late > jitPeak) jitPeak = late;
      jitSum += late; jitN++;
    }
    wakePrev = t;

    var now = ctx.currentTime;
    /* the wake-up was so late that a click would have been booked in the past —
       the only case where this scheduler can actually lose one */
    if (nextTick < now) { dropped++; nextTick = now + 0.01; }

    /* nothing drains the visual queue while the tab is hidden, so keep it bounded */
    if (vq.length > 400) vq.splice(0, vq.length - 200);

    while (nextTick < now + AHEAD) {
      var b = sig().beats;
      var inBar = sBeatInBar % b;
      var lvl = tickInBeat === 0 ? sig().pat[inBar] : -1;
      click(nextTick, lvl);
      placed++;
      vq.push({ t: nextTick, main: tickInBeat === 0, lvl: lvl, beat: sTotal, bar: sBar, inBar: inBar });
      nextTick += spb() / state.div;
      tickInBeat++;
      if (tickInBeat >= state.div) {
        tickInBeat = 0; sTotal++; sBeatInBar = inBar + 1;
        if (sBeatInBar >= b) { sBeatInBar = 0; sBar++; }
      }
    }
  }

  /* A plain setInterval running alongside, purely as a control. Its error is
     sampled AT each tick — comparing a mid-beat clock reading against a whole
     number of ticks would just measure where you are inside the beat. */
  function startShadow() {
    stopShadow();
    if (!ctx) return;
    shTicks = 0; shDrift = 0; shPeriod = spb(); shStart = ctx.currentTime;
    shTimer = setInterval(function () {
      if (!ctx) return;
      shTicks++;
      shDrift = ((ctx.currentTime - shStart) - shTicks * shPeriod) * 1000;
    }, shPeriod * 1000);
  }
  function stopShadow() { if (shTimer) { clearInterval(shTimer); shTimer = null; } }

  /* ── the silent preview clock (before the first click, and if audio is off) ─ */
  var pNext = 0, pInBar = 0, pBar = 1, pTotal = 0;

  function previewStep(now) {
    if (!pNext) pNext = now + 0.05;
    if (now - pNext > 0.6) pNext = now;          /* tab was hidden: rejoin the beat */
    var guard = 0;
    while (now >= pNext && guard++ < 16) {
      var b = sig().beats;
      var inBar = pInBar % b;
      applyBeat({ t: pNext, main: true, lvl: sig().pat[inBar], beat: pTotal, bar: pBar, inBar: inBar });
      pNext += spb();
      pTotal++; pInBar = inBar + 1;
      if (pInBar >= b) { pInBar = 0; pBar++; }
    }
  }

  /* ── visual state ───────────────────────────────────────────────────────── */
  var clock = { t: 0, beat: 0, lvl: 0 };
  var subFlash = 0, activeDot = -1, shownBar = -1, shownBeat = -1;
  var raf = null, stopAt = 0, lastAngle = 0;

  function applyBeat(e) {
    clock.t = e.t; clock.beat = e.beat; clock.lvl = e.lvl;
    if (activeDot !== e.inBar) {
      var kids = elDots.children;
      if (activeDot >= 0 && kids[activeDot]) kids[activeDot].classList.remove("on");
      if (kids[e.inBar]) kids[e.inBar].classList.add("on");
      activeDot = e.inBar;
    }
    if (shownBar !== e.bar) { shownBar = e.bar; elBar.textContent = pad3(e.bar); }
    if (shownBeat !== e.inBar) { shownBeat = e.inBar; elBeat.textContent = (e.inBar + 1) + "/" + sig().beats; }
  }

  function drain(now) {
    var main = null;
    while (vq.length && vq[0].t <= now + 0.004) {
      var e = vq.shift();
      if (e.main) main = e; else subFlash = 1;
    }
    if (main) applyBeat(main);
  }

  function pad3(n) { return n < 10 ? "00" + n : n < 100 ? "0" + n : String(n); }

  function frame() {
    raf = requestAnimationFrame(frame);
    var now = nowSec();

    if (state.mode === "preview" || (state.mode === "running" && !audioUsable())) previewStep(now);
    else if (state.mode === "running") drain(now);

    var damp = 1;
    if (state.mode === "stopped") {
      damp = Math.exp(-(now - stopAt) * 2.4);
      if (damp < 0.012) damp = 0;
    }

    var phase = clock.beat + (now - clock.t) / spb();
    var ang = reduce ? 0 : AMP * Math.cos(Math.PI * phase) * damp;
    lastAngle = ang;
    elArm.setAttribute("transform", "rotate(" + ang.toFixed(2) + " 100 206)");

    var since = now - clock.t;
    var pulse = state.mode === "stopped" ? 0 : Math.exp(-Math.max(0, since) * 8);
    var strength = clock.lvl === 2 ? 1 : clock.lvl === 1 ? 0.75 : 0.55;
    var p = pulse * strength;
    if (subFlash > 0) { p = Math.max(p, subFlash * 0.3); subFlash -= 0.14; if (subFlash < 0) subFlash = 0; }
    /* the arm is already frozen under reduced motion; keep the glow as a soft
       marker rather than a full-strength flash up to 4 times a second */
    if (reduce) p = state.mode === "stopped" ? 0 : Math.min(p, 0.3) + 0.12;

    elGlow.setAttribute("opacity", (p * 0.95).toFixed(3));
    elTip.setAttribute("r", (reduce ? 5 : 5 + p * 4.5).toFixed(2));

    if (now - hoodAt > 0.22) { hoodAt = now; updateHood(now); }

    if (state.mode === "stopped" && damp === 0) {
      elArm.setAttribute("transform", "rotate(0 100 206)");
      elGlow.setAttribute("opacity", "0");
      elTip.setAttribute("r", "5");
      cancelAnimationFrame(raf); raf = null;
    }
  }

  function ensureLoop() { if (!raf) raf = requestAnimationFrame(frame); }

  /* ── the instrument panel readouts ──────────────────────────────────────── */
  var hoodAt = 0;
  function updateHood(now) {
    if (state.mode !== "running" || !audioUsable()) return;
    if (jitN > 4) {
      elJit.textContent = "avg " + (jitSum / jitN).toFixed(1) + " ms · peak " + jitPeak.toFixed(1) + " ms late";
      rowJit.className = "hrow warn";
    }
    if (now - jitAt > 4) { jitAt = now; jitSum = 0; jitN = 0; jitPeak = 0; }

    elNote.textContent = "sample-exact · " + placed + " placed, " + dropped + " dropped";
    rowNote.className = "hrow " + (dropped ? "warn" : "ok");

    if (shTicks > 3) {
      var ms = Math.abs(shDrift), tail = " after " + shTicks + " ticks";
      elDrift.textContent = ms < 0.5 ? "level with the audio clock" + tail
        : (shDrift > 0 ? "+" : "−") + ms.toFixed(0) + " ms " +
          (shDrift > 0 ? "behind" : "ahead") + tail;
      rowDrift.className = "hrow " + (ms > 90 ? "bad" : ms < 0.5 ? "ok" : "warn");
    } else {
      elDrift.textContent = "counting…";
      rowDrift.className = "hrow";
    }
  }

  function resetHood() {
    elJit.textContent = "—";
    rowJit.className = "hrow";
    elNote.textContent = "waiting for start";
    rowNote.className = "hrow";
    elDrift.textContent = "—";
    rowDrift.className = "hrow";
  }

  /* ── transport ──────────────────────────────────────────────────────────── */
  function setStatus(kind, text) {
    elStatus.className = "status" + (kind ? " " + kind : "");
    elStatusText.textContent = text;
  }

  var blockCheck = null;

  /* Hand the beat to the wall clock and say why the room went quiet. */
  function fallBackToSilent(msg, note) {
    clearTimeout(blockCheck);
    if (timer) { clearInterval(timer); timer = null; }
    stopShadow();
    vq.length = 0;
    pNext = 0; pInBar = 0; pBar = 1; pTotal = 0;
    clock.t = performance.now() / 1000; clock.beat = 0; clock.lvl = sig().pat[0];
    resetHood();
    elNote.textContent = "no audio clock — nothing to place";
    $("blockNote").textContent = note;
    $("blockNote").hidden = false;
    elStageNote.textContent = msg;
    setStatus("preview", "SILENT");
    ensureLoop();
  }

  function audioBlocked() {
    if (state.mode !== "running" || !ctx || ctx.state === "running") return;
    fallBackToSilent(
      "The browser is holding sound back — press Start again.",
      "The browser blocked the audio clock from starting. Press Start once more — a direct click usually releases it.");
  }

  /* A context can report "running" while its clock stands still: no output
     device. Nothing would ever play and the arm would freeze mid-swing. */
  function checkClockMoving(t0, tries) {
    if (state.mode !== "running" || !ctx) return;
    if (ctx.state !== "running") { audioBlocked(); return; }
    if (ctx.currentTime - t0 > 0.05) return;
    /* a slow resume can look identical for a moment — give it one more look */
    if (tries > 0) {
      blockCheck = setTimeout(function () { checkClockMoving(t0, tries - 1); }, 1200);
      return;
    }
    stalled = true;
    fallBackToSilent(
      "This machine has no audio clock — the arm still keeps time.",
      "The audio clock never started ticking, so there is no output device to click on. The metronome keeps time silently.");
  }

  function start() {
    $("blockNote").hidden = true;
    stalled = false;
    var c = ensureAudio();
    if (c) {
      if (c.state !== "running") {
        var pr = c.resume();
        if (pr && pr.then) pr.then(null, audioBlocked);
      }
      master.gain.cancelScheduledValues(c.currentTime);
      master.gain.setValueAtTime(volGain(), c.currentTime);
      clearTimeout(blockCheck);
      blockCheck = setTimeout(function (t0) {
        return function () { checkClockMoving(t0, 1); };
      }(c.currentTime), 500);
    }
    state.mode = "running";
    tickInBeat = 0; sBeatInBar = 0; sBar = 1; sTotal = 0;
    vq.length = 0; placed = 0; dropped = 0;
    wakePrev = 0; jitPeak = 0; jitSum = 0; jitN = 0;
    clock.t = nowSec(); clock.beat = 0; clock.lvl = sig().pat[0];
    activeDot = -1; shownBar = -1; shownBeat = -1;
    if (c) {
      nextTick = c.currentTime + 0.09;
      jitAt = c.currentTime;
      if (timer) clearInterval(timer);
      timer = setInterval(scheduler, LOOKAHEAD_MS);
      scheduler();
      startShadow();
      elStageNote.textContent = "Every click below is booked before it is heard.";
      setStatus("run", "RUNNING");
    } else {
      pNext = nowSec(); pInBar = 0; pBar = 1; pTotal = 0;
      elStageNote.textContent = "No sound available here — the arm still keeps time.";
      setStatus("preview", "SILENT");
    }
    elPlayLabel.textContent = "Stop";
    elPlayGlyph.innerHTML = "&#9632;";
    elPlay.setAttribute("aria-label", "Stop the metronome");
    say("Started at " + state.bpm + " BPM in " + sig().id);
    ensureLoop();
  }

  function stop() {
    state.mode = "stopped";
    clearTimeout(blockCheck);
    $("blockNote").hidden = true;
    if (timer) { clearInterval(timer); timer = null; }
    stopShadow();
    /* clicks are booked up to AHEAD seconds out; duck the bus so Stop is silent now */
    if (ctx && master) {
      master.gain.cancelScheduledValues(ctx.currentTime);
      master.gain.setTargetAtTime(0.0001, ctx.currentTime, 0.012);
    }
    vq.length = 0;
    stopAt = nowSec();
    if (activeDot >= 0 && elDots.children[activeDot]) elDots.children[activeDot].classList.remove("on");
    activeDot = -1;
    elPlayLabel.textContent = "Start";
    elPlayGlyph.innerHTML = "&#9654;";
    elPlay.setAttribute("aria-label", "Start the metronome");
    setStatus("", "STOPPED");
    elStageNote.textContent = "Stopped. Press Start to pick it up again.";
    resetHood();
    say("Stopped");
    ensureLoop();
  }

  function toggle() { if (state.mode === "running") stop(); else start(); }

  /* ── tempo ──────────────────────────────────────────────────────────────── */
  var Y_TOP = 78, Y_BOT = 190, L_LO = Math.log(40), L_HI = Math.log(240);
  function bpmY(b) { return Y_TOP + (Math.log(b) - L_LO) / (L_HI - L_LO) * (Y_BOT - Y_TOP); }

  function termFor(b) {
    for (var i = 0; i < TERMS.length; i++) if (b < TERMS[i][0]) return i === 0 ? TERMS[0][1] : TERMS[i - 1][1];
    return TERMS[TERMS.length - 1][1];
  }

  var sayTimer = null;
  function say(msg) {
    clearTimeout(sayTimer);
    elLive.textContent = "";
    sayTimer = setTimeout(function () { elLive.textContent = msg; }, 110);
  }

  var tempoSay = null;
  function setBpm(v, src) {
    v = Math.round(v);
    if (v < 40) v = 40; if (v > 240) v = 240;
    if (v === state.bpm && src !== "init") return;
    state.bpm = v;
    elBpmBig.textContent = v;
    elTempoV.textContent = v;
    elTerm.textContent = termFor(v);
    if (elTempo.value !== String(v)) elTempo.value = String(v);
    elBob.setAttribute("transform", "translate(0," + bpmY(v).toFixed(1) + ")");
    captions();
    if (state.mode === "running" && audioUsable()) startShadow();
    if (src && src !== "init" && src !== "slider") {
      clearTimeout(tempoSay);
      tempoSay = setTimeout(function () { say(v + " BPM, " + termFor(v)); }, 260);
    }
  }

  /* ── tap tempo ──────────────────────────────────────────────────────────── */
  var taps = [], tapReset = null;
  function doTap() {
    var t = performance.now();
    if (taps.length && t - taps[taps.length - 1] > 2500) taps = [];
    taps.push(t);
    if (taps.length > 7) taps.shift();

    elTap.classList.add("hit");
    setTimeout(function () { elTap.classList.remove("hit"); }, 110);

    if (taps.length < 2) {
      elTapOut.innerHTML = "<b>1</b> tap · keep going";
    } else {
      var gaps = [], i;
      for (i = 1; i < taps.length; i++) gaps.push(taps[i] - taps[i - 1]);
      var sum = 0, lo = Infinity, hi = 0;
      for (i = 0; i < gaps.length; i++) { sum += gaps[i]; if (gaps[i] < lo) lo = gaps[i]; if (gaps[i] > hi) hi = gaps[i]; }
      var avg = sum / gaps.length;
      var bpm = 60000 / avg;
      if (bpm >= 30 && bpm <= 400) {
        setBpm(bpm, "tap");
        elTapOut.innerHTML = "<b>" + taps.length + "</b> taps · <b>" + Math.round(bpm) +
          "</b> BPM · spread " + Math.round(hi - lo) + " ms";
      } else {
        taps = [t];
        elTapOut.innerHTML = "too far apart · start again";
      }
    }
    clearTimeout(tapReset);
    tapReset = setTimeout(function () {
      taps = [];
      elTapOut.textContent = "tap 4 times on the beat";
    }, 3000);
  }

  /* ── build the pickers, dots and scale ──────────────────────────────────── */
  function buildDots() {
    var b = sig().beats, pat = sig().pat, html = "";
    for (var i = 0; i < b; i++) {
      var cls = "dot" + (pat[i] === 2 ? " acc" : pat[i] === 1 ? " mid" : "");
      html += '<span class="' + cls + '"></span>';
    }
    elDots.innerHTML = html;
    activeDot = -1;
    elBeat.textContent = "1/" + b;
    shownBeat = -1;
  }

  function captions() {
    var sg = sig(), acc = [];
    for (var i = 0; i < sg.beats; i++) if (sg.pat[i] > 0) acc.push(i + 1);
    var list = acc.length === 1 ? String(acc[0]) :
               acc.slice(0, -1).join(", ") + " & " + acc[acc.length - 1];
    var unit = sg.id.charAt(sg.id.length - 1) === "8" ? "an eighth note" : "a quarter note";
    $("sigNote").textContent = sg.beats + " to the bar · accent" + (acc.length > 1 ? "s" : "") +
      " on " + list + " · one click is " + unit;
    $("divNote").textContent = state.div === 1
      ? "one click per beat · " + state.bpm + " a minute"
      : state.div + " clicks per beat · " + (state.bpm * state.div) + " a minute";
  }

  function buildChips() {
    var s = "", i;
    for (i = 0; i < SIGS.length; i++) {
      s += '<button class="chip" type="button" data-sig="' + i + '" aria-pressed="' +
           (i === state.sig) + '">' + SIGS[i].id + '</button>';
    }
    $("sigChips").innerHTML = s;
    s = "";
    for (i = 0; i < DIVS.length; i++) {
      s += '<button class="chip" type="button" data-div="' + i + '" aria-pressed="' +
           (DIVS[i].n === state.div) + '">' + DIVS[i].label +
           '<span class="sub">' + DIVS[i].sub + '</span></button>';
    }
    $("divChips").innerHTML = s;
  }

  function buildScale() {
    var marks = [40, 46, 52, 60, 69, 80, 92, 104, 120, 138, 160, 184, 208, 240];
    var majors = { 40: 1, 60: 1, 80: 1, 120: 1, 160: 1, 240: 1 };
    var NS = "http://www.w3.org/2000/svg";
    var g = $("ticks");
    for (var i = 0; i < marks.length; i++) {
      var y = bpmY(marks[i]), maj = majors[marks[i]];
      /* ticks live to the RIGHT of the rod, numbers to the LEFT of it, so the
         sliding weight (x 89–111) never sits on top of a number */
      var ln = document.createElementNS(NS, "line");
      ln.setAttribute("class", "tick" + (maj ? " maj" : ""));
      ln.setAttribute("x1", maj ? "103" : "106"); ln.setAttribute("x2", "116");
      ln.setAttribute("y1", y.toFixed(1)); ln.setAttribute("y2", y.toFixed(1));
      g.appendChild(ln);
      if (maj) {
        var tx = document.createElementNS(NS, "text");
        tx.setAttribute("class", "tlabel");
        tx.setAttribute("text-anchor", "end");
        tx.setAttribute("x", "88"); tx.setAttribute("y", (y + 2.6).toFixed(1));
        tx.textContent = marks[i];
        g.appendChild(tx);
      }
    }
  }

  /* ── wiring ─────────────────────────────────────────────────────────────── */
  elPlay.addEventListener("click", toggle);
  elTap.addEventListener("click", doTap);
  $("slower").addEventListener("click", function () { setBpm(state.bpm - 1, "btn"); });
  $("faster").addEventListener("click", function () { setBpm(state.bpm + 1, "btn"); });

  elTempo.addEventListener("input", function () { setBpm(+elTempo.value, "slider"); });
  elTempo.addEventListener("change", function () { say(state.bpm + " BPM, " + termFor(state.bpm)); });

  elVol.addEventListener("input", function () {
    state.vol = +elVol.value;
    elVolV.textContent = state.vol;
    if (ctx && master && state.mode !== "stopped") master.gain.setTargetAtTime(volGain(), ctx.currentTime, 0.01);
  });

  $("sigChips").addEventListener("click", function (ev) {
    var b = ev.target.closest ? ev.target.closest("[data-sig]") : null;
    if (!b) return;
    var i = +b.getAttribute("data-sig");
    if (i === state.sig) return;
    state.sig = i;
    var kids = this.children;
    for (var k = 0; k < kids.length; k++) kids[k].setAttribute("aria-pressed", String(k === i));
    sBeatInBar = 0; pInBar = 0;
    buildDots();
    captions();
    say(sig().id + " time, " + sig().beats + " beats to the bar");
  });

  $("divChips").addEventListener("click", function (ev) {
    var b = ev.target.closest ? ev.target.closest("[data-div]") : null;
    if (!b) return;
    var i = +b.getAttribute("data-div");
    if (DIVS[i].n === state.div) return;
    state.div = DIVS[i].n;
    tickInBeat = 0;
    var kids = this.children;
    for (var k = 0; k < kids.length; k++) kids[k].setAttribute("aria-pressed", String(k === i));
    captions();
    say(DIVS[i].label + ", " + DIVS[i].n + " per beat");
  });

  document.addEventListener("keydown", function (ev) {
    if (ev.metaKey || ev.ctrlKey || ev.altKey) return;
    var tag = ev.target && ev.target.tagName;
    var onControl = tag === "BUTTON" || tag === "INPUT" || tag === "SELECT" || tag === "TEXTAREA";
    var k = ev.key;
    if (ev.code === "Space" || k === " " || k === "Spacebar") {
      if (onControl) return;           /* let the focused control handle it */
      ev.preventDefault();
      if (!ev.repeat) toggle();
    } else if (k === "t" || k === "T") {
      if (tag === "INPUT" || tag === "TEXTAREA") return;
      ev.preventDefault(); doTap();
    } else if (k === "ArrowUp" || k === "ArrowRight") {
      if (tag === "INPUT") return;
      ev.preventDefault(); setBpm(state.bpm + (ev.shiftKey ? 5 : 1), "kbd");
    } else if (k === "ArrowDown" || k === "ArrowLeft") {
      if (tag === "INPUT") return;
      ev.preventDefault(); setBpm(state.bpm - (ev.shiftKey ? 5 : 1), "kbd");
    }
  });

  document.addEventListener("visibilitychange", function () {
    if (document.hidden) {
      if (raf) { cancelAnimationFrame(raf); raf = null; }
    } else {
      pNext = 0;
      if (state.mode !== "stopped" || Math.exp(-(nowSec() - stopAt) * 2.4) > 0.012) ensureLoop();
    }
  });

  window.addEventListener("message", function (e) {
    var d = e.data;
    if (d && d.type === "sloprun:theme" && (d.theme === "light" || d.theme === "dark")) {
      root.setAttribute("data-theme", d.theme);
    }
  });

  /* ── boot ───────────────────────────────────────────────────────────────── */
  if (audioDead) {
    $("audioNote").hidden = false;
    elStageNote.textContent = "No Web Audio here — the arm still keeps time.";
  }
  buildScale();
  buildChips();
  buildDots();
  elVolV.textContent = state.vol;
  setBpm(state.bpm, "init");
  captions();
  clock.t = nowSec(); clock.beat = 0; clock.lvl = 2;
  ensureLoop();
})();
</script>
</body>
</html>

source-visible by construction · nothing is published here without its code

Comments

0 total

No comments yet. If you ran it, say what happened.

Report this post

Goes straight to the moderation queue. Enough independent reports and the post is suspended automatically until a human looks.

what is wrong