game room title: GAME ROOM ⇄ GAME SCROLL reel on the scroll aperture — TDD
Some checks failed
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline failed

The h2's second slot becomes a two-word vertical reel: GAME stays put, ROOM rests in view, SCROLL is parked one notch below in the slot's bottom fade. room-scroll.js toggles `.is-scroll` on the h2 from the SAME IntersectionObserver that already watches the table-hex aperture's scroll pane — ROOM slides up & out under the navbar line while SCROLL rises out of the page-aperture gradient (reverses on scroll-up). Table-phase only; the gate phase stays a plain GAME ROOM.

One translateY drives both orientations. Portrait: the word is a short horizontal row in a short slot. Landscape: writing-mode: vertical-rl (inherited from the rotated gutter wordmark) makes the word a tall letter-column, so the same translateY slides it ALONG the wordmark — the user-chosen landscape behaviour for free. Landscape uses a shallower --gr-fade + a letter inset so the space-between end-letters parked at the slot edges aren't dimmed by the dissolve.

Motion is deliberately old & rusty: a single cubic-bezier can overshoot at most once and can't oscillate, so the easing is a CSS linear() curve — stall against the grime, jerk free, clunk PAST the mark, then a damped end-wobble into place. Exposed as --gr-ease / --gr-dur / --gr-fade knobs on .gr-swap.

base.html's letter-splitter now also splits the two .gr-word words; the .gr-swap window ships data-letters-split="1" so the splitter skips it (no 'roomscroll' run). Reel SCSS is scoped to .gr-swap/.gr-word; `> span.gr-swap` ties `> span:last-child` at (0,4,3) and wins on later source order [[feedback-scss-import-order-specificity]].

TRAP: libsass does NOT strip `//` comments INSIDE a CSS custom-property value — they leak into the compiled output, making the linear() (hence the whole `transition` shorthand) invalid-at-computed-value-time, which silently resets to 0s/ease (no animation). Keep every annotation OUTSIDE the linear(). [[feedback-libsass-comment-in-custom-property]]

Reusable .gr-swap seam: my_sea gets GAME SEA → GAME SCROLL via a one-line header swap once its sea-scroll pane is built (deferred — the sea scroll doesn't exist yet).

Tests: 2 ITs (RoomScrollOfEventsTest) — reel markup renders in the table phase, stays plain in the gate phase; 1 FT (test_scroll_swaps_room_title_to_scroll) — scrolling the aperture toggles GAME ROOM ⇄ GAME SCROLL both ways. collectstatic'd room-scroll.js for the FT [[feedback-collectstatic-before-ft]].

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-06-02 01:05:00 -04:00
parent 114f0fd0db
commit cf1965c439
6 changed files with 187 additions and 6 deletions

View File

@@ -277,6 +277,89 @@ body {
> span[data-letters="3"] {
justify-content: space-around;
}
// ── GAME ROOM ⇄ GAME SCROLL title reel ─────────────────
// The second word becomes a two-word vertical reel inside
// the 55% title slot. The base word (ROOM) rests in view;
// SCROLL is parked one notch below, in the slot's bottom
// fade. room-scroll.js toggles `.is-scroll` on this <h2>
// when the table-hex aperture snaps to the provenance feed:
// ROOM slides up & out under the navbar line while SCROLL
// rises out of the bottom gradient — one reel, both words
// travelling up. Reverses on scroll-up.
//
// The slide axis is vertical in BOTH orientations: portrait
// the word is a short horizontal row; landscape (writing-
// mode: vertical-rl, inherited from the rotated wordmark)
// the word is a tall letter-column, so the same translateY
// slides it ALONG the wordmark — the user-chosen landscape
// behaviour falls out for free.
> span.gr-swap { // (0,4,3) ties > span:last-child; later source order wins
// Motion knobs — TWEAK here. The reel doesn't run on a
// single cubic-bezier any more: a bezier is one curve
// with two control points, so it can overshoot at most
// ONCE and can't oscillate. To make the word feel old &
// rusty — stall against the grime, jerk free, then clunk
// past its mark and wobble into place — `--gr-ease` is a
// CSS linear() easing: a piecewise list of `<output>
// <progress%>` stops, where output > 1 overshoots.
// The phases of the curve below, by stop:
// 0 → 0.02 15% .......... rusty grip, barely budges
// 0.16 25% → 0.8 56% .... breaks free, jerks across
// 1.05 65% → 1.12 71% ... clunks PAST the mark
// 0.965 78% → 1.05 84%
// → 0.99 89% → 1.015 95% → 1 .. damped end-wobble
// CRITICAL: keep NO inline `//` comments INSIDE the
// linear() value — libsass leaves them in the custom-
// property output, which invalidates the whole
// `transition` (it falls back to 0s/ease). Annotate
// ABOVE the declaration only. (Firefox 112+ / 151 here.)
--gr-ease: linear(
0, 0.015 7%, 0.02 25%,
0.16 25%, 0.46 41%, 0.8 56%,
1.15 65%, 1.12 71%,
0.935 78%, 1.05 84%, 0.99 89%, 1.015 95%,
1
);
--gr-dur: 0.95s; // heavier/slower so the stutter + wobble read
--gr-fade: 16%; // top/bottom dissolve depth
position: relative;
overflow: hidden;
padding: 0; // reset :last-child's padding-inline-start (re-applied on .gr-word)
// Symmetric top/bottom fade so the words dissolve into
// the navbar line (top) + the page aperture (bottom).
// Symmetric → rotation-invariant under landscape's
// rotate(180deg).
mask-image:
linear-gradient(to bottom,
transparent 0, #000 var(--gr-fade),
#000 calc(100% - var(--gr-fade)), transparent 100%);
-webkit-mask-image:
linear-gradient(to bottom,
transparent 0, #000 var(--gr-fade),
#000 calc(100% - var(--gr-fade)), transparent 100%);
}
.gr-word {
position: absolute;
inset: 0;
display: flex;
justify-content: space-between;
align-items: center;
padding-inline-start: 0.4em; // match > span:last-child word gap
transition: transform var(--gr-dur, 0.55s) var(--gr-ease, ease);
will-change: transform;
// 3-letter base word (e.g. my_sea's SEA) clusters like
// the top-level 3-letter suffixes instead of parking at
// the slot edges.
&[data-letters="3"] { justify-content: space-around; }
}
.gr-word--base { transform: translateY(0); } // resting in view
.gr-word--scroll { transform: translateY(100%); } // parked below the slot
&.is-scroll {
.gr-word--base { transform: translateY(-100%); } // up & out the top
.gr-word--scroll { transform: translateY(0); } // up into view
}
}
}
}
@@ -433,6 +516,19 @@ body {
// bottom edge of the first span — natural break between words.
}
// Title reel in landscape — the rotated wordmark is a tall letter-column
// that fills the gutter slot, so `space-between` parks the first/last
// letter at the very slot edges. Inset the letters (padding-inline = the
// vertical axis in writing-mode: vertical-rl) and use a shallower fade so
// the dissolve lives in those margins instead of dimming resting letters.
// translateY still slides the full slot length ALONG the wordmark.
body .container .row .col-lg-6 h2 > span.gr-swap {
--gr-fade: 7%;
}
body .container .row .col-lg-6 h2 .gr-word {
padding-inline: 7%; // top+bottom inset (vertical-rl) — supersedes the 0.4em start gap
}
// Footer → fixed right sidebar (mirrors navbar approach — explicit right boundary)
// Use body #id_footer (specificity 0,1,0,1) to beat base #id_footer (0,1,0,0)
// which compiles later in the output and would otherwise override height: 100vh.