$gate-node: 64px;
$gate-gap: 36px;
$gate-line: 2px;
.room-page {
position: relative;
display: flex;
align-items: center;
justify-content: center;
flex: 1;
min-height: 0;
overflow: hidden;
}
// Scroll-lock when gate is open. Uses html (not body) to avoid CSS overflow
// propagation quirk on Linux headless Firefox where body overflow:hidden can
// disrupt pointer events on position:fixed descendants.
// NOTE: may be superfluous — root cause of CI kit-btn failures turned out to be
// game-kit.js missing from git (was in gitignored STATIC_ROOT only).
html:has(.gate-backdrop) {
overflow: hidden;
}
// Aperture fill — solid --duoUser layer that covers the game table (.room-page).
// Uses position:absolute so it's clipped to .room-page bounds (overflow:hidden),
// naturally staying below the h2 title + navbar/footer in both orientations.
// Sits at z-90: below blur backdrops (z-100) which render on top via backdrop-filter.
// Fades in/out via opacity transition when a backdrop class is present.
#id_aperture_fill {
position: absolute;
inset: 0;
background: rgba(var(--duoUser), 1);
z-index: 90;
pointer-events: none;
opacity: 0;
transition: opacity 0.15s ease;
}
html:has(.gate-backdrop) #id_aperture_fill,
html:has(.sig-backdrop) #id_aperture_fill,
html:has(.role-select-backdrop) #id_aperture_fill {
opacity: 1;
}
// NB: `html.sea-open #id_aperture_fill { opacity: 1 }` was DROPPED in the felt
// rebuild (2026-06-07). The DRAW SEA felt now sits at z-5 INSIDE the hex pane
// (.sea-page--room), BELOW the z-90 fill — lighting the fill painted an opaque
// green sheet OVER the felt + spread (the 0.15s opacity transition is why the
// spread "flashed then vanished"). Same trap + fix as the CAST SKY felt.
// [[feedback-felt-aperture-fill-covers-felt]]
// ─── Table-hex aperture: binary scroll-snap toggle ─────────────────────────
// Mirrors my_sky's wheel<->form swap (`_sky.scss` body.sky-saved block). The
// aperture fills .room-page; from Role Select onwards it holds TWO panes —
// the hex (default) and the room's provenance scroll — and scroll-snaps
// between them. CRITICAL: the aperture and panes set NO z-index / transform /
// opacity / filter, so they create NO stacking context — the position strip's
// z-130 (a hex-pane descendant) still resolves in the root context, above the
// gate/sig overlays (z-100/120), exactly as when it lived at room-page root.
.room-aperture {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
// Gate phase (single pane): static + clipped, like the old .room-page.
overflow: hidden;
}
.room-pane {
position: relative; // containing block for the position strip (z-auto)
flex: 0 0 auto;
width: 100%;
// EXACTLY one aperture tall (not min-height) so the snap stops land at
// integer multiples of the viewport — the binary toggle, no mid-scroll.
height: 100%;
}
.room-hex-pane {
display: flex;
align-items: center;
justify-content: center;
}
// Two panes present (table phase) → engage the binary snap. `is-scrollable`
// is added by the template iff `room.table_status` is set (Role Select on).
.room-aperture.is-scrollable {
overflow-y: auto;
overflow-x: hidden;
scroll-snap-type: y mandatory;
overscroll-behavior-y: contain;
-webkit-overflow-scrolling: touch;
.room-pane {
scroll-snap-align: start;
scroll-snap-stop: always; // hard stop each section — no coasting
}
}
// While the CAST SKY felt is summoned (html.sky-open) the outer aperture must
// NOT scroll down to the reelhouse carousel (ATLAS/SCROLL/YARN/POST/PULSE) —
// the felt's OWN form↔wheel scroll-snap (.sky-page--room) is the only scroll
// in play. Pin the aperture to the hex pane (which the felt fills); restored
// the instant the felt closes (sky-open removed). overflow:hidden alone leaves
// the aperture parked on whichever pane it was showing — and CAST SKY is only
// reachable from the hex pane, so it pins to the hex every time.
html.sky-open .room-aperture.is-scrollable {
overflow-y: hidden;
scroll-snap-type: none;
}
// Same pin while the DRAW SEA felt is summoned (html.sea-open) — the felt fills
// the hex pane + the reelhouse below must stay out of reach.
html.sea-open .room-aperture.is-scrollable {
overflow-y: hidden;
scroll-snap-type: none;
}
.room-scroll-pane {
display: flex;
flex-direction: column;
// ── Horizontal game-views carousel ────────────────────────────────────
// The pane holds a row of 5 full-width views (ATLAS/SCROLL/POST/CHAT/
// PULSE) on native x scroll-snap, nested inside the aperture's binary y
// snap. room-views.js parks the initial scrollLeft on SCROLL (2nd view).
// The horizontal scrollbar is hidden — the root icon strip is the
// affordance. CRITICAL: no z-index/transform here (root stacking context).
.room-views {
flex: 1;
min-height: 0;
display: flex;
flex-direction: row;
overflow-x: auto;
overflow-y: hidden;
scroll-snap-type: x mandatory;
overscroll-behavior-x: contain;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
&::-webkit-scrollbar { width: 0; height: 0; }
}
.room-view {
flex: 0 0 100%;
width: 100%;
height: 100%;
min-width: 0;
box-sizing: border-box;
scroll-snap-align: start;
scroll-snap-stop: always; // hard stop each view — no coasting
display: flex;
flex-direction: column;
// Deeper bottom padding reserves a lane for the icon strip (absolute @
// bottom 0.85rem, ~2.1rem tall incl. the active scale) so the
// .applet-scroll card ENDS ABOVE the strip — the strip stands on its
// own beneath the card instead of overlapping its bottom edge.
padding: 0.75rem 0.75rem 3.5rem;
}
// Same %applet-box card chrome + rotated room-name title (`> h2`) as the
// Billscroll page's .applet-scroll. No --duoUser pane bg — the dark card
// sits straight on the room-page background, matching scroll.html.
.applet-scroll {
@extend %applet-box;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
#id_drama_scroll {
flex: 1;
min-height: 0;
overflow-y: auto;
display: flex;
flex-direction: column;
// Pin the "What happens next…?" buffer to the bottom when the feed
// is short (pure-CSS equivalent of billscroll's rAF marginTop nudge).
.scroll-buffer {
margin-top: auto;
display: flex;
justify-content: center;
align-items: baseline;
padding: 2rem 0 1rem;
opacity: 0.4;
font-size: 0.8rem;
text-transform: uppercase;
.scroll-buffer-text { letter-spacing: 0.33em; }
.scroll-buffer-dots {
display: inline-flex;
letter-spacing: 0;
span { display: inline-block; width: 0.7em; text-align: center; }
}
}
}
}
// ── Per-view title recolor + view-specific card styling ───────────────
// Each reelhouse view's rotated room-name `> h2` carries a distinct
// font/bg pair (user-spec 2026-06-08) so the five views read as
// colour-coded sections. ATLAS's per-source row accents (further down)
// echo the SCROLL + POST h2 bgs so a merged row's origin is legible.
// SCROLL — provenance feed. --priUser font on a --sixUser strip.
.room-view--scroll .applet-scroll > h2 {
color: rgba(var(--priUser), 1);
background-color: rgba(var(--sixUser), 1);
}
// YARN view — the room-scoped chat thread (most of it forthcoming). Inherits
// the full green-felt input-pill treatment POST used to carry: a --duoUser
// card, the green-tinted h2 strip, the input-style line pills + composer.
// SALVAGED here verbatim (the .post-* selectors renamed .yarn-*) because POST
// reverts to the plain applet wash below — so the styling survives for when
// YARN's backing model + markup land. Reuses post.html's composer markup
// language; the line/buffer/form/table selectors are the future .yarn-* ones.
.room-view--yarn {
.applet-scroll {
background-color: rgba(var(--duoUser), 1);
> h2 {
// Three translucent 0,0,0/0.125 layers and NO opaque base, so the
// --duoUser felt shows THROUGH for a green-tinted "partial mask".
background-color: rgba(0, 0, 0, 0.125);
background-image:
linear-gradient(rgba(0, 0, 0, 0.125), rgba(0, 0, 0, 0.125)),
linear-gradient(rgba(0, 0, 0, 0.125), rgba(0, 0, 0, 0.125));
}
}
#id_yarn_table {
list-style: none;
margin: 0;
padding: 0; // pills span the full card content width (= composer row)
flex: 1;
min-height: 0;
overflow-y: auto;
display: flex;
flex-direction: column;
justify-content: flex-end; // bottom-anchor short threads
// Each line is styled to LOOK LIKE the composer input below it
// (mirrors .form-control, _base.scss): a --priUser fill + a 0.1rem
// --secUser border at the same border-radius, full width, an up/down
// margin and content-driven (dynamic) height — so the thread reads as
// a stack of input-style pills on the --duoUser felt.
.yarn-line {
display: grid;
grid-template-columns: minmax(4rem, auto) 1fr minmax(3rem, auto);
align-items: baseline;
gap: 0.5rem;
width: 100%;
background-color: rgba(var(--priUser), 0.8);
border: 0.1rem solid rgba(var(--secUser), 0.5);
border-radius: calc((1rem + 1em) / 3); // == .form-control radius
margin: 0.35rem 0;
padding: 0.5rem 0.75rem;
box-shadow: 1px 1px 0.125rem 1px rgba(0, 0, 0, 0.6);
.yarn-line-author {
font-weight: bold;
color: rgba(var(--quaUser), 1);
white-space: nowrap;
font-size: 0.85rem;
}
.yarn-line-text { min-width: 0; overflow-wrap: anywhere; }
.yarn-line-time {
font-size: 0.75rem;
opacity: 0.5;
text-align: right;
white-space: nowrap;
}
}
.yarn-line-buffer { flex-shrink: 0; height: 0.25rem; }
}
.yarn-line-form {
flex-shrink: 0;
margin: 0;
padding-top: 0.25rem;
.composer-row {
display: flex;
gap: 0.5rem;
align-items: center;
}
.composer-row input.form-control { flex: 1; min-width: 0; width: auto; }
}
}
// POST view — reverts to the plain dark %applet-box wash (its green-felt
// input-pill styling moved to YARN above). Only its h2 is recoloured:
// --priUser font on a --sepUser strip.
.room-view--post {
.applet-scroll > h2 {
color: rgba(var(--priUser), 1);
background-color: rgba(var(--sepUser), 1);
}
// Bare-
normalization only — no list bullets/indent (there's no
// global ul reset), so the thread reads as plain feed rows on the
// default wash rather than a bulleted list. The pill chrome is gone.
#id_post_table {
list-style: none;
margin: 0;
padding: 0;
flex: 1;
min-height: 0;
overflow-y: auto;
}
}
// PULSE view — forthcoming. --priUser font on an --octUser strip.
.room-view--pulse .applet-scroll > h2 {
color: rgba(var(--priUser), 1);
background-color: rgba(var(--octUser), 1);
}
// ── ATLAS view — provenance + post-thread merged feed ─────────────────
// room-views.js rebuilds the body from the live SCROLL + POST DOM on
// activation; rows carry data-source so the two streams read distinctly.
.room-atlas-feed {
flex: 1;
min-height: 0;
overflow-y: auto;
display: flex;
flex-direction: column;
.atlas-row {
display: flex;
align-items: baseline;
gap: 0.4rem;
padding: 0.3rem 0;
font-size: 0.9rem;
border-inline-start: 2px solid transparent;
padding-inline-start: 0.5rem;
.atlas-row-who { font-weight: bold; color: rgba(var(--quaUser), 1); flex-shrink: 0; }
.atlas-row-body {
flex: 1;
min-width: 0;
overflow-wrap: anywhere;
// Struck (retracted/redacted) provenance rows read the same
// strikethrough they do in SCROLL (.drama-event-body.struck).
&.struck { text-decoration: line-through; opacity: 0.5; }
}
// The merged rows carry the ORIGINAL