/* MorphAvatar — a round inset. Square box forced circular; content centered. */
.morph-avatar {
    width: var(--morph-avatar-size, 54px);
    height: var(--morph-avatar-size, 54px);
    border-radius: 50%;
    flex: 0 0 auto;
    padding: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 700;
    overflow: hidden;
}

.morph-avatar.big {
    --morph-avatar-size: 72px;
    font-size: 20px;
}

.morph-avatar img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* MorphButton — raised at rest, pressed into an inset on :active. The press *is* the
   transition (raised → inset shadow), so it stays pure CSS, in the engine's charter. */
.morph-button {
    padding: var(--morph-button-pad, 9px 16px);
    border: none;
    border-radius: var(--morph-button-radius, 12px);
    font: inherit;
    font-weight: 600;
    font-size: 13px;
    cursor: pointer;
    background: var(--surface);
    color: var(--text);
    box-shadow: var(--morph-shadow-raised);
    transition: box-shadow 130ms ease;
}

/* The press is raised → inset (the canonical control inset). Focus / disabled / reduced-motion
   come from the shared `.morph-control` vocabulary (Theme/controls.css). */
.morph-button:active {
    box-shadow: var(--morph-shadow-inset);
}

/* Primary — accent fill; still presses in. */
.morph-button-primary {
    background: var(--accent);
    color: var(--on-accent);
}

.morph-button-primary:active {
    box-shadow: inset 3px 3px 8px rgba(0, 0, 0, 0.28);
}

/* MorphCard — structure on top of .morph-panel: head / body / foot are the panel's flex
   children (inheriting its 16px column gap). Pure layout + type scale; colours come from the
   theme tokens and the surface. */
.morph-card-head {
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.morph-card-title {
    margin: 0;
    font-size: 20px;
    color: var(--text);
}

.morph-card-sub {
    margin: 0;
    color: var(--text-muted);
    font-size: 13px;
}

.morph-card-body {
    display: flex;
    flex-direction: column;
    gap: 12px;
}

.morph-card-foot {
    display: flex;
    gap: 10px;
}

/* A card used as a navigation link (MorphCard Href): strip the anchor chrome, fill the grid
   cell, and lift the whole card on hover. The transform is on the wrapper, not the inner
   .morph-item, so it never fights the Morph engine's transition transforms. */
.morph-card-link {
    display: block;
    text-decoration: none;
    color: inherit;
    transition: transform 140ms ease;
}

.morph-card-link:hover {
    transform: translateY(-2px);
}

/* MorphChip — a raised pill. A flat span with the neumorphic shadow applied directly (the
   bevel surfaces are too heavy at this size). */
.morph-chip {
    display: inline-block;
    padding: 5px 12px;
    border-radius: 999px;
    font-size: 12px;
    font-weight: 600;
    background: var(--surface);
    color: var(--accent);
    box-shadow: var(--morph-shadow-raised-sm);
}

/* MorphCircle — a round surface. Square box forced circular, content centered. Default raised;
   the Style parameter swaps the surface (inset/cut-out) without touching this geometry. */
.morph-circle {
    width: var(--morph-circle-size, 72px);
    height: var(--morph-circle-size, 72px);
    border-radius: 50%;
    flex: 0 0 auto;
    padding: 0;
    display: grid;
    place-items: center;
    text-align: center;
    font-weight: 700;
    overflow: hidden;
}

.morph-circle.big {
    --morph-circle-size: 104px;
    font-size: 20px;
}

/* MorphDial — read-only gauge. Raised circle (the surface) carries the shadow; an SVG ring sits
   in the band; .morph-dial-well is the inset centre holding the number. */
.morph-dial {
    position: relative;
    width: var(--morph-dial-size, 128px);
    height: var(--morph-dial-size, 128px);
    border-radius: 50%;
    padding: 0;
    display: grid;
    place-items: center;
}

/* SVG ring, rotated so the fill starts at 12 o'clock. Stroke caps are round, so the arc ends
   are rounded; a full ring (fraction = 1) closes seamlessly. */
.morph-dial-arc {
    position: absolute;
    inset: 8px;
    width: calc(100% - 16px);
    height: calc(100% - 16px);
    transform: rotate(-90deg);
}

.morph-dial-track,
.morph-dial-fill {
    fill: none;
    stroke-width: 8;
    stroke-linecap: round;
}

.morph-dial-track {
    stroke: var(--shadow-dark);
    opacity: 0.35;
}

.morph-dial-fill {
    stroke: var(--accent);
    transition: stroke-dasharray 0.4s ease;
}

/* Inset well at the centre — sits above the ring and reads pressed-in. */
.morph-dial-well {
    position: relative;
    width: calc(var(--morph-dial-size, 128px) - 48px);
    height: calc(var(--morph-dial-size, 128px) - 48px);
    border-radius: 50%;
    background: var(--surface);
    box-shadow: var(--morph-shadow-inset);
    display: grid;
    place-items: center;
    align-content: center;
    gap: 1px;
}

.morph-dial-value {
    font-size: 22px;
    font-weight: 700;
    color: var(--text);
    line-height: 1;
}

.morph-dial-label {
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-muted);
}

/* MorphGrid — N equal columns; minmax(0,1fr) so wide children can't blow the tracks out. */
.morph-grid {
    display: grid;
    grid-template-columns: repeat(var(--morph-grid-cols, 2), minmax(0, 1fr));
    gap: var(--morph-grid-gap, 18px);
}

/* MorphHeader — a band with a text block on the lead edge and optional actions on the trailing
   edge. Sits on a raised surface; geometry tokenized for reshaping. */
.morph-header {
    display: flex;
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    border-radius: var(--morph-header-radius, 18px);
    padding: var(--morph-header-pad, 16px 22px);
}

.morph-header-text {
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.morph-header-title {
    margin: 0;
    font-size: var(--morph-header-size, 22px);
    color: var(--text);
}

.morph-header-sub {
    margin: 0;
    color: var(--text-muted);
    font-size: 14px;
}

.morph-header-actions {
    display: flex;
    gap: 10px;
    flex: 0 0 auto;
}

/* MorphImage — a picture in a Morph frame. The frame is a real surface, so it must round like
   one (the .morph-item carries no default radius); the inner <img> is clipped one pad tighter so
   the frame reads as an even mat around it. */
.morph-image {
    --morph-image-pad: 6px;
    padding: var(--morph-image-pad);
    border-radius: var(--morph-image-radius, 20px);
    overflow: hidden;
}

.morph-image-img {
    display: block;
    width: 100%;
    height: var(--morph-image-height, 140px);
    object-fit: cover;
    border-radius: calc(var(--morph-image-radius, 20px) - var(--morph-image-pad, 6px));
}

/* MorphPanel — the generic surface container. The neumorphic shadow comes from the surface
   (.neu-raised / .neu-inset / cut-out); this adds only the box: radius, padding, column layout.
   Geometry is tokenized so a consumer can reshape it without overriding selectors. */
.morph-panel {
    border-radius: var(--morph-panel-radius, 24px);
    padding: var(--morph-panel-pad, 24px);
    display: flex;
    flex-direction: column;
    gap: var(--morph-panel-gap, 16px);
}

/* MorphSlider — a native range input restyled neumorphic: an inset-groove track with a raised
   circular thumb. Pure CSS, no JS. The WebKit and Moz track/thumb pseudo-elements must be written
   as separate rules — a browser drops any selector list containing a pseudo-element it doesn't
   know, so combining them would void both. WebKit/Chromium is the verified target (the test
   matrix is Chromium-only); the ::-moz-* rules are best-effort and untested. Focus / disabled /
   reduced-motion come from the shared `.morph-control` vocabulary (Theme/controls.css). */
.morph-slider {
    appearance: none;
    -webkit-appearance: none;
    width: var(--morph-slider-width, 200px);
    height: 22px;
    margin: 0;
    background: transparent;
    cursor: pointer;
    /* Transparent at rest (no visual change), but rounds the :focus-visible ring into a pill that
       tracks the groove rather than a loose rectangle around the host box. */
    border-radius: 999px;
}

/* Track — inset groove. */
.morph-slider::-webkit-slider-runnable-track {
    height: 8px;
    border-radius: 999px;
    background: var(--surface);
    box-shadow: var(--morph-shadow-inset);
}

.morph-slider::-moz-range-track {
    height: 8px;
    border-radius: 999px;
    background: var(--surface);
    box-shadow: var(--morph-shadow-inset);
}

/* Thumb — raised circle. margin-top centres the 20px thumb on the 8px WebKit track. */
.morph-slider::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 20px;
    height: 20px;
    margin-top: -6px;
    border-radius: 50%;
    background: var(--surface);
    box-shadow: var(--morph-shadow-raised-sm);
}

.morph-slider::-moz-range-thumb {
    width: 20px;
    height: 20px;
    border: none;
    border-radius: 50%;
    background: var(--surface);
    box-shadow: var(--morph-shadow-raised-sm);
}

/* MorphStat — a value/label pair on an inset tile. The surface gives only the shadow, so the
   tile box (padding/radius/layout) lives here. */
.morph-stat {
    display: flex;
    flex-direction: column;
    gap: 6px;
    padding: var(--morph-stat-pad, 20px);
    border-radius: var(--morph-stat-radius, 18px);
}

.morph-stat-value {
    font-size: var(--morph-stat-size, 32px);
    font-weight: 800;
    color: var(--accent);
}

.morph-stat-label {
    font-size: 12px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-muted);
}

/* MorphToggle — a native checkbox restyled as a neumorphic switch. The element *is* the inset
   track; ::before is the raised knob. appearance:none drops the replaced-element restriction, so
   the pseudo-element renders (Chromium — the demo/test target). State is a pure CSS pseudo-class
   swap: :checked fills the track and slides the knob. Focus / disabled / reduced-motion come from
   the shared `.morph-control` vocabulary (Theme/controls.css). */
.morph-toggle {
    appearance: none;
    -webkit-appearance: none;
    flex: 0 0 auto;
    width: var(--morph-toggle-width, 48px);
    height: var(--morph-toggle-height, 26px);
    margin: 0;
    border-radius: 999px;
    background: var(--surface);
    box-shadow: var(--morph-shadow-inset);
    cursor: pointer;
    position: relative;
    transition: background 160ms ease;
}

.morph-toggle::before {
    content: "";
    position: absolute;
    top: 3px;
    left: 3px;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    background: var(--surface);
    box-shadow: var(--morph-shadow-raised-sm);
    transition: transform 160ms ease;
}

.morph-toggle:checked {
    background: var(--accent);
}

/* Slide the knob to the far end: track width − knob − (3px inset each side). */
.morph-toggle:checked::before {
    transform: translateX(calc(var(--morph-toggle-width, 48px) - 20px - 6px));
}

.morph-stage {
    display: block;
}

.morph-screen {
    display: block;
}

/* The per-item delay has two sources. A MorphSequence storyboard sets --seq-exit / --seq-enter
   (absolute ms) on the items it scripts; everything else falls through to the default random-depth
   formula — which is the var() FALLBACK here, so an unscripted item computes byte-for-byte what it
   did before the storyboard existed. See docs/morph/morph-sequence-storyboard.md. */
.morph-stage[data-phase="exit"] .morph-item,
.morph-stage[data-phase="flatten"] .morph-item {
    --morph-delay: var(--seq-exit, calc((var(--max-depth, 0) - var(--depth, 0)) * var(--depth-step, 0ms) + var(--rand, 0) * var(--scatter, 0ms)));
}

.morph-stage[data-phase="enter"] .morph-item,
.morph-stage[data-phase="rise"] .morph-item {
    --morph-delay: var(--seq-enter, calc(var(--depth, 0) * var(--depth-step, 0ms) + var(--rand, 0) * var(--scatter, 0ms)));
}

.morph-stage[data-phase="pre"] .morph-screen {
    animation: morph-pre-pulse 1.15s ease-in-out infinite;
}

@keyframes morph-pre-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.62; }
}

@keyframes morph-content-out {
    to {
        opacity: 0;
    }
}

@keyframes morph-content-in {
    from {
        opacity: 0;
    }
}

/* MorphLayout: the rearrange engine's container. position:relative anchors a leaver's
   absolute pin to this box (the same origin measureRects reports from). The caller styles
   the actual track (grid/flex) via the layout's Class; each item is wrapped in a host. */
.morph-layout {
    position: relative;
}

.morph-item-host {
    box-sizing: border-box;
}

.neu-cutout {
    position: relative;
    border-radius: 22px;
}

/* The frame is a pure bevel: a consumer class (e.g. .card) sizes it, but its
   padding must land on the floor — padding here would inset the floor and gap. */
.morph-item.neu-cutout {
    padding: 0;
}

/* The floor is a dark, saturated surface. As well as its colour, it re-maps the neumorphic
   shadow tokens so any Raised/Inset dropped on it — and the control bevels (chip/button/stat/
   dial) built from the same tokens — cast dark-indigo depth with a faint highlight instead of
   the beige/white pair that reads as a white glow on the purple. The composite --morph-shadow-*
   tokens resolve --shadow-dark/light lazily at point of use, so they adapt here for free. */
.cutout-floor {
    position: relative;
    border-radius: 22px;
    overflow: hidden;
    background:
        radial-gradient(120% 120% at 18% 12%,
            rgba(var(--cutout-shade), 0.29) 0%,
            rgba(var(--cutout-shade), 0) 42%),
        var(--cutout-bg);
    --shadow-dark: rgba(var(--cutout-shade), 0.55);
    --shadow-light: rgba(196, 170, 255, 0.18);
}

/* The floor is dark, so its own content reads light. Scope to the floor's own content (not nested
   morph-items) so a Raised/Inset dropped on the floor keeps the light-surface text tokens. */
.cutout-floor > *:not(.morph-item) {
    color: #fff;
    --text-muted: rgba(255, 255, 255, 0.72);
}

/* A nested Morph surface re-establishes the light material, so its CONTENTS revert to the beige
   pair (an avatar on a raised chip, tiles in an inset well…). The surface's OWN shadow still uses
   the floor tokens above — it is the thing resting on the purple. This generalises to any depth. */
.cutout-floor .neu-raised > *,
.cutout-floor .neu-inset > * {
    --shadow-dark: var(--surface-shadow-dark);
    --shadow-light: var(--surface-shadow-light);
}

.cutout-ring {
    position: absolute;
    top: 50%;
    left: 50%;
    width: 100%;
    height: 100%;
    transform: translate(-50%, -50%);
    border-radius: 22px;
    pointer-events: none;
    box-shadow:
        inset 5px 7px 12px rgba(var(--cutout-shade), 0.89),
        inset 2px 3px 4px rgba(var(--cutout-shade), 0.71),
        inset -5px -5px 10px rgba(var(--cutout-shade), 0.06),
        inset -1px -1px 1px rgba(255, 255, 255, 0.30),
        inset -2px -3px 10px rgba(190, 150, 255, 0);
}

.cutout-ring::before {
    content: "";
    position: absolute;
    inset: -2px;
    border-radius: 24px;
    box-shadow:
        inset 4px 4px 0 0 rgba(255, 255, 255, 0.55),
        inset -4px -4px 0 0 rgba(70, 60, 110, 0.35),
        0 4px 0 0 rgba(255, 255, 255, 0.40);
}

/* OPEN — floor stays shut through pop + hold; the ring (bevel) extrudes as a
   point, then the aperture scales open. Per-segment easing lives per-stop. */
@keyframes cutout-open-floor {
    0% { clip-path: inset(50% round 22px); }
    28.57% { clip-path: inset(50% round 22px); animation-timing-function: cubic-bezier(0.34, 1.23, 0.4, 1); }
    100% { clip-path: inset(0 round 22px); }
}

@keyframes cutout-open-ring {
    0% { width: 0; height: 0; animation-timing-function: cubic-bezier(0.2, 0.9, 0.25, 1.25); }
    11.69% { width: 8%; height: 8%; animation-timing-function: linear; }
    28.57% { width: 8%; height: 8%; animation-timing-function: cubic-bezier(0.34, 1.23, 0.4, 1); }
    100% { width: 100%; height: 100%; }
}

/* CLOSE — mirror: aperture collapses to the seed point, holds, shuts. */
@keyframes cutout-close-floor {
    0% { clip-path: inset(0 round 22px); animation-timing-function: cubic-bezier(0.34, 1.23, 0.4, 1); }
    71.43% { clip-path: inset(50% round 22px); }
    100% { clip-path: inset(50% round 22px); }
}

@keyframes cutout-close-ring {
    0% { width: 100%; height: 100%; animation-timing-function: cubic-bezier(0.34, 1.23, 0.4, 1); }
    71.43% { width: 8%; height: 8%; animation-timing-function: linear; }
    88.31% { width: 8%; height: 8%; animation-timing-function: cubic-bezier(0.2, 0.9, 0.25, 1.25); }
    100% { width: 0; height: 0; }
}

/* --morph-delay is 0 by default for Cutout (DepthStep/ScatterMax are 0, so it never staggered),
   making these delays a no-op in the normal morph. They exist so a MorphSequence can schedule a
   Cutout: the floor/ring (and their content below) inherit --morph-delay from the .morph-item and
   start at the step's --seq-enter / --seq-exit offset instead of immediately. */
.morph-stage[data-phase="enter"] .neu-cutout .cutout-floor {
    animation: cutout-open-floor var(--enter-dur) linear both;
    animation-delay: var(--morph-delay);
}

.morph-stage[data-phase="enter"] .neu-cutout .cutout-ring {
    animation: cutout-open-ring var(--enter-dur) linear both;
    animation-delay: var(--morph-delay);
}

.morph-stage[data-phase="exit"] .neu-cutout .cutout-floor {
    animation: cutout-close-floor var(--exit-dur) linear both;
    animation-delay: var(--morph-delay);
}

.morph-stage[data-phase="exit"] .neu-cutout .cutout-ring {
    animation: cutout-close-ring var(--exit-dur) linear both;
    animation-delay: var(--morph-delay);
}

/* Content reveal — slides in once the aperture is opening, leaves first on
   close. Timed as a fraction of the aperture's own duration so it tracks
   --enter-dur / --exit-dur instead of pinning to fixed ms. */
@keyframes cutout-content {
    from { opacity: 0; }
    to { opacity: 1; }
}

.morph-stage[data-phase="enter"] .neu-cutout .cutout-floor > *:not(.morph-custom) {
    animation: cutout-content calc(var(--enter-dur) * 0.41) ease-out both;
    animation-delay: calc(var(--morph-delay) + var(--enter-dur) * 0.55);
}

.morph-stage[data-phase="exit"] .neu-cutout .cutout-floor > *:not(.morph-custom) {
    animation: cutout-content calc(var(--exit-dur) * 0.5) ease-in reverse both;
    animation-delay: calc(var(--morph-delay) + var(--exit-dur) * 0.21);
}

/* Layout transition — per-item trigger (see raised.css). The cut-out's motion lives on its
   inner floor/ring layers, so the per-item selectors target those, mirroring the stage rules. */
.morph-item-enter .neu-cutout .cutout-floor {
    animation: cutout-open-floor var(--enter-dur) linear both;
}

.morph-item-enter .neu-cutout .cutout-ring {
    animation: cutout-open-ring var(--enter-dur) linear both;
}

.morph-item-exit .neu-cutout .cutout-floor {
    animation: cutout-close-floor var(--exit-dur) linear both;
}

.morph-item-exit .neu-cutout .cutout-ring {
    animation: cutout-close-ring var(--exit-dur) linear both;
}

.morph-item-enter .neu-cutout .cutout-floor > *:not(.morph-custom) {
    animation: cutout-content calc(var(--enter-dur) * 0.41) ease-out calc(var(--enter-dur) * 0.55) both;
}

.morph-item-exit .neu-cutout .cutout-floor > *:not(.morph-custom) {
    animation: cutout-content calc(var(--exit-dur) * 0.5) ease-in calc(var(--exit-dur) * 0.21) reverse both;
}

@media (prefers-reduced-motion: reduce) {
    .morph-stage .neu-cutout,
    .morph-stage .neu-cutout *,
    .morph-item-enter .neu-cutout,
    .morph-item-exit .neu-cutout,
    .morph-item-enter .neu-cutout *,
    .morph-item-exit .neu-cutout * {
        animation: none !important;
    }
}

.neu-inset {
    background: var(--surface);
    box-shadow:
        inset calc(var(--lift) * 5px) calc(var(--lift) * 5px) calc(var(--lift) * 10px) var(--shadow-dark),
        inset calc(var(--lift) * -5px) calc(var(--lift) * -5px) calc(var(--lift) * 10px) var(--shadow-light);
}

@keyframes inset-out {
    to {
        --lift: 0;
        opacity: 0;
    }
}

@keyframes inset-in {
    from {
        --lift: 0;
        opacity: 0;
    }
}

@keyframes inset-flatten {
    to {
        --lift: 0;
    }
}

@keyframes inset-up {
    from {
        --lift: 0;
    }
}

.morph-stage[data-phase="exit"] .neu-inset {
    animation: inset-out var(--exit-dur) var(--exit-ease) both;
    animation-delay: calc(var(--morph-delay) + var(--shell-hold, 0ms));
}

.morph-stage[data-phase="enter"] .neu-inset {
    animation: inset-in var(--enter-dur) var(--enter-ease) both;
    animation-delay: var(--morph-delay);
}

.morph-stage[data-phase="flatten"] .neu-inset {
    animation: inset-flatten var(--exit-dur) var(--exit-ease) both;
    animation-delay: var(--morph-delay);
}

.morph-stage[data-phase="rise"] .neu-inset {
    animation: inset-up var(--enter-dur) var(--enter-ease) both;
    animation-delay: var(--morph-delay);
}

.morph-stage[data-phase="pre"] .neu-inset {
    --lift: 0;
}

.morph-stage[data-phase="exit"] .neu-inset > *:not(.morph-item):not(.morph-custom) {
    animation: morph-content-out calc(var(--exit-dur) * var(--content-lead)) var(--exit-ease) both;
    animation-delay: var(--morph-delay);
}

.morph-stage[data-phase="enter"] .neu-inset > *:not(.morph-item):not(.morph-custom) {
    animation: morph-content-in calc(var(--enter-dur) * var(--content-trail)) var(--enter-ease) both;
    animation-delay: calc(var(--morph-delay) + var(--enter-dur) * (1 - var(--content-trail)));
}

/* Layout transition — see raised.css for the rationale (per-item trigger, reused keyframes). */
.morph-item-enter .neu-inset {
    animation: inset-in var(--enter-dur) var(--enter-ease) both;
    animation-delay: var(--morph-delay, 0ms);
}

.morph-item-exit .neu-inset {
    animation: inset-out var(--exit-dur) var(--exit-ease) both;
    animation-delay: var(--morph-delay, 0ms);
}

.morph-item-enter .neu-inset > *:not(.morph-item):not(.morph-custom) {
    animation: morph-content-in calc(var(--enter-dur) * var(--content-trail)) var(--enter-ease) both;
    animation-delay: calc(var(--morph-delay, 0ms) + var(--enter-dur) * (1 - var(--content-trail)));
}

.morph-item-exit .neu-inset > *:not(.morph-item):not(.morph-custom) {
    animation: morph-content-out calc(var(--exit-dur) * var(--content-lead)) var(--exit-ease) both;
    animation-delay: var(--morph-delay, 0ms);
}

@media (prefers-reduced-motion: reduce) {
    .morph-stage .neu-inset,
    .morph-stage .neu-inset > *:not(.morph-item),
    .morph-item-enter .neu-inset,
    .morph-item-exit .neu-inset,
    .morph-item-enter .neu-inset > *:not(.morph-item),
    .morph-item-exit .neu-inset > *:not(.morph-item) {
        animation: none !important;
    }
}

.neu-raised {
    background: var(--surface);
    box-shadow:
        calc(var(--lift) * 10px) calc(var(--lift) * 10px) calc(var(--lift) * 22px) var(--shadow-dark),
        calc(var(--lift) * -9px) calc(var(--lift) * -9px) calc(var(--lift) * 20px) var(--shadow-light);
}

@keyframes raise-out {
    to {
        --lift: 0;
        opacity: 0;
    }
}

@keyframes raise-in {
    from {
        --lift: 0;
        opacity: 0;
    }
}

@keyframes raise-flatten {
    to {
        --lift: 0;
    }
}

@keyframes raise-up {
    from {
        --lift: 0;
    }
}

.morph-stage[data-phase="exit"] .neu-raised {
    animation: raise-out var(--exit-dur) var(--exit-ease) both;
    animation-delay: calc(var(--morph-delay) + var(--shell-hold, 0ms));
}

.morph-stage[data-phase="enter"] .neu-raised {
    animation: raise-in var(--enter-dur) var(--enter-ease) both;
    animation-delay: var(--morph-delay);
}

.morph-stage[data-phase="flatten"] .neu-raised {
    animation: raise-flatten var(--exit-dur) var(--exit-ease) both;
    animation-delay: var(--morph-delay);
}

.morph-stage[data-phase="rise"] .neu-raised {
    animation: raise-up var(--enter-dur) var(--enter-ease) both;
    animation-delay: var(--morph-delay);
}

.morph-stage[data-phase="pre"] .neu-raised {
    --lift: 0;
}

.morph-stage[data-phase="exit"] .neu-raised > *:not(.morph-item):not(.morph-custom) {
    animation: morph-content-out calc(var(--exit-dur) * var(--content-lead)) var(--exit-ease) both;
    animation-delay: var(--morph-delay);
}

.morph-stage[data-phase="enter"] .neu-raised > *:not(.morph-item):not(.morph-custom) {
    animation: morph-content-in calc(var(--enter-dur) * var(--content-trail)) var(--enter-ease) both;
    animation-delay: calc(var(--morph-delay) + var(--enter-dur) * (1 - var(--content-trail)));
}

/* Layout transition — enter/exit fire per item (concurrently, on different items), so the
   trigger is a class on the MorphLayout host with the surface as its descendant, not a
   stage-wide [data-phase]. Keyframes are 100% reused; only the selector is new. --enter-dur
   etc. still resolve on the .neu-raised itself (MorphShape sets them inline), so the look
   stays per-style. --morph-delay defaults to 0 — a layout reflow has no depth cascade. */
.morph-item-enter .neu-raised {
    animation: raise-in var(--enter-dur) var(--enter-ease) both;
    animation-delay: var(--morph-delay, 0ms);
}

.morph-item-exit .neu-raised {
    animation: raise-out var(--exit-dur) var(--exit-ease) both;
    animation-delay: var(--morph-delay, 0ms);
}

.morph-item-enter .neu-raised > *:not(.morph-item):not(.morph-custom) {
    animation: morph-content-in calc(var(--enter-dur) * var(--content-trail)) var(--enter-ease) both;
    animation-delay: calc(var(--morph-delay, 0ms) + var(--enter-dur) * (1 - var(--content-trail)));
}

.morph-item-exit .neu-raised > *:not(.morph-item):not(.morph-custom) {
    animation: morph-content-out calc(var(--exit-dur) * var(--content-lead)) var(--exit-ease) both;
    animation-delay: var(--morph-delay, 0ms);
}

@media (prefers-reduced-motion: reduce) {
    .morph-stage .neu-raised,
    .morph-stage .neu-raised > *:not(.morph-item),
    .morph-item-enter .neu-raised,
    .morph-item-exit .neu-raised,
    .morph-item-enter .neu-raised > *:not(.morph-item),
    .morph-item-exit .neu-raised > *:not(.morph-item) {
        animation: none !important;
    }
}

/* Control-atom state vocabulary — the shared focus / disabled / reduced-motion rules every
   interactive control (button · toggle · slider) wears via the `morph-control` class. Promoted
   out of MorphButton so new controls can't silently re-invent and visibly disagree. Pairs with
   the --morph-shadow-* and --morph-disabled-opacity tokens in theme.css. Distinct from the
   staged .neu-* surfaces. */
.morph-control:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

.morph-control:disabled {
    opacity: var(--morph-disabled-opacity);
    cursor: not-allowed;
}

/* New controls add slide/press motion; honour the OS reduced-motion preference for all of them
   at once (the hand-rolled .morph-button transition had no such guard before this). */
@media (prefers-reduced-motion: reduce) {
    .morph-control {
        transition: none;
    }
}

@property --lift {
    syntax: "<number>";
    inherits: false;
    initial-value: 1;
}

:root {
    --bg: #e7ddcd;
    --surface: #ece3d5;
    --text: #4d4434;
    --text-muted: #9a8f78;
    --accent: #6d5efc;
    --on-accent: #fff;            /* text/icon that sits on an --accent fill */
    --green: #4a9e57;
    --orange: #d39a2c;
    --blue: #5d8cf0;
    /* The neumorphic surface pair. --shadow-dark/light are "the current surface's" pair — a floor
       (e.g. the cut-out) overrides them locally; --surface-shadow-* hold the theme/mode default so a
       nested surface can revert to it. A mode (theme.dark.css) overrides --surface-shadow-* once. */
    --surface-shadow-dark:  rgba(166, 143, 108, 0.72);
    --surface-shadow-light: rgba(255, 255, 255, 0.95);
    --shadow-dark:  var(--surface-shadow-dark);
    --shadow-light: var(--surface-shadow-light);

    /* Cut-out surface (consumed by Styles/Cutout) — themeable like any other surface: a theme
       recolours the cut-out by overriding --cutout-bg (floor fill) and --cutout-shade (the deep
       bevel base, given as an RGB triplet so the ring can vary its alpha: rgba(var(--cutout-shade), a)). */
    --cutout-bg: linear-gradient(150deg, rgb(102, 102, 225) 0%, rgb(166, 121, 251) 100%);
    --cutout-shade: 18, 8, 38;

    /* Canonical control-atom bevels — the shared vocabulary for interactive controls
       (button · chip · toggle · slider). Distinct from the staged .neu-* surfaces, which are
       larger and --lift-animated; these are smaller-scale and static. Unregistered custom
       properties on purpose (plain --x, not @property): a registered/typed property can't hold a
       multi-layer / inset box-shadow string. Two raised scales encode a real intent — shadow
       scale tracks element scale (a chip is lighter than a button); one inset serves every press
       and groove. */
    --morph-shadow-raised:    4px 4px 10px var(--shadow-dark), -4px -4px 10px var(--shadow-light);
    --morph-shadow-raised-sm: 3px 3px 7px  var(--shadow-dark), -3px -3px 7px  var(--shadow-light);
    --morph-shadow-inset:     inset 3px 3px 7px var(--shadow-dark), inset -3px -3px 7px var(--shadow-light);

    /* Disabled affordance — tokenized here now that a second control (toggle/slider) consumes it. */
    --morph-disabled-opacity: 0.5;
}

/* Dark mode — overrides the light token set from theme.css, applied when <html data-theme="dark">.
   Only tokens are overridden; every component re-skins for free (theming-modes-plan.md §1).

   Neumorphism on dark is the hard part: the highlight must be a FAINT light (~5% white) and the
   shadow near-black, or the raised/inset bevels go flat. --shadow-dark/light are NOT redefined here
   — they reference --surface-shadow-* (theme.css), so overriding that pair flows everywhere,
   including the cut-out's nested-surface revert. */
:root[data-theme="dark"] {
    --bg:           #1b1e24;
    --surface:      #23272f;
    --text:         #e7eaf0;
    --text-muted:   #9aa1ad;
    --accent:       #8b93ff;          /* lifted so it reads on dark */
    --on-accent:    #15171d;          /* dark text on the lighter accent */

    --surface-shadow-dark:  rgba(0, 0, 0, 0.55);
    --surface-shadow-light: rgba(255, 255, 255, 0.05);

    --cutout-bg:    linear-gradient(150deg, #3a3f96 0%, #5a49d6 100%);
    --cutout-shade: 0, 0, 8;
}

