// OVE — app identity + version stamp. // // Single source of truth for the product name and version shown in the UI // (header, footer, document title). Bump APP_VERSION whenever we ship a // large change so the running build can always be identified at a glance — // useful both for the z.ai Version History and for naming local backups // (v6.0 = rename to Open Visualization Engine + version indicator). // // STANDING RULE (since v7.6): every APP_VERSION bump MUST come with a // matching entry at the top of RELEASE_NOTES in src/lib/release-notes.ts — // that list powers the header's "What's New!" dialog. A dev-mode guard // warns in the console when the two drift apart. // // History: // 7.19 — Automatic true-black (user request, "Texe-rings"): the v7.17 // trueblack tail is no longer opt-in. Root cause of the smear // the user saw: the persistence fade multiplies the framebuffer // by persist in 8-bit steps, and round-to-nearest leaves fixed // points -- at persist 0.78 every premultiplied value 1..2 // satisfies round(v*0.78) == v, so ring trails settle into a // faint permanently-stuck wash (colored, not gray, because the // script's channels divide by different constants and each // channel sticks at its own quantum). No multiply-only // compositing can fix that (proven: unsticking v=1 needs a // multiplier < 0.5, which halves the trail every frame), so the // fix is a subtractive -1/255/frame tail -- and the reason it // could become automatic is a NEW implementation: shader mode 22 // in the WebGL compositor (blend-gpu.ts) clamps // max(premult-1/255, 0) on rgb AND alpha in one pass. That is // exact in 8-bit (black stays a true 0, unlike the Canvas2D // 'difference' fill's 0<->1 flicker) and alpha-PRESERVING, so // behind/clear art keeps its holes (which now heal to truly // empty too -- stuck alpha quanta sweep like everything else). // Engine policy: the sweep runs by default exactly in the // sticky zone (persist >= 0.5), so low-persist scripts pay // nothing and nothing visual changes where the multiply already // decayed fully. The old 'difference' fill survives only as the // no-WebGL fallback, and it stands down once a script has drawn // with behind/clear (the alphaArt latch -- pre-latched from the // header blend, latched at draw time on a per-frame env.blend // switch). The trueblack: header becomes a tri-state: auto // (default, the new behavior), on (forced at any persist < 1, // the v7.17 flag), off (never -- the wash look, for authors who // want it). Texe-rings itself needs no edit: at 0.78 the auto // sweep clears each channel's quanta in 2-3 frames after the // multiply stalls, and the background returns to pure black. // 7.18 — "Beautiful Spectrogram" (user request, twelfth Zai-Magic // preset): a true scrolling spectrogram as a pure script -- the // v7.12 bgx feedback slide moves the whole framebuffer left // every frame (the time axis) while persist 1 (v7.14.1's // zero-fade framebuffer) keeps the history locked, so the print // only ever leaves by scrolling off the left edge. The write // head is a 256-point vertical line at the right edge: sv (the // per-point spectrum) drives a megabuf attack/slow-decay level // per frequency bin, which indexes a six-anchor palette table // (also megabuf) -- deep dark blue background, then pink -> red // -> orange -> yellow -> white as bins get loud. Two tricks make // it read clean: (1) the stroke is exactly four scroll-steps // wide with its per-frame alpha pre-divided by the accumulation // factor (core 0.9 + halo 0.22*3.2 passes, times four overlapping // steps ~= 1.6*4), so overlapping columns sum to exactly full // palette strength and the quiet field renders as SOLID navy // rather than banded; (2) a half-second erase phase (per-frame // persist override to 0.45) drains the previous preset's // framebuffer before the zero-fade lock engages, and a 15x // three-second fill sweep (plus a replay on stage resize) paints // the navy field across the whole screen instead of making the // visitor wait 25 s for the crawl to cover it. // 7.17 — True-black trail fade (user request, "Orange Burst"): the // persistence fade multiplies the framebuffer by persist in // 8-bit steps, so any persist >= 0.5 leaves fixed points // (round(v*p) == v -- at 0.88 every value 1..4 is stuck) and long // trails settle into a gray wash that a bg* blow-out then smears // into every corner; scripts were forced below 0.5, where the // trail dies in ~5 frames and the blow-out echo becomes // unreadable. New opt-in header trueblack: on adds a subtractive // tail right after the alpha erase: one fullscreen 'difference' // fill of rgb(1,1,1) = an exact -1/255 per pixel per frame, which // sweeps the stuck quantum values to a true 0 while bright // pixels never notice (a 0.93 multiply halves them every ~9 // frames anyway). Costs one extra fullscreen fill, only when the // flag is on; runs inside the persist < 1 branch so persist 1 // keeps its zero-fade (permanent) semantics. The tail forces // pixels opaque (alpha composites to 1 under 'difference'), so it // must never be combined with 'behind'/'clear' art -- documented // in the Help panel's file-format section, off by default. Also: // the script parser's unknown-header error now lists all nine // valid keys (ysign had gone missing from the list). // 7.16 — Presets tab rearranged (user request): "instead of having the // two sections side by side ... the 'Load Preset' button section // is on top with full width use of the preset section, the height // set to where there is no empty space being wasted (even when a // preset is selected). The preset tree should be placed below // that with its width expanded to use the whole width of the // Presets section ... so the preset names won't be clipped due to // a restriction of horizontal space." The old 58/42 side-by-side // split is gone: the management panel (selection header, action // buttons, club-mode randomizer) sits ON TOP at the section's // full width and its natural height -- never stretched, so the // dead space that used to sit under the buttons when a preset was // selected is gone. Its buttons pack into one compact grid row on // desktop (Load Preset | Rename | Delete for an own preset with // the primary Load cell wider; Load | Copy to my presets for an // embedded one; a 2x3 grid -- Create | Subfolder | Rename over // Delete | Export | Import -- for a user folder), all cells one // uniform h-9; phones keep the previous stacking (primary action // full-width, pairs below). The tree follows BELOW at full width // and fills every bit of height the tab has left (the tab body's // h-full flex chain; capped at 420 px only below lg, where the // page itself scrolls and the cap keeps nested scrolling sane) -- // full-width rows mean preset names never clip against the old // 58% column's horizontal squeeze. The club-mode scope line and // the active RANDOM/SEQUENCE pill now share one row (truncate + // shrink-0) instead of stacking. // 7.15.2 — Facebook page + theme switch relocation (user request): the // project now has its own Facebook page, and the footer's // right-hand group carries the link (facebook.com/openvisengine) // beside the version stamp -- an icon-only button (lucide // Facebook glyph) that opens in a new tab, same hover/focus // treatment as the portfolio link. The light/dark tick switch // (v6.3) moved out of the header into the Settings tab's // Appearance section, as a "Light / dark mode" row ABOVE the // base-theme cards -- the header is one control lighter and all // appearance controls now live in one place. Mechanics are // identical: checking = the white theme, unchecking returns to // the visitor's stored dark base (kept fresh by the base cards), // both choices persist per visitor, and the mount guard keeps // the SSR render mismatch-free. The row states the current side // ("Light theme" / "Dark theme") and explains the return-to-base // behavior in its caption. // 7.15.1 — the silence hold (user request): "if silence is detected, // it needs to place an immediate override and hold on the beat // detection / prediction ... if silence is no longer detected, // release the hold ... (the waveforms effects should still come // through but if there is no beat, the visual should stop acting // like there is a beat)." Sustained near-digital quiet on the // OVERALL level (< 0.006 for 300 ms — the 3-band mean the engine // already computes; digital silence reads exactly 0, mastered // music stays above ~0.05 even between hits) now places a HOLD: // nothing fires — real or predicted — the tempo estimate and // confidence freeze exactly where they are (the old path let the // predictor fake beats for up to ~2 s into a silence while its // confidence quietly eroded), and beatphase freezes for scripts // instead of saturating at "beat imminent". The hold is STICKY // (only a decisive level above 0.015 releases — noise-floor // wobble never does) and the release is immediate, re-anchoring // the grid from the phase captured the frame the audio went QUIET // (before the debounce window, so a pause/resume keeps perfect // time — the music continues from the exact position it stopped // at); after a hard cut the comb + first real beats re-align // within a couple of periods. The 300 ms debounce only has to // beat the ~800 ms the old flux-domain path needed before it // started faking beats // (bassGone = 1.6 periods at 120 BPM), so inter-attack dips in // sparse masters never trip it. "Set Beat" taps release the hold // outright — the manual override. Band/waveform data is NOT // gated: it still flows (and is simply ~0, because the audio is // gone). Scripts get a silent variable (1 while held) and the // beat indicator shows an amber HOLD badge while it is active. // BeatFrame.silent carries the state; rust-agent/src/beat.rs // ports the same gate (its bass level doubles as the silence // signal) with the same thresholds. // 7.15 — the complete blend mode collection (user request): the blend // header grew from six keywords to a 41-mode registry // (src/lib/ava/blend.ts) covering every compositing family in the // request list -- the full Photoshop set, the GIMP grain pair, // the After Effects stencil/silhouette masks, Apple/CSS plus- // lighter/plus-darker and the Pegtop exotica (negation, phoenix, // reflect, glow). Native keywords map straight to Canvas2D // operators; the 21 modes Canvas2D lacks run through a new WebGL // compositor (blend-gpu.ts) with per-pixel formulas from the W3C // compositing spec, the glsl-blend collection and GIMP (phoenix = // min - max + 1, verified against four independent sources; hard // mix = vivid light thresholded at 0.5). 'subtract' is now a TRUE // clamped subtract, replacing 7.14.1's difference approximation. // The framebuffer went alpha-capable so behind and clear work // honestly: the persistence fade is an alpha ERASE // (destination-out) and the bg* wipe a clearRect -- over the // black stage both read pixel-identically to the old opaque // fills, so every existing preset renders as before. The blend // variable can switch the mode per frame (same contract as // persist; it carries the mode's 1-based registry number because // the expression language is numeric-only -- the Help table lists // every number). Names are case/separator-insensitive // with aliases (color burn = colorburn = ColorBurn; plus-lighter // = additive; plus-darker = linear burn). Help panel gained a // grouped 41-row Blend modes table; parser errors point at it. // WebGL-less browsers fall back to the nearest native operator // per mode. // 7.14.1 — Script engine: blend modes + zero-fade persistence (user // request, Brainstorm preset session). The renderer's content // pass could only composite additively ('lighter') or plain // alpha; the persistence header was hard-capped at 0.98, so a // feedback blow-out ALWAYS faded -- the user wanted colors that // ride outward forever at full brightness, replaced at the center // by new color. Blend header extended to six values: multiply, // screen, subtract (canvas 'difference' |dst-src| -- Canvas2D has // no clamped true-subtract op; difference is the classic // approximation and can never climb to white) and replace // (source-over with the CORE pass at FULL alpha -- fresh strokes // overwrite the pixels under them instead of stacking, so a // figure redrawn over its own accumulating trails keeps its color // instead of saturating to white; halo pass 0.25, dots core // alpha 1.0, solid fill alpha 1.0). persist header + env override // widened 0..0.98 -> 0..1; at exactly 1 the fade fill is SKIPPED // entirely (engine.ts). The pairing this was asked for: blend // replace + persist 1 + the v7.12 bgzoom/bgrot feedback = a // static-speed blow-out that never fades, the fresh drawing // replacing the old color at the center -- the blow-out itself is // the cleanup. Help panel + parser error strings updated; no // library changes (existing presets keep additive/alpha and their // behavior byte-for-byte). // 7.14 — Arctic Rainbow re-imagined (user request): the old stand-in was // plain white columns of rain dots sweeping upward. Rebuilt as a // music-driven water spray, stateless projectile physics in the // [point] program: each of up to 120 droplets is a deterministic // parabola (own apex height 1.26..1.88, own flight time 1.9..3.4 s, // phase-staggered clock; every splash cycle re-rolls its origin, // drift and arch) -- launch from below the bottom edge, slow rise, // hang, arch left or right, fall back off-screen. Drop count // follows the music (vol_att/bass_att envelope, fast attack 6/s, // slow release 1.2/s: silence = 0 drops, loud = all 120). Each // drop owns a hue that shifts by its own per-drop amount on every // detected beat, plus a subtle gradient along its tail. The tail // is 12 explicit trail samples (0.014..0.44 s back along the arc) // drawn as a tapered polyline in lines mode, gated by smoothstep // on the fall speed: nothing while rising (crisp dot), fading in // as the arch turns downward -- no persistence smear (persist // 0.12). Slot map through cell centers (the v7.13.1 lesson), 14 // slots per drop (separator + head + 12 trail), n = 1680. // EMBEDDED_SEED_VERSION 7.13.1 -> 7.14 re-seeds the library. // 7.13.1 — Beat Matrix endpoint fix (user report): the rightmost column // of dots sat one row short -- its bottom dot missing -- while a // phantom dot spilled one column past the wall's right edge at // the top. Root cause: the engine's point position i spans 0..1 // INCLUSIVE (classic AVS Superscope convention, engine.ts: i = // k/(n-1)), but Beat Matrix's grid math assumed exclusive i = k/n: // floor(i*cols) turned the last point into col 16 (one past the // 16-column wall) at row 0, and column 15 was left with nine // dots (rows 0-8, no bottom). Fix: the script now maps through // cell centers -- cell = i*(cols*rows-1)+0.5, col = // floor(cell/rows), row = floor(cell-col*rows) -- which puts // every floor() half a cell away from an integer boundary, so // the endpoint can never spill and float dust can never shuffle // a dot. Full 16x10 bijection verified headlessly (all 160 // points in 160 distinct cells, col<=15, row<=9, bottom-right // cell present). Sweep of the other 100 presets: every other // floor(i*M) use is benign -- angular presets close their loop // (bar=bars lands exactly on bar 0's angle), Aurora Veil's // phantom lands off-screen (x=1.19), the megabuf particle // presets draw their phantom slot at alpha 0, and the converted // Winamp classics rendered identically under original AVS // (which also uses inclusive i). EMBEDDED_SEED_VERSION 7.12 -> // 7.13.1 re-seeds the library so every visitor gets the fixed // Beat Matrix. // 7.13 — one build, every feature: the parallel development lines are // merged. This tree now carries BOTH the community OVE-Directory // (v7.11 here) AND the parallel line's additions — the spectrum // 16 kHz cap (its 7.10.1), the screen wake lock + respectful // Demo button + one-click Save (its 7.11, folded into this // history as 7.10.2 to keep one clean timeline), and the // AVS-style background movement family (bgrot / bgx / bgy / // bgzoom) with the eleventh Zai-Magic preset "osc-rotations" // (its 7.12). Git three-way merge against the shared v7.10 // ancestor (identical tree hashes verified on both sides); the // only hand-merged surface was the Script tab toolbar, which // now carries the one-click Save and "Upload to OVE-Directory" // side by side, and the two changelogs (here + release-notes). // 7.12 — background movement + osc-rotations. Scripts can now move the // whole framebuffer every frame, the way the classic Winamp AVS // plugins did: bgrot (radians/frame, + clockwise on screen), // bgx / bgy (per-frame drift in the -1..1 screen convention, y // up), bgzoom (< 1 draw-in, > 1 blow-out) -- read after [frame]/ // [beat] so this frame's values apply this frame. The engine // snapshots the canvas into an offscreen buffer, wipes, and // draws the snapshot back through the transform, so the // persistence fade blends the moving trails: "speed accelerates // to the beat, a hard hit flips the spin, fade to black stays // slow" is pure script (a beatage envelope drives the rate; a // [beat] section flips a dir variable on hard hits via bass vs // bass_att). Neutral readings (0/0/0/1) skip the pass entirely // (zero cost for every preset that doesn't move); non-finite // values fall back to neutral (a NaN in a canvas transform // matrix would black the screen with no error); per-frame // clamps: rot ±0.6 rad, drift ±0.25, zoom 0.5..2. New 11th // Zai-Magic preset "osc-rotations" (user-requested): a // stationary centered oscilloscope (time-domain v) whose color // is mirror-symmetric -- points equidistant left/right of // center shift together, the center points hold their own hue // -- over a background rotating cw/ccw to the music that // temporarily accelerates on the beat; hard hits // (bass > 1.25 × bass_att) flip the spin; a whisper of draw-in // (bgzoom ~0.999) spirals the trails inward; persist 0.965 // keeps the fade slow so colors blend. Help panel: bg* variable // rows + three new example formulas. EMBEDDED_SEED_VERSION // 7.10 -> 7.12 (library re-seed adds the preset; the share // codec's dictionary stays frozen per its compatibility // contract -- bg* identifiers ride the pair packing like any // custom variable). Headless validation of the preset's // compiled programs: quiet 0.004 rad/frame, hard beat 0.026 // (6.5×) + direction flip, soft beat no flip, mirrored points // share hue / center distinct // 7.11 — the community OVE-Directory. A sixth tab (Directory) lists every // script published to the community directory — a separate VPS at // s3374dir.openvisengine.com, reached ONLY through this server's // backend proxy (/api/ove-directory/*), which holds the API key in // env and re-validates every upload (the share string must decode // AND compile). Publishing: the Script tab gains "Upload to // OVE-Directory" (enabled only while the editor shows ✔ Compiled OK // with no runtime error); the script travels as its Script⇢Hash // string, so the directory stores a fraction of the source. The // listing carries each entry's name, type and date, a 1–5 star // widget (the voter id is an HttpOnly cookie minted by the proxy; // one vote per script per visitor, enforced upstream), the live // "Average: #.#" to the right of the stars, and sorting by Name / // Type / Date / Rating (done upstream, both directions). "Get" // fetches the full record, decodes the hash back into the exact // script, files it in this browser's preset tree under // root/user created/OVE-Directory (content-deduped, names numbered // "(1) (2) (3)…" on collision) and offers "Open in editor". // Duplicate uploads resolve as success (409 DUPLICATE_HASH carries // the existing row); duplicate votes show the stored aggregates. // 7.10.2 — wake lock, a respectful Demo button, one-click save. While any // audio source is live the site holds a Screen Wake Lock so phone // and tablet screens stay on (released the moment the engine goes // idle so the normal inactivity dimming returns; browsers silently // drop the sentinel on hide, so a visibilitychange watcher // re-acquires it on return while music is still going; // unsupported browsers degrade silently). The Demo Track button no // longer force-engages the Sequence checkbox: a visitor who loaded // a preset (tree double-click / Load / Copy to my presets / created // one), brought their own script material into the editor (typed, // pasted, Hash⇢Script, file), saved their own default startup // script, or runs their own Random/Sequence mix keeps their // selection -- Demo just plays the track (the showcase's tree // re-scope, 20 s interval and retire-on-end all skip too; a new // per-visit manualChoice flag + a boot-script snapshot decide, and // ensureDefaultPreset gained the same guard so mic/file/link never // clobber a custom draft either; the showcase now only starts for // a visit that never picked or typed anything). Script tab: a valid // script shows the Save button with NO save target needed -- one // click creates a preset under "user created", auto-named from the // script's name: header (a hash import of "Spectrum Bars:[▼…]" // saves as "Spectrum Bars"), "Empty Name" when nameless, " (1)"/ // " (2)"… suffixes on conflicts, and whenever the name had to be // invented a "Give This Preset a Name" dialog opens to rename the // just-saved preset in one step (the preset is always saved FIRST, // renamed second, so nothing is ever lost to a naming problem). // Hash⇢Script imports clear the stale save target (the editor now // holds foreign content) and pass the decoded label through as the // save-name fallback when the script has no name: header // 7.10.1 — the frozen right edge of spectrum visuals. getspec()/sv mapped // the bar field across the whole FFT (bin 2 ... Nyquist, ~12 Hz to // 24 kHz), but music carries almost nothing above ~16 kHz and every // lossy source low-passes even lower (192 kbps MP3 at ~18.6 kHz, // YouTube AAC at ~15.5 kHz; measured across the demo collection: // 0.000% of total magnitude above 18.6 kHz), so the rightmost ~20% // of every spectrum visual read a permanent zero -- bars that never // moved. getSpectrum() now tops the field out at 16 kHz // (SPEC_TOP_HZ, matching the spectral-flux cap in getBands()), so // the right edge lands on the 12-16 kHz "air" band where cymbals, // hats and synth sparkle live. rust-agent's spectrum_at() mirrors // the change, and the help panel's sv entry now states the span. // 7.10 — ten demo tracks + first-person changelog + declutter. The demo // source is now a COLLECTION: ten mastered tracks in // public/demo-tracks/ (demo-01.mp3 … demo-10.mp3, filename order // 1→10, replacing the single Neural Mecha Pulse file). Each Demo // click plays the visitor's PLACE in the collection once through; // the place advances when a pass truly ends — natural finish or // the Stop button ('ended'/'stop' teardown reasons, a new // onDemoEnded signature) — and wraps 10→1; switching to another // source keeps the place. The place persists per visitor in // localStorage (ove.demoTrack.next), so returning picks up where // they left off. The engine caches one decoded buffer at a time, // keyed by track index (~50–60 MB PCM each — all ten would be half // a gigabyte). The header chip + footer SRC label show the current // track's name (Demo Track N). RELEASE_NOTES rewritten in the // operator's first-person voice (no more third-person marketing // copy or addressing the visitor), and every em dash in the // interface replaced with a plain double minus ( -- ) — including // the embedded preset library's descriptions and script comments // (EMBEDDED_SEED_VERSION 7.01 → 7.10 re-seed). Declutter: the // landing menu's source list line, the presets panel's two // read-only paragraphs, the Random/Sequence hint line, the empty // user-folder note reworded, the Settings theme/highlight/Native // paragraphs, the link dialog's privacy footnote and the footer's // engine-ok dot are all gone — compile errors still toast, so the // always-green badge was noise // 7.9 — footer credit + What's New polish. The footer's blank middle now // carries the maker's signature: "© {year} · Site Created & // Deployed by Sum'ting White" (year generated from the clock, // hydration-guarded) with a "View my portfolio at zcreations.dev" // link that opens zcreations.dev in a new tab — centered in the // blank area from xl (mx-auto between the stats and the version // stamp), dropping to its own centered row on narrower screens so // nothing ever crowds. The What's New dialog got a typographic // tidy-up: a hairline divider above every release box // (border-zinc-800 — a ramp step, so it matches every base theme // through the globals.css remaps), and each change row became an // invisible two-column grid — the kind tag top-right justified // against the column boundary, the description top-left with word // wrap — so all descriptions start on the same pixel column // 7.8 — share strings 2.0: ~72% shorter than the script (was ~49%). The // base-62 idea, cranked to 11,172… 32,164: after the symbol // dictionary (which grew ~170 data-mined entries — every function // WITH its open paren, every engine variable, the AVS idioms // "hue = hue + dt * " and "t = t + dt * ", " * 0.", ") * ", // blank-line+section skeletons, "linewidth: " / "blend: additive" // headers, ~70 number literals, and the library's common // variable names incl. chroma) the whole body is read as ONE BIG // NUMBER and rewritten in base 32,164 — one digit per character // from the CJK + Hangul blocks, ~15 bits a character vs base-62's // 5.95 (so "1400" costs ~0.85 of a character). The decoder gets // the body's alphabet back from a ~32-character bitmap header // instead of ~80 literal symbols. Short scripts where the header // wouldn't pay keep the v7.7 symbol form (encoder picks whichever // is shorter); ▲ strings from v7.7 still decode; still lossless // for ANY text, still copy-safe, still self-checking. Corpus // measure: median share string is 27% of the script's length // (best 22%), e.g. Bass Bloom 1341 → 326 characters // 7.7 — script sharing + settings declutter. Script⇢Hash / Hash⇢Script: // two buttons in the Script tab turn the editor's script into ONE // short shareable Unicode string ("Bass Bloom:[▲…①①①①①①①①]") and // paste one back — the script returns exactly as written (lossless // for ANY text, compiling NOT required, so WIP scripts can be // shared for debugging). The string is built from built-in symbol // references (♤ = line return, ♡ = beats, ♠ = beatage, ♦ = space, // ♩♪♫♬ = the four sections, one symbol per function/variable, plus // a pair-packing layer that folds leftover identifier/number text // two chars per CJK code point) — ~50% shorter than the script on // real presets. Copy-safe by construction (no raw whitespace or // control chars; platform-injected variation selectors / zero- // widths stripped on paste) and self-checking: 8 check digits (an // FNV-1a digest rendered as ①–⑯) reject strings that were cut off // by a character limit or altered in transit, instead of decoding // into garbage. Also: the Music Services log in / "log out of all" // section in Settings is parked (MUSIC_SERVICES_UI flag) — the // built-in house account already powers Stream-a-Link for // everyone; the section returns reworked in a later update // 7.6.1 — header declutter: the Set Beat button is gone from the header // (tap-tempo lives in the Beat tab, and the Space shortcut works // anywhere — both unchanged). The beat indicator dot stays. // 7.6 — "What's New!" dialog: a Sparkles button in the header (left of // the theme switch) opens the visitor-facing changelog — every // release from v4.0 on, newest first, each with one-line Added / // Improved / Fixed / Removed notes (src/lib/release-notes.ts, // color-coded tags, the running version badged "current", // scrollable with the thin scrollbar). A small emerald dot on the // button marks updates the visitor hasn't seen yet — per visitor // in localStorage, cleared on open (mount-guarded, no hydration // mismatch). Standing rule added: version bumps ship with their // release note; a dev-mode guard warns when they drift // 7.5.1 — Link dialog width fix: text no longer escapes the dialog box. // Root cause: Radix dialogs lay their children in an implicit CSS // grid whose single column sizes to the widest child's min-content; // the RECENT LINKS rows carry truncate spans (white-space: nowrap), // and Chromium measures a nowrap flex item's full text width for // that measurement — so a long saved title (e.g. "Bobby Foxx — This // Is Why We Can't Have Nice Things (Official Music Video)") blew // the column ~40 px past the 448 px dialog, and EVERY child (title, // description, input + Load, rows, footer) laid out beyond the // right border. Fix: the shared Dialog/AlertDialog content now pins // the column to minmax(0,1fr) — capped at the available width, so // no nowrap content can ever stretch a dialog again (covers every // dialog app-wide) — the Link dialog widened to max-w-lg for // comfier wrapping, and min-w-0 + break-words added on the // recents/playlist containers and error text as belt-and-braces // 7.5 — yt-dlp compatibility: single tracks resolve by DOWNLOAD + CONVERT // instead of proxying the extractor's stream URL. The old stream // URLs were IP-locked (googlevideo bakes in the resolver's IP), // expired within hours, and arrived as webm/opus — a container // Safari & co. refuse ("The media resource … was not suitable"). // The server now runs the operator's pipeline per track: // yt-dlp -f bestaudio/best -x --audio-format mp3 // --audio-quality 0 -o "