- 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>
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>
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>
- The bullet linked THEMING_AUDIT_2026-05-27.md, which does not exist (the
2026-06-01 audit itself flags the link as broken). Repointed to the audit that
does exist, THEMING_AUDIT_2026-06-01.md.
- eidos-motion.md was described as "propuesta motion (deferred, partially
superseded by TSC)" — the same stale contradiction fixed in §14. It is the
implemented two-moment motion system (F1–F7). Corrected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes the renumber-safe split of THEMING.md.
- §8 "añadir componente" + §9 "definir theme" -> src/uix/eidos/THEMING_GUIDE.md
(E4 authoring guides).
- §15 "comparación con librerías" + §17 "FAQ — decisiones polémicas" ->
src/uix/eidos/THEMING_NOTES.md (E3 comparison + rationale).
THEMING.md keeps numbered pointer-stubs at §8/§9/§15/§17; §16 anti-patterns,
the ## Referencias section, and the dated §20-34 changelog stay in place. All
34 sections and every "THEMING §N" citation across the corpus + code survive.
THEMING 2571 -> 1930 lines across the three commits; it is now a leaner E1
layer reference with TSC (E2), the guides (E4) and the notes (E3) as siblings.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First piece of the THEMING split, using the renumber-safe stub approach so the
many "THEMING §N" citations across the corpus + code keep resolving.
§7 (Token Scope Contract) and §18 (universal coverage, v2.2) move to a new
src/uix/eidos/TSC.md — the eidos visual canon (E2), with CANON.md-style
frontmatter. THEMING.md keeps numbered pointer-stubs at §7/§18, so section
numbers (and therefore §23/§25/§26/§27/§28 citations) are untouched. 2550 -> 2271
lines; 34 headers intact, TOC anchors still resolve.
Remaining split pieces (own commits): guides §8/§9 -> E4, comparison §15 +
FAQ §17 -> E3.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
THEMING §14 described motion as "deferred / data-motion-ref does not exist /
superseded by the TSC event:* scope". eidos-motion.md says the opposite: the
two-moment model is implemented (F1-F7, 2026-06-04), the engine was relocated to
arts/motion as a service, and it is the event:* scope that is now obsolete. §14
was a stale snapshot contradicting the canonical motion doc.
Rewrite §14 as a lean pointer to eidos-motion.md (canonical, current) with a
status note flagging the obsolete data-motion-ref / event:* framing, and keep
the theming-specific bit (the --motion-scale-lift "pickup" token). Renumber-safe
— §14 stays §14, so the §N citations across the corpus are untouched.
The full stratum-split of THEMING (TSC -> E2, guides -> E4, comparison/FAQ ->
E3) is deferred: it renumbers sections that are cited by number across the
corpus + code, so it needs a dedicated pass with a citation sweep.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 1b of the doc-corpus cleanup: reference docs should read as timeless.
Pulled the inline "Handoff 2026-05-1x" blocks out of the six layer READMEs
(README, morfo, sema, soma, eidos, active-uix) into
docs/process/handoffs-2026-05.md, with docs/process/README.md explaining that
process/ holds ephemeral, time-stamped artifacts (hand-offs, audits, plans) —
never the source of truth.
README.md keeps its useful "visión de conjunto" pointer to active_architecture
+ GUIA, minus the hand-off framing.
Deferred on purpose: active_architecture §0 (embeds the "contratos mínimos"
table other docs link to — separated during the architecture pass) and
CLAUDE.md's ~10 session hand-offs (it is the per-session instructions file;
its own step). Docs-only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`timers.schedule/interval/scheduleAt` now accept `key: null` — the engine mints a
unique `auto:N` key via an internal counter (with a `has()` collision guard) and
the caller drives the timer through the returned handle. Naming a timer stays a
feature (cancel-by-name, `replace`, dedup, scoped `cancelAll`), not a tax on every
call. Anonymous timers live in the `auto` scope. +6 engine tests; the race-safety
path is unchanged (the key is resolved before the (id,key,version) machinery).
Uses it to close the raw `setTimeout`/`setInterval` and direct `getComputedStyle`
violations the audit flagged — instead of waiving the uix.timers / $adom rules:
- code-block, relative-time: raw setTimeout/setInterval -> eidos.timers (key:null)
- cropper: raw setTimeout throttle gate -> uix.timers (key:null)
- textarea, s-text, s-text-virtual-list: getComputedStyle(el) -> dom.getWindow(el)
The earlier audit call that these were "nil functional gain" was wrong: the rule
is the rule, and the friction (manual per-instance key invention) was removed at
the framework level rather than used as grounds to skip the rule.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Removed orphan deprecated type aliases with no real consumers: sema `IntentPolicy`
(legacy single-axis policy, superseded by intentRequirement + intentGuidance) and
its re-export; eidos `RecipeTokenObject` (→ RecipeTokenDeclaration) and the comment
that named it. Left `IntentExpectedFamily` and `SemaEventLabel` — both are still
consumed by public morfo types / sema functions, so removing them is an API rename,
not dead-code cleanup (documented in SYSTEM-AUDIT).
Translated 3 non-quote Spanish comments to English (sema/chans/types,
sema/projection/index, morfo/year-grid) per the English-comments rule. Book quotes
stay in Spanish (they're the canonical source).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Apply the icon↔type correlation (option 1): the icon follows the font the
component actually shows, no font step-up. button icon-size-sm: icon-xs→icon-sm
(follows its font); radio-cards icons follow the card title (16/16/18/18/20,
were 18-32 hand-inflated). STATIC_SIZE's font step-down stays — control-density
convention, not a bug.
Fix pre-existing button bug: the Icon's inline `width: var(--icon-size)` beat
`[data-button] svg { inline-size }`, pinning the button icon at 18px regardless
of size. button now feeds `--icon-size` from `--_button-icon-size`, so the icon
follows the button size (14/16/16/18/20). Verified in browser.
CONTINUE-icon-type-scale.md updated with the resolution + a pending audit of the
same Icon-sizing bug in other components.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Icon scale large steps now track the type scale (validated by eye in a study
harness at /uix/icon-scale-study): --icon-size-xl 24→32, --icon-size-xxl 48→52,
so the icon keeps its optical weight beside its paired text. 0 impact — nothing
consumed those steps yet.
button: icon-size per size references var(--icon-size-X) (mapped by value → same
rendered px, now density-scalable) instead of hardcoded px.
Docs: THEMING.md §5 documents the type↔icon correlation; CONTINUE-icon-type-scale.md
captures the pending 1:1 work (size = font = icon per name, dropping the font
step-down) for the next session.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Size was applied via `var(--qr-code-size-${resolvedSize})` (interpolated), so
the literal token names never appeared in source and the recipe-css-contract
test flagged --qr-code-size-{xs..xl} as orphaned. Use an explicit literal map so
each name appears verbatim; behaviour unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The recipe defined per-color tonal tokens inconsistently — only fulfill/loss had
`track`/`border`, so soft/surface/outline rendered with no background or border
for the other 6 colors (primary/secondary/neutral/affirm/risk/threat):
`palette-track`/`palette-border` referenced undefined `--button-{color}-track`/
`-border`, which collapsed the background/border declarations. Added the missing
track+border to all 6 (mapped to `var(--color-{role}-track|border)` like
fulfill/loss); the recipe is now uniform (7 slots per color). Verified all 8
colors × soft/surface/outline in dark + light.
Demo: new "Gallery" tab — a 6 variants × 8 colors matrix of real <Button>s
(hierarchy via `color`, evaluative palette via `intent`).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The logo overlay rendered invisible. Inside a viewBox-scaled foreignObject a
fixed px padding (var(--space-1)) equals that many *modules*, overflowing the
cleared centre and collapsing the content box; inline-size/block-size also
don't size an <svg> element. Fix: proportional inline padding + physical
width/height. The logo now renders and still decodes at ECC H. Dropped the
now-orphan --qr-code-overlay-padding token + regenerated base.css.
Demo: content presets (URL / vCard / WiFi / unicode) prove the encoder takes
any UTF-8 string, not just URLs (each verified decoding via jsQR); logo on by
default; quiet-zone chips fixed to canonical [4, 8, 16] (was [2, …], below the
spec's 4-module minimum). README documents the quiet zone as modules, not
pixels — the canonical unit; for px layout spacing, pad the container in CSS.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Own QR encoder in $libs/qr (ISO/IEC 18004: segment analysis →
Reed-Solomon over GF(256) → matrix layout → 8-mask penalty selection),
no npm dependency. Eidos <QrCode> renders the module matrix as one SVG
path with square/rounded/dots cell shapes; function patterns stay square
(via the encoder's `reserved` mask) so every shape scans. Optional logo
clears the centre + auto-boosts ECC to H. <QrCode.DownloadTrigger>
exports PNG/SVG, baking resolved colours into the clone first (a
serialized SVG loses the document's CSS custom properties). Fixed-tone
default colours for scannability; themeable via color/background.
morfo qr-code (scope: eidos; parts Provider/Pattern/Overlay/
DownloadTrigger) + 'path' added to MorfoElement; recipe with 11
--qr-code-* tokens; canonical demo + nav entry. 11 encoder unit tests;
verified end-to-end via jsQR round-trip + browser.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
orientation is now breakpoint-aware like size: `orientation={{ base:'vertical',
md:'horizontal' }}` resolves via Eidos.resolve and forwards a concrete value to
the soma (keyboard nav handles both axes). Documented in the README (new
"Responsive orientation" section + example) and the demo (a "responsive" chip +
note + API row) — the first consumer-facing example of the ResponsiveProp object
syntax in the repo. Browser-verified: flips at md (768px). check 0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The hover rule out-specified the checked rule, so hovering a selected card
reverted it to the neutral (unselected) hover colour — visually undoing the
selection. Now the neutral hover excludes `[data-state='checked']`, and a
dedicated checked-hover rule keeps the active colour and just deepens it a
notch (border → --_radio-group-palette-solid-hover, tint 7% → 12%). Selection
state is never affected by hover.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New eidos component built ON TOP of RadioGroup with zero duplicated
behaviour. RadioCards reuses RadioGroup's soma wholesale (selection,
roving focus, role=radio, native <form> via HiddenInput) and adds only
the card chrome: icon · title · description + an always-present corner
radio. Vertical, horizontal and grid layouts.
- morfo: scope:['eidos'], thin contract for presentation parts only
(Provider marker, Item-card, Indicator, Icon, Content, Title,
Description). The interactive contract + commit-select stay in
radio-group, reused via its soma. No new soma, no new sema.
- eidos: `radio-cards` recipe (card chrome) + wrappers + radio-cards.css.
radio-group.css is NOT loaded here, only its foundation tokens — the
card owns its visual, reusing --_radio-group-palette-* (inlined, since a
:root token referencing the palette is invalid-at-computed-value).
- demo: canonical structure (always-on stage + trace, tabs, chip/switch
controls, Untitled-UI example patterns) + nav entry + README.
check 0 · eidos-lint 0 invalid. Browser-verified: select (click + keyboard
roving), disabled, the three layouts, controls drive the preview.
Follow-up: icon/circle sizes are still px (mirroring radio-group's own size
tokens) — making the control-size scale density-aware is a separate,
family-wide change deferred per request.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
NumberField and CssField are the same visual (a bordered field + input +
increment/decrement triggers + scrubber, split/stacked layouts, sizes/
variants/colors, themeable glyphs); only their value model differs. They
were two cloned recipes + CSS that drifted — a refinement to one (square
flush buttons, divider, contrast) didn't reach the other.
Unify into ONE shared source (the toggle-group structural-identity pattern):
- New eidos/components/spin-field: recipe key `spin-field` (`--spin-field-*`)
+ spin-field.css with all the stepper-field rules, selecting `[data-spin-
field*]`. Loaded via the foundation @import in index.css.
- number-field + css-field morfos declare structural identity (`data-spin-
field*` presence attrs on each part). The Provider emits them via syncAttrs;
the sub-parts emit them in their soma `props` getter (number-field's soma
hardcodes sub-part attrs rather than syncing the morfo).
- Removed the `number-field` / `css-field` recipe keys; their CSS files are
now stubs. A theme tints one component by scoping `[data-number-field] {
--spin-field-… }`.
- css-field thereby adopts number-field's refined steppers (square, flush,
divider) — the drift fix the user asked for, now structural (no clone).
Verified bit-for-bit in browser: number-field identical to baseline (split
flush, stacked symmetric xs..xl, RTL, glyph token/children override);
css-field now square/flush/divider. eidos-lint invalid 0; recipe contract
passes (no orphans, loads-once); check + morfo:check clean for these.
Docs: THEMING sections 33 (glyph tokens now `--spin-field-*`) + 34 (the
shared layer); number-field / css-field READMEs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
NumberField and CssField steppers gain a `stacked` compact layout and
themeable default glyphs.
- Glyphs render from recipe tokens via `:empty::before` (split plus/minus,
stacked up/down triangles); passing children overrides per instance,
a theme overrides the token. 4 tokens per component, unicode-escaped.
- CssField gains the `steppers` prop (split | stacked); its stacked layout
mirrors NumberField (self-contained — the split layout left untouched).
- NumberField split: square buttons flush to the field edge with an inner
divider and higher hover/focus contrast (control-radius 0).
- Stacked rows use minmax(0,1fr) + a height-proportional glyph so the two
arrows stay symmetric and uncut at xs.
- Demos: steppers controls + stepper-layout / sizes / themed-glyph
showcases; code snippets use the themed defaults.
- Docs: THEMING section 33, number-field and css-field READMEs.
Note: also sweeps in the pre-existing palabras WIP already in the tree.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each input drew its focus ring differently — the foundation [data-archetype]
:focus-visible ring landing on the <input>, ad-hoc per-component input rings
(css-field/number-field), the textarea's own color-mix — so editing showed a
DOUBLE frame (inner + outer), inconsistent across components.
Unify into one theme-parameterised two-ring model:
- New --focus-ring-inner-width (0 by default), folded into --focus-ring and every
field *-focus-shadow: inner inset ring + outer ring. 0 = single outer frame;
raise it in a theme to show the inner line everywhere at once.
- Foundation [data-archetype]:focus-visible now excludes form-internal elements
(:not(input):not(textarea):not(select):not([data-archetype='segment'])) — their
focus is shown by the wrapping control.
- Removed the ad-hoc input rings (css-field/number-field); textarea uses
var(--focus-ring).
- Cleaned the now-dead per-data-color _accent-* cascade in css-field/number-field
(recorded as backlog in THEMING.md §32).
Verified with real keyboard on field/css-field/number-field/date-field/textarea:
single outer frame. Also includes the earlier textarea `resize` fix (morfo
data-resize values + soma stops inlining resize + eidos materialises it).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>