---
format: typebulb/v1
name: Hush
---

**code.tsx**

```tsx
import React, { useState, useEffect, useRef, useCallback } from "react";
import { createRoot } from "react-dom/client";

class NoiseMonitor {
  private ctx: AudioContext | null = null;
  private analyser: AnalyserNode | null = null;
  private stream: MediaStream | null = null;
  private buffer: Float32Array | null = null;
  private rafId: number | null = null;
  private muted = false;
  private smoothed = 0;
  private mediaRec: MediaRecorder | null = null;

  constructor(private onLevel: (level: number) => void) {}

  async start(): Promise<void> {
    if (!this.stream) {
      this.stream = await navigator.mediaDevices.getUserMedia({ audio: true });
    }
    if (!this.ctx) {
      this.ctx = new AudioContext();
      const source = this.ctx.createMediaStreamSource(this.stream);
      this.analyser = this.ctx.createAnalyser();
      this.analyser.fftSize = 2048;
      this.buffer = new Float32Array(this.analyser.fftSize);
      source.connect(this.analyser);
    }
    if (this.ctx.state === "suspended") await this.ctx.resume();
    this.tick();
  }

  private tick = () => {
    if (!this.analyser || !this.buffer) return;
    this.analyser.getFloatTimeDomainData(this.buffer);
    let sum = 0;
    for (let i = 0; i < this.buffer.length; i++) sum += this.buffer[i] * this.buffer[i];
    const rms = Math.sqrt(sum / this.buffer.length);
    const raw = Math.min(100, rms * 400);
    // EMA smoothing across frames (~100ms time constant)
    this.smoothed = this.smoothed * 0.7 + raw * 0.3;
    this.onLevel(this.muted ? 0 : Math.round(this.smoothed));
    this.rafId = requestAnimationFrame(this.tick);
  };

  setMuted(m: boolean) {
    this.muted = m;
    if (m) this.smoothed = 0;
  }

  startRecording(): void {
    if (!this.stream) return;
    const mime = MediaRecorder.isTypeSupported("audio/webm;codecs=opus")
      ? "audio/webm;codecs=opus"
      : MediaRecorder.isTypeSupported("audio/webm")
      ? "audio/webm"
      : "";
    this.mediaRec = mime
      ? new MediaRecorder(this.stream, { mimeType: mime })
      : new MediaRecorder(this.stream);
    const chunks: Blob[] = [];
    this.mediaRec.ondataavailable = e => { if (e.data.size > 0) chunks.push(e.data); };
    (this.mediaRec as any)._chunks = chunks;
    this.mediaRec.start();
  }

  stopRecording(): Promise<string> {
    return new Promise(resolve => {
      const rec = this.mediaRec;
      if (!rec) return resolve("");
      rec.onstop = () => {
        const chunks = (rec as any)._chunks as Blob[];
        const blob = new Blob(chunks, { type: rec.mimeType || "audio/webm" });
        const reader = new FileReader();
        reader.onload = () => resolve(reader.result as string);
        reader.onerror = () => resolve("");
        reader.readAsDataURL(blob);
        this.mediaRec = null;
      };
      try { rec.stop(); } catch { resolve(""); }
    });
  }

  stop() {
    if (this.rafId) cancelAnimationFrame(this.rafId);
    this.rafId = null;
    this.stream?.getTracks().forEach(t => t.stop());
    this.stream = null;
    this.ctx?.close().catch(() => {});
    this.ctx = null;
    this.analyser = null;
    this.buffer = null;
    this.smoothed = 0;
  }
}

const DEFAULT_TTS = "shut the fuck up";

class ShushPlayer {
  private ctx: AudioContext | null = null;
  private buffer: AudioBuffer | null = null;

  private getCtx(): AudioContext {
    if (!this.ctx) this.ctx = new AudioContext();
    return this.ctx;
  }

  async loadFromDataUrl(url: string) {
    const res = await fetch(url);
    const arrayBuf = await res.arrayBuffer();
    this.buffer = await this.getCtx().decodeAudioData(arrayBuf);
  }

  clearCustom() { this.buffer = null; }
  hasCustom() { return this.buffer !== null; }

  play(): Promise<void> {
    return this.buffer ? this.playBuffer(this.buffer) : this.playTts(DEFAULT_TTS);
  }

  private async playBuffer(buffer: AudioBuffer): Promise<void> {
    const ctx = this.getCtx();
    if (ctx.state === "suspended") await ctx.resume();
    return new Promise(resolve => {
      const src = ctx.createBufferSource();
      src.buffer = buffer;
      src.connect(ctx.destination);
      src.onended = () => resolve();
      src.start();
    });
  }

  private playTts(text: string): Promise<void> {
    return new Promise(resolve => {
      const u = new SpeechSynthesisUtterance(text);
      u.onend = () => resolve();
      u.onerror = () => resolve();
      window.speechSynthesis.speak(u);
    });
  }
}

/** Sets the dynamic background color on the root element. Both html and body
 *  reference --bg-dynamic via CSS, so a single set covers the entire viewport
 *  (including the iOS Safari area outside `100vh`). null clears the override. */
const setRootBg = (color: string | null) => {
  const root = document.documentElement;
  if (color === null) root.style.removeProperty("--bg-dynamic");
  else root.style.setProperty("--bg-dynamic", color);
};

const ControlSlider = ({ label, value, display, min, max, step, onChange }: any) => (
  <div className="slider-row">
    <div className="label-container"><span className="label">{label}</span><span className="value">{display}</span></div>
    <input type="range" min={min} max={max} step={step} value={value} onChange={e => onChange(e.target.value)}/>
  </div>
);

function App() {
  const monitorRef = useRef<NoiseMonitor | null>(null);
  const playerRef = useRef<ShushPlayer | null>(null);
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
  const historyRef = useRef<number[]>([]);
  const breachStartRef = useRef<number | null>(null);
  const lastAboveRef = useRef<number>(0);
  const armedRef = useRef(true);
  const calibSamplesRef = useRef<number[] | null>(null);
  const lastHistPushRef = useRef<number>(0);
  const wakeLockRef = useRef<any>(null);

  const [permission, setPermission] = useState<"idle" | "granted" | "denied">("idle");
  const [listening, setListening] = useState(false);
  const [threshold, setThreshold] = useState(() => {
    const v = parseInt(localStorage.getItem("hush-threshold") || "");
    return Number.isFinite(v) ? v : 25;
  });
  const [continuousSecs, setContinuousSecs] = useState(() => {
    const v = parseInt(localStorage.getItem("hush-patience") || "");
    return Number.isFinite(v) ? v : 3;
  });
  const [warnings, setWarnings] = useState(0);
  const [currentLevel, setCurrentLevel] = useState(0);
  const [calibrating, setCalibrating] = useState(false);
  const [recState, setRecState] = useState<"idle" | "recording" | "has-custom">("idle");
  const [breachActive, setBreachActive] = useState(false);
  const [breachElapsed, setBreachElapsed] = useState(0);
  const [bgLevel, setBgLevel] = useState(0);
  const [flashing, setFlashing] = useState(false);

  const thresholdRef = useRef(threshold);
  const continuousSecsRef = useRef(continuousSecs);
  useEffect(() => {
    thresholdRef.current = threshold;
    localStorage.setItem("hush-threshold", String(threshold));
  }, [threshold]);
  useEffect(() => {
    continuousSecsRef.current = continuousSecs;
    localStorage.setItem("hush-patience", String(continuousSecs));
  }, [continuousSecs]);

  // Init player + load saved recording
  useEffect(() => {
    playerRef.current = new ShushPlayer();
    const saved = localStorage.getItem("hush-shush");
    if (saved) {
      playerRef.current.loadFromDataUrl(saved)
        .then(() => setRecState("has-custom"))
        .catch(() => localStorage.removeItem("hush-shush"));
    }
  }, []);

  const clearBreach = useCallback(() => {
    breachStartRef.current = null;
    setBreachActive(false);
  }, []);

  const syncRecStateFromPlayer = useCallback(() => {
    setRecState(playerRef.current?.hasCustom() ? "has-custom" : "idle");
  }, []);

  const playShush = useCallback(async () => {
    const player = playerRef.current;
    if (!player) return;
    monitorRef.current?.setMuted(true);
    await player.play();
    setTimeout(() => monitorRef.current?.setMuted(false), 600);
  }, []);

  const triggerFlash = useCallback(() => {
    setFlashing(true);
    document.documentElement.classList.add("flashing");
    let count = 0;
    const id = window.setInterval(() => {
      setRootBg(count % 2 === 0 ? "#fff" : "#000");
      count++;
      if (count >= 8) {
        window.clearInterval(id);
        document.documentElement.classList.remove("flashing");
        setFlashing(false);
      }
    }, 125);
  }, []);

  const fireWarning = useCallback(() => {
    setWarnings(w => w + 1);
    triggerFlash();
    playShush();
  }, [triggerFlash, playShush]);

  const handleLevel = useCallback((level: number) => {
    const now = performance.now();

    if (calibSamplesRef.current) calibSamplesRef.current.push(level);

    const t = thresholdRef.current;
    const GRACE_MS = 400;

    if (level > t) {
      lastAboveRef.current = now;
      if (armedRef.current) {
        if (breachStartRef.current === null) {
          breachStartRef.current = now;
          setBreachActive(true);
        } else if (now - breachStartRef.current >= continuousSecsRef.current * 1000) {
          fireWarning();
          armedRef.current = false;
          clearBreach();
        }
      }
    } else {
      const sinceAbove = now - lastAboveRef.current;
      if (sinceAbove > GRACE_MS) {
        if (breachStartRef.current !== null) clearBreach();
        armedRef.current = true;
      }
    }

    // Throttle React state updates / history pushes to ~10Hz
    if (now - lastHistPushRef.current > 100) {
      lastHistPushRef.current = now;
      setCurrentLevel(level);
      const hist = historyRef.current;
      hist.push(level);
      if (hist.length > 300) hist.shift();
      setBreachElapsed(
        breachStartRef.current !== null
          ? (now - breachStartRef.current) / 1000
          : 0
      );

      // Moving average over the patience window drives the bg-color gradient.
      const windowSize = continuousSecsRef.current * 10;
      const slice = hist.slice(-windowSize);
      const avg = slice.length ? slice.reduce((a, b) => a + b, 0) / slice.length : 0;
      setBgLevel(avg);
    }
  }, [fireWarning, clearBreach]);

  // Drive root background color from the moving-average noise level.
  // Skipped while flashing (the flash interval owns the bg directly).
  // Cleared when not listening so the CSS theme rule takes over again.
  useEffect(() => {
    if (!listening) {
      setRootBg(null);
      return;
    }
    if (flashing) return;
    const max = 220;
    const r = Math.round((max * Math.min(100, bgLevel)) / 100);
    setRootBg(`rgb(${r}, 0, 0)`);
  }, [bgLevel, flashing, listening]);

  // Cleanup on unmount — don't leave a red bg behind.
  useEffect(() => {
    return () => {
      setRootBg(null);
      document.documentElement.classList.remove("flashing");
    };
  }, []);

  // Graph drawing loop
  useEffect(() => {
    let raf = 0;
    const draw = () => {
      const canvas = canvasRef.current;
      if (canvas) {
        const dpr = window.devicePixelRatio || 1;
        const cw = canvas.clientWidth;
        const ch = canvas.clientHeight;
        if (canvas.width !== cw * dpr) canvas.width = cw * dpr;
        if (canvas.height !== ch * dpr) canvas.height = ch * dpr;
        const w = canvas.width;
        const h = canvas.height;
        const ctx = canvas.getContext("2d")!;
        ctx.clearRect(0, 0, w, h);

        const hist = historyRef.current;
        const t = thresholdRef.current;

        // Threshold line
        const ty = h - (t / 100) * h;
        ctx.strokeStyle = "#664a2c";
        ctx.lineWidth = 1 * dpr;
        ctx.setLineDash([6 * dpr, 4 * dpr]);
        ctx.beginPath();
        ctx.moveTo(0, ty);
        ctx.lineTo(w, ty);
        ctx.stroke();
        ctx.setLineDash([]);

        // Level line + fill
        if (hist.length > 1) {
          const denom = 299;
          ctx.lineWidth = 1.5 * dpr;
          ctx.strokeStyle = "#9a9a9a";

          ctx.beginPath();
          for (let i = 0; i < hist.length; i++) {
            const x = (i / denom) * w;
            const y = h - (hist[i] / 100) * h;
            if (i === 0) ctx.moveTo(x, y);
            else ctx.lineTo(x, y);
          }
          ctx.stroke();

          ctx.lineTo(((hist.length - 1) / denom) * w, h);
          ctx.lineTo(0, h);
          ctx.closePath();
          ctx.fillStyle = "rgba(150, 150, 150, 0.06)";
          ctx.fill();
        }
      }
      raf = requestAnimationFrame(draw);
    };
    raf = requestAnimationFrame(draw);
    return () => cancelAnimationFrame(raf);
  }, []);

  const startListening = async (): Promise<boolean> => {
    try {
      const m = monitorRef.current ?? new NoiseMonitor(handleLevel);
      await m.start();
      monitorRef.current = m;
      setPermission("granted");
      setListening(true);
      armedRef.current = true;
      breachStartRef.current = null;
      lastAboveRef.current = 0;

      if ("wakeLock" in navigator) {
        try {
          wakeLockRef.current = await (navigator as any).wakeLock.request("screen");
        } catch {}
      }

      // Warm up speech synthesis inside the user gesture so it can speak later
      // from the breach handler (iOS Safari requires the first speak() to be
      // inside a user activation event).
      try {
        const warmup = new SpeechSynthesisUtterance(" ");
        warmup.volume = 0;
        window.speechSynthesis.speak(warmup);
      } catch {}

      return true;
    } catch {
      setPermission("denied");
      return false;
    }
  };

  const stopListening = () => {
    monitorRef.current?.stop();
    monitorRef.current = null;
    setListening(false);
    setCurrentLevel(0);
    setBreachElapsed(0);
    setBgLevel(0);
    historyRef.current = [];
    clearBreach();
    armedRef.current = true;
    if (recState === "recording") syncRecStateFromPlayer();
    wakeLockRef.current?.release().catch(() => {});
    wakeLockRef.current = null;
  };

  const toggleListening = () => listening ? stopListening() : startListening();

  const calibrate = async () => {
    if (!listening) {
      const ok = await startListening();
      if (!ok) return;
    }
    if (!monitorRef.current) return;
    setCalibrating(true);
    calibSamplesRef.current = [];
    setTimeout(() => {
      const samples = calibSamplesRef.current ?? [];
      calibSamplesRef.current = null;
      if (samples.length > 0) {
        const peak = Math.max(...samples);
        const avg = samples.reduce((a, b) => a + b, 0) / samples.length;
        const newThreshold = Math.max(Math.round(Math.max(avg * 2.5, peak * 1.4, 5)), 5);
        setThreshold(Math.min(100, newThreshold));
      }
      setCalibrating(false);
    }, 2500);
  };

  const adjustThreshold = (delta: number) => {
    setThreshold(t => Math.max(1, Math.min(100, t + delta)));
  };

  const resetWarnings = () => setWarnings(0);

  const toggleRecording = async () => {
    if (recState === "recording") {
      const m = monitorRef.current;
      if (!m) { syncRecStateFromPlayer(); return; }
      const dataUrl = await m.stopRecording();
      m.setMuted(false);
      if (!dataUrl || !playerRef.current) { syncRecStateFromPlayer(); return; }
      try {
        await playerRef.current.loadFromDataUrl(dataUrl);
        try { localStorage.setItem("hush-shush", dataUrl); } catch {}
        setRecState("has-custom");
      } catch {
        syncRecStateFromPlayer();
      }
    } else {
      const ok = listening || await startListening();
      if (!ok || !monitorRef.current) return;
      monitorRef.current.setMuted(true);
      monitorRef.current.startRecording();
      setRecState("recording");
    }
  };

  const clearRecording = () => {
    localStorage.removeItem("hush-shush");
    playerRef.current?.clearCustom();
    setRecState("idle");
  };

  return (
    <div className="container">
      <header>
        <h1 className="title-text">hush</h1>
        <div className="subtitle-text">Shut the fuck up</div>
      </header>

      <div className={`graph-wrap ${breachActive ? "is-breaching" : ""} ${listening ? "is-listening" : ""}`}>
        <canvas ref={canvasRef} className="graph"/>
        {breachElapsed > 0 && (
          <div className="breach-elapsed">
            {breachElapsed.toFixed(1)}s / {continuousSecs}s
          </div>
        )}
        <div className="readout">
          <div className="readout-cell">
            <div className="readout-value">{currentLevel}</div>
            <div className="readout-label">Level</div>
          </div>
          <div className="readout-divider"/>
          <div className="readout-cell">
            <div className="readout-value">{threshold}</div>
            <div className="readout-label">Limit</div>
          </div>
          <div className="readout-divider"/>
          <div className="readout-cell warnings-cell">
            <div className="readout-value">{warnings}</div>
            <div className="readout-label">Warnings</div>
          </div>
        </div>
      </div>

      <div className="controls">
        <div className="threshold-row">
          <button className="step-btn" onClick={() => adjustThreshold(-1)} aria-label="Lower limit">−</button>
          <button
            className={`calibrate-btn ${calibrating ? "calibrating" : ""}`}
            onClick={calibrate}
            disabled={calibrating}
          >
            {calibrating ? "Listening…" : "Calibrate"}
          </button>
          <button className="step-btn" onClick={() => adjustThreshold(1)} aria-label="Raise limit">+</button>
        </div>

        <ControlSlider
          label="Patience"
          value={continuousSecs}
          display={`${continuousSecs}s`}
          min="1" max="10" step="1"
          onChange={(v: string) => setContinuousSecs(parseInt(v))}
        />

        <div className="record-row">
          <button className={`record-btn ${recState}`} onClick={toggleRecording}>
            {recState === "recording" ? "Stop Recording" : recState === "has-custom" ? "Re-record" : "Record Custom"}
          </button>
          <button className="ghost-btn" onClick={playShush}>Preview</button>
          {recState === "has-custom" && (
            <button className="ghost-btn" onClick={clearRecording}>Use default</button>
          )}
        </div>

        <button className="ghost-btn reset-btn" onClick={resetWarnings}>Reset warnings</button>

        <div className="btn-wrapper">
          <button className={`play-btn ${listening ? "active" : ""}`} onClick={toggleListening}>
            {listening ? "Stop" : "Start"}
          </button>
        </div>

        {permission === "denied" && (
          <div className="warning-text">No mic. Grant permission and reload.</div>
        )}
        {listening && (
          <div className="hint-text">Keep this tab visible — backgrounded tabs get suspended.</div>
        )}
      </div>
    </div>
  );
}

createRoot(document.getElementById("root")!).render(<App />);
```
**styles.css**

```css
:root {
  --bg: #0a0a0a;
  --text: #666;
  --text-bright: #ccc;
  --accent: #222;
  --line: #2a2a2a;
  --warning: #c25656;
  --warning-glow: rgba(194, 86, 86, 0.25);
  --thumb: #888;
}

html[data-theme="light"] {
  --bg: #ffffff;
  --text: #888;
  --text-bright: #333;
  --accent: #ddd;
  --line: #e4e4e4;
  --warning: #c44;
  --warning-glow: rgba(196, 68, 68, 0.18);
  --thumb: #555;
}

html, body {
  background-color: var(--bg-dynamic, var(--bg));
  transition: background-color 0.4s ease-out;
}

html.flashing, html.flashing body {
  transition: none;
}

body {
  margin: 0;
  font-family: system-ui, -apple-system, sans-serif;
  color: var(--text);
  display: grid;
  place-items: center;
  min-height: 100vh;
}

.title-text, .subtitle-text {
  font-weight: 200;
  letter-spacing: 0.4rem;
  text-transform: uppercase;
  margin: 0;
}

.title-text {
  font-size: 1.9rem;
  color: var(--text-bright);
}

.subtitle-text {
  font-size: 0.8rem;
  color: var(--text-bright);
  margin-top: 0.4rem;
}

.container {
  text-align: center;
  width: 100%;
  max-width: 460px;
  padding: 1rem 1rem 1.25rem;
  box-sizing: border-box;
}

header { margin-bottom: 1.25rem; }

.graph-wrap {
  position: relative;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.015);
  margin-bottom: 1.25rem;
  transition: border-color 0.3s, box-shadow 0.3s;
}

.graph-wrap.is-breaching {
  border-color: var(--warning);
  box-shadow: 0 0 30px var(--warning-glow);
}

.graph {
  display: block;
  width: 100%;
  height: 140px;
}

.breach-elapsed {
  position: absolute;
  top: 10px;
  right: 12px;
  font-size: 0.7rem;
  color: var(--warning);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.05rem;
  pointer-events: none;
}

.readout {
  display: flex;
  align-items: center;
  justify-content: space-around;
  border-top: 1px solid var(--line);
  padding: 0.6rem 0;
}

.readout-cell { flex: 1; }

.readout-divider {
  width: 1px;
  height: 26px;
  background: var(--line);
}

.readout-value {
  font-size: 1.4rem;
  font-weight: 300;
  color: var(--text-bright);
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

.readout-label {
  font-size: 0.6rem;
  letter-spacing: 0.15rem;
  text-transform: uppercase;
  margin-top: 0.35rem;
  color: var(--text);
}

.warnings-cell .readout-value { color: #b66; }

.controls {
  display: flex;
  flex-direction: column;
  gap: 1.1rem;
  align-items: center;
}

.threshold-row {
  display: flex;
  gap: 0.85rem;
  align-items: center;
}

.step-btn {
  background: transparent;
  color: var(--text);
  border: 1px solid var(--accent);
  width: 38px;
  height: 38px;
  border-radius: 50%;
  font-size: 1.1rem;
  cursor: pointer;
  transition: all 0.2s;
  display: flex;
  align-items: center;
  justify-content: center;
}

.step-btn:hover {
  color: var(--text-bright);
  border-color: var(--line);
}

.calibrate-btn {
  background: transparent;
  color: var(--text);
  border: 1px solid var(--accent);
  padding: 0.55rem 1.4rem;
  border-radius: 50px;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.12rem;
  cursor: pointer;
  transition: all 0.2s;
  width: 140px;
}

.calibrate-btn:hover:not(:disabled) { color: var(--text-bright); }

.calibrate-btn.calibrating {
  color: #6a8;
  border-color: #364;
  animation: pulse 1.5s infinite;
}

.calibrate-btn:disabled { cursor: default; }

.slider-row { width: 100%; max-width: 280px; }

.label-container { display: flex; justify-content: space-between; margin-bottom: 0.5rem; }

.label, .value {
  font-size: 0.65rem;
  letter-spacing: 0.1rem;
}

.label {
  text-transform: uppercase;
}

.value {
  color: var(--text-bright);
  font-variant-numeric: tabular-nums;
}

input[type="range"] {
  width: 100%;
  height: 2px;
  background: #181818;
  appearance: none;
  outline: none;
}

html[data-theme="light"] input[type="range"] { background: #eee; }

input[type="range"]::-webkit-slider-thumb {
  appearance: none;
  width: 12px;
  height: 12px;
  background: var(--thumb);
  border-radius: 50%;
}

.record-row {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
}

.record-btn {
  background: transparent;
  color: var(--text);
  border: 1px solid var(--accent);
  padding: 0.55rem 1.1rem;
  border-radius: 50px;
  font-size: 0.65rem;
  text-transform: uppercase;
  letter-spacing: 0.12rem;
  cursor: pointer;
  transition: all 0.2s;
}

.record-btn:hover { color: var(--text-bright); }

.record-btn.recording {
  color: var(--warning);
  border-color: var(--warning);
  animation: pulse 1.5s infinite;
}

.record-btn.has-custom { color: var(--text-bright); }

@keyframes pulse {
  50% { opacity: 0.55; }
}

.ghost-btn {
  background: transparent;
  border: none;
  color: var(--text);
  font-size: 0.65rem;
  text-transform: uppercase;
  letter-spacing: 0.12rem;
  cursor: pointer;
  padding: 0.55rem 0.7rem;
  transition: color 0.2s;
}

.ghost-btn:hover { color: var(--text-bright); }

.reset-btn { margin-top: -0.75rem; }

.btn-wrapper { margin-top: 0.25rem; }

.play-btn {
  background: transparent;
  color: var(--text);
  border: 1px solid var(--accent);
  padding: 1rem 2rem;
  border-radius: 50px;
  font-size: 0.9rem;
  text-transform: uppercase;
  letter-spacing: 0.15rem;
  cursor: pointer;
  width: 180px;
  transition: all 0.2s;
}

.play-btn.active {
  border-color: var(--text-bright);
  color: var(--text-bright);
}

.warning-text, .hint-text {
  font-size: 0.7rem;
  margin-top: 0.25rem;
  max-width: 320px;
  line-height: 1.5;
}

.warning-text { color: var(--warning); }
.hint-text { color: var(--text-bright); }
```
**index.html**

```html
<div id="root"></div>

```
**config.json**

```json
{
  "dependencies": {
    "react": "^19.2.3",
    "react-dom": "^19.2.3"
  },
  "description": "Hush - Sound the alarm when noise gets out of hand"
}
```