Future Vision / Zero Limits
Industrial Design Systems

System diagnostic / This browser

Measuring what this browser can do…

Full diagnostic

Control / Scene state

One set of values every module binds to, shared live across pages and tabs.

Appearance
All modules

Module 01 / CSS MOTION

Motion without JavaScript

Calm motion, declared in CSS

FVZL—FS01Module 01Live since 2026-10-08
Fig. 01 / Live stage Open stage

Loading stage…

Module notes

The FVZL site moves the way its products do: calmly. Turn a camera, count a watch's parts, take the headphones apart and page through the line-up, all with motion declared in CSS and enhanced only where the browser supports it.

This browser
checking…
Native path
Native CSS — no animation library needs Scroll-driven animation + Anchor positioning + View Transitions + Cross-page transitions
Fallback
Static plates, same content needs nothing extra
Status
Live since 2026-10-08
Browser support
FeatureChromeSafariFirefox
Scroll-driven animation11526Not yet
Anchor positioning12526147, partial
View transitions11118144
Cross-page transitions12618.2144, partial
Popover + @starting-style11717.5129
@property8516.4128
First version with each feature · caniuse, 2026-10-08
Tags
cssmotion

How this module works

The stage is plain HTML and one stylesheet. Every movement on it is declared in CSS and run by the browser itself, tied to scroll, to an anchor or to a change of state. Turn JavaScript off and all four plates still work; the slides just change without the move.

With Reduce Motion on, or in a browser without scroll-driven animation (Firefox 157), the same plates hold still: a contact sheet of all twelve angles, the final figures, slides that swap.

  1. Fig. 01.1 / Scroll timeline

    The turn

    The FK01’s plate is three stage-widths wide and starts in its middle, and its picture is sticky, so scrolling either way turns the camera instead of moving it: a full turn each side. A registered integer counts 0 → 24 along the plate’s view timeline, and each of the 12 renders shows when the count, modulo 12, lands on its number. Stepping the animation keeps every engine on whole frames (Safari would otherwise blend two).

    @property --frame { syntax: "<integer>"; inherits: true; initial-value: 0; }
    
    .plate-turn  { view-timeline: --turn inline; }
    .turn-frames {
      animation: turn steps(25, jump-none) both;
      animation-timeline: --turn;
      animation-range: contain 0% contain 100%;
    }
    .turn-frames img { /* 1 on its own angle, 0 on every other */
      --d: calc(mod(var(--frame), 12) - var(--i));
      opacity: max(0, 1 - var(--d) * var(--d));
    }
    @keyframes turn { to { --frame: 24; } }
  2. Fig. 01.2 / View timeline

    The count

    animation-range: entry ties an animation to the plate’s arrival rather than to the clock. A counter prints a registered integer, so the FW01’s figures count up with no script, and each module row starts a little later than the one before.

    .count {
      --n: var(--to);              /* the real figure, e.g. 550 */
      counter-reset: n var(--n);
      animation: count linear both;
      animation-timeline: --sheet;
      animation-range: entry 15% entry 95%;
    }
    .count::before { content: counter(n); }
    @keyframes count { from { --n: 0; } }
  3. Fig. 01.3 / Anchor positioning

    Take it apart

    Scrolling plays the FH01’s explode: 20 frames rendered from its explode animation, picked by the same kind of view timeline as the turn. Once it’s apart, the pins arrive. Each is a button with popovertarget, so it opens its note without script; the note sits in the top layer, tethered to its pin, and the browser tries the fallbacks when there’s no room above. @starting-style gives it a state to fade in from. The pins are placed from the parts manifest, projected through the render camera at build time.

    <button popovertarget="note-power" style="anchor-name: --pin-power">
    <div popover id="note-power" style="position-anchor: --pin-power">
    
    .note {
      position-area: top;
      position-try-fallbacks: flip-block, --right, --left;
      transition: opacity .2s, translate .3s,
        display .3s allow-discrete, overlay .3s allow-discrete;
    }
    @starting-style {
      .note:popover-open { opacity: 0; translate: 0 6px; }
    }
  4. Fig. 01.4 / View transition

    The line-up

    All seven FVZL products on a carousel projector. The one movement that needs script: startViewTransition() takes a snapshot of the old slide, lets the page change, then animates between the two. Everything about the move is CSS: its direction, and its tempo, which follows Energy.

    document.startViewTransition(() => show(next));
    
    .proj-frame { view-transition-name: slide; }
    ::view-transition-old(slide) {
      animation: var(--vt-dur) var(--ease-calm) both slide-out;
    }
    ::view-transition-new(slide) {
      animation: var(--vt-dur) var(--ease-calm) both slide-in;
    }
  5. Fig. 01.5 / Scene state

    The control panel, in CSS

    The Control panel writes three custom properties. CSS reads them directly: Signal tints the pins, needle and code, Light warms the studio, and Energy sets the tempo. The readout in the stage’s corner formats the values with round(), mod() and counters.

    /* 0 at noon → 1 at midnight */
    --warmth: calc(max(var(--scene-time) - .5, .5 - var(--scene-time)) * 2);
    filter: sepia(calc(var(--warmth) * .45))
            brightness(calc(1 - var(--warmth) * .3));
    
    /* registered <integer>s, printed by counters */
    --m:  calc(mod(round(var(--scene-time) * 1440), 1440));
    --hh: calc(round(down, var(--m) / 60, 1));
    --mm: calc(mod(var(--m), 60));
    counter-reset: hh var(--hh) mm var(--mm);
  6. Fig. 01.6 / This page

    The site itself

    The same features run the site around the stage: the card you opened grew into this page through a cross-page view transition, the hairline along the top of the window is a scroll timeline, and the Status and Control panels are anchored popovers.

    @view-transition { navigation: auto; }
    
    .scroll-progress {
      animation: grow linear both;
      animation-timeline: scroll(root block);
    }