From 641657dbceb4c8584800e77bf8a222711d7d3102 Mon Sep 17 00:00:00 2001 From: Cornelius Ukena Date: Fri, 28 Aug 2026 16:19:56 -0500 Subject: [PATCH 1/2] feat(css): add CSS-only scroll reveal system Replace JS data-animate usage with a zero-JS reveal system built on scroll-driven animations and container style queries: - enter: variant hides any utility's declarations until the element's parent enters the viewport; sensing is ambient, so no trigger class exists and grids fire row by row automatically - presets (fade, slide-*, zoom), stagger utilities including live column-count cycling backed by grid-fill math, and composition with core duration/ease/delay through their published vars - scroll-latch opts a group into one-shot reveals; replay on re-entry is the default, consistent in every browser - tiers via @supports: reduced-motion and print get static content, Firefox gets a load-time fade, scroll-driven-animation browsers get scroll-triggered reveals Templates migrate from data-animate to enter:* classes; the home template carries hard-coded demo patterns for every mechanism. --- src/css/app.css | 1 + src/css/utilities/grid-fill.css | 43 +++ src/css/utilities/scroll-trigger.css | 377 +++++++++++++++++++++++++++ templates/README.md | 32 ++- templates/blocks/_billboard.twig | 8 +- templates/blocks/_group.twig | 2 +- templates/blocks/_imageText.twig | 10 +- templates/blocks/_text.twig | 6 +- templates/common/_footer.twig | 4 +- templates/headers/_header.twig | 2 +- templates/home/_entry.twig | 128 +++++++++ 11 files changed, 580 insertions(+), 33 deletions(-) create mode 100644 src/css/utilities/scroll-trigger.css diff --git a/src/css/app.css b/src/css/app.css index b9fcfd0..acc4925 100644 --- a/src/css/app.css +++ b/src/css/app.css @@ -10,6 +10,7 @@ @import '$css/utilities/grid-fill.css'; @import '$css/utilities/misc.css'; @import '$css/utilities/parallax.css'; +@import '$css/utilities/scroll-trigger.css'; @import '$css/utilities/viewport.css'; @import '$css/components/drawer.css'; diff --git a/src/css/utilities/grid-fill.css b/src/css/utilities/grid-fill.css index 3a4c6d4..85ef80d 100644 --- a/src/css/utilities/grid-fill.css +++ b/src/css/utilities/grid-fill.css @@ -1,3 +1,46 @@ +/* Auto-fill grid. Also publishes what the scroll-trigger stagger + utilities need: the live column count and each child's row + position (--stagger-context). Both are computed on the direct + children, not the grid, because container units resolve against + ancestor containers only; substituting there also locks in the + grid's own gap (from the gap-* shim in scroll-trigger.css) + before a descendant's gap-* could shadow it. + + The count uses the browser's own track math: + floor((width + gap) / (min + gap)). The division runs through + tan(atan2()) because calc() cannot divide lengths in Firefox, + and BOTH operands are pre-resolved through registered + properties because Chromium's atan2() silently strips relative + units (rem, cqw) from raw operands. */ + +@property --grid-fill-avail { + syntax: ''; + inherits: false; + initial-value: 0px; +} + +@property --grid-fill-track { + syntax: ''; + inherits: false; + initial-value: 1px; +} + +@property --grid-fill-count { + syntax: ''; + inherits: true; + initial-value: 1; +} + @utility grid-fill-* { + --grid-fill-min: --spacing(--value(integer)); + + container-type: inline-size; grid-template-columns: repeat(auto-fill, minmax(min(--spacing(--value(integer)), 100%), 1fr)); + + & > * { + --grid-fill-avail: calc(100cqw + var(--tw-gap, 0rem)); + --grid-fill-track: calc(var(--grid-fill-min) + var(--tw-gap, 0rem)); + --grid-fill-count: max(1, round(down, tan(atan2(var(--grid-fill-avail), var(--grid-fill-track))), 1)); + --stagger-context: calc(sibling-index() - 1); + } } diff --git a/src/css/utilities/scroll-trigger.css b/src/css/utilities/scroll-trigger.css new file mode 100644 index 0000000..eb6e794 --- /dev/null +++ b/src/css/utilities/scroll-trigger.css @@ -0,0 +1,377 @@ +/* ============================================================ + Scroll-triggered one-shot reveals. Zero JS. + + Any element hides itself with `enter:*` utilities (any + property) and transitions to its real styles the first time its + PARENT enters the viewport. No trigger class exists: sensing is + ambient and always on; the parent of animated content becomes + the sensor automatically. A grid therefore fires row by row: + +
    +
  • +
    + + Or use a preset behind the variant (fade built in): enter:fade, + enter:slide-up, enter:slide-down, enter:slide-left, + enter:slide-right, enter:zoom: + +
    + + Reveals replay on re-entry by default, in every browser. + One-shot is opt-in via `scroll-latch`: on an existing wrapper it + absorbs that wrapper's trigger duty and moves sensing up one + level; or insert a latch element where no spare wrapper exists. + See the Latch section. (Native animation-trigger play-once could + one-shot Chrome 146+ without a latch, but that would make + one-shot behavior inconsistent across browsers; deliberately not + used.) + + Custom display:contents elements cannot sense (no box, dead + timeline); picture and x-svelte are handled by promoting their + parent, others need a real box or an explicit wrapper. + + Tiers, auto-selected via @supports: + 1. Reduced motion or print: static, visible content. + 2. No scroll-driven animations (Firefox): a generic fade-in + plays once shortly after page load instead of on scroll. + Firefox style queries ignore animation-driven custom + properties, so the per-element hidden states cannot be used + there. + 3. Scroll-driven animations + style queries (Chrome, Safari 26): + scroll-triggered; replays on re-entry unless the content sits + inside an explicit scroll-latch. + + Safety invariant: `enter:*` output only matches inside + `@container scroll-trigger style(--in-view: false)`, and only a + gated, working sensor ever sets that flag or names that + container. Unsupported browsers, reduced motion, and print all + resolve to plain visible content. + ============================================================ */ + +@property --stagger { + syntax: ''; + inherits: true; + initial-value: 0; +} + +/* Published by grid-fill-* on its direct children, so stagger + utilities on only-child content deeper in the item still know + where the item sits. Never publish it from more than one element + per chain (a latch as an only child would clobber it to 0). The + registration doubles as the fallback for browsers + without sibling-index(): the calc() is rejected and the value + stays 0. */ +@property --stagger-context { + syntax: ''; + inherits: true; + initial-value: 0; +} + +/* Core `delay-*` emits only transition-delay; re-emit it with a + composable var, mirroring core's own --tw-duration / --tw-ease. */ +@property --tw-delay { + syntax: '*'; + inherits: false; +} + +@utility delay-* { + --tw-delay: calc(--value(integer) * 1ms); + --tw-delay: --value(--transition-delay, [*]); + + transition-delay: calc(--value(integer) * 1ms); + transition-delay: --value(--transition-delay, [*]); +} + +/* Same idea for the gap: core emits no var, and the grid-fill + column math (see grid-fill.css) needs to read it. Core gap-* / + gap-x-* still emit alongside these. */ +@utility gap-* { + --tw-gap: --spacing(--value(integer)); + --tw-gap: --value([*]); +} + +@utility gap-x-* { + --tw-gap: --spacing(--value(integer)); + --tw-gap: --value([*]); +} + +/* ============================================================ + Detector + ============================================================ */ + +@keyframes scroll-trigger { + to { + --in-view: true; + } +} + +/* Sensing is ambient and ALWAYS ON: everywhere in the document, + whatever directly parents animated content (or a latch, or + animated content behind a display:contents wrapper like + picture / x-svelte) becomes a sensor, and its content fires when + IT enters the viewport. So a grid fires row by row (each li + senses), a flat group fires off its wrapper, and a section's + content fires off its innermost wrapper, with no trigger class + anywhere. + + Never sensors: body itself (a direct enter:* child of body needs + a wrapper), latches (they absorb their parent's trigger duty + instead, see below), animated elements themselves (an element + cannot query its own flag), display:contents elements (no box, + dead timeline; their parent is promoted instead), and elements + with their own animate-* utilities (the sensor animation would + clobber them; their content queries the next sensor up). + + Sensing is also suspended INSIDE latches: a latch is a one-shot + boundary, and its entire subtree binds to the latch's flag as + one group. Without this, any wrapper between the latch and its + content would become a sensor and silently bypass the latch. */ +body :has(> [class*='enter:'], > .scroll-latch, > :is(picture, x-svelte) > [class*='enter:']):not(:is(.scroll-latch, .scroll-latch *, [class*='enter:'], picture, x-svelte, [class*='animate-'])) { + @media (prefers-reduced-motion: no-preference) { + @supports (animation-timeline: view()) and (animation-range: entry) { + --in-view: false; + + container-name: scroll-trigger; + animation: scroll-trigger steps(1, jump-end) both; + animation-timeline: view(block); + animation-range: entry 0% entry var(--scroll-trigger-at, 40%); + } + } + + /* No container -> query unknown -> nothing hides in print. */ + @media print { + container-name: none; + } +} + +/* Threshold knob; the custom property inherits, so place it on any + ancestor of the sensors it should tune, or on the body for a + site-wide default. */ +@utility scroll-trigger-* { + --scroll-trigger-at: calc(--value(integer) * 1%); +} + +/* ============================================================ + The variant + ============================================================ */ + +@custom-variant enter { + @container scroll-trigger style(--in-view: false) { + @slot; + } +} + +/* ============================================================ + Presets + Convenience hidden states for common entrances, meant to be + used behind the enter: variant (enter:slide-up, enter:zoom). + All include a fade. They compose (enter:zoom enter:blur-sm + with core blur), except that slide directions share `translate`, + so use one direction per element. For custom distances or + scales, use the primitives (enter:translate-y-16, + enter:scale-75) instead. + + These are plain hidden-state declarations: used bare, without a + variant, they hide the element unconditionally. + ============================================================ */ + +@utility fade { + opacity: 0; +} + +@utility slide-up { + opacity: 0; + translate: 0 --spacing(8); +} + +@utility slide-down { + opacity: 0; + translate: 0 --spacing(-8); +} + +@utility slide-left { + opacity: 0; + translate: --spacing(8) 0; +} + +@utility slide-right { + opacity: 0; + translate: --spacing(-8) 0; +} + +@utility zoom { + opacity: 0; + scale: 90%; +} + +/* ============================================================ + Reveal mechanics, auto-attached to any element using enter:* + (matches the class attribute text; do not use @apply with + enter:* utilities). + + Gated like the detector: browsers without scroll-driven + animations (and reduced-motion users) must not get all-property + transitions on these elements, or unrelated restyles (dev-mode + CSS injection, breakpoint changes) animate on them. + + Unlayered on purpose: it must beat the base-layer transition + defaults, while core duration-* / ease-* / delay-* still win + through their vars. + ============================================================ */ + +@media (prefers-reduced-motion: no-preference) { + @supports (animation-timeline: view()) and (animation-range: entry) { + :where([class*='enter:']) { + transition-property: all; + transition-duration: var(--tw-duration, 500ms); + transition-timing-function: var(--tw-ease, var(--ease-out)); + transition-delay: var(--tw-delay, calc(var(--stagger, 0) * var(--stagger-step, 100ms))); + } + + /* Anything that ends in the hidden state applies instantly: + the initial dev-mode CSS injection, breakpoint changes, + and mid-reveal interrupts all have pending as their + destination and must not animate into it. The reveal + itself is unaffected; its transition spec is read from + the visible base state above. */ + @container scroll-trigger style(--in-view: false) { + :where([class*='enter:']) { + transition-property: none; + } + } + } + + /* No scroll timelines (Firefox): play a generic entrance once, + shortly after load, instead of the scroll-triggered reveal. + Per-element hidden states cannot drive this tier: Firefox + style queries ignore animation-driven custom property + values, so the pending query would match forever and leave + content hidden. A plain keyframe animation needs no query; + duration, easing, delay, and stagger still apply via their + vars, and `both` fill keeps elements hidden through their + delay. */ + @supports not (animation-timeline: view()) { + :where([class*='enter:']) { + animation: enter-fade var(--tw-duration, 500ms) var(--tw-ease, var(--ease-out)) both; + animation-delay: calc(150ms + var(--tw-delay, calc(var(--stagger, 0) * var(--stagger-step, 100ms)))); + } + } +} + +@keyframes enter-fade { + from { + opacity: 0; + } +} + +/* ============================================================ + Latch (opt-in one-shot) + Makes reveals one-shot, consistently in every browser. A latch + is never a sensor; instead its PARENT is promoted to sensor, + and sensing is suspended inside it: `enter:*` content at ANY + depth within the latch binds to the latch's flag and fires as + one group. So: + + - on an existing wrapper, it absorbs that wrapper's trigger duty + and moves sensing up one level (zero new elements): + +
    +
    +
    + + - in a grid, latching the li moves sensing to the ul (whole grid + fires as one group); to keep row-by-row AND one-shot, insert + the latch inside each li instead: + +
  • +
    +
    + + Placement rules: a latch fires when its PARENT enters, so put it + on a wrapper INSIDE the group whose entry should time it (making + that group the sensor), never on the group itself, and never on + sections or page landmarks: sibling sections share one tall + parent, so latching them makes everything fire together off that + parent, and a latch directly under body has no sensor at all and + simply disables its subtree's reveals. + + A 1ms paused animation runs once when the wrapper's flag flips + and re-publishes `--in-view: true` permanently (a finished + animation stays finished), shadowing the wrapper's reversible + flag for everything inside. + + Do NOT latch by putting an infinite transition-delay on the + pending state instead: every style change that lands while + pending (dev-mode CSS injection, breakpoint changes, htmx/Sprig + swaps) reads that delay from its destination state and freezes + forever. + ============================================================ */ + +@keyframes scroll-latch { + to { + --in-view: true; + } +} + +@utility scroll-latch { + @media (prefers-reduced-motion: no-preference) { + @supports (animation-timeline: view()) and (animation-range: entry) { + container-name: scroll-trigger; + animation: scroll-latch 1ms linear forwards paused; + + @container scroll-trigger style(--in-view: true) { + animation-play-state: running; + } + } + } + + /* Same print escape as the trigger: without it, latched content + resolves its pending state against the latch and prints blank + for sections the reader never scrolled to. */ + @media print { + container-name: none; + } +} + +/* ============================================================ + Stagger + Always applied to the animating element. `stagger` counts the + element's own position among its siblings, plus the inherited + `--stagger-context` that grid-fill-* publishes from each grid + child, so it works both for flat sibling groups and for grid + items whose animating element is an only child of its cell. + + `stagger-n-*` cycles the delays: with stagger-n-3, siblings 1-3 + get steps 0-2 and siblings 4-6 repeat the same steps. (The + hyphen is required; Tailwind functional utilities must end in + `-*`, so `stagger-n3` is not expressible.) + + In grids, same-row cells sense on the same frame (equal top + edge and, via align-items: stretch, equal height), so a cycled + stagger (stagger-fill / stagger-n-*) reads as a left-to-right + sweep per row. + ============================================================ */ + +@utility stagger { + --stagger: calc(var(--stagger-context, 0) + sibling-index() - 1); +} + +@utility stagger-* { + --stagger: --value(integer); +} + +@utility stagger-n-* { + --stagger: mod(var(--stagger-context, 0) + sibling-index() - 1, --value(integer)); +} + +/* Cycle by the grid's CURRENT column count: inside a grid-fill-* + grid, each row sweeps with the same delays at every width, with + no per-breakpoint classes. Reads --grid-fill-count, computed by + grid-fill.css from the real column math. */ +@utility stagger-fill { + --stagger: mod(var(--stagger-context, 0) + sibling-index() - 1, var(--grid-fill-count, 1)); +} + +@utility stagger-step-* { + --stagger-step: calc(--value(integer) * 1ms); +} diff --git a/templates/README.md b/templates/README.md index b9563c3..48ff2d3 100644 --- a/templates/README.md +++ b/templates/README.md @@ -163,9 +163,9 @@ Each block type follows consistent patterns: ```twig {# Conditional rendering based on content #} {% if block.content|plain %} -
    +
    - +
    {% endif %} @@ -175,7 +175,7 @@ Each block type follows consistent patterns: - Content existence checks using `|plain` filter - Color theming via `swatch()` function -- Animation attributes with `data-animate` +- Scroll reveals via `enter:*` classes (sensing is automatic; `scroll-latch` opts into one-shot) - Responsive padding classes - Semantic HTML structure @@ -248,26 +248,24 @@ Consistent theming across all components: ### Animation Integration -Consistent animation attributes for frontend JavaScript using Motion One library: +CSS-only scroll reveals via the utilities in `src/css/utilities/scroll-trigger.css` (no JavaScript): ```twig -
    -
    -
    -
    +
    +
    +

    +
    +

    ``` **Animation System Features:** -- **Property Syntax**: `property: startValue, endValue` (endValue defaults to 0 if omitted) -- **Multiple Properties**: Separated by semicolons (`;`) -- **Default Opacity**: Automatically adds `opacity: 0, 1` if not specified -- **Timing Controls**: - - `data-animate-duration="0.5"` - Animation duration in seconds - - `data-animate-delay="0.2"` - Delay before animation starts - - `data-animate-ease="easeOut"` - Easing function - - `data-animate-repeat` - Repeats animation when re-entering viewport -- **Smart Image Loading**: Waits for image load before animating +- **Trigger**: none needed; sensing is always on. Every `enter:*` element fires when its parent enters the viewport (grids fire row by row automatically). `scroll-trigger-60` tunes the entry threshold and may sit on any ancestor, including the body for a site-wide default. +- **Hidden States**: any utility behind the `enter:` variant, or presets `enter:fade`, `enter:slide-up/down/left/right`, `enter:zoom` (all include the fade) +- **Timing**: core `duration-*`, `ease-*`, `delay-*` classes on the revealing element +- **Stagger**: `stagger` (DOM order), `stagger-N` (explicit), `stagger-n-N` (cycle every N), `stagger-fill` (cycle by a `grid-fill-*` grid's live column count), `stagger-step-N` (ms per step), always on the revealing element +- **One-Shot**: reveals replay on re-entry by default in every browser; opt in to one-shot with `scroll-latch` on the wrapper above the content (its parent takes over sensing) or an inserted latch element +- **Degradation**: Firefox plays a generic fade-in after load; reduced-motion and print get static visible content ### Responsive Design Patterns diff --git a/templates/blocks/_billboard.twig b/templates/blocks/_billboard.twig index ed8cd07..9817586 100644 --- a/templates/blocks/_billboard.twig +++ b/templates/blocks/_billboard.twig @@ -1,7 +1,7 @@ {% set backgroundImage = block.backgroundImage.eagerly().one %} {% if backgroundImage is not null %} -

    +
    {{ image(backgroundImage, { fit: 'crop', width: 800, @@ -20,18 +20,18 @@
    -

    +

    {{ block.heading|heading }}

    {% if block.excerpt|plain %} -
    +
    {{ block.excerpt }}
    {% endif %} {{ link(block.button, { class: 'mt-2 button button-white', }, 'div')|attr({ - data: { animate: true }, + class: 'enter:fade delay-300', }) }}
    diff --git a/templates/blocks/_group.twig b/templates/blocks/_group.twig index 4f6e0a5..7f85998 100644 --- a/templates/blocks/_group.twig +++ b/templates/blocks/_group.twig @@ -1,4 +1,4 @@ -
    +
    {{ include('blocks/_entry', { blocks: block.nested, diff --git a/templates/blocks/_imageText.twig b/templates/blocks/_imageText.twig index 72581bd..9d51888 100644 --- a/templates/blocks/_imageText.twig +++ b/templates/blocks/_imageText.twig @@ -1,5 +1,5 @@ {% set image %} -
    +
    {{ image(block.featuredImage.eagerly().one, { fit: 'crop', width: 430, @@ -26,23 +26,23 @@
    {% endset %} -
    +
    {{ not block.isFlipped ? image }}
    -

    +

    {{ block.heading|heading }}

    -
    +
    {{ block.body }}
    {{ link(block.button, { class: 'button button-blue', }, 'div')|attr({ - data: { animate: true }, + class: 'enter:fade delay-300', }) }}
    diff --git a/templates/blocks/_text.twig b/templates/blocks/_text.twig index 869bf53..fd13bcf 100644 --- a/templates/blocks/_text.twig +++ b/templates/blocks/_text.twig @@ -1,16 +1,16 @@ {% if block.richText|plain or block.heading|plain %} -
    +
    {% if block.heading|plain %} -
    +

    {{ block.heading|heading }}

    {% endif %} {% if block.richText|plain %} -
    +
    {{ block.richText|media(976) }}
    {% endif %} diff --git a/templates/common/_footer.twig b/templates/common/_footer.twig index aacf94e..9e4e76c 100644 --- a/templates/common/_footer.twig +++ b/templates/common/_footer.twig @@ -4,8 +4,8 @@ .one %}
    -
    -
    +
    +
    diff --git a/templates/headers/_header.twig b/templates/headers/_header.twig index 785d20f..8dddf02 100644 --- a/templates/headers/_header.twig +++ b/templates/headers/_header.twig @@ -11,7 +11,7 @@
    -
    +

    {{ block.heading|default(entry.title)|heading }}

    diff --git a/templates/home/_entry.twig b/templates/home/_entry.twig index 9026be4..b1821df 100644 --- a/templates/home/_entry.twig +++ b/templates/home/_entry.twig @@ -9,4 +9,132 @@ nested: false, branded: true, }) }} + + {# ========================================================================= + Scroll-trigger demo + Hard-coded examples for the scroll reveal (`enter:*`) utilities in + src/css/utilities/scroll-trigger.css. Sections 3-6 are one-shot: a + scroll-latch on the content wrapper INSIDE each section makes the + section itself the sensor and latches everything within. The hero + and the row-by-row grid replay on re-entry. Latches always go on a + wrapper inside the group, never on the section (a latch fires when + its PARENT enters, so latching the section hands timing to the tall + shared container). Remove this section once real content uses the + pattern. + ========================================================================= #} +
    + + {# 1. Basic enter: fade and rise, fires on load when above the fold #} +
    +

    + Scroll reveals, no JavaScript +

    +

    + Everything below animates in once, the first time it enters the viewport. +

    +
    + + {# 2. Card grid: ambient sensing means each li (the parent of its + card) fires as ITS row enters the viewport, and stagger-fill + sweeps the columns by the grid's live column count. No classes + on the li at all. Rows replay on re-entry; see the presets grid + below for the one-shot latch pattern. #} +
    +

    Staggered grid, row by row

    +
      + {% for index in 1..6 %} +
    • +
      +

      Card {{ index }}

      +

      + Rows fire on entry; columns sweep by live count. +

      +
      +
    • + {% endfor %} +
    +
    + + {# 3. Per-child choreography: one latched wrapper, each child times + itself. The section is the sensor; scroll-trigger-60 on it + waits until 60% of the section has entered. #} +
    +

    Choreographed figure

    +
    + Placeholder landscape +
    + The image desaturates and settles first, then this caption slides in. +
    +
    +
    + + {# 4. Arbitrary properties: color sweep, letter-spacing, bar width. + Any utility works inside enter:*, not just opacity and transform. #} +
    +

    Beyond opacity

    +
    +

    + This panel reveals by transitioning background and text color. +

    +

    + Letter-spacing tightens into place +

    +
    +
    +
    +
    +
    + + {# 5. Presets: convenience hidden states behind the enter: variant, + all with fade built in. Composable with core utilities, e.g. + enter:zoom enter:blur-sm; custom distances use the primitives, + e.g. enter:fade enter:translate-y-16. The ul carries + scroll-latch, so the SECTION is the sensor and the whole grid + fires as one staggered composition (stagger-n-3 cycles the + delays in threes) and stays revealed on every browser. #} +
    +

    Presets

    +
      + {% for preset in [ + 'enter:fade', + 'enter:slide-up', + 'enter:slide-down', + 'enter:slide-left', + 'enter:slide-right', + 'enter:zoom', + 'enter:zoom enter:blur-sm', + 'enter:fade enter:translate-y-16', + ] %} +
    • +
      +

      {{ preset }}

      +
      +
    • + {% endfor %} +
    +
    + + {# 6. Explicit stagger and timing control: manual stagger-* values, + a wider stagger-step, and an easing override #} +
    +

    Manual timing

    +
    + {% for stagger in ['stagger-0', 'stagger-1', 'stagger-2', 'stagger-3', 'stagger-4'] %} + + {{ stagger }} + + {% endfor %} +
    +
    + +
    {% endblock %} From 769d2be5ca539fce6d071659034ead40f6fbfdca Mon Sep 17 00:00:00 2001 From: Cornelius Ukena Date: Fri, 28 Aug 2026 16:22:27 -0500 Subject: [PATCH 2/2] chore(templates): drop scroll reveal demos from the home template --- templates/home/_entry.twig | 128 ------------------------------------- 1 file changed, 128 deletions(-) diff --git a/templates/home/_entry.twig b/templates/home/_entry.twig index b1821df..9026be4 100644 --- a/templates/home/_entry.twig +++ b/templates/home/_entry.twig @@ -9,132 +9,4 @@ nested: false, branded: true, }) }} - - {# ========================================================================= - Scroll-trigger demo - Hard-coded examples for the scroll reveal (`enter:*`) utilities in - src/css/utilities/scroll-trigger.css. Sections 3-6 are one-shot: a - scroll-latch on the content wrapper INSIDE each section makes the - section itself the sensor and latches everything within. The hero - and the row-by-row grid replay on re-entry. Latches always go on a - wrapper inside the group, never on the section (a latch fires when - its PARENT enters, so latching the section hands timing to the tall - shared container). Remove this section once real content uses the - pattern. - ========================================================================= #} -
    - - {# 1. Basic enter: fade and rise, fires on load when above the fold #} -
    -

    - Scroll reveals, no JavaScript -

    -

    - Everything below animates in once, the first time it enters the viewport. -

    -
    - - {# 2. Card grid: ambient sensing means each li (the parent of its - card) fires as ITS row enters the viewport, and stagger-fill - sweeps the columns by the grid's live column count. No classes - on the li at all. Rows replay on re-entry; see the presets grid - below for the one-shot latch pattern. #} -
    -

    Staggered grid, row by row

    -
      - {% for index in 1..6 %} -
    • -
      -

      Card {{ index }}

      -

      - Rows fire on entry; columns sweep by live count. -

      -
      -
    • - {% endfor %} -
    -
    - - {# 3. Per-child choreography: one latched wrapper, each child times - itself. The section is the sensor; scroll-trigger-60 on it - waits until 60% of the section has entered. #} -
    -

    Choreographed figure

    -
    - Placeholder landscape -
    - The image desaturates and settles first, then this caption slides in. -
    -
    -
    - - {# 4. Arbitrary properties: color sweep, letter-spacing, bar width. - Any utility works inside enter:*, not just opacity and transform. #} -
    -

    Beyond opacity

    -
    -

    - This panel reveals by transitioning background and text color. -

    -

    - Letter-spacing tightens into place -

    -
    -
    -
    -
    -
    - - {# 5. Presets: convenience hidden states behind the enter: variant, - all with fade built in. Composable with core utilities, e.g. - enter:zoom enter:blur-sm; custom distances use the primitives, - e.g. enter:fade enter:translate-y-16. The ul carries - scroll-latch, so the SECTION is the sensor and the whole grid - fires as one staggered composition (stagger-n-3 cycles the - delays in threes) and stays revealed on every browser. #} -
    -

    Presets

    -
      - {% for preset in [ - 'enter:fade', - 'enter:slide-up', - 'enter:slide-down', - 'enter:slide-left', - 'enter:slide-right', - 'enter:zoom', - 'enter:zoom enter:blur-sm', - 'enter:fade enter:translate-y-16', - ] %} -
    • -
      -

      {{ preset }}

      -
      -
    • - {% endfor %} -
    -
    - - {# 6. Explicit stagger and timing control: manual stagger-* values, - a wider stagger-step, and an easing override #} -
    -

    Manual timing

    -
    - {% for stagger in ['stagger-0', 'stagger-1', 'stagger-2', 'stagger-3', 'stagger-4'] %} - - {{ stagger }} - - {% endfor %} -
    -
    - -
    {% endblock %}