/* Synfonos — the motion language, CSS half (Kapı 8 rule 8).
 *
 * `tokens.css` owns the ruler and `motion.js` owns the four things script has to trigger.
 * This file is the vocabulary in between: the handful of classes and state rules that
 * every screen shares, so that a press on the Ana ekran and a press on Ayarlar are the
 * same press. It is linked LAST on all seven screens — after each page's own sheet and
 * after `chrome.css` — for one reason only: where a rule here and a rule there have the
 * same specificity, the motion language should win.
 *
 * NO NUMBER IN THIS FILE IS A NUMBER. Every duration and every curve is a token, and
 * `motion.test.mjs` fails the build on a raw millisecond inside a `transition` or on a
 * `cubic-bezier()` written outside `tokens.css`. That is not tidiness: it is the only
 * thing that makes the reduced-motion block at the foot of `tokens.css` actually reach
 * every animated property in the product, rather than most of them.
 *
 * WHY THE PRESS IS AN ANIMATION AND NOT A TRANSITION. The obvious spelling —
 * `button { transition: scale var(--dur-fast) …; } button:active { scale: … }` — loses in
 * eleven places, because eleven controls in this tree already carry a `transition`
 * SHORTHAND on a class selector (`.wc-btn`, `.tab`, `.btn`, `.switch`, `.checkbox`,
 * `.radio`, `.railitem`, `.up-btn`, `#addsrc`, `#devbtn`, `.evoring-chip`), and a
 * shorthand resets `transition-property` wholesale. Writing this as a transition would
 * therefore mean editing eleven other rules' property lists and remembering to edit the
 * twelfth the day it is written. A one-shot `animation` on `:active` collides with
 * nothing — no control in this tree sets `animation` on itself, only on a spinner child
 * — so the press is uniform by construction rather than by vigilance. The cost is that
 * the release snaps back rather than easing back; at a scale delta of 0.03 over the
 * length of a click, that is not visible, and uniformity is.
 *
 * THE INDEPENDENT TRANSFORM PROPERTIES (`scale`, `translate`) ARE USED ON PURPOSE. They
 * compose with an existing `transform` instead of replacing it, which is what lets a
 * pressed ring chip keep the `left`/`top` the ring paints on it sixty times a second, and
 * what lets a FLIPped chip use `transform` for its inversion while `scale` stays free for
 * the press underneath it.
 */

/* ===== 1 · press ===========================================================
 *
 * Every button, every pill, every segment. Disabled controls are excluded in both
 * spellings the product uses — `[disabled]` on real buttons, `[aria-disabled]` on the
 * ones that must stay focusable to explain themselves.
 *
 * `:where()` keeps the whole list at zero specificity, so a screen that needs a different
 * press for one control can say so with a plain class selector and win. */
@keyframes motion-press { to { scale: var(--press-scale); } }

:where(
  button,
  [role='radio'],
  [role='tab'],
  [role='switch'],
  [role='checkbox'],
  .segment > button
):not([disabled]):not([aria-disabled='true']):active {
  animation: motion-press var(--dur-fast) var(--ease-exit) forwards;
}

/* ===== 2 · hover lift ======================================================
 *
 * Two pixels, fast, on the card shapes a pointer can actually act on.
 *
 * `.card` — the Ana ekran's side card — IS DELIBERATELY NOT IN THIS LIST, and the reason
 * was the rope (removed by K-66, 15 Sep; the no-lift choice stands). `EvoRing.setRopeTarget` anchored the rope to that card's own bounding box
 * (`_anchor()`, throttled to 250 ms), so lifting the card two pixels on hover would drag
 * the rope's landing point with it and leave the one continuously-drawn object on the
 * screen twitching every time the pointer crossed a 384px-wide box. It gets the deeper
 * shadow and keeps its position — a lift you can see without a lift that moves anything.
 * Everything inside it (the mute/solo buttons, the fader) keeps its own press. */
:where(.chancard, .notif-card) {
  transition:
    translate var(--dur-fast) var(--ease-enter),
    box-shadow var(--dur-fast) var(--ease-enter);
}
:where(.chancard, .notif-card):hover {
  translate: 0 var(--hover-lift);
  box-shadow: var(--elev-card);
}
:where(.card, #emptycard) {
  transition: box-shadow var(--dur-fast) var(--ease-enter);
}
:where(.card, #emptycard):hover { box-shadow: var(--elev-pop); }

/* ===== 3 · the window strip's controls (rule 7) ============================
 *
 * The three caption buttons already changed colour on hover; they did not MOVE, which on
 * a strip with no other motion in it is the flattest corner of the product. The glyph
 * grows a tenth on hover and dips on press — the button's own box is untouched, because a
 * caption button that changes size changes the window's drag region under the cursor.
 *
 * `[data-live='no']` (a page opened outside the shell for design review) is excluded in
 * both of its spellings: `chrome.css`'s own `.wc[data-live='no']` for the standalone bar,
 * and `app.css`'s `.titlebar[data-live='no']` for the Ana ekran's merged one. A control
 * that is drawn visibly unavailable must not answer the pointer as if it worked. */
.wc-btn svg { transition: scale var(--dur-fast) var(--ease-enter); }
.wc:not([data-live='no']) .wc-btn:hover svg,
.titlebar:not([data-live='no']) .wc-btn:hover svg { scale: 1.1; }
.wc:not([data-live='no']) .wc-btn:active svg,
.titlebar:not([data-live='no']) .wc-btn:active svg { scale: 0.92; }

/* ===== 4 · the content crossfade ===========================================
 *
 * `motion.js`'s `swap(el)` adds `.is-swap` for one animation and takes it off again. It
 * is the single answer to "the words in this box just changed": a settings pane, a state
 * pill, an AutoDial phase, a channel card's badge. Four pixels of rise, not twelve —
 * this is a change of content inside a box that stayed put. */
@keyframes motion-swap {
  from { opacity: 0; translate: 0 4px; }
  to { opacity: 1; translate: 0 0; }
}
.is-swap { animation: motion-swap var(--dur-med) var(--ease-enter); }

/* The bigger cousin, for a whole SCREEN's worth of content changing under a fixed frame:
 * `firstrun.js`'s two stages, twelve pixels. `.is-stage-swap` is passed to the same
 * `swap()` helper as a class name; nothing else differs. */
@keyframes motion-stage {
  from { opacity: 0; translate: 0 12px; }
  to { opacity: 1; translate: 0 0; }
}
.is-stage-swap { animation: motion-stage var(--dur-med) var(--ease-enter); }

/* ===== 5 · staggered arrival ===============================================
 *
 * `motion.js`'s `staggerIn` sets `--stagger-i` per element and adds one of these two
 * classes; the delay itself is computed HERE, off the token, so the reduced-motion block
 * that zeroes `--stagger-step` flattens the whole group without JavaScript involved.
 *
 * Two rhythms: rows at 30 ms (a group of settings arriving), digits at 40 (the four
 * numbers of a pairing code, which have to be countable as they land). */
@keyframes motion-stagger {
  from { opacity: 0; translate: 0 6px; }
  to { opacity: 1; translate: 0 0; }
}
.is-stagger-in {
  animation: motion-stagger var(--dur-med) var(--ease-enter) both;
  animation-delay: calc(var(--stagger-i, 0) * var(--stagger-step));
}

@keyframes motion-digit {
  from { opacity: 0; translate: 0 8px; scale: 0.9; }
  to { opacity: 1; translate: 0 0; scale: 1; }
}
.is-digit-in {
  animation: motion-digit var(--dur-med) var(--ease-enter) both;
  animation-delay: calc(var(--stagger-i, 0) * var(--stagger-digit));
}

/* ===== 6 · FLIP ============================================================
 *
 * `.is-flip-move` is added by `motion.js` for the length of one inversion and removed on
 * `transitionend`; `.is-flip-enter` is the arrival of a row that was not in the previous
 * frame. Both are on `transform`, not on `translate`, because the inversion `flip()`
 * writes is a `transform` and the two have to be the same property to interpolate. */
.is-flip-move { transition: transform var(--dur-med) var(--ease-enter); }

@keyframes motion-flip-enter {
  from { opacity: 0; scale: 0.92; }
  to { opacity: 1; scale: 1; }
}
.is-flip-enter { animation: motion-flip-enter var(--dur-med) var(--ease-enter); }

/* ===== 7 · a card arriving from the top edge ===============================
 *
 * The state/notification strip and the feedback toast are the same element on the Ana
 * ekran (`#outbanner`, app.js's `renderStateCard`) and they arrive the same way: down
 * from behind the strip above them, settling into their slot. `overflow` is not clipped
 * on the artboard's ancestors, so the travel is short on purpose — a 44 px strip
 * dropping its own height reads as a drawer, not as an alert. */
@keyframes motion-drop-in {
  from { opacity: 0; translate: 0 -100%; }
  to { opacity: 1; translate: 0 0; }
}
.is-drop-in { animation: motion-drop-in var(--dur-med) var(--ease-enter); }

/* AND THE SAME STRIP LEAVING (FT-1, 7 September 2026). Every other banner on the Ana ekran
 * goes out by having `hidden` set on it, which is a `display: none` and therefore a pop —
 * acceptable while the only thing leaving was a fault that cleared itself, and NOT
 * acceptable for the firewall card, whose departure is the one piece of evidence a person
 * gets that the button they just pressed did something. `--ease-exit` and not
 * `--ease-enter`: the ruler's own rule (Kapı 8 rule 8) is ease-out coming in, ease-in
 * going out. `both` holds the last keyframe for the frame between the animation ending and
 * `firewallcard.js`'s timer setting `hidden`, so the strip cannot flash back to full
 * opacity on its way out. */
@keyframes motion-drop-out {
  from { opacity: 1; translate: 0 0; }
  to { opacity: 0; translate: 0 -100%; }
}
.is-drop-out { animation: motion-drop-out var(--dur-med) var(--ease-exit) both; }

/* ===== 8 · the theme crossfade =============================================
 *
 * `prefs.js` puts this class on `<html>` for one `--dur-med` whenever `data-theme`
 * actually CHANGES — never on the first application of the sheet, which happens one IPC
 * round trip after load and must stay the instant repaint `prefs.js`'s own header
 * promises.
 *
 * `!important` and `*` are both deliberate and both bounded. Every surface, rule, glyph
 * and shadow in the product is a token that moves with the theme, and there is no set of
 * class selectors that names them all; the class is on the page for a quarter of a second
 * and then gone. Only paint properties are listed — nothing here can move a box, so a
 * theme swap cannot shift the layout under a pointer mid-click. */
html.is-theme-swap,
html.is-theme-swap *,
html.is-theme-swap *::before,
html.is-theme-swap *::after {
  transition:
    background-color var(--dur-med) var(--ease-enter),
    background-image var(--dur-med) var(--ease-enter),
    color var(--dur-med) var(--ease-enter),
    border-color var(--dur-med) var(--ease-enter),
    outline-color var(--dur-med) var(--ease-enter),
    fill var(--dur-med) var(--ease-enter),
    stroke var(--dur-med) var(--ease-enter),
    box-shadow var(--dur-med) var(--ease-enter) !important;
}

/* ===== 9 · reduced motion ==================================================
 *
 * The tokens above are already zero under this query (`tokens.css`), which makes every
 * transition here instant. An `animation` with a zero DURATION still runs — it just runs
 * in no time — but `animation-delay` and `both`-mode fill can still hold an element at
 * its `from` keyframe for a frame, and a person who asked for stillness should not see a
 * flicker of `opacity: 0`. So the animations are switched off outright rather than
 * shortened, which the six per-screen blocks already in this tree also do. */
@media (prefers-reduced-motion: reduce) {
  .is-swap,
  .is-stage-swap,
  .is-stagger-in,
  .is-digit-in,
  .is-flip-enter,
  .is-drop-in,
  .is-drop-out { animation: none !important; }
  .is-flip-move { transition: none !important; }
}

/* ===== K-57 · A COLOUR CHANGES BY CROSSING, NEVER BY JUMPING ==============
 *
 * The owner, 10 Sep ~13:00: *"renk degisimi bir animasyonla olmasi lazim, direk degisim
 * olmamali"*. Pressing a swatch used to repaint the tick, the puck, the row's dot and the
 * ring's band in the same frame the reply landed — four instant swaps, which is the one
 * thing K-48's "her seye animasyon" rule already ruled out for everything else on screen.
 *
 * FOUR SURFACES, ONE RULER. `--dur-med` (240 ms, "something arriving") on `--ease-enter`
 * — never linear, K-48's own words. The band is the fourth and it is NOT here: a conic
 * gradient is a background-image and no browser interpolates one, so `EvoRing._tickBand`
 * crosses its stops in JS on the same 240 ms and the same cubic. The other three are
 * ordinary paint properties whose USED value changes when `--chip`/`--swatch-color` does,
 * and a transition on the used value is all they need.
 *
 * REDUCED MOTION IS ABSOLUTE AND COSTS NOTHING HERE: `tokens.css` already resolves every
 * `--dur-*` to `0ms` under `prefers-reduced-motion: reduce`, so these rules become
 * instant swaps without a second declaration — and `EvoRing` reads the same setting
 * through `motion.js` and snaps instead of crossing.
 *
 * NOTHING ANIMATES UNDER A HAND. `[data-hand='1']` is set by `app.js`'s `onHandHold` — the
 * SAME pointer latch K-50 gave the master fader, extended to say so in CSS as well as in
 * JS — for exactly as long as a drag lasts. A slider whose fill eased toward the finger
 * would lag it, which is the defect K-50 was raised against arriving from the other side.
 */
.evoring-chip,
.evoring-chip--presence {
  transition:
    box-shadow var(--dur-med) var(--ease-enter),
    color var(--dur-med) var(--ease-enter),
    background-color var(--dur-med) var(--ease-enter);
}
.swatch::before,
.swatch-check,
.dot,
.chancard-dot {
  transition:
    background-color var(--dur-med) var(--ease-enter),
    border-color var(--dur-med) var(--ease-enter),
    color var(--dur-med) var(--ease-enter),
    opacity var(--dur-fast) var(--ease-enter);
}
[data-hand='1'],
[data-hand='1']::before,
[data-hand='1']::-webkit-slider-thumb { transition: none !important; }

/* K-57 · THE SLIDER FILL, THE ONE SURFACE THE FOUR ABOVE LEFT JUMPING.
 *
 * `#chanlevel` and `.hslider` paint their filled track as a `linear-gradient` that reads
 * `--fill-color` (`app.css`), and `setFill` writes that property inline. A gradient is a
 * `background-image`, which no browser interpolates, and an UNREGISTERED custom property
 * is a token stream that can only swap. Registering it as a `<color>` gives it a computed
 * value that is a real colour, and a real colour transitions — so the gradient is rebuilt
 * on each frame from an interpolated ink while the image itself is never transitioned.
 *
 * `inherits: true` IS LOAD-BEARING. The property is set on the `<input>` but read in its
 * `::-webkit-slider-runnable-track`, and that part gets it by inheritance; a non-inherited
 * registration would hand the track the initial value and paint every fill transparent.
 *
 * ONLY THE COLOUR. `--fill` (the position) stays unregistered on purpose: an unregistered
 * property cannot interpolate, so the fill's edge follows the thumb in the same frame and
 * can never lag a finger (K-50). The hand rule above zeroes the colour's transition too —
 * it is `transition: none !important` on the same element — and reduced motion needs no
 * clause: `tokens.css` resolves `--dur-med` to `0ms`.
 *
 * NOTHING IN THE MARKUP MOVES. The transition lives in computed style; `setFill` still
 * writes `style="--fill-color: …"` once per change, never per frame, so a panel's
 * `outerHTML` is byte-identical from the first frame of the cross to the last — the walk's
 * probe cannot read it as a control answering (the `deed61d` trap). */
@property --fill-color {
  syntax: '<color>';
  inherits: true;
  initial-value: transparent;
}
#chanlevel,
.hslider { transition: --fill-color var(--dur-med) var(--ease-enter); }
