The B2 rollout is complete, so the ARCHETYPE_COHERENCE_AUDIT §13 "Superficie /
elevación" axis flips A1 → LIVE. Per the audit's update rule (row → LIVE +
guard), add `elevation-plane.test.ts`: every floating overlay's eidos Content
wrapper must stamp `data-depth` so the plane paints its elevation. 13 wrappers
pass (dropdown/context-menu + their sub-contents, menubar, navigation-menu,
popover, link-preview, command, select, combobox, dialog, drawer). tooltip +
listbox are excluded by design (variant-as-material / inline list).
Catches the menubar-class regression — a panel that reuses a shared content
selector but forgets data-depth silently loses its elevation when the donor
recipe is pruned.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per the elevation doctrine (variant = emphasis, elevation = plane), the
select / combobox popups now take their surface · border · shadow from the
`overlay` plane via `data-depth="overlay"` — constant across variants. The
variant (surface / outline / ghost) tints only the trigger / control, no
longer the popup elevation.
- Drop the content's own border / bg / shadow + the per-variant elevation
overrides (outline/ghost were `raised` / lighter / borderless).
- Point the arrows at `--depth-overlay-surface` / `--depth-overlay-border`.
- Prune select's 6 now-orphaned `content-*` elevation recipe tokens.
Verified in-browser (measured): combobox surface AND ghost both paint
bg oklch(0.285) + 1px border + overlay shadow from the plane (ghost was
transparent/borderless before — now flattened to the plane). select is the
identical change; its open popup hangs the preview renderer (no console
errors — preview infra), so it rides the same plane mechanism + check 0 +
eidos-lint 0 invalid + contract test (no new orphans).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two more single-elevation overlays join the plane:
- link-preview: hover-card Content carries `data-depth="overlay"`; drop the
hardcoded border / bg / shadow + point the arrow at `--depth-overlay-surface`.
- command: the palette panel carries `data-depth="overlay"`; drop its hardcoded
border / bg / shadow. Inside `<Command.Dialog>` the existing
`[data-dialog-content] [data-command]` override (higher specificity) strips
the plane so the dialog panel stays the sole surface — no double-elevation.
Verified in-browser: link-preview + inline command paint bg oklch(0.285) + 1px
border + shadow/halo from the plane; command-in-dialog is stripped
(transparent / border 0 / shadow none); check 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend the dropdown's overlay-plane adoption to the rest of the menu family:
- context-menu: Content / SubContent carry `data-depth="overlay"`; prune the
recipe's content-border / bg / shadow tokens + point the arrow at
`--depth-overlay-surface`.
- menubar: its per-menu panel reuses `[data-dropdown-menu-content]`, so the
dropdown's prune (f80f2225) had left it with NO elevation — restore it by
stamping `data-depth="overlay"` on the eidos Content wrapper. (regression fix)
- navigation-menu: mega-menu panel carries `data-depth="overlay"`; drop the
hardcoded border / bg / shadow from the recipe CSS.
Radius / font / color stay each panel's own. Verified in-browser: all three
paint bg oklch(0.285) + 1px border + shadow/halo from the plane; check 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Content / SubContent now carry `data-depth="overlay"` and take their
surface · border · shadow · halo from the overlay plane instead of the
recipe. Prune the now-redundant recipe tokens (content-border-width /
border-color / bg / shadow) — the plane is the single source, no
double-paint — and point the arrow fill at `--depth-overlay-surface` so
it always matches the panel. Radius / font / color stay the panel's own.
Verified in-browser: open menu paints bg oklch(0.285) + 1px border +
shadow/halo from the plane; orphan-token contract test passes; check 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The picker root was `flex-direction: column`, stacking label + start + end
into three rows. The `1fr auto 1fr` grid that would place start — end on one
row lives on `[data-time-range-field]`, which the picker never renders (its
field provider is context-only, ref=null) — so it never activated.
Mirror the date-range-picker reference: the root becomes a wrapping flex row,
the label claims its own full row (`flex: 1 0 100%`), and each endpoint
re-flexes to share one line (a non-auto basis overrides the `inline-size: 100%`
that `[data-time-field-input]` inherits from time-field.css). Root height
102 → 60px; no console errors; eidos-lint 0 invalid.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Non-modal menus:
- DropdownMenu + ContextMenu default to trapFocus=false + preventScroll=false
(were modal — hard focus-trap + body scroll-lock that made them feel stuck).
Menus stay navigable while open (Chakra-style); the props remain for an opt-in
modal menu. morfo focus.trap=false + the demo control defaults aligned.
list-surface — ONE shared size source for list surfaces:
- new lib/list-surface.css (--list-* keyed on [data-list-surface][data-size], 1:1
with the control sizes) + lib/list-surface-context.ts (shared size context for
portaled sub-panels). DropdownMenu/ContextMenu/Menubar/Listbox bridge their
--{c}-* size tokens to --list-*; ContextMenu gained the size axis it lacked.
Row-height doctrine — unified to CONTROL-HEIGHT:
- archetypes.css [data-archetype=item/option] gains a fallback-guarded control-
height FLOOR (min-block-size: var(--list-item-height, auto) + padding-block:
var(--list-item-py, var(--space-1-5))) — ZERO regression off a list-surface
(the ~45 non-list archetype consumers are untouched). Select/Combobox/Command
stamp data-list-surface so their option rows inherit the floor, matching menu
items. Verified: select option md 36px / lg 44px = menu items; bare archetype
item unchanged (0 floor, --space-1-5 padding).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- New MOTION_GUIDE.md — the task-oriented developer front door: the `motion` prop, the three
USE domains (event/state/content), the full preset catalog, recipes (animate in/out, loop,
state transitions, staggered list in&out, springs, custom presets), reduced-motion, theming
tokens, [data-debug-stagger], and constraints. Links the model (eidos-motion) + the engine
(arts/motion) + history (RFC).
- eidos-motion.md: note the 3-domain USE framing over the engine's 2 moments (the content
domain is a usage pattern over the state machinery); fix the `fallback` overclaim — it is
NOT auto-applied (the spring honours ctx.reduced itself, per the audit fix).
- docs/README.md (corpus map) + eidos/README.md: motion was undiscoverable — add the guide to
E4 Guides + the "I want to…" table + the eidos Motion section.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three fixes from the motion-ecosystem audit:
- `spring()` honours `ctx.reduced`: snap to the target and settle instead of running
the physics. The engine only short-circuits `reduce: 'instant'`, the driver ignored
`ctx.reduced`, and Presence never applied the declared CSS `fallback` — so spring-pop
played the full bounce under `prefers-reduced-motion`.
- `<Motion>` leave(): a loop preset is infinite — detect `animation-iteration-count:
infinite` and unmount promptly instead of holding the node for one loop iteration
(1–2s, still looping) on removal.
- Loop durations are now canonical `:root` tokens (`--motion-loop-{name}`), declared
like the `--duration-*` scale instead of living only as a `var()` fallback.
Tests: engine-motion (spring reduced) + motion (loop tokens); check 0 errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A `/demos/motion` page showcasing the content domain:
- Loops (spin/pulse/ping/bounce) on clean SVG via `motionAttrs`.
- Enter → loop → exit lifecycle on a pigeon SVG adapted from the `/demos` paloma
(the page has no ActiveEidos provider, so it loads the static eidos CSS and
replicates `<Motion>`'s exit inline).
- Staggered list: container-driven cascade IN (first item first) and OUT in reverse
(last leaves first), retained then unmounted — the §D.13.2 bridge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- `motion` prop: the Card is the state-domain pilot. Becoming `selected` plays the
preset once via `data-motion-state` (mount-guarded — animates on the transition,
not on mount/deselect). `card-emerge` yields to the motion system when `motion`
is set (otherwise toggling `data-motion-state` re-triggers the mount fade).
- Nested-interactive guard: clicking a control inside an interactive Card (a Footer
button, a link) must not select the Card. `handleActivate`/`handleKeydown` bail
when the event originates from an interactive descendant, and the `:active`
press-scale is suppressed via `:not(:has(button:active, …))` — the press cascade
reaches the ancestor `<button>`, which `stopPropagation` can't touch.
- Demo: `motion` chip control on the live Card.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Close the content domain of the universal `motion` prop (RFC §D.12/§D.13):
- State domain: a dedicated `data-motion-state` attribute lets a component with
its OWN data-state machine (Card selected/idle) drive a preset on its transition
without clobbering its semantic `data-state`. Enter-only (animate-on, snap-off).
New emphasis preset `select-pop` (no opacity-from-0, for still-visible elements).
- Loops (content domain, infinite): Tailwind's canonical `spin`/`pulse`/`ping`/
`bounce`, emitted as un-gated `[data-animation-style='X'] { animation: … infinite }`
rules. Themeable per loop via `--motion-loop-{name}`; reduced motion stops them.
- Staggered-exit bridge: the container-driven cascade now covers exit too —
`[data-stagger][data-state='closed'] > …` cascades children OUT in reverse
(`--motion-stagger-index-rev`, last leaves first). The container is retained for
the stagger window then unmounts (pragmatic CSS bridge for bounded lists; the
parent-waits-for-children version still needs the retired PresenceGroup).
- `[data-debug-stagger]`: opt-in dev affordance — a CSS counter badge per child
showing its `:nth-child - 1` index (the analog of UIX_DEBUG_MOTION).
- Fix `<Motion>` exit: a `{ duration }`-only Svelte transition never completes (the
node sticks at the exit frame, a re-show reuses the dead node); add a `tick` no-op
so it times the unmount.
Generated base.css regenerated; motion.test 27/27; docs (eidos-motion + RFC) updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a `motion` chip-picker (slide-fade · scale-fade · fade · spring-pop,
default spring-pop) on the live `Popover.Content`, plus the prop in the
generated code snippet. Demonstrates the spring engine tier woken in
4878ccfb running live on an overlay: selecting `spring-pop` stamps
`data-animation-style="spring-pop"`, which Presence resolves to the
registered JS `spring` driver and runs via `motion.run`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- focus ring: adopt canonical `--focus-ring-*` (outline, HCM-safe, primary-tint) — drops the per-color fork
- press archetype: new `STATIC_PRESS` primitive (`--press-scale: 0.985` / `--press-duration: 80ms`) emitted from foundation. Button switches from hardcoded `translateY(1px)` to canonical `scale(var(--press-scale))`. Card recipe repointed to the shared token — single press signature across the system
- intrinsic box: `inline-size`/`block-size: fit-content` defends the Button against flex/grid stretch (was filling 400×160 in a grid; now 94×36). `data-block` still opts into width-stretch, `data-icon-only` still square
- variant=emphasis only: `plain` no longer mutates layout (`min-block-size: auto` + `padding-inline: 0` removed) — same control box as the other 5 variants. Inline link-like actions belong to <Link>, not here
- spinner composition: Button drops its bespoke SVG ring + `@keyframes button-spinner-spin` + `--button-spinner-duration: 720ms` and composes the canonical <Spinner variant="ring"> via two new modes: `presentation` (no role=status/aria-live, marks aria-hidden — Button owns the a11y) + `color="inherit"` (currentColor + 25% track). One spinner across the system, unified duration (0.9s) and reduced-motion behaviour
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Until now every shipped motion preset was `driver: 'css'`: the `arts/motion`
engine's `spring` driver (semi-implicit-Euler physics, velocity handoff) was
built + tested but never CONSUMED. This ships the first JS preset, `spring-pop`
— a PHYSICAL pop (overshoot + settle, the curve no cubic-bezier expresses; low
damping on enter, stiffer exit). Run by `uix.motion` through soma's Presence
(`motion.run` + `pending()`), with a CSS `scale-fade` fallback for reduced-motion
/ SSR. Use on a Presence surface: `<Popover.Content motion="spring-pop">`.
Key architecture decision: JS presets are registered DIRECTLY with the engine in
`ActiveEidos`, NOT via `config.motion.presets`. The `EidosConfig` is serializable
(deep-cloned via `clonePlain`/`structuredClone`), and a `MotionRun` is a function —
`structuredClone` throws on it (DataCloneError). Rather than make the whole config
clone function-aware (which flattened special types and broke ~23 config tests),
JS presets — framework built-ins — live outside the cloneable config. The config
stays plain; `clonePlain` is untouched.
- `lib/motion/presets/js.ts`: `BUILTIN_JS_PRESETS` (`spring-pop`).
- `active-eidos`: registers `BUILTIN_JS_PRESETS` directly with `uix.motion`.
- `registry.ts`: `spring-pop` added (type-safe `motion` prop).
- `motion.test`: spring registration + driver test; F7 typegen extended to JS
presets; fixed a stale shared-axis regex (the container-driven enter branch from
`9d40ebe9` made the selector a comma-list — a pre-existing failure surfaced here).
motion.test 23/23; the config-dependent suites are back to their pre-existing
(unrelated, other-track) baseline — this adds zero failures. `check` clean for
the touched files.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The tooltip's `delayed-open` entrance (the per-side slide, and now the bounce) NEVER
fired: the content only mounts when `open` becomes true, and the hover-delay timer
set `internalTooltipState = 'instant-open'` AT THAT MOMENT — so the content always
mounted as `instant-open` (no entrance). `delayed-open` was only ever the pending
state while the content was still unmounted. A long-standing dead animation.
Fix: the hover-delay timer now keeps `'delayed-open'` as the content mounts, so the
eidos `[data-state='delayed-open']` entrance plays. `'instant-open'` stays reserved
for focus / skip-delay (intentionally no entrance). Test updated (it asserted the
buggy `instant-open`); tooltip provider suite 4/4.
Also makes the bounce clearly perceptible (it was too subtle to notice even when
forced): scale 0.5 → 1.18 (overshoot) → 0.9 (undershoot) → 1.06 (second bounce) → 1,
over `--duration-slow`. Verified end-to-end in-browser: hover → content mounts
`delayed-open` → `animation-name: eidos-tooltip-bounce-in`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the tooltip's per-side 2px slide-in with a scale BOUNCE: grow from 0.7,
overshoot to 1.06, dip to 0.97 (the undershoot — the "rebote"), settle to 1, over
`--duration-moderate`. It scales from `transform-origin` (the floating origin,
toward the anchor), so it keeps a directional feel without per-side keyframes —
8 keyframes collapse to 2.
Bespoke recipe keyframe, NOT a registered `motion` preset: the tooltip's state is
3-valued (`delayed-open` / `instant-open` / `closed`), not the generic `open` /
`closed` the shared presets key on — so a registered preset wouldn't fire here.
This also confirms the architecture boundary: components whose state shape doesn't
match open/closed use a bespoke recipe animation, not the `motion` selector.
`instant-open` (keyboard focus) keeps no entrance; the reduced-motion block (kills
the animation) already covers the new rules — verified the bounce curve in-browser
(scale 0.7 → 1.06 → 0.97 → 1). Recipe-only change, no generator/base.css touched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Marks (b) step 2 done (Badge pilot `0a6d112c` + `<Motion>` wrapper `54268717`)
and records the two findings from building them:
- The helper only works on components WITHOUT their own `data-state`. `motionAttrs`
/ `<Motion>` force `data-state="open"`, which would clobber a stateful
component's own machine (Card selected/idle, checkbox checked). The state domain
is a separate path (its `motion` would tie to its own transitions, but the
presets key on open/closed specifically).
- The `data-motion` collision. That attr belongs to reduced-motion: arts/prefs
projects the `motion` pref (system|allow|reduce) onto `<html data-motion>`, and
render-css emits `[data-motion='reduce'] …` + the prefers-reduced-motion media
query. So `<Motion>` emits NO part marker — only the functional attrs. Confirms
the content domain is reduced-motion-safe for free (the overrides key on
data-state, which motionAttrs sets).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The ergonomic companion to `motionAttrs`: wrap any content to animate it IN and
OUT. `<Motion motion="scale-fade">…</Motion>` plays the preset's enter on mount
and its exit on removal (Svelte `out:` flips to `data-state='closed'` + retains
the node for the eidos-declared duration, like `<Cascade.Item>`). Rides the
existing state-presets — no new mechanism, no JS engine.
- morfo `motion.ts` (scope ['eidos']): one Provider part declaring `data-state`.
- eidos `motion/`: component (spreads `motionAttrs` + `out:` retention) + types +
index + README (incl. the `<Motion>` vs `motionAttrs` table + the content-domain
boundary).
- badge demo: a `<Motion>` show/hide example next to the prop showcase.
Caught + fixed a real collision: `data-motion` is already the reduced-motion prefs
attr (`[data-motion='reduce']`), so the wrapper emits NO part-presence marker (a
prefs `closest('[data-motion]')` would have matched it) — only the functional
`data-animation-style` + `data-state` from `motionAttrs`.
Verified at runtime: enter (data-state=open → `scale-in, fade-in`, no data-motion)
and exit (Hide → retained with data-state=closed → `scale-out, fade-out`). `check`
clean for the morfo + component + demo.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First real consumer of `motionAttrs`: `<Badge>` gains a `motion` prop so a
designer can apply any registered preset as the badge's entrance — `<Badge
motion="scale-fade">` plays the preset's enter on mount, zero per-component
machinery (the component just spreads `{...motionAttrs(motion)}`).
Badge is the clean content-domain pilot: pure eidos primitive, no morfo events,
no own `data-state` (so the helper's presentation `data-state="open"` doesn't
clash — unlike Card, which has selected/idle + a built-in emerge). Additive +
non-breaking: `motion` defaults to undefined (no animation).
- badge/types.ts: `motion?: MotionPresetName`.
- badge/badge.svelte: spread `motionAttrs(motion)` on the span.
- badge demo: a "Motion" showcase (fade / scale-fade / slide-fade + Replay) —
the prop demonstrated per the testbed doctrine.
Verified at runtime: the three motion badges render `data-animation-style` +
`data-state="open"` with the right enter `animation-name` (fade-in / scale-in,
fade-in / slide-from-bottom, scale-in, fade-in). `check` clean for badge. (A
fresh badge screenshot was blocked by preview-infra flakiness this round; the
enter keyframes themselves were confirmed visually earlier via the cascade.)
Establishes the pattern: any component becomes animation-selectable by accepting
`motion` + spreading the helper. RFC §D.12 (b).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The universal selector for the CONTENT domain (RFC §D.12). `motionAttrs(preset)`
returns the attrs that make ANY element play a registered preset's enter on mount:
<div {...motionAttrs('scale-fade')}>…</div> // fades + scales in on mount
Resolves the open "how does content fire" decision — and reverses the §D.12 plan's
guess. Instead of a new `data-motion-play` trigger (which would need a generator
change + a base.css regen, currently entangled with an unrelated `press`/chronos
WIP), it reuses a constant presentation `data-state="open"` — the same mechanism
`<Cascade.Item>` already uses for content. The event/state/content line is about
who OWNS the animation (firma / state-machine / the prop directly), not about
whether the CSS trigger is `data-state`: content is "always shown" with no machine
to flip it. Reuses the enter/exit + reduced-motion rules verbatim; `'none'`/
undefined opts out; flash-free when spread in markup (`backwards` fill).
Verified in-browser: an element with the produced attrs gets `animation-name:
scale-in, fade-in`, enter progresses 0%(op0,scale .985)→50%(.84,.998)→100%(1,1);
`'none'` returns no attrs. `check` clean for the new file + export.
Step 2 (pending): a `<Motion>` wrapper (out: retention → exit) + wiring the
`motion` prop into real components.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`<Cascade>` was the last component using `animation` as its preset selector — a
vestige of the coordinated `cascade-*` system retired in Plan A. Every other
animatable component (Dialog, Popover, Drawer, …) already uses `motion`. Renames
`<Cascade>`'s prop + its context field `animation` → `motion`, so the framework
has ONE universal selector name (RFC §D.12 step a). `MotionPresetName` and the
low-level `data-animation-style` attr are unchanged.
- cascade: types `animation?` → `motion?`; context `CascadeContext.animation` →
`.motion`; component destructure + getter; item reads `ctx?.motion`.
- docs/demo: README props table + usage, index usage, panel-cascade demo.
Verified at runtime: `<Cascade motion="scale-fade">` produces identical wiring —
items carry `data-animation-style="scale-fade"`, `animation-name: scale-in,
fade-in`, structural index 0/1/2/3. `check` clean for the touched files; no
dangling `animation`-prop reference remains (only the `data-animation-style` attr).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Captures the design framing agreed this session, BEFORE implementation:
§D.12 — One universal `motion` prop on any component; the discriminant ("does the
animation realize a perceptual event?") decides the domain: event (firma, morfo+sema,
the prop is override/violation) / state (data-state preset) / content (prop is
primary). Supersedes §5's M5/M6 "motion vs animation separate" — its justification
(the coordinated `cascade-*` system) was retired in Plan A. Honest positioning:
content/state = parity with Framer/SwiftUI; event = above (motion coupled with
sound/haptic from one evaluated signal). Caveat: the JS engine (spring/handoff) is
built but dormant — the "above" on the engine axis needs a first spring preset.
§D.13 — The contracts the CSS model demands + failure modes, separating what the
mechanics already guard from what's deferred:
- structure: `:nth-child` ignores comments/`{#if}` (guarded); intermediate ELEMENT
wrapper breaks the count (the contract); a cascading wrapper becomes its own scope
(`inherits:false` isolates the index); `display:contents` = known dead-zone.
- exit: animated surface exits via Presence/`out:`, NEVER raw `{#if}`; unit-exit works
today (parent opacity carries the children); per-child staggered exit needs JS
lifecycle (PresenceGroup, deferred) → the container-driven rule is enter-only.
- debug: the index is `inherits:false` (no ancestor can step on it); a
`[data-debug-stagger]` mode to surface index/delay/name is pending.
Pointer added in eidos-motion.md. No code changed — design record only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes the menu-family event-driven cascade: a SubContent fly-out now cascades
its items in on open, same as the root panel. The SubContent has its own
`data-state`, so the existing container-driven preset rule applies unchanged — it
only needed the `[data-stagger]` marker on the panel + `--motion-stagger-each` on
the sub-content recipe selector. Its items are the same `DropdownMenu.Item`
(already `data-animation-style="fade"`). Menubar inherits via the shared
DropdownMenu.SubContent.
Verified at runtime: SubContent panel carries `data-stagger` + `data-state=open`,
`--motion-stagger-each: 20ms`, items `name:fade-in` with consecutive `:nth-child`
index 0/1/2 (a flat submenu cascades cleanly). `check` clean for the touched
components.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extends the just-landed DropdownMenu event-driven item cascade to its siblings
so the framework is coherent — every menu in the family fades its items in
staggered on open, same mechanism, zero new generator code (rides the
container-driven preset branch shipped with DropdownMenu).
- ContextMenu (own recipe): Content marks the panel `[data-stagger]` +
`--motion-stagger-each: var(--motion-stagger)`; item / checkbox-item /
radio-item carry `data-animation-style="fade"`.
- Menubar (ONE line): soma `Menubar.Content` delegates to `DropdownMenuContent`,
so its panel already carries `data-dropdown-menu-content` (→ the dropdown
recipe rhythm applies) and re-uses `DropdownMenu.Item` (→ already
`data-animation-style="fade"`). It only adds the `[data-stagger]` marker.
Verified at runtime: ContextMenu (scrubbed opacity gradient 0.96/0.84/0.00),
Menubar (panel carries `data-stagger` + `data-dropdown-menu-content`, items
`name:fade-in`, structural index from `:nth-child` 0/1/2/6/7). `check` clean for
the touched components. (Preview tab runs backgrounded → rAF paused; verified by
scrubbing `animation.currentTime` via WAAPI rather than waiting for playback.)
Deliberately NOT rolled out: select / combobox / command — live-filtered or
long-scroll lists where an open cascade is questionable UX.
Docs: ContextMenu README (Motion parity note), Menubar index doc, DropdownMenu
README (family cross-reference), RFC §D.11.4 (rollout).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Productizes the event-driven cascade on a real component (not the explicit
`<Cascade>` orchestrator). When a DropdownMenu opens, its items fade in
staggered by structural order — the appearance flows from the `open` (emerge)
event, nothing in the app wires it.
Architecture: a +1 GENERIC selector branch in the motion generator, NOT a
per-item `data-state`. A `menuitem` isn't open/closed — the menu is — and eidos
writing a state attr is a smell. The new container-driven form
`[data-stagger][data-state='open'] > [data-animation-style='X']` fires each
child's ENTER off the container's `data-state` (which soma already writes via
the morfo `commits`); items carry only `data-animation-style`. ENTER only: the
coordinated EXIT cascade (retain the container until children finish) is the
deferred soma PresenceGroup work, so a closing container can't drive a child
exit that would be cut off on unmount.
- render-css.ts: `renderCssPresetRules` adds the container-driven enter branch
(same declarations, shared comma selector) — reuses the existing presets +
the foundation `:nth-child` stagger index; zero new system.
- DropdownMenu: Content marks itself `[data-stagger]` +
`--motion-stagger-each: var(--motion-stagger)` (20ms, themeable, no magic
number); item/checkbox-item/radio-item carry `data-animation-style="fade"`
(overridable). `fade` so item opacity doesn't fight the panel's own scale.
- regen generated/base.css (8 css presets gain the container-driven enter).
Verified in browser (frozen frame): index from `:nth-child`, `delay = idx ×
each`, opacity gradient in flight (Ruler 0.98 → Grid 0.89 → Guides 0.69, Log
out 0.00). Honest limit (documented): only DIRECT-child items cascade — items in
a <Group>/<RadioGroup> DOM wrapper aren't `:nth-child` of the panel, so they
appear instantly (the "transparent intermediate").
Docs: dropdown README §Motion, cascade README §2 (concrete consumer), RFC §D.11.4.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- dialog + drawer adopt data-depth="modal" (surface·border·base-shadow from the plane);
recipes pruned of orphan content-bg/border/shadow. The bundle's on-surface font fixes
the Times New Roman of portaled content (Decisión 8).
- Dialog.Close inherits the dialog size via context.ts, capped at md (sm→sm, md/lg/xl/full→md;
full is layout, not a bigger control — Radix/Mantine/MD3). THEMING §5 container→part norm.
- Dialog title → h3 typography role (--dialog-title-* points at --style-h3-*, override knob kept).
- Dialog trigger + close compose the framework <Button> via the asChild `child` snippet
(the trigger was a passthrough that rendered soma's native <button>); variant/color/size
defer to the canonical Button defaults (solid/primary) — no hardcoded ghost/neutral.
- demo layout (web/routes/uix/+layout@.svelte): "Display" control — density·scaling·border-width
applied to :root (reaches portaled overlays) to observe the interrelated theming axes live.
- update the depth-plane test (z is token-only; on-surface font) + the archetype audit hand-off.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The cascade is no longer a separate system — it's the existing state-presets + the
Material stagger + one foundation rule. Retires the --cascade-* reinvention.
- render-css: foundation structural-index writer — `[data-stagger] > *:nth-child` →
`--motion-stagger-index` (forward) / `:nth-last-child` → `--motion-stagger-index-rev`
(reverse). Nobody writes the index; soma never writes a visual var. Every preset's
enter/exit rule consumes it (parallel = `--motion-stagger-each` 0, cascade = N).
- Cascade rewritten as a thin EXPLICIT orchestrator: marks `[data-stagger]`, reflects
open→data-state, passes the preset to items via context; the `out:` retention flips
to data-state=closed (reusing the exit preset), reading the duration via dom.getWindow.
Drops the whole `--cascade-*` namespace + bespoke keyframes.
Docs: MOTION_SERVICE_RFC §D.11 — the final model (three orthogonal axes sema/motion/
eidos, the full lifecycle, the realization ladder snap->transition->animation->JS, the
firma-vs-realization seam, the cascade = existing system + one rule, the motion service
as the engine). eidos-motion aligned.
Verified: browser (enter scale-in/fade-in + nth-child index, exit reverse, zero inline
writes), npm run check 0 mine, motion.test 22/22, eidos-lint 0 invalid.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remove the parallel motion-coordination service per the redesign: animation is a
channel of the EVENT's firma (sema owns it across all channels), not a parallel axis.
Code is gone; the RFC stays as historical record with a retirement banner.
- morfo: drop MorfoPart.animation + its schema/compile/types/exports
- arts/motion: drop CoordinatedPreset / MotionConfig.coordinated
- eidos: drop BUILTIN_COORDINATED_PRESETS / renderCoordinatedPresetRules / the
animation:none neutralization + registry block; regen generated/base.css
- soma: Presence reduced to a single-surface island; delete presence-group /
dom-cascade / coordination; dropdown-menu loses the cascade wiring
- delete Reveal / Rail (morfo + soma + demos)
Docs: MOTION_SERVICE_RFC gains the retirement banner + §D.8 (retirada) + §D.9
(verified model: morfo->soma->sema->eidos pipeline + the two hard rules — soma
never writes a visual --var; data-event-* is a single-target stamp, not a bus) +
§D.10 (Fase 1 prototype). Fix stale "5 canales" claim in GUIA §11; eidos-motion +
dropdown README aligned.
Fase 1 prototype (web/routes/temas/animations/panel-cascade): validates the model
end to end — panel->cards cascade (enter/exit), dynamic removal with Svelte out:
retention, nested cascade — all via :nth-child + custom-property inheritance, with
zero JS visual writes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Coherence audit (ARCHETYPE_COHERENCE_AUDIT_2026-06-19): 8 archetypes + 8 closed
decisions, the §13 construction contract, per-component deviation matrix (88/117
deviate). Master guides (soma/COMPONENT_GUIDE, eidos/components/README) point to it.
A1 — elevation bundle via `data-depth`: the plane paints surface·border·shadow·halo
(added `border` to the depth bundle). z-index is NOT painted — stacking is a
positioning concern owned by the floating-wrapper. Popover migrated to the overlay
plane (panel + arrow consume it); recipe orphans pruned.
Popover fixes: arrow un-clipped via an inner [data-popover-viewport] (Radix-style;
the absolute arrow escapes the scroll clip), sized via density tokens (not soma's
hardcoded 10×5), border-seam overlap keyed on data-side, hover-bounce guard in
scheduleHoverOpen (ignore hover-open when already open). Dialog.Close composes
IconButton/Button (neutral) — closes the Lote ② debt.
Option lists: density-driven padding/radius via --space-* + concentric radius,
ScrollArea adoption, emerge open/close events (combobox/select/command), and the
canonical option-highlight in archetypes.css.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- <ColorField.Input swatch> renders an opt-in leading colour chip showing
the field's current value, sized to the control icon (--icon-size-{size})
via a concrete token — never 1em, so it cannot recompute/jitter.
- field-segment-state: drop the active-segment font-weight 600; bold widened
the ch-based segment ~0.5px, resizing the highlight on every increment. The
accent background already marks the active segment.
- color-field stays full-width like the whole field/select family — width is
container-driven, not field-driven (no per-component exception).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Consolidate the per-field / per-picker duplication the audit surfaced:
- Shared control trigger — new field/field-control-trigger.css selected by a
`data-field-control-trigger` marker: flush (no border, no shadow), UNIFORM
neutral hover tint (not the field accent), font-relative size. Migrated
date-picker + the password visibility toggle; deletes their per-picker chrome.
(date-range / time / time-range pickers still to migrate.)
- Shared segment state layer — new field/field-segment-state.css selected by a
`data-field-segment` marker (date / time / color fields): one hover (editable)
+ active (focus) rule. Fixes a specificity bug where the hover selector
out-ranked :focus, so hovering the active segment downgraded its highlight.
- Segment fixed width — min-inline-size: calc(2ch + 2*segment-px) +
font-variant-numeric: tabular-nums so a segment never reflows when its value
changes 1<->2 digits or between glyphs (no horizontal jitter). date-field done.
- date-range-picker trigger uses the same calendar icon as date-picker (was a
distinct calendar-range glyph).
- Generated element ids use the framework-neutral `uix-` prefix instead of the
layer name `soma-` (createId / useId + announce / menubar inline ids) — the
doctrine that bans data-soma-* / --soma-* keeps a layer name out of ids too.
- field.css: label + helper text scale one step below the control (font-relative
calc); optional-indicator weight to regular.
Foundation tokens added: --field-segment-hover-bg, --field-control-trigger-hover-bg.
Verified in-browser by measurement (date-field segments: states + no-jitter;
date-picker trigger: flush + uniform hover). check = 58 baseline (0 new);
recipe-css-contract clean (only pre-existing palabras WIP).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Supersede the "compact control md=14" archetypes (Fase 7) with a single
rule: control text scales 1:1 with its size (md=16) across all components,
so changing --font-size-md re-themes the whole system. Applied to button,
badge, breadcrumb, calendar, pagination, radio-group, toolbar, file-upload,
tag-group, stepper, toggle, tooltip (the field family was already 1:1).
Button + search-field icons now follow the font 1:1 (icon ~= font+2;
density-independent, like the type scale). Fix button icon vertical
centering: the [data-button-icon] slot didn't flex-center, so the inline
<svg> sat on the text baseline ~1px below the button center.
Exceptions left as-is (not control text): avatar (initials sized to the
circle diameter), accordion (section-heading prose scale), password-field
icon-size (it is the visibility-trigger button box, control-coupled by
design), radio-cards (icon follows the card title).
THEMING.md section 5 rewritten: 1:1 universal mapping + "density is
orthogonal to typography" (density scales space + control-height only;
font + icon ride only the global --scaling zoom).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Field font scale: input 1:1 (size N -> --font-size-N, md now 16px), label one
step below (10/12/14/16/18). Recipes: field, spin-field, date/time/color-field,
search-field, password-field, select, editable, tags-input. Segmented-field
labels via CSS calc; generic Field label via label-font-size tokens.
- Picker triggers (date/time/range): box now scales off the local
--_X-field-height + font:inherit so the 1em icon follows the field font; added
the missing xs size variant to time-picker + time-range-picker.
- color-field/time-field commit-set fires + sounds on increment (compare by
ColorValue.hex; time-field passes fallbackTarget for the double-registered
'input' part).
- color-field format-select: reverted the segment-active tint back to its focus ring.
- Docs: THEMING.md field font rule; COMPONENT_COHERENCE_AUDIT.md.
words / palabras / chronos untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Prototipo descartable (web/routes/temas/animations/menu-native) que valida la
Decisión A del re-diseño del servicio de motion: el lifecycle de presencia se
apoya en Svelte nativo ({#if} + transition:|global) en lugar de la maquinaria
custom de F2 (DomCascade/pending/beginEnter).
Validado en navegador real:
- entrada escalonada sin ease-vs-jump; salida con stagger invertido
- exit-heavy nativo (panel retenido hasta el ultimo item, sin pending())
- adicion dinamica -> entrada individual (el contenedor arbitra opening/closing)
- roll-up del panel como firma de cierre opcional (when: exit:'after')
RFC Apendice D.7 documenta resultados + hallazgos (grid-template-rows no anima
por WAAPI; usar height/interpolate-size).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Documents the second coordination mode (DomCascade) wired in M9 F1/F1c/F2: the design, the contract (staggerChildren), pending() exit-heavy, the dropdown-menu cascade, and the four coexistence fixes (transition collision, disabled opacity, dismiss-fade panel signature, trigger/dismissal toggle). Updates the status note, TOC and §15 roadmap; adds the children-DOM cross-link to eidos-motion.md and an opt-in 'animation' Motion section to the soma README.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
F1c (exit-heavy): DomCascade.pending() aggregates the items' finished so the owner Presence holds the subtree until the exit cascade settles (PresenceOptions.pending, Presence.startPhase). beginEnter drives the enter from the off-state with the transition suppressed (items mount in the on-state, so a passive mirror would ease toward off instead of jumping). pending() settles each finisher (then(noop,noop)) so one cancelled row can't collapse the wait.
F2: wire the children-DOM cascade into the real dropdown-menu. Content part declares animation.surface+staggerChildren; provider routes an opt-in 'animation' prop, runs a DomCascade over getCascadeRows (every visible row incl. disabled), forwards a pending hook through the floating shell. Coexistence fixes surfaced by the menu: item rows hand their transition to the cascade-* preset via a higher-specificity rule (longhands keep the stagger delay); disabled opacity gated off during the cascade; the panel's own dismiss-fade signature suppressed in cascade mode (data-cascade) so it doesn't fade the panel before the rows finish; the trigger is excluded from the menu's dismissal so the toggle closes (pre-existing bug). Verified in a real browser (Playwright): enter+exit cascade with reversed stagger, all four close methods, disabled row fades. dom-cascade 9/9, check 0 new errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
El piloto M9 (dropdown-menu) destapó que el PresenceGroup coordina children
que son `Presence` REGISTRADOS (Reveal/Rail), pero un menú real es owner-
`Presence` (el content monta como unidad) + children-DOM (items por selector,
sin `Presence`). 3 grietas en 3 capas: registro, index per-item, y escribir
una CSS-var per-element (que ActiveDom no cubría).
Modo children-DOM (la 2ª coordinación): el owner PROPAGA su lifecycle a los
items DOM — escribe data-animation-style + data-starting/ending-style (espejo
del owner) + --motion-stagger-index/-count (por orden DOM), reutilizando el
preset coordinado de eidos SIN cambios (los items quedan con los mismos attrs
que un child-`Presence` produciría).
F1 (núcleo):
- morfo: `MorfoPart.animation.staggerChildren?: string` (types + schema +
invariante: requiere surface, excluye children).
- adom: `ActiveDom.writeProperty/removeProperty` per-element (la pieza que
faltaba; + stub disabled).
- soma: `DomCascade` (`dom-cascade.svelte.ts` — `sync`/`clear` puros y
testeables, `watch()` corre el $effect espejo) + barrel export.
F1b (validación aislada): demo `/temas/animations/dom-cascade` (owner Presence
+ items DOM + DomCascade.watch). Cazó un bug que el test no veía: DomCascade
usa $effect → debe ser `.svelte.ts`, no `.ts` (el test solo ejercita sync/clear,
nunca watch()). Cascada de ENTRADA verificada en navegador.
PENDIENTE (en la memoria del proyecto): F1c exit-heavy (el owner espera a los
items — getAnimations no ve subtree), F2 el dropdown-menu real (focus-trap/
dismissal/submenús), y volcar §M9 al RFC.
Tests: dom-cascade 5/5, morfo 70/70. svelte-check: 0 errores en mis archivos.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Un componente coordinado puede componer la cascada (motion) con sonido/háptico
(sema) en el mismo evento. El cabo: el meta-canal visual de sema estampa
data-event-* siempre que el evento tenga algún canal (channels != []), así que
channels:['sound'] arrastraba la firma visual genérica (present-rise), que pelea
con la cascada.
Solución EN MOTION (sema intacto): eidos neutraliza su PROPIA firma visual sobre
las superficies coordinadas. render-css.ts > renderCoordinatedPresetRules emite
`[data-animation-style='cascade-X'][data-event-phase='active'] { animation: none
!important }`. La firma usa `animation` -> muere; la cascada es `transition` ->
sobrevive. Aditivo: inerte hasta que un coordinado dispare un evento sema.
Ejemplo: Reveal pasa a modo (c) — open y close declaran channels:['sound']
(emerge suena, pitch 600) + expression:'family-default'; suena al abrir y al
cerrar a la vez que la cascada, sin pelea visual. Rail queda como modo (b) puro.
Layout de /temas/animations con events:{ sound: true }; la demo /reveal explica
el modo (c).
Honestidad: la versión inicial del RFC Apéndice B afirmaba que el modo (c)
componía "gratis" — falso (verificado en engine.ts/visual.ts). Corregido: el
Apéndice B documenta ahora el acoplamiento real y el opt-out en motion.
Tests: eidos/motion 25/25 (neutralización) · morfo 70/70 · morfo:vocabulary
limpio. Audio verificado en navegador (suena al abrir y cerrar).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Continuación del servicio de motion-coordination (sobre M1–M6 ya commiteado).
- M5 — naming `animation` / `motion` SEPARADAS por rol (no unificar: tras M6
son sistemas distintos — `motion`=momento `--state`/`data-state`/eidos;
`animation`=coordinado/`data-starting-ending-style`/soma). `animation` gana
type-safety vía el registry augmentable `MotionCoordinatedPresets` +
`CoordinatedPresetName` en `$motion` (la capa compartida, para que soma lo
tipe sin importar eidos); eidos lo puebla con `cascade-slide/-fade/-scale`
por declaration-merging — espejo de `EidosMotionPresets`.
- Reversa fluida — JS handoff (§8.3). El motor expone `MotionHandle.peek()`
(posición + velocidad por propiedad) y la reinyecta en la fase inversa vía
`MotionContext.handoff`: el `spring` continúa desde la posición/velocidad
actual en vez de reiniciar desde el `from`. Orquestado enteramente en el
engine (`takeHandoff` — captura en `cancel`, consume en `run`, y cancela un
run en vuelo cuando el caller no pre-cancela): cubre el camino island
(overlay-spring) y elimina un apilamiento preexistente. La reversa de los
coordinados (transiciones CSS) ya era fluida.
- Demo `/temas/animations/compuesto` — un `Rail` de `<Button>`s que usa los
presets coordinados PREDEFINIDOS de eidos (cero CSS de animación en la
página); el selector cambia `animation` en vivo y cada Button compone su
press (firma sema) con la cascada (motion coordinado).
- Docs — RFC Apéndice B (convivencia de los tres sistemas visuales: firma
`--event` / state-preset `--state` / coordinado, con diagrama + ejemplo
Button-con-tokens), §5 reescrito (separadas por rol), §8.3/§15 al día, y
cross-link del tercer eje desde eidos-motion.md.
Tests: arts/motion 9/9 (handoff) + eidos/motion 24/24 (M5 paridad).
svelte-check: 0 errores en mis archivos.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Servicio de motion cross-layer: morfo declara superficies animables +
coordinación; soma coordina presencia/lifecycle; eidos posee lo visual;
arts/motion ejecuta por-nodo. RFC en eidos/MOTION_SERVICE_RFC.md.
- M1 — contrato MorfoPart.animation (types/schema/compile); children
como { enter?, exit? }.
- M2 — PresenceGroup (soma/layers/presence-group.ts), rune-free;
Presence.group descubre el coordinador por context.
- M3 — exit con retención de DOM (§8.1).
- M4 — interrupción/reversa (§8.3): token de generación + motion.cancel
en flip + toHandle resuelve finished en cancel (sin AbortError suelto).
- M5 — prop `animation` enrutada a las parts surface:true del morfo
compilado (routeAnimation); Panel/Item emiten data-animation-style.
- M6 — stagger auto-derivado del orden de registro (--motion-stagger-*,
inversa en exit) + presets coordinados en la librería de eidos
(MotionConfig.coordinated; cascade-slide/-fade/-scale) que reaccionan
a data-starting/ending-style, NO a data-state.
Helper Coordination (soma/layers/coordination.ts) extraído y validado
por DOS consumidores reales: Reveal (raíz virtual + Panel owner) y Rail
(raíz=owner, together). Demos en /temas/animations/{reveal,rail,
presence-group}.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New component-authoring rule, per request: when building a component (or its
demo, or any UI), compose the framework's existing soma/eidos components rather
than re-implementing primitives inline. If a needed building block does not exist
as a framework component, flag the gap so it gets built as a proper reusable
component (its own morfo + soma + eidos) and then composed — never an ad-hoc
reinvention that drifts from the system. The picker family (A27) is the canonical
example of composing existing components.
Full rule in COMPONENT_GUIDE "Before You Start" §4; one-line pointer added to
CLAUDE.md Key Conventions so every session loads it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
From the arts/ audit (docs/process/arts-audit-2026-06.md):
- connection: drop 'export * from helpers' (leaked internal builders) and name-export the public error surface; remove the redundant EngineConnections.close() alias (consolidated to closeConnection, consistent with openConnection/reconnectConnection/closeAll) and retarget its destructure-safety test.
- http: extract the inline ATTEMPT_COMPLETED diagnostic message into a named builder (attemptCompletedLogMessage); output preserved.
- color: drop stale 'Phase 0 / not consumed yet' comment (uix.color is consumed by Eidos).
Verified: vitest connection/http/color green, type-check clean, prettier clean. The QrCode morfo fix (literal('path')) is left in src/uix/morfo/schema.ts because that file also carries unrelated in-progress animation-morfo work — not swept in here.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There is no sium-specific Svelte adapter by design: forms (createForm / Form.Provider) consume any schema via ~standard, Form.AutoFields renders via ~sium introspection, and langs/logger inject through createEngineSium(). sium is page-scoped and not an ActiveUix service. Fixes README (Estado, dep rules, Integracion Svelte, troubleshooting) + core/types.ts comments.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The icon<->typography track is resolved (option 1: the icon follows the font the
component actually uses; cards follow the title, never inflated). The decisions
now live correctly in THEMING §5 (just corrected) + the recipes, so the hand-off
is no longer needed — most of it was the superseded "1:1 total" plan.
Its two live residual TODOs move to pendiente.md (the eidos pending tracker):
audit the Icon <svg> CSS-override bug in other components, and decide whether to
delete the /uix/icon-scale-study test bench. Recorded in the corpus hand-off.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
THEMING §5 documented "prominent icons (cards): the icon does NOT follow its
title, it bumps to xl/xxl" — the pre-continuation approach. The final decision
and the shipped code reject it: the radio-cards recipe sets icon-size to
sm/sm/md/md/lg (16/16/18/18/20, following the title) with an explicit comment
("the icon follows the title; a bigger card icon comes from a bigger title,
never from inflating the icon"). The doc had drifted from the code.
Corrected §5 to state the implemented rule: cards/titles icons follow the
title's font; prominence comes from a bigger title, never an inflated icon.
Found while verifying that CONTINUE-icon-type-scale.md's decisions actually
match THEMING before retiring it — they did not, on this point.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>