/* Cinematic and Thematic Transitions
 * Charge-driven visual physics for Spw components.
 *
 * The --charge property (0→1) is written to elements by SpwBus and drives all
 * cinematic effects from a single source of truth. CSS transitions on @property
 * --charge enable smooth interpolation without JavaScript animation loops.
 *
 * Charge levels:
 *   0           neutral    — element at rest
 *   0.25        charging   — pointer hovered
 *   0.50        projecting — drag in progress
 *   0.65        active     — pointer down
 *   0.90        sustained  — held 420ms+ (pin threshold)
 *   1.0         manifest   — fully expressed
 *
 * CSS data contract:
 *   --charge               number 0→1 on any element (set by bus)
 *   --spw-operator-color   inherited from [data-spw-operator] context
 *   data-spw-charge        charging | active | sustained | manifest
 *   data-spw-gesture       neutral | charging | active | sustained | projecting
 */

/* ── @property registration ──────────────────────────────────────────────── */
/* Registering --charge as a typed CSS property enables interpolation,
   which means transitions on charge-derived expressions actually animate. */

@property --charge {
    syntax: '<number>';
    inherits: false;
    initial-value: 0;
}

/* ── Base timing handles (if spw-tokens.css is not loaded first, fallbacks) ─ */

:root {
    /* Cinematic timing aligns with the luxury-mechanical character:
       charge feels like a precision instrument reading — deliberate,
       no oscillation, exact landing. Resonance breathes slowly.     */
    --cinematic-ease:     cubic-bezier(0.32, 0, 0.0, 1); /* mechanical */
    --cinematic-duration: 0.48s;
    --charge-in-duration:  220ms;
    --charge-out-duration: 440ms;
    --charge-ease-in:  cubic-bezier(0.22, 0, 0.0, 1);    /* chassis entry */
    --charge-ease-out: cubic-bezier(0.0,  0, 0.0, 1);    /* precision release */
}

/* ── Charge transition on all tracked elements ───────────────────────────── */
/* Any element that receives --charge from the bus gets a smooth transition.
   We use :where() to keep specificity at 0 so component rules can override. */

:where([data-spw-form], [data-spw-charge], .spw-delimiter) {
    transition:
        --charge         var(--charge-in-duration)  var(--charge-ease-in),
        filter           var(--charge-in-duration)  var(--charge-ease-in),
        box-shadow       var(--charge-in-duration)  var(--charge-ease-in),
        outline-color    var(--charge-in-duration)  var(--charge-ease-in),
        background-color var(--charge-out-duration) var(--charge-ease-out);
}

/* Discharge (return to rest) uses a slower, trailing ease */
:where([data-spw-form]:not([data-spw-charge])) {
    transition:
        --charge         var(--charge-out-duration) var(--charge-ease-out),
        filter           var(--charge-out-duration) var(--charge-ease-out),
        box-shadow       var(--charge-out-duration) var(--charge-ease-out),
        outline-color    var(--charge-out-duration) var(--charge-ease-out),
        background-color var(--charge-out-duration) var(--charge-ease-out);
}

/* ── Operator color inheritance ──────────────────────────────────────────── */
/* Each operator type resolves its color token so cinematic effects
   can reference a single --spw-operator-color without type-specific branches. */

[data-spw-operator="frame"]     { --spw-operator-color: var(--op-frame-color);     }
[data-spw-operator="object"]    { --spw-operator-color: var(--op-object-color);    }
[data-spw-operator="ref"]       { --spw-operator-color: var(--op-ref-color);       }
[data-spw-operator="probe"]     { --spw-operator-color: var(--op-probe-color);     }
[data-spw-operator="action"]    { --spw-operator-color: var(--op-action-color);    }
[data-spw-operator="stream"]    { --spw-operator-color: var(--op-stream-color);    }
[data-spw-operator="merge"]     { --spw-operator-color: var(--op-merge-color);     }
[data-spw-operator="binding"]   { --spw-operator-color: var(--op-binding-color);   }
[data-spw-operator="meta"]      { --spw-operator-color: var(--op-meta-color);      }
[data-spw-operator="normalize"] { --spw-operator-color: var(--op-normalize-color); }
[data-spw-operator="pragma"]    { --spw-operator-color: var(--op-pragma-color);    }
[data-spw-operator="surface"]   { --spw-operator-color: var(--op-surface-color);   }
[data-spw-operator="layer"]     { --spw-operator-color: var(--op-layer-color);     }
[data-spw-operator="baseline"]  { --spw-operator-color: var(--op-baseline-color);  }
[data-spw-operator="topic"]     { --spw-operator-color: var(--op-topic-color, hsl(192 62% 32%)); }

/* ── Dimensional field — manifests at threshold ───────────────────────────  */
/* The field is an ambient glow that appears when charge crosses 0.25.
   It deepens at active (0.65) and pulses at sustained (0.90). */

[data-spw-form][data-spw-charge] {
    --field-opacity:  calc(var(--charge, 0) * 0.7);
    --field-radius:   calc(4px + var(--charge, 0) * 20px);
    --field-spread:   calc(var(--charge, 0) * 12px);
    --field-color:    color-mix(
        in srgb,
        var(--spw-operator-color, var(--teal, hsl(180 100% 28%))) calc(var(--charge, 0) * 50%),
        transparent
    );

    filter: drop-shadow(0 0 var(--field-radius) var(--field-color));
}

/* At active charge and above, add a second, tighter glow ring */
[data-spw-charge="active"],
[data-spw-charge="sustained"],
[data-spw-charge="manifest"] {
    filter:
        drop-shadow(0 0 var(--field-radius) var(--field-color))
        drop-shadow(0 0 4px color-mix(in srgb, var(--spw-operator-color, var(--teal)) 80%, white));
}

/* Sustained hold — pulse animation to signal pin threshold crossing */
[data-spw-charge="sustained"] {
    animation: charge-sustain-pulse 1.4s var(--cinematic-ease) infinite;
}

@keyframes charge-sustain-pulse {
    0%, 100% {
        filter:
            drop-shadow(0 0 12px color-mix(in srgb, var(--spw-operator-color, var(--teal)) 30%, transparent))
            drop-shadow(0 0 4px  color-mix(in srgb, var(--spw-operator-color, var(--teal)) 70%, white));
    }
    50% {
        filter:
            drop-shadow(0 0 28px color-mix(in srgb, var(--spw-operator-color, var(--teal)) 50%, transparent))
            drop-shadow(0 0 8px  color-mix(in srgb, var(--spw-operator-color, var(--teal)) 90%, white));
    }
}

/* ── Projecting (drag) — directional field smear ─────────────────────────── */

[data-spw-gesture="projecting"] {
    --drag-dx:       0px;
    --drag-dy:       0px;
    filter:
        drop-shadow(
            calc(var(--drag-dx) * 0.08) calc(var(--drag-dy) * 0.08) 16px
            color-mix(in srgb, var(--spw-operator-color, var(--teal)) 40%, transparent)
        );
    transition: none; /* live drag needs immediate response */
}

/* ── Outline affordance — focus ring scales with charge ─────────────────── */

[data-spw-form][data-spw-charge] {
    outline: 1px solid color-mix(
        in srgb,
        var(--spw-operator-color, var(--teal)) calc(var(--charge, 0) * 70%),
        transparent
    );
    outline-offset: calc(1px + var(--charge, 0) * 4px);
}

/* ── Background wash — deepens with charge ───────────────────────────────── */

[data-spw-form="brace"][data-spw-charge] {
    --wash-alpha: calc(var(--charge, 0) * 0.12);
    background-color: color-mix(
        in srgb,
        var(--spw-operator-color, var(--teal)) calc(var(--charge, 0) * 14%),
        transparent
    );
}

/* ── Pinned elements retain residual charge ──────────────────────────────── */

[data-spw-pinned="true"] {
    --charge: 0.30;
    outline: 1px dashed color-mix(in srgb, var(--spw-operator-color, var(--teal)) 38%, transparent);
    outline-offset: 3px;
}

/* ── Operator sigil — charge-scaled brightness ───────────────────────────── */

[data-spw-charge] .frame-sigil,
[data-spw-charge] .frame-card-sigil {
    opacity: calc(0.55 + var(--charge, 0) * 0.45);
    transition: opacity var(--charge-in-duration) var(--charge-ease-in);
}

/* ── Semantic phase state indicators ─────────────────────────────────────── */
/* Phase panels and sigils reflect the operator's semantic phase position. */

[data-spw-stateful][data-spw-phase] .frame-sigil[data-spw-phase-prefix] {
    opacity: 0.65;
}

[data-spw-stateful][data-spw-phase] .frame-sigil[data-spw-phase-postfix] {
    opacity: 1;
    text-shadow: 0 0 6px color-mix(in srgb, var(--spw-operator-color, var(--teal)) 50%, transparent);
}

/* ── Lattice phase — salience shifts across developmental cycle ──────────── */
/* The body carries data-spw-lattice-phase. Operator chips relevant to the
   current learning phase are brought forward; others recede. */

[data-spw-lattice-phase="curiosity"]  .spec-pill[data-cluster~="runtime"]     { --charge: 0.40; }
[data-spw-lattice-phase="competence"] .spec-pill[data-cluster~="structure"]   { --charge: 0.40; }
[data-spw-lattice-phase="coherence"]  .spec-pill[data-cluster~="orchestration"]{ --charge: 0.40; }
[data-spw-lattice-phase="principal"]  .spec-pill[data-cluster~="expressivity"] { --charge: 0.40; }

/* ── Spirit phase body — ambient palette shifts ───────────────────────────── */
/* The document root carries data-spw-spirit-phase.
   Phases modulate the global charge receptivity (how vivid the field appears). */

[data-spw-spirit-phase="initiation"]     { --phase-receptivity: 0.70; }
[data-spw-spirit-phase="resistance"]     { --phase-receptivity: 1.00; }
[data-spw-spirit-phase="transformation"] { --phase-receptivity: 0.85; }
[data-spw-spirit-phase="expression"]     { --phase-receptivity: 0.90; }
[data-spw-spirit-phase="return"]         { --phase-receptivity: 0.55; }

/* ── SVG operator grammar — animated flow lines ───────────────────────────── */

.cinematic-host {
    filter: drop-shadow(0 0 0 transparent);
    transition: filter var(--cinematic-duration) var(--cinematic-ease);
}

.cinematic-host:hover {
    filter: drop-shadow(0 0 14px color-mix(in srgb, var(--teal) 22%, transparent));
}

.spw-svg-surface {
    transition: transform var(--charge-release-duration, 120ms) var(--charge-release-ease, ease-out);
}

.spw-svg-surface:hover {
    transform: scale(1.005);
}

@keyframes flow-dash {
    to { stroke-dashoffset: -40; }
}

/* Default flow: slow drift at ambient charge */
.spw-svg-flow--animated {
    animation: flow-dash 4s linear infinite;
}

/* Faster flow when the SVG surface is inside a charged context */
[data-spw-charge="active"]    .spw-svg-flow--animated { animation-duration: 2.2s; }
[data-spw-charge="sustained"] .spw-svg-flow--animated { animation-duration: 1.2s; }

/* ── Chromatic aberration filter hook ────────────────────────────────────── */
/* Attach .filter-aberration to an SVG or container to invoke depth texture.   */

.filter-aberration {
    filter: url('#cinematic-aberration');
}

/* ── Parallel shimmer — lattice relationship highlight ───────────────────── */

.spw-parallel-shimmer {
    animation: spw-parallel-pulse 1.8s var(--cinematic-ease) infinite;
}

@keyframes spw-parallel-pulse {
    0%, 100% { box-shadow: 0 0 0   transparent; }
    50%      { box-shadow: 0 0 14px color-mix(in srgb, var(--spw-operator-color, var(--teal)) 40%, transparent); }
}

/* ── Pop snap (haptics) ───────────────────────────────────────────────────── */

@keyframes spw-pop-snap {
    0%   { transform: scale(1);    opacity: 1;   }
    40%  { transform: scale(0.88); opacity: 0.9; }
    70%  { transform: scale(1.06); opacity: 1;   }
    100% { transform: scale(1);                  }
}

.spw-pop-snap {
    animation: spw-pop-snap 180ms var(--cinematic-ease) forwards;
}

/* Grounded tokens have settled into the baseline (.) operator.
   Keep the cue readable: a slight quieting, never a heavy fade. */

[data-spw-grounded="true"] {
    opacity: 0.92;
    /* Soft dot-underline marks the token as baseline-settled, not removed */
    text-decoration: underline dotted;
    text-decoration-color: color-mix(in srgb, var(--spw-operator-color, var(--teal)) 24%, transparent);
    text-underline-offset: 3px;
    filter: none; /* suppress charge glow — this element is at rest */
    transition:
        opacity          var(--charge-out-duration) var(--charge-ease-out),
        text-decoration  var(--charge-out-duration) var(--charge-ease-out);
}

.frame-card[data-spw-grounded="true"],
.frame-panel[data-spw-grounded="true"],
.software-card[data-spw-grounded="true"],
.site-frame[data-spw-grounded="true"] {
    opacity: 1;
    text-decoration: none;
}

/* Un-grounding snaps back to full salience */
[data-spw-grounded="false"] {
    opacity: 1;
    text-decoration: none;
}

/* ── Resonance — cross-context coherence feedback ──────────────────────── */
/* Resonance is sparse and earned (recurring pinned clusters), never constant.
   The outline breathes gently; it does not animate at rest.
   Design contract: selective coupling, not universal amplification.        */

@keyframes spw-resonance-breathe {
    0%, 100% { outline-offset: 2px; outline-width: 1.5px; }
    50%      { outline-offset: 6px; outline-width: 1px;   }
}

[data-spw-resonance="high"] {
    outline: 1.5px solid color-mix(in srgb, var(--resonance-accent, var(--teal)) 60%, transparent);
    animation: spw-resonance-breathe 1100ms var(--cinematic-ease, ease) infinite;
}

[data-spw-resonance="medium"] {
    outline: 1px solid color-mix(in srgb, var(--resonance-accent, var(--teal)) 35%, transparent);
}

@media (prefers-reduced-motion: reduce) {
    [data-spw-resonance="high"],
    [data-spw-resonance="medium"] {
        animation: none;
        outline-offset: 2px;
    }
}

/* ── Valence — approach / avoid orientation ─────────────────────────────── */
/* Valence is user-authored (never inferred). Approach tints warm-green;
   avoid tints muted-red. Both must meet contrast requirements.             */

[data-spw-valence="positive"] {
    --spw-valence: 1;
    box-shadow: inset 0 -2px 0 var(--valence-pos, hsl(160 55% 38%));
}

[data-spw-valence="negative"] {
    --spw-valence: -1;
    box-shadow: inset 0 -2px 0 var(--valence-neg, hsl(8 65% 44%));
}

/* Bias axis — objective ↔ subjective stance ────────────────────────────── */
/* Positive bias leans toward subjective (personal salience);
   negative bias leans toward objective (shared structure).                 */
[data-spw-stance="objective"] { --spw-bias: -1; }
[data-spw-stance="subjective"] { --spw-bias:  1; }

/* ── Sigil charge direction — prefix vs postfix orientation ───────────────── */
/* Prefix sigils (#>name) denote effective charge entering — energy moving
   from operator into subject. Postfix (name#>) denotes charge resolved —
   subject has absorbed the operator's intent.
   CSS biases glow direction and transition character accordingly.           */

[data-spw-charge-direction="prefix"] .frame-card-sigil,
[data-spw-charge-direction="prefix"] .frame-sigil {
    text-shadow:
        4px 0 8px color-mix(in srgb, var(--spw-operator-color, var(--teal)) 18%, transparent);
    transition: text-shadow var(--arc-spark-duration, 80ms) var(--arc-spark-ease, ease);
}

[data-spw-charge-direction="postfix"] .frame-card-sigil,
[data-spw-charge-direction="postfix"] .frame-sigil {
    text-shadow:
        -4px 0 8px color-mix(in srgb, var(--spw-operator-color, var(--teal)) 12%, transparent);
    transition: text-shadow var(--arc-dissipate-duration, 520ms) var(--arc-dissipate-ease, ease);
}

/* ── Arc-discharge transitions — layered onto charge states ───────────────── */
/* Spark in, dwell at peak, dissipate slowly on release.                    */

[data-spw-form][data-spw-charge="charging"] {
    transition:
        --charge     var(--arc-spark-duration, 80ms)  var(--arc-spark-ease, ease),
        filter       var(--arc-spark-duration, 80ms)  var(--arc-spark-ease, ease),
        box-shadow   var(--arc-dwell-duration, 260ms) var(--arc-dwell-ease, ease),
        outline-color var(--arc-spark-duration, 80ms) var(--arc-spark-ease, ease);
}

[data-spw-form][data-spw-charge="active"] {
    transition:
        --charge     var(--arc-dwell-duration, 260ms) var(--arc-dwell-ease, ease),
        filter       var(--arc-dwell-duration, 260ms) var(--arc-dwell-ease, ease),
        box-shadow   var(--arc-dwell-duration, 260ms) var(--arc-dwell-ease, ease),
        outline-color var(--arc-dwell-duration, 260ms) var(--arc-dwell-ease, ease);
}

:where([data-spw-form]:not([data-spw-charge]):not(:hover)) {
    transition:
        --charge          var(--arc-dissipate-duration, 520ms) var(--arc-dissipate-ease, ease),
        filter            var(--arc-dissipate-duration, 520ms) var(--arc-dissipate-ease, ease),
        box-shadow        var(--arc-dissipate-duration, 520ms) var(--arc-dissipate-ease, ease),
        outline-color     var(--arc-dissipate-duration, 520ms) var(--arc-dissipate-ease, ease),
        background-color  var(--arc-dissipate-duration, 520ms) var(--arc-dissipate-ease, ease);
}

/* ── Phase mode — material dominance shifts per reading mode ──────────────
   html[data-spw-mode] is set by JS when the user switches between reading
   modes (surface → syntax → artifacts). Each mode makes one material family
   dominant; the other two recede. This is layered atop charge so the two
   systems compose without collision.                                        */

html[data-spw-mode="surface"] {
    --phase-paper-weight: 1;
    --phase-glass-weight: 0.08;
    --phase-metal-weight: 0;
    --phase-density:      0.22;
}
html[data-spw-mode="syntax"] {
    --phase-paper-weight: 0.52;
    --phase-glass-weight: 0.9;
    --phase-metal-weight: 0.12;
    --phase-density:      0.55;
}
html[data-spw-mode="artifacts"] {
    --phase-paper-weight: 0.2;
    --phase-glass-weight: 0.18;
    --phase-metal-weight: 0.92;
    --phase-density:      0.80;
}

/* Material overlay: each site-frame gets a thin composite wash
   whose channels are gated by phase-weight variables.
   Only phase-dominant material reads; others stay near-invisible. */
.site-frame { position: relative; }
.site-frame::after {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    border-radius: inherit;
    background:
        linear-gradient(
            180deg,
            rgba(255,255,255,calc(var(--phase-paper-weight, 1) * 0.10)) 0%,
            transparent 38%
        ),
        linear-gradient(
            135deg,
            rgba(100,230,230,calc(var(--phase-glass-weight, 0) * 0.08)) 0%,
            transparent 55%
        ),
        linear-gradient(
            180deg,
            rgba(20,34,36,calc(var(--phase-metal-weight, 0) * 0.08)) 0%,
            rgba(255,255,255,calc(var(--phase-metal-weight, 0) * 0.03)) 100%
        );
    transition:
        background var(--duration-deliberate, 360ms) var(--ease-settle, ease);
    z-index: 0;
}
/* Keep content above the phase overlay */
.site-frame > * { position: relative; z-index: 1; }

/* Spacing breathes with density */
.site-frame,
.frame-panel,
.frame-card {
    --density-padding-adjust: calc(var(--phase-density, 0.3) * -0.22rem);
}

/* ── Brace field physics — charge expressed as left/right field boundary ──
   Left edge: accumulation (teal / cool charge building)
   Right edge: release (warm discharge / resolved state)
   The brace feels like a dielectric — energy enters left, resolves right.  */

[data-spw-form="brace"] {
    --brace-glow-alpha:   calc(var(--charge, 0) * 0.26);
    --brace-edge-alpha:   calc(0.14 + var(--charge, 0) * 0.44);
    --brace-inner-density: calc(0.02 + var(--charge, 0) * 0.07);
    position: relative;
}

/* Inner directional wash — left accumulates, right dissipates */
[data-spw-form="brace"]::before {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    border-radius: inherit;
    background: linear-gradient(
        90deg,
        rgba(0, 128, 128, var(--brace-inner-density, 0.02)) 0%,
        transparent 28%,
        transparent 68%,
        rgba(160, 42, 77, calc(var(--brace-inner-density, 0.02) * 0.8)) 100%
    );
    opacity: calc(0.4 + var(--charge, 0) * 0.5);
    transition: opacity var(--charge-release-duration, 120ms) var(--charge-release-ease, ease-out);
    z-index: 0;
}

/* Edge inset shadows — charge makes boundaries visible as fields */
[data-spw-form="brace"][data-spw-charge="charging"] {
    box-shadow:
        inset 2px 0 0 rgba(0, 128, 128, var(--brace-edge-alpha, 0.28)),
        0 0 10px rgba(0, 128, 128, var(--brace-glow-alpha, 0.12));
}
[data-spw-form="brace"][data-spw-charge="active"] {
    box-shadow:
        inset 3px 0 0 rgba(0, 128, 128, calc(var(--brace-edge-alpha, 0.28) + 0.1)),
        inset -2px 0 0 rgba(160, 42, 77, 0.14),
        0 0 18px rgba(0, 128, 128, calc(var(--brace-glow-alpha, 0.12) + 0.07));
}
[data-spw-form="brace"][data-spw-charge="sustained"],
[data-spw-form="brace"][data-spw-charge="manifest"] {
    box-shadow:
        inset 4px 0 0 rgba(0, 128, 128, 0.60),
        inset -3px 0 0 rgba(160, 42, 77, 0.24),
        0 0 26px rgba(0, 128, 128, 0.20);
}

/* Brace emission — brief outward pulse when content is resolved/copied */
[data-spw-brace-state="emitting"] {
    animation: brace-emit 360ms var(--ease-release, ease-out) 1;
}
@keyframes brace-emit {
    0%   { filter: brightness(1)    scale(1); }
    38%  { filter: brightness(1.07) scale(1.008); }
    100% { filter: brightness(1)    scale(1); }
}

/* ── Directional gradient — page-level left accumulation / right release ──
   Applied to the page body as an ambient field signal.
   Not a dramatic effect — a structural undercurrent.                       */
body::before {
    content: "";
    position: fixed;
    inset: 0;
    pointer-events: none;
    z-index: -1;
    background:
        linear-gradient(
            90deg,
            rgba(0, 96, 88, 0.028) 0%,
            rgba(0, 96, 88, 0.010) 12%,
            transparent 38%,
            transparent 72%,
            rgba(96, 40, 56, 0.014) 100%
        );
}

/* ── Reactive spine — SVG op-node charge states ───────────────────────────
   The .op-node--charged class is added by spw-reactive-spine.js.
   Charge states on SVG nodes use opacity and filter rather than box-shadow
   because SVG doesn't support box-shadow.                                  */

.op-node circle {
    transition:
        opacity  var(--touch-acknowledge, 90ms)  var(--ease-precise, ease),
        filter   var(--touch-acknowledge, 90ms)  var(--ease-precise, ease),
        r        var(--touch-commit, 180ms)       var(--ease-joint, ease);
}
.op-node text {
    transition: opacity var(--touch-acknowledge, 90ms) var(--ease-precise, ease);
}

.op-node--charged circle {
    filter: drop-shadow(0 0 4px color-mix(in srgb, var(--active-op-color, hsl(180 100% 28%)) 32%, transparent));
    opacity: 1;
}
.op-node--charged[data-spw-charge="charging"] circle {
    filter: drop-shadow(0 0 5px color-mix(in srgb, var(--active-op-color) 28%, transparent));
}
.op-node--charged[data-spw-charge="active"] circle {
    filter:
        drop-shadow(0 0 8px color-mix(in srgb, var(--active-op-color) 42%, transparent))
        brightness(1.06);
}
.op-node--charged[data-spw-charge="sustained"] circle {
    filter:
        drop-shadow(0 0 14px color-mix(in srgb, var(--active-op-color) 58%, transparent))
        brightness(1.1);
    animation: spine-sustain 900ms var(--ease-joint) infinite;
}
@keyframes spine-sustain {
    0%, 100% { filter: drop-shadow(0 0 10px color-mix(in srgb, var(--active-op-color) 42%, transparent)) brightness(1.06); }
    50%      { filter: drop-shadow(0 0 18px color-mix(in srgb, var(--active-op-color) 64%, transparent)) brightness(1.12); }
}

/* Operator-specific spine node colors */
.op-node--frame     { --active-op-color: var(--op-frame-color,     hsl(180 100% 28%)); }
.op-node--object    { --active-op-color: var(--op-object-color,    hsl(36 80% 36%));   }
.op-node--probe     { --active-op-color: var(--op-probe-color,     hsl(268 55% 42%));  }
.op-node--ref       { --active-op-color: var(--op-ref-color,       hsl(210 70% 38%));  }
.op-node--action    { --active-op-color: var(--op-action-color,    hsl(180 100% 22%)); }
.op-node--stream    { --active-op-color: var(--op-stream-color,    hsl(160 60% 32%));  }
.op-node--surface   { --active-op-color: var(--op-surface-color,   hsl(180 70% 30%));  }
.op-node--layer     { --active-op-color: var(--op-layer-color,     hsl(0 0% 40%));     }
.op-node--baseline  { --active-op-color: var(--op-baseline-color,  hsl(42 18% 30%));   }
.op-node--binding   { --active-op-color: var(--op-binding-color,   hsl(24 65% 34%));   }
.op-node--meta      { --active-op-color: var(--op-meta-color,      hsl(220 45% 36%));  }
.op-node--merge     { --active-op-color: var(--op-merge-color,     hsl(188 55% 34%));  }
.op-node--normalize { --active-op-color: var(--op-normalize-color, hsl(95 34% 34%));   }
.op-node--pragma    { --active-op-color: var(--op-pragma-color,    hsl(0 40% 38%));    }

/* ── Selective touch interaction — controls vs readable content ───────────
   Operator chips, walls, handles, and gesture surfaces: no text selection.
   Prose, code, export blocks: always selectable.                          */

button,
[role="button"],
[role="tab"],
[data-spw-operator],
[data-spw-wall],
[data-spw-handle],
[data-touch-ui],
.operator-chip,
.spec-pill,
.mode-switch,
.frame-sigil,
.frame-card-sigil,
.spw-objective-wall,
.spw-subjective-wall,
.spw-boon-wall,
.spw-bane-wall {
    -webkit-user-select: none;
    user-select: none;
    -webkit-touch-callout: none;
    touch-action: manipulation;
}

p,
li,
pre,
code,
blockquote,
[data-copyable],
[data-readable],
.frame-panel p,
.frame-card span,
.inline-note,
.frame-note {
    -webkit-user-select: text;
    user-select: text;
}

/* ── Copy button states ───────────────────────────────────────────────────
   Supports the shared handleCopyButton helper in spw-copy.js.             */

[data-copy-button][aria-busy="true"],
.copy-button[aria-busy="true"] {
    opacity: 0.70;
    pointer-events: none;
}
.copy-status {
    min-height: 1.1em;
    font-size: 0.78rem;
    color: var(--ink-soft);
    font-family: var(--site-mono-font);
    transition: opacity var(--duration-snap) var(--ease-mechanical);
}
