ACS Documentation
ACS (Audio Cascading Style Sheets) is a CSS-like declarative
stylesheet language for audio. Where CSS describes how an HTML element should
look, ACS describes how it should sound. Same selectors, same
cascade, same specificity rules — but properties target audio: sound,
volume, pitch, room, sound-mood.
One <link rel="audiostyle">, one <script>,
and your buttons sound like buttons. No event handlers, no play()
calls. The cascade does the wiring.
Everything is declarative: rules live in .acs files, the runtime
attaches one document-level listener per event type, and at fire time it
walks up from the click target resolving the cascade at each ancestor — so dynamic
state (added classes, attribute changes) is always current.
Install
Pick whichever distribution channel fits your stack — there is no build step required.
1 · Browser via CDN (zero install)
<link rel="audiostyle" href="my-style.acs" />
<script type="module"
src="https://cdn.jsdelivr.net/gh/Grkmyldz148/acs@main/poc/runtime.js"></script>
jsDelivr auto-mirrors the GitHub repo. For pinned versions use @v0.9.2 instead of @main.
2 · npm
npm install acs-audio
Then in any entry file:
import "acs-audio"; // side-effect import — auto-binds <link rel="audiostyle">
The runtime is framework-agnostic: works with React, Vue, Svelte, plain HTML, etc. TypeScript types ship in the package.
3 · Self-host
Download dist/runtime.mjs from the
GitHub Releases
page and serve it next to your app. The runtime auto-loads defaults.acs
(49 calibrated built-in presets) from the same directory, so keep it co-located.
4 · VSCode / Cursor / VSCodium extension
One-click install from your editor's marketplace:
For Visual Studio Code on macOS, Windows, Linux. Auto-updates.
marketplace.visualstudio.com →For Cursor, VSCodium, Theia, Gitpod, Code-OSS. Same package.
open-vsx.org →.vsix
Air-gapped, offline, custom forks. Download from GitHub Releases.
github.com/.../releases →Or via the editor's CLI:
code --install-extension audio-cascading-style-sheets.acs-language # VSCode
cursor --install-extension audio-cascading-style-sheets.acs-language # Cursor
codium --install-extension audio-cascading-style-sheets.acs-language # VSCodium
Provides syntax highlighting, outline, folding, hover docs, live linter with
fuzzy-match hints (did you mean "master-volume"?), ▶ audition
CodeLens, sound picker webview with knob-based layer editor, and 30+ snippets.
Your first stylesheet
Create my-style.acs next to your index.html:
:root { master-volume: 0.85; room: medium-room; }
button { sound-on-click: tap-tactile; }
button.primary { sound-on-click: pop; pitch: +1st; }
button.danger { sound-on-click: error; volume: 0.7; }
input:on-focus { sound: tick; volume: 0.4; }
input:on-input { sound: click-soft; }
[role=alert] { sound-on-appear: notify; }
dialog[open] { sound-on-appear: modal-open; room: small-room; }
@media (prefers-reduced-sound: reduce) {
:root { master-volume: 0; }
}
Link it from your HTML:
<link rel="audiostyle" href="my-style.acs" />
<script type="module" src="https://cdn.jsdelivr.net/gh/Grkmyldz148/acs@main/poc/runtime.js"></script>
That's it. Click any button — the runtime resolved the cascade, looked up the preset, played a sound.
AudioContext
until the user interacts. ACS auto-resumes on the first
pointerdown / keydown. The first click of a session
unlocks audio for everything that follows.
How it works
The runtime is a tiny browser-side parser plus a Web Audio engine:
.acs files
→ parse — brace-aware CSS-like parser, handles @media nesting
→ split: :root → master config, @sound → custom presets, rest → resolver
→ buildBindings — produces resolver(el) → { click[], enter[], focus[], input[] }
↓
ONE document-level listener per event type. At fire time:
walk up from event.target,
call resolver(ancestor) at each step (dynamic state always current),
flatten matching rules,
apply inheritance for inherited props,
trigger preset with merged decls
↓
preset runner → voice graph (modal / tones / pluck / osc / noise)
→ optional saturation + pan
→ room chain (dry + convolver + wet)
→ master EQ → master gain → limiter → speakers
Calibration runs once at startup: every preset is offline-rendered, K-weighted RMS
is measured, and a per-preset multiplier is baked so volume: 0.5 sounds
equally loud whether the preset is a tap or a gong. Post-cal spread across all 49
built-in presets is 4.8 dB — well under the ITU-R BS.1770 broadcast
tolerance.
Concepts
ACS is structured like CSS. If you can read a CSS file, you can read ACS. Below is the full mental model — each subsection is a concept, each concept is a single ACS file you could write end-to-end.
Properties
ACS properties are kebab-case, just like CSS. Three categories: per-event triggers, per-element modifiers, and root-level master config.
Per-event triggers
| Property | Fires when | Example |
|---|---|---|
sound-on-click | Click bubbles to this element | tap-tactile |
sound-on-enter | pointerenter (hover-style) | tick |
sound-on-focus | focusin | tick |
sound-on-input | Form input event | click-soft |
sound-on-appear | Element added to DOM (MutationObserver) | notify |
sound-on-leave | Element removed from DOM | modal-close |
sound | Generic — used inside a state pseudo-class like :on-click, :on-input | pop |
Per-element modifiers (inheritable)
| Property | Default | Range / values |
|---|---|---|
volume | 1 | 0..1 · or X !raw to bypass calibration |
pitch | 0 | Semitones, e.g. +1st, -3st |
room | dry | dry · small-room · medium-room · large-room · hall · plate |
sound-mood | — | 9 overlays (see Moods) |
sound-mood-mix | 1 | 0..1 wet/dry blend |
pan | 0 | -1 (left) → 1 (right) |
Root-only (master)
| Property | Effect |
|---|---|
master-volume | Global gain (post-limiter input) |
master-eq | Tone tilt at master bus |
quality | low | medium | high — caps voice pool, modal partials, reverb tail |
Cascade & selectors
Specificity is computed exactly like CSS:
(inline=irrelevant, ids, classes/attrs/pseudo-classes, types/pseudo-elements).
More specific wins; !important overrides; later wins on tie.
| Selector | Specificity | Example |
|---|---|---|
| type | 0,0,1 | button |
| class | 0,1,0 | .primary |
| attribute | 0,1,0 | [data-variant="ghost"] |
| id | 1,0,0 | #cta |
| compound | sum | button.primary[disabled] = 0,2,1 |
| descendant | sum | dialog[open] button = 0,1,2 |
| universal | 0,0,0 | * |
| pseudo-state | 0,1,0 | :on-click, :on-input |
Inheritance: sound-mood, sound-mood-mix, volume,
pitch, room, pan all inherit. Set
sound-mood: lofi on body and the entire app gets a
muffled overlay until you override on a child.
Layer sources
Every @sound is built from one or more layers. A layer
is a single voice graph — pick one of these source primitives, configure it, the
runtime renders it through saturation → pan → room → master.
Layer source types
| Source key | Meaning | Best for |
|---|---|---|
modal | Modal IIR resonator (frequency + ratios + decays) | Bells, gongs, struck objects |
tones | Additive sine bank — comma-separated frequencies | Chords, bell-like clusters, UI dings |
osc | Single oscillator (sine | triangle | square | sawtooth) with optional fm | Synth tones, FM bells, sweeps |
pluck | Karplus-Strong plucked string | Guitar/harp-like accents |
noise | Filtered noise (white | pink | brown) | Snares, claps, breath, swooshes |
Concrete examples
@sound my-bell {
body {
tones: 880hz, 1320hz, 2200hz; /* additive */
decays: 0.6s, 0.4s, 0.25s;
gain: 0.4;
}
}
@sound my-pluck {
body { pluck: 440hz; brightness: 0.7; decay: 1.2s; gain: 0.5; }
}
@sound my-snare {
body { noise: white; filter: bandpass; cutoff: 2khz; q: 2; decay: 80ms; gain: 0.6; }
snap { osc: sine; freq: 200hz; decay: 30ms; gain: 0.3; }
}
Multiple layers stack additively — both fire simultaneously and the runtime sums them.
@sound blocks
@sound <name> { ... } registers a custom preset by name. Once
registered, any selector can use it via sound: my-name,
sound-on-click: my-name, etc.
@sound bubble {
body { osc: sine; freq: 540hz; pitch-from: 540hz to 880hz; decay: 80ms; gain: 0.5; }
pop { noise: white; filter: highpass; cutoff: 4khz; decay: 12ms; gain: 0.2; }
}
button.tooltip-trigger:on-hover { sound: bubble; pitch: +2st; }
Re-defining a preset name overrides the built-in. So
@sound tap { ... } in your stylesheet replaces the calibrated tap.
The 49 built-ins are loaded first, your stylesheet last — last wins.
@sample at-rule
Bring your own audio files. Body-less; registers a URL under a preset name. The file is fetched + cached on first trigger.
@sample chime url("/sfx/chime.wav");
@sample crunch url("/sfx/crunch.mp3");
button[data-action="confirm"] { sound-on-click: chime; }
button[data-action="delete"] { sound-on-click: crunch; volume: 0.7; }
Sample volume is normalized through the same calibration pipeline as procedural presets (first-trigger latency: ~30 ms while it caches).
Mood overlays
A sound-mood is an inheritable filter that paints over any
sound triggered on the element or its descendants. Nine built-in moods ship; mix
wet/dry with sound-mood-mix.
| Mood | Effect |
|---|---|
warm | Soft lowpass + gentle saturation |
bright | High-shelf boost + lowpass relief |
glassy | Resonant peak around 4–6 kHz |
metallic | Comb filter at audible delays |
organic | Slow LFO on cutoff + mild detune |
punchy | Transient enhancer + tight gate |
retro | Bandpass + bitcrush |
airy | Subtle high-shelf + reverb send |
lofi | Lowpass + sample-rate reduction + wow/flutter |
.retro-app { sound-mood: lofi; }
.lab { sound-mood: glassy; sound-mood-mix: 0.4; } /* 40% wet */
Rooms (reverb)
Each element resolves one room — the cascaded room value.
The runtime maintains lazy convolver chains keyed by room name; first use
instantiates, subsequent triggers reuse.
| Room | Tail (s) | Use case |
|---|---|---|
dry | 0 | Default; bypasses convolver |
small-room | 0.4 | UI taps, modals, intimate alerts |
medium-room | 0.9 | Default for most apps |
large-room | 1.6 | Concert / cinematic feel |
hall | 2.8 | Long, lush, ambient |
plate | 1.2 | Dattorro plate — classic studio reverb |
CSS-var bridge
Any ACS value can read a CSS custom property via var(--token). The
runtime resolves against getComputedStyle(documentElement) at
trigger time — so dark/light theme switches are picked up live.
:root { --beep: 880hz; }
button.primary { sound: my-tone; }
@sound my-tone {
body { tones: var(--beep); decay: 120ms; }
}
String declarations (sound, room, mood) and numeric ones (volume, pitch, frequencies) all resolve through the same bridge.
@media & accessibility
ACS supports the same @media nesting as CSS, with two ACS-specific media features:
@media (prefers-reduced-sound: reduce) { :root { master-volume: 0; } }
@media (input-modality: keyboard) { button:on-focus { sound: tick; } }
prefers-reduced-sound is still a CSS proposal — the runtime
evaluates it gracefully (always false) until browsers adopt it. Your
stylesheet stays correct; users with screen readers / reduced-motion preferences
won't hear extras when the media query lands.
VSCode & Cursor extension
Installs from both major marketplaces. Same package ID
(audio-cascading-style-sheets.acs-language),
same version everywhere.
- VSCode Marketplace — marketplace listing
- Open VSX (Cursor / VSCodium / Theia / Gitpod) — open-vsx listing
- Manual
.vsixfrom GitHub Releases
Features
- TextMate grammar — strings, numbers, properties, at-rules, selectors all coloured.
- Document outline (
DocumentSymbolProvider) — every@soundappears in the outline panel. - Folding ranges for
@soundbodies,@mediablocks. - Hover docs for every property, preset, room, mood.
- Live linter — "unknown property 'master-volum' — did you mean 'master-volume'?"
- ▶ Audition CodeLens — click above any
@soundto hear it. - 🔊 Open in Picker CodeLens — opens the interactive layer editor.
- 30+ snippets covering bell / FM-bell / snare / kick / pluck / fast-tap recipes plus cascade blocks for buttons / toasts / switches / inputs / dialogs.
Sound picker
The picker is a webview shipped inside the extension. Two modes:
- Browser — intent-based gallery with hover-preview, keyboard nav, ASCII waveform thumbs.
- Layer editor — knob-based DSL editor for the
@soundblock of any preset. Source dropdown, freq, ratios/decays/gains text inputs, FM modulator, pitch-from sweep, detune, brightness (pluck), filter + cutoff, attack/decay, gain/drive/pan/start. Drag-to-reorder layers; live waveform; Reset to original; Copy as@soundfor paste-ready output.
Open via the editor title-bar 🔊 icon while in an .acs file, or run
"ACS: Open Sound Picker" from the command palette.
CLI tools
Three Node scripts ship with the npm package — pure ESM, no transpile.
Lint
npx acs-audio/lint poc/*.acs
# or
node node_modules/acs-audio/tools/lint-acs.mjs my-style.acs
Validates property names, preset references, mood names, room names. Fuzzy-match suggestions for typos.
Format
node node_modules/acs-audio/tools/format.mjs my-style.acs
Pretty-prints in-place. Aligns properties, normalizes whitespace, sorts @sound blocks alphabetically by preset name (optional).
Compile
node node_modules/acs-audio/tools/compile-acs.mjs --pretty --out style.json my-style.acs
Pre-parses to a JSON AST. Useful for build-time inlining or shipping a minified runtime that skips the parser.
Skills (AI agents)
ACS ships a Claude / Cursor "skill" that lets an AI agent design a sound for you
from a natural-language prompt or by reverse-engineering an audio file. The skill
sits in skills/create-acs-sound/ in the repo.
What it does
- Prompt input — "Give me a soft pop that's brighter than the default" → emits a calibrated
@soundblock. - Audio input — Drop a
.wav, the skill measures source/envelope/effects and reverse-engineers a matching@sound. - Mixed — Audio + qualifier ("warmer", "punchier") refines the measured definition.
- Round-trip render — The skill renders the result to a WAV preview and validates within tolerance bands (gain budget, frequency bounds, duration cap).
Install
One command via the
skills
CLI (vercel-labs) — supports Claude Code, Cursor, Codex,
OpenCode, Gemini CLI, GitHub Copilot, Warp, Windsurf, and 50+ other agent runtimes:
npx skills add Grkmyldz148/acs-skills
The CLI prompts you to pick which agent(s) to install for, then drops the skill into their conventional path (e.g. .claude/skills/create-acs-sound/ for Claude Code, .cursor/skills/... for Cursor, etc.).
Grkmyldz148/acs-skills is an auto-mirror of the
skill source
in the monorepo — a GitHub Action pushes the snapshot on every change to main.
Manual install (fallback)
# clone the repo
git clone https://github.com/Grkmyldz148/acs.git
# copy the skill into your agent's skills directory:
cp -r acs/skills/create-acs-sound ~/.claude/skills/
# or reference SKILL.md directly:
acs/skills/create-acs-sound/SKILL.md
The skill is a directory of rules/*.md atomic decision documents plus pipeline orchestration in SKILL.md. Compatible with the vercel-labs skills CLI ecosystem (Claude Code, Cursor, Codex, OpenCode, and 50+ others).
Pipeline
1. pipeline-detect-input → prompt | audio | both
2. pipeline-pick-base-layer → match event-class tokens (event-click, event-tap, …)
3. pipeline-apply-mood → adjective tokens mutate source/filter/effects
4. pipeline-decide-layering → 1, 2, or 3 layers based on event class
5. pipeline-emit-and-render → emit @sound, optional WAV preview, validate
Authoring your own skill
Each rule is a markdown file with frontmatter declaring its category
(event, layer, mood, effect,
pipeline, validate, interpret) and a body
containing the rule's logic + an example @sound block. Run
node src/build.mjs to compile SKILL.md from the rules.
See the skills directory in the repo for the full source.
Programmatic API
Most use is declarative — these are escape hatches.
Trigger directly
window.ACS.trigger({ sound: "pop", pitch: "+2st", volume: 0.7 }, "click");
Set master config
window.ACS.setMasterConfig({
"master-volume": 0.7,
room: "small-room",
quality: "high",
});
Watch a stylesheet
window.ACS.watch("/style.acs", { interval: 1000 });
// re-fetches + re-binds on changes — useful in dev
Subscribe to triggers
const unsubscribe = window.ACS.onTrigger(({ preset, decls, eventKey, target }) => {
console.log(preset, eventKey, target);
});
Devtools overlay
window.ACS.devtools.mount();
// fixed-position panel: last 20 triggers + preset / source / factor / mood / room
window.ACS.devtools.unmount();
The overlay subscribes via onTrigger, so the cost when not mounted is zero.
Framework helpers
window.ACS.helpers.useSound takes a hooks injection so the runtime
has no React peer dep. Works with any hook-shaped library.
import { useCallback } from "react";
const ding = window.ACS.helpers.useSound({ useCallback }, "ding");
<button onClick={ding}>Notify</button>
Other helpers:
window.ACS.helpers.play("pop", { pitch: "+1st" });
window.ACS.helpers.attach(myButton, "ding", "click");
Custom melodies — deep dive
This section is for authors who want to write @sound blocks from
scratch instead of using the 49 built-ins. The mental model is the same one used
by the create-acs-sound skill — read it once and you'll
understand both.
Step 1 — Pick an archetype
| You want | Use |
|---|---|
| Bell-like ding | tones: f1, f2, f3 with descending decays |
| Wood / drum knock | modal: f, ratios: 1 r2 r3, decays: d1 d2 d3 |
| Plucked string | pluck: f, brightness: 0..1, decay: t |
| Filtered noise (snare/clap/breath) | noise: white, filter: bandpass, cutoff: f, q: q |
| FM bell | osc: sine, freq: f, fm: { ratio: r, depth: d } |
Step 2 — Author the body layer
@sound my-sound {
body {
/* source key */
tones: 880hz, 1320hz, 2200hz;
/* envelope */
decays: 0.6s, 0.4s, 0.25s; /* per-tone if multi */
attack: 4ms; /* default 0 */
/* level */
gain: 0.4; /* before calibration */
}
}
Step 3 — Stack a click layer
Most "satisfying" UI sounds are body + click. The body gives the tonal identity; the click gives the sharp transient.
@sound my-tap {
body { tones: 1.4khz; decay: 50ms; gain: 0.4; }
click { noise: white; filter: highpass; cutoff: 4khz; decay: 8ms; gain: 0.3; }
}
Step 4 — Validate
- Gain budget: sum of layer
gainvalues should be ≤ 1. - Frequency bounds: keep fundamentals between 80 Hz – 8 kHz for UI sounds.
- Duration cap: tail under 0.5 s for taps, under 1.5 s for toasts/notifications.
- Run the linter —
npx acs-audio/lint your-style.acs. - Audition — ▶ CodeLens above the
@soundblock in VSCode.
Step 5 — Calibrate (optional, advanced)
ACS's calibration pipeline assigns each preset a BAKED_FACTOR so
volume: 0.5 sounds equally loud across the library. For your own
presets the runtime calibrates on-the-fly at first trigger, but you can pre-bake:
node node_modules/acs-audio/analyzer/defaults-loudness.mjs --bake my-style.acs
This writes a JSON sidecar with K-weighted RMS measurements; the runtime reads it on load and applies the factor before volume.
Layer keys reference
Complete list of keys you can put inside any layer block.
Source (pick one)
| Key | Value | Notes |
|---|---|---|
tones | Comma-separated frequencies | Additive sine bank |
modal | Frequency | Pair with ratios, decays, gains |
osc | sine | triangle | square | sawtooth | Pair with freq |
pluck | Frequency | Pair with brightness, decay |
noise | white | pink | brown | Pair with filter |
Modulation / shaping
| Key | Value | Applies to |
|---|---|---|
fm | { ratio, depth } | osc |
pitch-from | start to end | Sweep |
detune | Cents | osc |
filter | lowpass | highpass | bandpass | tpt-* | noise / osc |
cutoff | Frequency | filter |
q | Number | filter (resonance) |
Envelope
| Key | Value |
|---|---|
attack | ms / s |
decay | ms / s |
release | ms / s |
decays | Comma-separated (per partial) |
Output
| Key | Value |
|---|---|
gain | 0..1 |
drive | 0..1 (saturation) |
pan | -1..1 |
start | Offset (ms / s) — delays this layer |
realtime | true — route through AudioWorklet for sub-1ms latency |
Built-in preset library (49)
Auto-loaded as defaults.acs. All calibrated; cross-preset spread 4.8 dB.
Override any by re-defining @sound <name> in your stylesheet.
UI taps & clicks
tap · tap-tactile · click · click-soft · tick · pop · thunk · woodblock
Bells & pings
bell · bell-soft · bell-bright · chime · chime-soft · ding · ping · ting
Drums & percussion
kick · snare · hat · clap
States & flow
success · complete · confirm · error · denied · buzz · notify · mention · badge · prompt
Modals & menus
modal-open · modal-close · drawer-open · drawer-close · dropdown-open · dropdown-close · page-enter · page-exit
Toggles & transitions
toggle-on · toggle-off · swoosh · whoosh · carriage-return
Texture & ambient
glass · gong · string · sparkle · pluck-soft · pluck-bright
Theme packs (8)
Drop-in stylesheets that re-bind the entire UI vocabulary.
| Theme | Vibe |
|---|---|
apple.acs | Restrained UI taps, modal-open like macOS |
material.acs | Punchy, slightly tonal, ripple-style |
retro.acs | 8-bit blips, CRT-flavored |
brutalist.acs | Industrial thunks, no decorum |
cinematic.acs | Long tails, sub-rumbles |
bauhaus.acs | Clean tones, primary-color UI mapped to fundamentals |
terminal.acs | Dry typewriter clacks + carriage returns |
ambient.acs | Diffused chimes, all-medium-room |
<link rel="audiostyle" href="https://cdn.jsdelivr.net/gh/Grkmyldz148/acs@main/poc/themes/apple.acs" />
Browser support
Anything with the Web Audio API — Chrome, Firefox, Safari ≥ 14, Edge.
| Feature | Chrome | Firefox | Safari | Edge |
|---|---|---|---|---|
Core (<link rel=audiostyle> + cascade) | ✅ | ✅ | ✅ 14+ | ✅ |
AudioWorklet (realtime: true) | ✅ | ✅ | ✅ 14.1+ | ✅ |
prefers-reduced-sound (proposal) | — | — | — | — |
var(--token) resolution | ✅ | ✅ | ✅ | ✅ |
Troubleshooting
Nothing plays on first interaction
Browsers block AudioContext until user gesture. The runtime auto-resumes on pointerdown / keydown; the first click of the session may be silent if it lands before the resume callback. Open devtools, check window.ACS.audioContext.state — should be "running" after one interaction.
Some buttons silent, others not
Cascade specificity: a more-specific selector with an unknown preset name silently overrides a working base rule. Run the linter — npx acs-audio/lint my-style.acs — it catches typo'd preset names.
Sounds clipping / too loud
Lower master-volume on :root. The limiter prevents distortion but won't make peaks pleasant. If a specific preset is too loud, override its baked factor via volume: X !raw.
Worklet errors in console
The runtime falls back to the main thread automatically. Set realtime: false on offending layers to suppress the warning, or open an issue with your browser/version.
Stylesheet edits don't refresh
The runtime fetches once. In dev, opt-in to live-reload: window.ACS.watch("/my-style.acs", { interval: 1000 }).
FAQ
Does ACS depend on PostCSS?
No. ACS is a separate language with its own parser. .acs files are
served as-is and parsed in the browser by the runtime. There's no build step,
no preprocessor, no relationship to CSS tooling.
Can I use it with React / Vue / Svelte?
Yes — the runtime is framework-agnostic. npm install acs-audio, then
import "acs-audio" once at the entry point. Selectors target DOM
elements regardless of which framework rendered them.
Does it work with server-side rendering?
The runtime is browser-only — it imports cleanly under SSR (no top-level DOM
access) but does its work after hydration. Add <link rel="audiostyle">
in your document head and the runtime takes over once the page is interactive.
Is there a way to disable all sounds at runtime?
Yes — window.ACS.setEnabled(false) mutes everything without
unmounting. setEnabled(true) resumes. Or via stylesheet:
:root { master-volume: 0; }.
Why "cascading style sheets" for audio?
Because the cascade is the right abstraction. UI sound design is a cross-cutting concern — buttons sound a certain way, primary buttons louder, primary buttons inside dialogs softer. CSS is exactly the language for "rules that compose through specificity and inheritance." Re-using its mental model (and its tooling — see the VSCode extension) lets authors transfer 95% of what they already know.
Can I mix .acs files from multiple sources?
Yes. The runtime loads defaults.acs first (built-in presets), then
every <link rel="audiostyle"> in document order. Later wins on
ties; specificity wins always. You can drop a theme pack and override just the
bits you want in your own file.
How do I attribute the project?
MIT license, attribution welcome but not required. If you do credit, link to audiocss.dev or github.com/Grkmyldz148/acs.