/* ==========================================================================
   Cyber City · motion layer v1.1 · 2026-10-02
   "Power on, run the current once, then rest."

   The CSS half of the motion identity. It holds
     1. what moves with no JavaScript at all: the first-screen power-on, the hover and focus moves
        of buttons, links and cards, the current on rules and floor lines, the page crossfade;
     2. the LED ticker, which moves only when motion.js has started it, so it never moves without
        its control;
     3. the resting look of everything motion.js drives with GSAP (answer lines, dot readouts,
        headline reveals), so a page with no GSAP, or with reduced motion, is simply finished.
   Load order: fonts.css, tokens.css (v0.3 or later), pixel-kit.css, foundations.css, motifs.css, then this file.
   Grammar: A power on · B current · C assemble · D field. Rules and reasons: motion.md.

   Movement lives inside (prefers-reduced-motion: no-preference) blocks: with reduced motion
   nothing travels, grows or loops, and every final state is on screen from the first frame.
   Movement is translate, scale and opacity only (the individual `translate` and `scale` properties,
   so `transform` stays free for layout, for foundations.css and for GSAP). Layout never animates.

   Three switches on <html>, all set by someone else and only read here:
     data-ecc-booted          this session has already seen a first screen power on (the head snippet of
                              motion.md sets it): later page views appear at full strength;
     data-ecc-motion="paused" the site's "Pause motion" switch (no attribute = motion on): nothing starts
                              by itself and the current does not run. See THE MOTION SWITCH at the end;
     data-theme="night"       Night. Anything else is Day. The device's dark mode never decides.
   Nothing in this file loops: every animation here runs once.
   ========================================================================== */

/* The motion values this layer reads are tokens (tokens.css v0.3, from tokens.json): --ecc-rise, --ecc-standby,
   --ecc-stagger-voxel, --ecc-budget, --ecc-ticker-loop, --ecc-press, and the hooks --ecc-trace and --ecc-line-case.
   This file declares no --ecc-* name of its own. */

/* ============================================================ A · POWER ON */
/* First view, no JavaScript: add .ecc-boot and data-ecc-boot to the first-screen container, mark each
   module with data-ecc-module and give it its place in grid order with style="--i:N" (0 = top left).
   Modules go from standby to on with a 6px rise, 60ms apart. --n is the module count: past 9 modules
   the step shrinks so the whole screen still lands inside the 1.2s budget (one frame, 20ms, inside it).
   `backwards` holds the first keyframe during the delay and leaves nothing behind when it ends.
   Standby is dim, not invisible: everything is painted in the first frame, so the largest element
   counts for LCP at first paint. (Measured on this page: booting from opacity 0 held LCP about one
   second behind the first paint; from standby it lands with it.) */
.ecc-boot {
  --_step: min(var(--ecc-stagger), calc((var(--ecc-budget) - 20ms - var(--ecc-d-boot)) / max(1, var(--n, 9) - 1)));
}
@media (prefers-reduced-motion: no-preference) {
  .ecc-boot [data-ecc-module] {
    animation: ecc-power-on var(--ecc-d-boot) var(--ecc-ease-out) backwards;
    animation-delay: calc(var(--i, 0) * var(--_step));
  }
  /* the module's pip is its power light: it snaps on once the module is up */
  .ecc-boot [data-ecc-module] .ecc-pip {
    animation: ecc-light-on var(--ecc-d-fast) var(--ecc-ease-pixel) backwards;
    animation-delay: calc(var(--i, 0) * var(--_step) + var(--ecc-d-base));
  }
  /* Once per session. The self-test belongs to the first page a visitor opens. On every later page view
     <html> carries data-ecc-booted (set before first paint by the head snippet in motion.md), and a first
     screen is simply on, at full strength, from the first frame. A replay asked for on purpose adds
     .is-replay (motion.js does). A container that carries the class without data-ecc-boot is not a first
     screen (a demo that plays the power-on further down a page), so it is left alone. */
  html[data-ecc-booted] .ecc-boot[data-ecc-boot]:not(.is-replay) [data-ecc-module],
  html[data-ecc-booted] .ecc-boot[data-ecc-boot]:not(.is-replay) [data-ecc-module] .ecc-pip { animation: none; }
}
@keyframes ecc-power-on { from { opacity: var(--ecc-standby); translate: 0 var(--ecc-rise); } }
@keyframes ecc-light-on { from { opacity: .15; } }

/* ============================================================== B · CURRENT */
/* A pulse runs a line once, pad to spark. On a rule or a floor line: add .is-live (motion.js does it
   when a first screen powers on). On a product card it runs when the card is hovered or focused.
   The pulse is lime ink with a lime tip on light surfaces and plain lime on dark ones. */
.ecc-rule { position: relative; overflow-x: clip; }
.ecc-rule::after,
.ecc-floorline::after {
  content: ""; position: absolute; left: 0; top: 50%; width: 22%; height: 3px; margin-top: -1.5px;
  border-radius: 1px; pointer-events: none; opacity: 0; translate: -101% 0;
  background: linear-gradient(90deg, transparent, var(--ecc-spark) 55%, var(--ecc-lime));
}
@media (prefers-reduced-motion: no-preference) {
  .ecc-rule.is-live::after,
  .ecc-floorline.is-live::after { animation: ecc-current var(--ecc-d-slow) var(--ecc-ease-current) 1; }
}
@keyframes ecc-current {
  from { opacity: 1; translate: -101% 0; }
  to { opacity: 1; translate: 460% 0; }             /* 100 / 22 of its own width crosses the line */
}

/* The answer line: an SVG overlay that motion.js routes from a pad on the product to the spark beside
   a plain answer. This is its drawn, resting state.
   It is mandatory on the home hero and on the first screen of every product page: one product, two
   answers in plain sentences (motion.md 2 and 14.13). */
.ecc-line { position: relative; }
.ecc-line__hot { position: absolute; width: 1px; height: 1px; pointer-events: none; }
.ecc-line__svg {
  position: absolute; left: 0; top: 0; width: 100%; height: 100%; z-index: 3;
  overflow: visible; pointer-events: none;
}
.ecc-line__case {                     /* a surface-coloured casing keeps the trace readable over the product */
  fill: none; stroke: var(--ecc-line-case); stroke-width: 5; stroke-linejoin: miter; opacity: .9;
}
.ecc-line__trace {
  fill: none; stroke: var(--ecc-trace); stroke-width: 1.5; stroke-linejoin: miter;
}
/* the travelling pulse: a lime core on a softer halo (lime ink on light, lime on dark); motion.js shows it */
.ecc-line__glow { fill: none; stroke: color-mix(in srgb, var(--ecc-spark) 55%, transparent); stroke-width: 4.5; opacity: 0; }
.ecc-line__core { fill: none; stroke: var(--ecc-lime); stroke-width: 2; opacity: 0; }
.ecc-line__pad {
  fill: var(--ecc-surface); stroke: var(--ecc-trace); stroke-width: 1.5;
  shape-rendering: crispEdges;
}
.ecc-line__vox { fill: var(--ecc-spark); shape-rendering: crispEdges; }
.ecc-line__ring { fill: none; stroke: var(--ecc-spark); stroke-width: 1.5; opacity: 0; }
.ecc-line__answer {
  display: grid; grid-template-columns: 9px minmax(0, 1fr); column-gap: 12px; align-content: center;
  min-height: var(--ecc-hit); color: var(--ecc-ink); text-decoration: none; border-radius: 4px;
}
/* the square at the head of an answer sits on the first line of the sentence: the spark, or the pad when the
   answers sit under the product (see "Under the product" below) */
.ecc-line__answer .ecc-spark,
.ecc-line__answer .ecc-pad { grid-row: 2; grid-column: 1; align-self: start; margin-top: calc((1.25em * 1.25 - 9px) / 2); }
.ecc-line__answer .ecc-pad { display: none; }
.ecc-line__key {
  grid-column: 2; font-family: var(--ecc-f-mono); font-size: var(--ecc-t-micro); font-weight: var(--ecc-fw-mono);
  font-stretch: var(--ecc-wdth-mono); letter-spacing: var(--ecc-ls-label); text-transform: uppercase; line-height: 1.5;
  color: var(--ecc-ink-2);
}
.ecc-line__say {
  grid-column: 2; font-size: 1.25em; font-weight: var(--ecc-fw-strong); font-stretch: var(--ecc-wdth-heading); line-height: 1.25;
  letter-spacing: -.01em; text-wrap: balance;
}
/* The plain answer: the sentence alone, with no mono label above it. This is the answer of a first screen. */
.ecc-line__answer--plain .ecc-spark,
.ecc-line__answer--plain .ecc-pad { grid-row: 1; }
/* Under the product. Where there is no room beside the product (a phone, a narrow tile) the answers sit
   under it as plain lines and no trace is drawn: each answer starts with its own pad instead of ending in
   the spark. The answer carries both squares in its markup and shows one. Put .ecc-line--under on the
   stage, or, in a layout that changes with its width, repeat the two display rules inside that layout's
   own query. motion.js draws a trace only to a spark that is shown, so it follows either way. */
.ecc-line--under .ecc-line__answer .ecc-spark { display: none; }
.ecc-line--under .ecc-line__answer .ecc-pad { display: inline-block; }

/* ============================================================= C · ASSEMBLE */
/* Dot readout (Doto). motion.js gathers voxels into the text on a canvas laid over it, or counts
   its numbers up. The real text is always in the DOM and is what stays. */
.ecc-readout {
  position: relative; display: inline-block; white-space: nowrap;
  font-family: var(--ecc-f-dot); font-variation-settings: var(--ecc-dot-settings); font-weight: var(--ecc-fw-dot);
  line-height: 1; letter-spacing: .04em;
}
.ecc-readout__text { display: inline-block; transition: opacity 120ms linear; }
.ecc-readout.is-assembling .ecc-readout__text { opacity: 0; transition: none; }
.ecc-readout__vox { position: absolute; display: block; pointer-events: none; transition: opacity 120ms linear; }
.ecc-readout__vox.is-out { opacity: 0; }

/* ============================================================== TOUCH */
/* Press and focus are foundations.css: a control moves down --ecc-press (1px) while it is held, in --ecc-d-press
   (90ms), and the focus ring appears at once, with no animation. This layer adds three moves. */

/* 1. The pip of the primary button stretches into a short line: the current is ready. */
@media (prefers-reduced-motion: no-preference) {
  .ecc-btn--primary::before { transform-origin: 100% 50%; transition: scale var(--ecc-d-fast) var(--ecc-ease-out); }
  .ecc-btn--primary:focus-visible::before,
  .ecc-btn--primary.is-hover::before,
  .ecc-btn--primary.is-pressed::before { scale: 2.25 1; }
  @media (hover: hover) and (pointer: fine) {
    .ecc-btn--primary:hover::before { scale: 2.25 1; }
  }
  .ecc-btn--primary:disabled::before,
  .ecc-btn--primary[aria-disabled="true"]::before { scale: none; }
}

/* 2. An underline that draws from the left (header links). */
.ecc-underline { position: relative; text-decoration: none; }
.ecc-underline::after {
  content: ""; position: absolute; left: var(--u-inset, 0); right: var(--u-inset, 0); bottom: var(--u-bottom, 9px);
  height: 2px; background: var(--ecc-spark); scale: 0 1; transform-origin: 0 50%;
}
.ecc-underline:focus-visible::after,
.ecc-underline.is-hover::after { scale: 1 1; }
@media (hover: hover) and (pointer: fine) {
  .ecc-underline:hover::after { scale: 1 1; }
}
@media (prefers-reduced-motion: no-preference) {
  .ecc-underline::after { transition: scale var(--ecc-d-fast) var(--ecc-ease-out); }
  .ecc-underline:focus-visible::after,
  .ecc-underline.is-hover::after { transition-duration: var(--ecc-d-base); }
  @media (hover: hover) and (pointer: fine) {
    .ecc-underline:hover::after { transition-duration: var(--ecc-d-base); }
  }
}

/* 3. Card (.ecc-lift on the card, .ecc-lift__obj on the product photo): on hover or keyboard focus the
   product lifts 4px, its contact shadow tightens and one pulse runs the floor line.
   Leaving (160ms) is quicker than arriving (240ms). Touch screens get none of it. */
@media (prefers-reduced-motion: no-preference) {
  .ecc-lift__obj { transition: translate var(--ecc-d-fast) var(--ecc-ease-out); }
  .ecc-lift .ecc-stage__floor,
  .ecc-lift .pk-stage__floor { transition: scale var(--ecc-d-fast) var(--ecc-ease-out), opacity var(--ecc-d-fast) linear; }
  .ecc-lift:focus-within .ecc-lift__obj,
  .ecc-lift.is-hover .ecc-lift__obj { translate: 0 -4px; transition-duration: var(--ecc-d-base); }
  .ecc-lift:focus-within .ecc-stage__floor,
  .ecc-lift:focus-within .pk-stage__floor,
  .ecc-lift.is-hover .ecc-stage__floor,
  .ecc-lift.is-hover .pk-stage__floor { scale: .88 1; opacity: calc(.7 * var(--ecc-stage-day)); transition-duration: var(--ecc-d-base); }
  .ecc-lift:focus-within .ecc-floorline::after { animation: ecc-current var(--ecc-d-slow) var(--ecc-ease-current) 1; }
  @media (hover: hover) and (pointer: fine) {
    .ecc-lift:hover .ecc-lift__obj { translate: 0 -4px; transition-duration: var(--ecc-d-base); }
    .ecc-lift:hover .ecc-stage__floor,
    .ecc-lift:hover .pk-stage__floor { scale: .88 1; opacity: calc(.7 * var(--ecc-stage-day)); transition-duration: var(--ecc-d-base); }
    .ecc-lift:hover .ecc-floorline::after { animation: ecc-current var(--ecc-d-slow) var(--ecc-ease-current) 1; }
  }
}

/* ============================================================== THE TICKER */
/* LED ticker: the one marquee a page may carry (the home hero). "Run the current once. Then rest."
   It runs ONE loop when it is first seen, then rests at its start. The track holds the pillars twice,
   so the end of a loop is its start and nothing jumps. Its control sits at the end of the strip:
   a real button that pauses the running loop, or plays one more.
   It holds still while the pointer is on its screen, while something inside its screen has keyboard
   focus, off screen, in a hidden tab, and while the site's motion switch is on pause.
   It moves only when motion.js has started it (.is-running): a strip with no script has no control,
   so it does not move. With reduced motion it never moves: the pillars sit on the screen as a wrapped
   list, with no control.
   Markup: .ecc-led.ecc-ticker[data-ecc-ticker] > .ecc-ticker__view > .ecc-ticker__track (the pillars, then
   the same pillars again with data-ecc-dup), then button.ecc-ticker__ctl[data-ecc-ticker-toggle][hidden].
   Pair it with .ecc-led (motifs.css). */
.ecc-ticker { display: flex; align-items: center; height: 54px; }
/* the screen the pillars pass through. Its left padding is as wide as the fade of its mask, so at rest
   the first pillar starts where the mask is fully open. The padding is clipped with the rest. */
.ecc-ticker__view {
  display: flex; align-items: center; align-self: stretch; flex: 1; min-width: 0; overflow: hidden; padding-left: 28px;
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 28px, #000 calc(100% - 28px), transparent 100%);
  mask-image: linear-gradient(90deg, transparent 0, #000 28px, #000 calc(100% - 28px), transparent 100%);
}
.ecc-ticker__track { display: flex; flex: none; margin: 0; padding: 0; list-style: none; white-space: nowrap; }
.ecc-ticker__track > * {
  display: inline-flex; align-items: center; gap: 28px; padding-right: 28px;
  font-family: var(--ecc-f-dot); font-variation-settings: var(--ecc-dot-settings); font-weight: var(--ecc-fw-dot);
  font-size: 25px; line-height: 1; letter-spacing: .06em; text-transform: uppercase;
}
.ecc-ticker__track > *::after {       /* the light between two pillars: a square voxel, like every pip */
  content: ""; flex: none; width: 7px; height: 7px; border-radius: var(--ecc-r-voxel);
  background: var(--ecc-lime); box-shadow: 0 0 10px color-mix(in srgb, var(--ecc-lime) 55%, transparent);
}
/* The control: 44 by 44 at the end of the strip. Its two glyphs are drawn from squares (an inline SVG of
   rects): play while the ticker rests or is paused, pause while it moves. motion.js names it for what a
   press does ("Play the ticker", "Pause the ticker") and sets aria-pressed to true while this button holds
   the ticker paused. It carries foundations' .ecc-iconbtn as well, for the 1px press.
   The screen is dark in both themes, so the colours and the focus ring are the screen's own: the ring is
   lime and sits inside the button, on the screen, never on the surface around it. */
.ecc-ticker__ctl {
  flex: none; display: grid; place-items: center; width: var(--ecc-hit); height: var(--ecc-hit); margin: 0 5px 0 0; padding: 0;
  border: 0; border-radius: var(--ecc-r-sm); background: transparent; color: var(--ecc-on-panel); cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.ecc-ticker__ctl:focus-visible { outline: var(--ecc-focus-width) solid var(--ecc-lime); outline-offset: -4px; }
@media (hover: hover) and (pointer: fine) {
  .ecc-ticker__ctl:hover { background: color-mix(in srgb, var(--ecc-on-panel) 10%, transparent); }
}
.ecc-ticker__ctl:active { background: color-mix(in srgb, var(--ecc-on-panel) 16%, transparent); }
.ecc-ticker__ctl svg { display: block; }
.ecc-ticker__ctl rect { fill: currentColor; shape-rendering: crispEdges; }
@media (forced-colors: active) {      /* Windows contrast themes drop the screen: the control keeps an edge, and its glyph the button's text colour */
  .ecc-ticker__ctl { border: 1px solid ButtonText; color: ButtonText; }
  .ecc-ticker__ctl:focus-visible { outline-color: Highlight; }
}
.ecc-ticker__pause { display: none; }
.ecc-ticker.is-running:not(.is-paused) .ecc-ticker__pause { display: inline; }
.ecc-ticker.is-running:not(.is-paused) .ecc-ticker__play { display: none; }
@media (prefers-reduced-motion: no-preference) {
  /* one loop: no fill, so when it ends the track is back at its start, which is where the loop ended */
  .ecc-ticker.is-running .ecc-ticker__track { will-change: translate; animation: ecc-tick var(--ecc-ticker-loop) linear 1; }
  .ecc-ticker__view:focus-within .ecc-ticker__track,
  .ecc-ticker.is-paused .ecc-ticker__track,
  .ecc-ticker.is-away .ecc-ticker__track { animation-play-state: paused; }
  @media (hover: hover) and (pointer: fine) {
    .ecc-ticker__view:hover .ecc-ticker__track { animation-play-state: paused; }
  }
}
@keyframes ecc-tick { to { translate: -50% 0; } }
@media (prefers-reduced-motion: reduce) {
  .ecc-ticker { height: auto; padding: 14px 16px; }
  .ecc-ticker__view { overflow: visible; padding-left: 0; -webkit-mask-image: none; mask-image: none; }
  .ecc-ticker__track { flex: 1; flex-wrap: wrap; row-gap: 10px; white-space: normal; }
  .ecc-ticker__track > [data-ecc-dup] { display: none; }
  .ecc-ticker__track > :nth-last-child(1 of :not([data-ecc-dup]))::after { display: none; }
  .ecc-ticker__ctl { display: none; }
}

/* ========================================================= PAGE TRANSITION */
/* Between pages: a 160ms crossfade where the browser supports cross-document view transitions,
   a plain navigation everywhere else. Nothing waits for an exit animation.
   The group is set as well as the two snapshots: left alone it keeps the browser's 250ms and the
   new page stays under the transition that much longer. */
@media (prefers-reduced-motion: no-preference) {
  @view-transition { navigation: auto; }
}
::view-transition-group(root),
::view-transition-old(root),
::view-transition-new(root) { animation-duration: var(--ecc-d-fast); animation-timing-function: linear; }

/* ======================================================= THE MOTION SWITCH */
/* <html data-ecc-motion="paused">: the visitor pressed the site's "Pause motion" switch (the pixel kit
   sets the attribute and remembers it; no attribute means motion is on). While it is on pause nothing
   starts by itself and the current does not run: a first screen is simply on, the ticker holds where it
   is and its own control steps aside (one switch governs it now), no pulse runs a rule, a floor line or
   an answer line. What a visitor touches still answers: colour, the 1px press, the card lift, the
   underline. The layout does not change, so turning the switch never moves the page.
   motion.js follows the same attribute and the "pk:motionchange" event (motion.md 15). */
html[data-ecc-motion="paused"] .ecc-boot[data-ecc-boot] [data-ecc-module],
html[data-ecc-motion="paused"] .ecc-boot[data-ecc-boot] [data-ecc-module] .ecc-pip,
html[data-ecc-motion="paused"] .ecc-rule.is-live::after,
html[data-ecc-motion="paused"] .ecc-floorline.is-live::after,
html[data-ecc-motion="paused"] .ecc-lift .ecc-floorline::after { animation: none; }
html[data-ecc-motion="paused"] .ecc-ticker .ecc-ticker__track { animation-play-state: paused; }
html[data-ecc-motion="paused"] .ecc-ticker__ctl { visibility: hidden; }
