The book thesis, playable: ONE press fires the firma across channels, with a mixer to mute each and watch them layer. The 4 visible channels (color · motion · depth · shape) compose from one event with REAL eidos tokens — verified: press -> primary->affirm-solid + scale(0.93) + shadow overlay->raised + --shape-smoothing 2->3.4. Sound + haptic via Web Audio / Vibration (in-app the sema engine emits them from the same event). + the two-moment model section. CHANNELS_SYNTHESIS.md ties the five channel RFCs: two moments x N channels from one event, the layer ownership (sema runs sound/haptic + projects data-event-*; eidos materializes the visual channels), the cross-modal firma, the runtime-builder quintet. Verified: check 0 errors. Browser: all four visible channels compose on press. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>active-uix
parent
ac2ef0f19a
commit
21057c398f
@ -0,0 +1,81 @@
|
||||
# Los canales como un solo sistema — síntesis ("Diseñando lo que ocurre")
|
||||
|
||||
> **Capstone** de los RFCs por canal. La tesis del libro *Diseñando lo que ocurre*: una
|
||||
> interacción **no es** color, ni movimiento, ni sonido — es **un solo suceso perceptual
|
||||
> repartido en N canales**, bajo el modelo de **dos momentos**. Esto ata
|
||||
> `COLOR_ENGINE_RFC` · `TYPOGRAPHY_ENGINE_RFC` · `DEPTH_ENGINE_RFC` · `SHAPE_ENGINE_RFC` ·
|
||||
> `STRUCTURE_ENGINE_RFC`. Demo vivo: **`/temas/orquesta`** (el mezclador).
|
||||
|
||||
## 1. El modelo de dos momentos (transversal)
|
||||
|
||||
Todo canal —no solo motion— respeta el mismo eje doble:
|
||||
|
||||
| Momento | Atributo | Qué es | Quién |
|
||||
|---|---|---|---|
|
||||
| **estado** | `data-state` | lo que el elemento **es** (persistente): color de reposo, plano, forma, posición | soma / morfo |
|
||||
| **evento** | `data-event-*` | lo que le **ocurre** (transitorio, durante el `hold`): el tinte que pulsa, la sombra que recede, la esquina que se afirma, el tick, la vibración | sema (proyecta) → eidos (materializa visual) |
|
||||
|
||||
El orden entre ambos lo fija `sequence` (`pre` / `coincident` / `post`). Eidos lee **los dos**:
|
||||
`presets` sobre `data-state`, `signatures` sobre `data-event-*`.
|
||||
|
||||
## 2. Los 8 canales del libro → sistemas Eidos
|
||||
|
||||
| Canal (libro) | Sistema | RFC / motor |
|
||||
|---|---|---|
|
||||
| tiempo + movimiento | **motion** | `arts/motion` + `eidos/lib/motion` (firma + presets) |
|
||||
| presencia + profundidad | **depth** | `DEPTH_ENGINE_RFC` |
|
||||
| **forma** | **shape** | `SHAPE_ENGINE_RFC` |
|
||||
| color | **color** | `COLOR_ENGINE_RFC` |
|
||||
| sonido | **sema · sound** | `sema/chans/sound` |
|
||||
| háptica | **sema · haptic** | `sema/chans/haptic` |
|
||||
|
||||
Más el **espacio** (`STRUCTURE_ENGINE_RFC`) — estructural, **solo-estado** (el escenario, no el
|
||||
suceso): no tiene momento-evento, y eso es deliberado.
|
||||
|
||||
## 3. Quién posee qué (la narrativa de capas)
|
||||
|
||||
- **Sema** ejecuta **2 canales runtime** (`sound` + `haptic`) y **proyecta el meta-canal visual**
|
||||
(estampa `data-event-*` durante el hold). El motor no conoce DOM/CSS.
|
||||
- **Eidos** materializa los **canales visuales** (motion · depth · shape · color) reaccionando en
|
||||
CSS a `data-event-*` + `data-state`. Es el único dueño visual.
|
||||
|
||||
(Narrativa canónica completa en `CLAUDE.md` → "Sema: open channel registry".)
|
||||
|
||||
## 4. La composición — un evento, N canales
|
||||
|
||||
La **firma** (`BUILTIN_SIGNATURES` + `BUILTIN_KEYFRAMES`) es **cross-modal**: un solo keyframe
|
||||
lleva varias modalidades. Ejemplo real (`press-squeeze`, familia `contact`):
|
||||
|
||||
```
|
||||
contact · press → scale 0.96 (motion)
|
||||
+ box-shadow → plano (depth recede)
|
||||
+ --shape-smoothing 2→3 (shape: la esquina se afirma)
|
||||
+ tick (sema sound)
|
||||
+ vibración (sema haptic)
|
||||
```
|
||||
|
||||
Un `engine.emit(...)` estampa `data-event-*` (los canales visuales reaccionan) **y** dispara
|
||||
sound + haptic — desde el **mismo** evento. No hay 6 sistemas coordinándose a mano; hay un
|
||||
suceso que se expresa en 6 dimensiones.
|
||||
|
||||
## 5. Builders runtime — el quinteto (jaula abierta)
|
||||
|
||||
Cada eje retunable en runtime, mismo patrón (semilla → bloque gestionado), opt-in sobre la
|
||||
escala authored:
|
||||
|
||||
`applyColorScheme` · `applyTypeScale` · `applyDepth` · `applyShape` · `applySpacing`
|
||||
|
||||
(Futuro: `applyTheme(seed)` que componga varios desde una semilla compartida.)
|
||||
|
||||
## 6. Posición frente a referentes
|
||||
|
||||
Ningún referente reúne los canales bajo **un modelo único**. Material tiene morph de forma +
|
||||
motion (ad-hoc, sin sonido/háptica integrados); Apple, continuidad + materiales (atado a
|
||||
plataforma); todos, color. **Eidos los reúne** —los 6 perceptuales + el estructural— sobre el
|
||||
modelo de dos momentos, disparados desde un evento, con builders runtime y registro abierto.
|
||||
Eso es el framework, no la suma de sus partes.
|
||||
|
||||
## 7. Demo
|
||||
|
||||
`/temas/orquesta` — el mezclador: pulsa un control y silencia cada canal para oír su parte. Los
|
||||
4 visibles usan tokens reales; sound + haptic los aproxima el demo (en la app, motor sema).
|
||||
@ -0,0 +1,281 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Capstone — "Diseñando lo que ocurre" made literal: ONE interaction firing many channels at
|
||||
* once, with a mixer to mute each and watch them layer.
|
||||
*
|
||||
* The 4 visible channels (color · motion · depth · shape) use REAL eidos tokens, composed
|
||||
* explicitly per-channel so each is independently mutable. Sound + haptic are fired with the
|
||||
* Web Audio / Vibration APIs directly — in-app the sema engine emits them from the SAME event
|
||||
* (the canonical sound + haptic channels); here they're approximated so the mixer can mute them.
|
||||
*
|
||||
* The point: an interaction is not color, nor motion, nor sound — it is one perceptual event
|
||||
* across N channels, under the two-moment model. This is the book's thesis, playable.
|
||||
*/
|
||||
import '@/uix/eidos/index.css'
|
||||
|
||||
let theme = $state<'light' | 'dark'>('dark')
|
||||
$effect(() => {
|
||||
const el = document.documentElement
|
||||
const prev = el.getAttribute('data-theme')
|
||||
el.setAttribute('data-theme', theme)
|
||||
return () => {
|
||||
if (prev === null) el.removeAttribute('data-theme')
|
||||
else el.setAttribute('data-theme', prev)
|
||||
}
|
||||
})
|
||||
|
||||
type ChannelId = 'color' | 'motion' | 'depth' | 'shape' | 'sound' | 'haptic';
|
||||
const CHANNELS: { id: ChannelId; label: string; note: string; eidos: boolean }[] = [
|
||||
{ id: 'color', label: 'color', note: 'tinte de intención (→ affirm)', eidos: true },
|
||||
{ id: 'motion', label: 'movimiento', note: 'escala bajo el dedo', eidos: true },
|
||||
{ id: 'depth', label: 'profundidad', note: 'la sombra recede', eidos: true },
|
||||
{ id: 'shape', label: 'forma', note: 'la esquina se afirma', eidos: true },
|
||||
{ id: 'sound', label: 'sonido', note: 'un tick (motor sema)', eidos: false },
|
||||
{ id: 'haptic', label: 'háptica', note: 'una vibración (motor sema)', eidos: false }
|
||||
];
|
||||
|
||||
// Visible channels on by default; sound + haptic OFF (audible/tactile side effects — sema canon).
|
||||
let channels = $state<Record<ChannelId, boolean>>({
|
||||
color: true,
|
||||
motion: true,
|
||||
depth: true,
|
||||
shape: true,
|
||||
sound: false,
|
||||
haptic: false
|
||||
});
|
||||
|
||||
let pressed = $state(false);
|
||||
let pressCount = $state(0);
|
||||
let lastFired = $state<string[]>([]);
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
let actx: AudioContext | undefined;
|
||||
|
||||
function tick(): void {
|
||||
try {
|
||||
actx ??= new (window.AudioContext ?? (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext)();
|
||||
const o = actx.createOscillator();
|
||||
const g = actx.createGain();
|
||||
o.type = 'triangle';
|
||||
o.frequency.value = 660;
|
||||
o.connect(g);
|
||||
g.connect(actx.destination);
|
||||
const t = actx.currentTime;
|
||||
g.gain.setValueAtTime(0.0001, t);
|
||||
g.gain.exponentialRampToValueAtTime(0.16, t + 0.005);
|
||||
g.gain.exponentialRampToValueAtTime(0.0001, t + 0.13);
|
||||
o.start(t);
|
||||
o.stop(t + 0.14);
|
||||
} catch {
|
||||
/* audio unavailable — no-op */
|
||||
}
|
||||
}
|
||||
|
||||
function fire(): void {
|
||||
pressCount += 1;
|
||||
lastFired = CHANNELS.filter((c) => channels[c.id]).map((c) => c.label);
|
||||
if (channels.sound) tick();
|
||||
if (channels.haptic) navigator.vibrate?.(18);
|
||||
pressed = true;
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => (pressed = false), 300);
|
||||
}
|
||||
|
||||
const activeCount = $derived(Object.values(channels).filter(Boolean).length);
|
||||
</script>
|
||||
|
||||
<svelte:head><title>La orquesta · Eidos</title></svelte:head>
|
||||
|
||||
<div class="root" data-theme={theme}>
|
||||
<header class="masthead">
|
||||
<div class="mast-row">
|
||||
<p class="kicker">Eidos · diseñando lo que ocurre</p>
|
||||
<button class="theme-toggle" onclick={() => (theme = theme === 'light' ? 'dark' : 'light')}>
|
||||
{theme === 'light' ? '◐ claro' : '◑ oscuro'}
|
||||
</button>
|
||||
</div>
|
||||
<h1 class="mast-title">Un evento, <em>seis canales</em></h1>
|
||||
<p class="mast-lede">
|
||||
Una interacción no es color, ni movimiento, ni sonido — es <strong>un solo suceso
|
||||
perceptual</strong> repartido en varios canales, a la vez. Hemos construido los seis. Aquí
|
||||
tocan juntos: pulsa, y <strong>silencia</strong> cada uno para oír su parte.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<!-- ── La orquesta ──────────────────────────────────────────────────── -->
|
||||
<section class="section">
|
||||
<div class="sec-head">
|
||||
<h2 class="sec-title">El mezclador</h2>
|
||||
<p class="sec-note">
|
||||
Pulsa el botón: los canales activos disparan desde <em>un</em> evento. Apaga la forma y la
|
||||
esquina deja de afirmarse; apaga la profundidad y la sombra no recede. Un instrumento
|
||||
menos, el mismo gesto.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="stage">
|
||||
<div class="stage-target">
|
||||
<button
|
||||
class="target"
|
||||
class:ch-color={channels.color}
|
||||
class:ch-motion={channels.motion}
|
||||
class:ch-depth={channels.depth}
|
||||
class:ch-shape={channels.shape}
|
||||
data-shape={channels.shape ? 'continuous' : undefined}
|
||||
data-pressed={pressed}
|
||||
onpointerdown={fire}
|
||||
>Confirmar</button>
|
||||
<span class="stage-hint">pulsa →</span>
|
||||
</div>
|
||||
|
||||
<div class="mixer">
|
||||
{#each CHANNELS as c}
|
||||
<button class="track" data-on={channels[c.id]} onclick={() => (channels[c.id] = !channels[c.id])}>
|
||||
<span class="track-led" data-on={channels[c.id]}></span>
|
||||
<span class="track-meta">
|
||||
<span class="track-label">{c.label}</span>
|
||||
<span class="track-note">{c.note}</span>
|
||||
</span>
|
||||
<span class="track-tag" data-layer={c.eidos ? 'eidos' : 'sema'}>{c.eidos ? 'eidos' : 'sema'}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="trace">
|
||||
<span class="trace-k">último evento</span>
|
||||
<span>1 press → {lastFired.length} canal(es){lastFired.length ? `: ${lastFired.join(' · ')}` : ' — pulsa'}</span>
|
||||
<span class="trace-sep">·</span>
|
||||
<span class="trace-k">activos</span><span>{activeCount}/6</span>
|
||||
<span class="trace-sep">·</span>
|
||||
<span class="trace-k">presses</span><span>{pressCount}</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── Los dos momentos ─────────────────────────────────────────────── -->
|
||||
<section class="section">
|
||||
<div class="sec-head">
|
||||
<h2 class="sec-title">Dos momentos, un solo modelo</h2>
|
||||
<p class="sec-note">
|
||||
Todo canal respeta el mismo rigor: lo que el elemento <em>es</em> (estado, persistente) vs
|
||||
lo que le <em>ocurre</em> (evento, durante el hold). El mezclador de arriba dispara el
|
||||
momento-<em>evento</em>; el reposo es el momento-<em>estado</em>.
|
||||
</p>
|
||||
</div>
|
||||
<div class="moments">
|
||||
<div class="moment">
|
||||
<span class="moment-h">estado · <code>data-state</code></span>
|
||||
<p class="moment-p">color de reposo · plano de profundidad · forma en reposo · posición. Persistente — soma/morfo.</p>
|
||||
</div>
|
||||
<div class="moment moment-event">
|
||||
<span class="moment-h">evento · <code>data-event-*</code></span>
|
||||
<p class="moment-p">el tinte que pulsa · la sombra que recede · la esquina que se afirma · el tick · la vibración. Transitorio — sema, durante el hold.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="footnote">
|
||||
<p>
|
||||
<strong>Esto es el framework, no seis sistemas sueltos.</strong> Cada canal tiene su motor y
|
||||
su demo (<code>/temas/{'{'}color · tipografia · profundidad · forma · estructura{'}'}</code>),
|
||||
pero la tesis del libro es que <em>componen</em>: un evento sema reparte la firma por
|
||||
motion + depth + shape + color (visual) y sound + haptic (sema), bajo el modelo de dos
|
||||
momentos. Aquí los cuatro visibles usan tokens reales de eidos; sonido y háptica los
|
||||
aproxima el demo (en la app los emite el motor sema desde el mismo evento). Ningún referente
|
||||
reúne los seis canales sobre un modelo único.
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
min-block-size: 100vh;
|
||||
background:
|
||||
radial-gradient(130% 90% at 50% -15%, var(--color-surface-raised), transparent 55%),
|
||||
var(--color-surface-default);
|
||||
color: var(--color-content-primary);
|
||||
font-family: var(--font-family-primary);
|
||||
padding: clamp(1.5rem, 5vw, 5rem) clamp(1.25rem, 5vw, 6rem) 6rem;
|
||||
--rule: var(--color-border-default);
|
||||
}
|
||||
|
||||
.masthead { max-inline-size: 56rem; margin-block-end: clamp(3rem, 8vw, 6rem); }
|
||||
.mast-row { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-block-end: 2.5rem; }
|
||||
.kicker { margin: 0; font-family: var(--font-family-mono); font-size: var(--font-size-xs); letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-content-muted); }
|
||||
.theme-toggle { font-family: var(--font-family-mono); font-size: var(--font-size-xs); color: var(--color-content-secondary); background: var(--color-surface-raised); border: 1px solid var(--rule); border-radius: 999px; padding: 0.5rem 1rem; cursor: pointer; }
|
||||
.theme-toggle:hover { color: var(--color-content-primary); }
|
||||
.mast-title { font-family: var(--font-family-display); font-size: var(--font-size-xxxl); font-weight: 600; line-height: 1; letter-spacing: -0.03em; margin: 0 0 1.25rem; text-wrap: balance; }
|
||||
.mast-title em { font-style: italic; color: var(--color-primary-text, var(--color-content-primary)); }
|
||||
.mast-lede { max-inline-size: 52ch; font-size: var(--font-size-lg); line-height: 1.5; color: var(--color-content-secondary); margin: 0; text-wrap: pretty; }
|
||||
.mast-lede strong { color: var(--color-content-primary); font-weight: 600; }
|
||||
|
||||
.section { margin-block-end: clamp(3.5rem, 9vw, 7rem); }
|
||||
.sec-head { margin-block-end: 2.5rem; }
|
||||
.sec-title { font-family: var(--font-family-display); font-size: var(--font-size-xl); font-weight: 600; letter-spacing: -0.01em; margin: 0 0 0.5rem; }
|
||||
.sec-note { margin: 0; max-inline-size: 64ch; font-size: var(--font-size-sm); line-height: 1.55; color: var(--color-content-muted); }
|
||||
.sec-note em { color: var(--color-content-primary); font-style: normal; font-weight: 600; }
|
||||
code { font-family: var(--font-family-mono); font-size: 0.9em; color: var(--color-content-secondary); background: var(--color-surface-muted); padding: 0.1em 0.4em; border-radius: 4px; }
|
||||
|
||||
/* The orchestra */
|
||||
.stage { display: grid; grid-template-columns: 1fr; gap: clamp(1.5rem, 4vw, 3rem); align-items: center; }
|
||||
@media (min-width: 50rem) { .stage { grid-template-columns: 1fr 22rem; } }
|
||||
.stage-target { display: flex; flex-direction: column; align-items: center; gap: 1.25rem; min-block-size: 16rem; justify-content: center; border-radius: var(--radius-lg); border: 1px solid color-mix(in srgb, var(--rule) 60%, transparent); background: var(--color-surface-default); padding: clamp(2rem, 5vw, 4rem); }
|
||||
.stage-hint { font-family: var(--font-family-mono); font-size: var(--font-size-xs); color: var(--color-content-muted); }
|
||||
|
||||
/* the target — each channel's effect gated by its class + [data-pressed], all transitioning
|
||||
together from one event. --shape-smoothing transitions because it's a registered @property. */
|
||||
.target {
|
||||
font-family: var(--font-family-primary);
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 600;
|
||||
padding: 1.1rem 2.5rem;
|
||||
border: 0;
|
||||
border-radius: 22px;
|
||||
background: var(--color-primary-solid);
|
||||
color: var(--color-primary-on-solid, #fff);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
transform 150ms var(--ease-out, cubic-bezier(0.2, 0, 0, 1)),
|
||||
box-shadow 150ms var(--ease-out, ease),
|
||||
background-color 150ms ease,
|
||||
--shape-smoothing 150ms ease;
|
||||
will-change: transform, box-shadow;
|
||||
}
|
||||
.target.ch-depth { box-shadow: var(--depth-overlay-shadow), var(--depth-overlay-halo); }
|
||||
.target.ch-motion[data-pressed='true'] { transform: scale(0.93); }
|
||||
.target.ch-depth[data-pressed='true'] { box-shadow: var(--depth-raised-shadow); }
|
||||
.target.ch-shape[data-pressed='true'] { --shape-smoothing: 3.4; }
|
||||
.target.ch-color[data-pressed='true'] { background: var(--color-affirm-solid, var(--color-primary-solid)); }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.target { transition: background-color 150ms ease; }
|
||||
.target[data-pressed='true'] { transform: none; }
|
||||
}
|
||||
|
||||
.mixer { display: flex; flex-direction: column; gap: 0.5rem; }
|
||||
.track { display: flex; align-items: center; gap: 0.85rem; padding: 0.7rem 0.9rem; border-radius: var(--radius-md); border: 1px solid var(--rule); background: var(--color-surface-raised); cursor: pointer; text-align: start; transition: border-color 160ms, background 160ms; }
|
||||
.track[data-on='false'] { opacity: 0.5; }
|
||||
.track[data-on='true']:hover { border-color: color-mix(in srgb, var(--color-primary-solid) 40%, var(--rule)); }
|
||||
.track-led { inline-size: 0.7rem; block-size: 0.7rem; border-radius: 50%; flex: none; background: var(--color-surface-muted); border: 1px solid var(--rule); transition: background 160ms, box-shadow 160ms; }
|
||||
.track-led[data-on='true'] { background: var(--color-primary-solid); border-color: var(--color-primary-solid); box-shadow: 0 0 8px color-mix(in srgb, var(--color-primary-solid) 60%, transparent); }
|
||||
.track-meta { display: flex; flex-direction: column; gap: 0.1rem; flex: 1; min-inline-size: 0; }
|
||||
.track-label { font-family: var(--font-family-mono); font-size: var(--font-size-sm); color: var(--color-content-primary); }
|
||||
.track-note { font-size: var(--font-size-xs); color: var(--color-content-muted); }
|
||||
.track-tag { font-family: var(--font-family-mono); font-size: 0.62rem; text-transform: uppercase; letter-spacing: 0.06em; padding: 0.15rem 0.45rem; border-radius: 4px; }
|
||||
.track-tag[data-layer='eidos'] { background: color-mix(in oklab, #12a594 22%, transparent); color: #2dd4bf; }
|
||||
.track-tag[data-layer='sema'] { background: color-mix(in oklab, var(--color-primary-solid) 24%, transparent); color: var(--color-primary-text, #c4b5fd); }
|
||||
|
||||
.trace { margin-block-start: 2rem; display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; font-family: var(--font-family-mono); font-size: var(--font-size-xs); color: var(--color-content-secondary); padding-block-start: 1.25rem; border-block-start: 1px solid color-mix(in srgb, var(--rule) 60%, transparent); }
|
||||
.trace-k { color: var(--color-content-muted); text-transform: uppercase; letter-spacing: 0.06em; }
|
||||
.trace-sep { color: var(--color-content-faint, var(--color-content-muted)); }
|
||||
|
||||
/* Two moments */
|
||||
.moments { display: grid; grid-template-columns: 1fr; gap: 1.25rem; }
|
||||
@media (min-width: 44rem) { .moments { grid-template-columns: 1fr 1fr; } }
|
||||
.moment { padding: 1.5rem; border-radius: var(--radius-lg); border: 1px solid var(--rule); background: var(--color-surface-raised); display: flex; flex-direction: column; gap: 0.6rem; }
|
||||
.moment-event { background: var(--color-surface-overlay); box-shadow: var(--depth-overlay-shadow), var(--depth-overlay-halo); }
|
||||
.moment-h { font-family: var(--font-family-mono); font-size: var(--font-size-xs); text-transform: uppercase; letter-spacing: 0.06em; color: var(--color-content-secondary); }
|
||||
.moment-p { margin: 0; font-size: var(--font-size-sm); line-height: 1.55; color: var(--color-content-muted); }
|
||||
|
||||
.footnote { max-inline-size: 60rem; padding-block-start: 2rem; border-block-start: 1px solid var(--rule); }
|
||||
.footnote p { margin: 0; font-size: var(--font-size-sm); line-height: 1.6; color: var(--color-content-muted); text-wrap: pretty; }
|
||||
.footnote strong { color: var(--color-content-primary); font-weight: 600; }
|
||||
</style>
|
||||
Loading…
Reference in new issue