Files
python-tdd/src/static_src/scss/_room.scss
Disco DeDisco 7165974905 table hex layout: fill aperture + enlarge hex from 160×185 → 200×231 ; chair clearance preserved
User report: hex felt smaller than the aperture even at portrait (off-centered, room to spare on top + bottom), and chair labels overlapped the hex edges at landscape — progressively worse as the hex grew at larger viewports. Three contributors stacked: (1) `.room-shell { max-height: 80vh }` capped the shell at 80% of viewport height even when the .room-page aperture had more room — at 1789×1031 this donated 228px (1053→825) of aperture height to dead margin; (2) scene design 360×300 was wider than tall (1.2 aspect) but landscape aperture is narrower than tall (~1.4), so the height cap bottlenecked scene-scale at min(aperture_w/360, aperture_h/300) instead of letting the hex grow; (3) chair font-size scales w. rem (clamp(14,2.4vmin,22)) but chair position scales w. --table-scale — at large viewports rem maxes at 22 so labels widen and push chair icons further from box-center toward the hex (visual "creep") ; fix: remove the 80vh cap (`max-height: 80vh` → `height: 100%` on .room-shell L340) so the shell stretches to fill the .room-page aperture; bump hex from 160×231 to 200×231 (regular pointy-top w. width = height × √3/2 = 200 * 1.1547 — comment in _room.scss updated); apothem of 200-wide pointy-top regular hex is 100px exact (200/√3 × √3/2), so `$pos-d` 110px → 140px gives 40px design-units of radial chair clearance (was 30); derived `$pos-d-x: round(140*0.5) = 70`, `$pos-d-y: round(140*0.866) = 121` for slot 2/3/5/6 diagonal anchors at 60° from horizontal (matches existing geometry approach); scene design height 300 → 320 to leave enough vertical headroom at large landscape that the rem-driven (font-size 1.6rem × scale) chair icons + labels don't clip the aperture top/bottom edges — at 1789×1111 w. scene_H=300 the AC/BC label tops sat AT aperture top (y=-21 vs aperture y=-22), bumping to 320 drops scale from 4.05 → 3.54 and leaves 76px of headroom; SCENE_H in room.js bumped to 320 to match (Math.min(w/SCENE_W, h/SCENE_H) sets --table-scale CSS var via transform: scale on .room-table-scene) ; visual verification via Claudezilla across three viewports (no test layer per user preference — layout regression coverage via spot-check on next room render) — iPhone-14 portrait 566×875: hex 243×281 → 314×363 (+29% wider, fills 55% of aperture width vs 44% before); mid landscape 1149×781: hex 333×385 → 493×569 (+48% wider, 56% vs 38% before); large landscape 1789×1111: hex 440×509 → 708×818 (+61% wider, 48% vs 30% before — the most dramatic improvement, matching user's "progressively worse the larger the hex grows" observation). Chair clearance now uniform 40 design-units radially across all scales; AC/BC labels stay 76px inside aperture top at the largest viewport ; dead `$seat-r`/`$seat-r-x`/`$seat-r-y` consts at L357-359 left in place (unused elsewhere in codebase but out of scope for this layout fix) ; full IT/UT 999 green in 46s — no regressions; .table-hex / .table-hex-border / .room-table-scene / .table-seat positioning consts are the only refs to these dimensions across SCSS & JS so no cascade beyond room layout. Unblocks Sprint 2+ (My Sea applet will share the same hex CSS, parameterized, per user's intent for future friend-invite up-to-6-person rooms)

Code architected by Disco DeDisco <discodedisco@outlook.com>
Git commit message Co-Authored-By:
Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 18:19:11 -04:00

893 lines
27 KiB
SCSS
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

$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;
}
#id_room_menu {
position: fixed;
bottom: 6.6rem;
right: 0.5rem;
z-index: 314;
background-color: rgba(var(--priUser), 0.95);
border: 0.15rem solid rgba(var(--secUser), 1);
box-shadow:
0 0 0.5rem rgba(var(--secUser), 0.75),
0.12rem 0.12rem 0.5rem rgba(0, 0, 0, 0.25)
;
border-radius: 0.75rem;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
// 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,
html.sea-open #id_aperture_fill {
opacity: 1;
}
.gate-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(4px);
z-index: 100;
pointer-events: none;
}
.gate-overlay {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 120;
overflow-y: auto;
overscroll-behavior: contain;
-webkit-overflow-scrolling: touch;
pointer-events: none;
margin-top: 5rem;
}
.gate-modal {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 0.5rem;
min-width: 26rem;
pointer-events: auto;
border: none;
background-color: transparent;
.gate-title-panel {
border: 0.1rem solid rgba(var(--terUser), 0.25);
border-radius: 0.5rem;
padding: 0.75rem;
background: rgba(var(--priUser), 1);
}
.gate-top-row {
display: flex;
flex-direction: row;
gap: 0.5rem;
}
.gate-main-panel {
flex: 3;
min-width: 0;
display: flex;
flex-direction: column;
align-items: center;
border: 0.1rem solid rgba(var(--terUser), 0.25);
border-radius: 0.5rem;
padding: 0.75rem;
background: rgba(var(--priUser), 1);
}
.gate-roles-panel {
flex: 1;
min-width: 5rem;
display: flex;
align-items: center;
justify-content: center;
border: 0.1rem solid rgba(var(--terUser), 0.25);
border-radius: 0.5rem;
padding: 0.75rem;
background: rgba(var(--priUser), 1);
.launch-game-btn { margin-top: 0; }
}
.gate-invite-panel {
display: flex;
flex-direction: column;
gap: 0.4rem;
border: 0.1rem solid rgba(var(--terUser), 0.25);
border-radius: 0.5rem;
padding: 0.75rem;
background: rgba(var(--priUser), 1);
}
.gate-header {
text-align: center;
h1 {
font-size: 2rem;
color: rgba(var(--secUser), 0.6);
margin-bottom: 1rem;
text-align: justify;
text-align-last: center;
text-justify: inter-character;
text-transform: uppercase;
text-shadow:
1px 1px 0 rgba(255, 255, 255, 0.125), // highlight (up-left)
var(--title-shadow-offset) var(--title-shadow-offset) 0 rgba(0, 0, 0, 0.8) // shadow (down-right)
;
span {
color: rgba(var(--quaUser), 0.6);
}
margin: 0 0 0.5rem;
}
.gate-status-wrap {
display: flex;
justify-content: center;
align-items: baseline;
opacity: 0.5;
font-size: 0.75em;
text-transform: uppercase;
letter-spacing: 0.15em;
.status-dots {
display: inline-flex;
span {
display: inline-block;
width: 0.5em;
text-align: center;
}
}
}
}
.token-slot {
position: relative;
display: flex;
flex-direction: row;
border: 2px solid rgba(var(--terUser), 0.7);
border-radius: 0.4rem;
background: rgba(0, 0, 0, 0.35);
min-width: 180px;
&.locked {
opacity: 0.3;
pointer-events: none;
}
&.ready {
border-color: rgba(var(--terUser), 1);
button.token-rails {
box-shadow:
0 0 0.6rem rgba(var(--terUser), 0.6),
0 0 1.6rem rgba(var(--terUser), 0.25)
;
.rail { background: rgba(var(--terUser), 1); }
}
}
&.pending,
&.claimed {
box-shadow:
0 0 0.6rem rgba(var(--terUser), 0.5),
0 0 1.4rem rgba(var(--terUser), 0.2),
;
.token-return-btn { text-shadow: 0 0 0.5rem rgba(var(--terUser), 0.8); }
&:hover {
border-color: rgba(var(--terUser), 1);
background: rgba(0, 0, 0, 0.55);
box-shadow:
0 0 0.8rem rgba(var(--terUser), 0.75),
0 0 2rem rgba(var(--terUser), 0.35),
;
}
}
.token-rails,
button.token-rails {
display: flex;
flex-direction: row;
align-items: stretch;
padding: 0.6rem 0.45rem;
gap: 0.2rem;
border-right: 1px solid rgba(var(--terUser), 0.35);
.rail {
display: block;
width: 2px;
background: rgba(var(--terUser), 0.55);
border-radius: 1px;
}
}
button.token-rails {
background: transparent;
border: none;
outline: none;
border-right: 1px solid rgba(var(--terUser), 0.35);
cursor: pointer;
border-radius: 0.3rem 0 0 0.3rem;
&:hover {
background: rgba(var(--terUser), 0.1);
.rail { background: rgba(var(--terUser), 1); }
}
}
.token-return-btn {
position: absolute;
inset: 0;
background: transparent;
border: none;
outline: none;
cursor: pointer;
border-radius: inherit;
}
.token-panel {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0.45rem 0.75rem;
gap: 0.15rem;
.token-denomination {
font-size: 1.5em;
font-weight: bold;
color: rgba(var(--terUser), 1);
line-height: 1;
}
.token-insert-label,
.token-insert-btn {
&::before {
content: '';
}
font-size: 0.6em;
text-transform: uppercase;
letter-spacing: 0.08em;
text-align: center;
line-height: 1.3;
}
.token-return-label {
font-size: 0.55em;
text-transform: uppercase;
letter-spacing: 0.06em;
opacity: 0.5;
line-height: 1.3;
text-align: center;
}
}
}
}
// Narrow viewport — scale down, 2×3 slot grid (portrait mobile + narrow desktop)
@media (max-width: 700px) {
// Floor the gatekeeper modal below the position-strip circles (~1.5rem top + 3rem height)
.gate-overlay {
padding-top: 5.5rem;
}
.gate-modal {
padding: 1.25rem 1.5rem;
.gate-header {
h1 { font-size: 1.5rem; }
}
.token-slot { min-width: 150px; }
}
}
// ─── Room shell layout ─────────────────────────────────────────────────────
.room-shell {
display: flex;
flex-direction: row;
align-items: stretch;
gap: 2rem;
width: 100%;
height: 100%;
align-self: stretch;
}
// ─── Table hex + seat positions ────────────────────────────────────────────
//
// .table-hex: regular pointy-top hexagon.
// clip-path polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%)
// on a 200×231 container gives equal-length sides (height = width × 2/√3).
//
// Seats use absolute positioning from the .room-table centre.
// Clockwise from top: slots 1→2→3→4→5→6.
$seat-r: 140px;
$seat-r-x: round($seat-r * 0.866); // 121px
$seat-r-y: round($seat-r * 0.5); // 70px
// Seat edge-midpoint geometry (pointy-top hex).
// 200×231 hex → apothem = 100px; $pos-d = 140 leaves 40px design-units of
// chair clearance radially. $pos-d-x / $pos-d-y are the x/y components for
// diagonal seats (cos/sin of 60° from horizontal).
$pos-d: 140px;
$pos-d-x: round($pos-d * 0.5); // 70px
$pos-d-y: round($pos-d * 0.866); // 121px
// ─── Position strip ────────────────────────────────────────────────────────
// Numbered gate-slot circles sit above the gate backdrop/overlay (z 130 > 120
// > 100) but below the role-select fan (z 200), tray (310), and menus (310+).
// .room-page is position:relative with no z-index, so its absolute children
// share the root stacking context with the fixed overlays.
// When the gate modal or role-select fan is open, suppress pointer events so
// the strip doesn't intercept clicks or hover effects on the modal beneath it
// (landscape: strip overlaps centered card fan too).
// Must target .gate-slot directly — it has an explicit pointer-events: auto
// override that wins over a rule on the parent .position-strip alone.
html:has(.gate-backdrop) .position-strip .gate-slot,
html:has(.role-select-backdrop) .position-strip .gate-slot { pointer-events: none; }
// Re-enable clicks on confirm/reject/drop-token forms inside slots
html:has(.gate-backdrop) .position-strip .gate-slot form,
html:has(.gate-backdrop) .position-strip .gate-slot button { pointer-events: auto; }
.position-strip {
position: absolute;
top: 1rem;
left: 0;
right: 0;
z-index: 130;
display: flex;
justify-content: center;
gap: round($gate-gap * 0.6);
pointer-events: none;
.gate-slot {
position: relative;
width: round($gate-node * 0.75);
height: round($gate-node * 0.75);
border-radius: 50%;
border: $gate-line solid rgba(var(--terUser), 0.5);
background: rgba(var(--priUser), 1);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex-shrink: 0;
pointer-events: auto;
font-size: 1.8rem;
transition: opacity 0.6s ease, transform 0.6s ease;
box-shadow:
0.1rem 0.1rem 0.12rem rgba(var(--priUser), 0.25),
0.12rem 0.12rem 0.25rem rgba(0, 0, 0, 0.25),
0.25rem 0.25rem 0.25rem rgba(var(--priUser), 0.12)
;
&.role-assigned {
opacity: 0;
transform: scale(0.5);
pointer-events: none;
box-shadow:
0.1rem 0.1rem 0.12rem rgba(var(--terUser), 0.25),
0.12rem 0.12rem 0.25rem rgba(0, 0, 0, 0.25),
0.25rem 0.25rem 0.25rem rgba(var(--terUser), 0.12)
;
}
&.filled, &.reserved {
background: rgba(var(--terUser), 0.9);
border-color: rgba(var(--terUser), 1);
color: rgba(var(--priUser), 1);
}
&.filled:hover, &.reserved:hover {
box-shadow:
-0.1rem -0.1rem 1rem rgba(var(--ninUser), 1),
-0.1rem -0.1rem 0.25rem rgba(0, 0, 0, 1),
0.05rem 0.05rem 0.5rem rgba(0, 0, 0, 1);
}
.slot-number { font-size: 0.7em; opacity: 0.5; }
.slot-gamer { display: none; }
form {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
}
&:has(.drop-token-btn) {
background: rgba(var(--terUser), 1);
border-color: rgba(var(--ninUser), 0.5);
&:hover {
box-shadow:
-0.1rem -0.1rem 1rem rgba(var(--ninUser), 1),
-0.1rem -0.1rem 0.25rem rgba(0, 0, 0, 1),
0.05rem 0.05rem 0.5rem rgba(0, 0, 0, 1);
}
}
}
}
@media (max-width: 700px) {
.position-strip {
gap: round($gate-gap * 0.3);
.gate-slot {
width: round($gate-node * 0.75);
height: round($gate-node * 0.75);
}
}
}
.room-table {
flex: 2;
position: relative;
display: flex;
align-items: center;
justify-content: center;
min-height: 0;
}
// Fixed design-size scene; JS scales it to fill .room-table via transform: scale().
// Design dims: seat reach is ±140px H / ±121px V from centre + seat element size.
// scene H of 320 leaves vertical headroom at large landscape so the rem-scaled
// chair icons + labels don't clip the aperture top/bottom edges.
.room-table-scene {
width: 360px;
height: 320px;
position: relative;
display: flex;
align-items: center;
justify-content: center;
transform-origin: center center;
}
// Hex border: clip-path clips CSS borders, so the ring is a wrapper with the
// same hex polygon at a slightly larger size. 0.25rem each side — subtle only.
.table-hex-border {
width: calc(200px + 0.5rem);
height: calc(231px + 0.5rem);
clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
background: rgba(var(--quaUser), 1);
filter: drop-shadow(0 0 6px rgba(var(--quaUser), 0.5));
display: flex;
align-items: center;
justify-content: center;
}
.table-hex {
width: 200px;
height: 231px;
clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
// Six gradients — one per hex face — each perpendicular to that face so the
// shadows follow the hex geometry rather than the rectangular bounding box.
// CSS angle convention: 0°=up, 90°=right. Shadow goes FROM face INWARD.
// Left face → 90° Right face → 270°
// Top-left face → 150° Top-right face → 210°
// Bottom-left face → 30° Bottom-right face→ 330°
background:
linear-gradient(90deg, rgba(0, 0, 0, 0.2) 0%, transparent 15%),
linear-gradient(90deg, rgba(var(--quaUser), 0.1) 0%, transparent 15%),
linear-gradient(270deg, rgba(0, 0, 0, 0.2) 0%, transparent 15%),
linear-gradient(270deg, rgba(var(--quaUser), 0.1) 0%, transparent 15%),
linear-gradient(210deg, rgba(0, 0, 0, 0.2) 0%, transparent 15%),
linear-gradient(210deg, rgba(var(--quaUser), 0.1) 0%, transparent 15%),
linear-gradient(150deg, rgba(0, 0, 0, 0.2) 0%, transparent 15%),
linear-gradient(150deg, rgba(var(--quaUser), 0.1) 0%, transparent 15%),
linear-gradient(30deg, rgba(0, 0, 0, 0.2) 0%, transparent 15%),
linear-gradient(30deg, rgba(var(--quaUser), 0.1) 0%, transparent 15%),
linear-gradient(330deg, rgba(0, 0, 0, 0.2) 0%, transparent 15%),
linear-gradient(330deg, rgba(var(--quaUser), 0.1) 0%, transparent 15%),
rgba(var(--duoUser), 1);
display: flex;
align-items: center;
justify-content: center;
}
// Outside .room-table-scene so it isn't scaled by scaleTable().
// Positioned absolute so it floats over the hex without affecting flex layout.
#id_pick_sigs_wrap {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 10;
}
.table-center {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
// "Gravity settling . . ." / "Levity appraising . . ." shown after a polarity
// group confirms their sigs while the other group is still selecting.
// Pulsing opacity signals active waiting without being jarring.
#id_hex_waiting_msg {
font-size: 0.7rem;
letter-spacing: 0.06em;
color: rgba(var(--terUser), 0.8);
text-align: center;
margin: 0.4rem 0 0;
animation: hex-wait-pulse 2.4s ease-in-out infinite;
}
@keyframes hex-wait-pulse {
0%, 100% { opacity: 0.75; }
50% { opacity: 0.3; }
}
.table-seat {
position: absolute;
display: grid;
grid-template-columns: auto auto;
grid-template-rows: auto auto;
column-gap: 0.25rem;
align-items: center;
transform: translate(-50%, -50%);
pointer-events: none;
// Edge midpoints, clockwise from 3 o'clock (slot drop order → role order)
&[data-slot="1"] { left: calc(50% + #{$pos-d}); top: 50%; }
&[data-slot="2"] { left: calc(50% + #{$pos-d-x}); top: calc(50% + #{$pos-d-y}); }
&[data-slot="3"] { left: calc(50% - #{$pos-d-x}); top: calc(50% + #{$pos-d-y}); }
&[data-slot="4"] { left: calc(50% - #{$pos-d}); top: 50%; }
&[data-slot="5"] { left: calc(50% - #{$pos-d-x}); top: calc(50% - #{$pos-d-y}); }
&[data-slot="6"] { left: calc(50% + #{$pos-d-x}); top: calc(50% - #{$pos-d-y}); }
// Chair: col 1, spans both rows
.fa-chair {
grid-column: 1;
grid-row: 1 / 3;
font-size: 1.6rem;
color: rgba(var(--secUser), 0.4);
transition: color 0.6s ease, filter 0.6s ease;
}
// Abbreviation: col 2, row 1
.seat-role-label {
grid-column: 2;
grid-row: 1;
font-size: 0.8rem;
font-weight: 600;
letter-spacing: 0.05em;
color: rgba(var(--secUser), 1);
}
// Status icon: col 2, row 2, centred under the abbreviation
.position-status-icon {
grid-column: 2;
grid-row: 2;
justify-self: center;
font-size: 0.8rem;
&.fa-ban { color: rgba(var(--priRd), 1); }
&.fa-circle-check { color: rgba(var(--priGn), 1); }
}
// Left-side positions: flip column order so chair is closest to the table
&[data-slot="3"], &[data-slot="4"], &[data-slot="5"] {
.fa-chair { grid-column: 2; }
.seat-role-label { grid-column: 1; }
.position-status-icon { grid-column: 1; }
}
&.active .fa-chair {
color: rgba(var(--terUser), 1);
filter: drop-shadow(0 0 4px rgba(var(--ninUser), 1));
}
// After role confirmed: chair settles to full-opacity --secUser (no glow)
&.role-confirmed .fa-chair {
color: rgba(var(--secUser), 1);
filter: none;
}
.seat-portrait {
width: 36px;
height: 36px;
border-radius: 50%;
border: 2px solid rgba(var(--terUser), 1);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
opacity: 0.6;
}
.seat-label {
font-size: 0.65rem;
opacity: 0.5;
max-width: 80px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
// Arc of mini cards — visible only on the currently active seat
.seat-card-arc {
display: none;
position: absolute;
width: 18px;
height: 26px;
border-radius: 2px;
border: 1px solid rgba(var(--terUser), 0.7);
background: rgba(var(--quaUser), 0.9);
// Three fanned cards stacked behind the portrait
&::before,
&::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
border: inherit;
background: inherit;
}
&::before { transform: rotate(-18deg) translate(-4px, 2px); }
&::after { transform: rotate( 18deg) translate( 4px, 2px); }
}
&.active .seat-portrait {
opacity: 1;
border-color: rgba(var(--secUser), 1);
box-shadow: 0 0 0.5rem rgba(var(--ninUser), 0.5);
}
&.active .seat-card-arc {
display: block;
transform: translateY(-28px); // float above the portrait
}
}
// ─── Card stack ────────────────────────────────────────────────────────────
.card-stack {
width: 90px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
border: 2px solid rgba(var(--quiUser), 1);
background: rgba(var(--terUser), 1);
cursor: default;
transition: box-shadow 0.2s ease;
position: relative;
&::before {
content: "ROLE";
font-size: 0.6rem;
letter-spacing: 0.14em;
color: rgba(var(--quiUser), 1);
}
.fa-ban {
position: absolute;
font-size: 1.4rem;
}
&[data-state="eligible"] {
cursor: pointer;
border: 2px solid rgba(var(--quiUser), 1);
box-shadow:
0 0 0.6rem rgba(var(--ninUser), 1),
0 0 1.6rem rgba(var(--secUser), 0.25);
}
&[data-state="ineligible"] {
opacity: 0.4;
cursor: not-allowed;
}
}
// ─── Card dimensions ───────────────────────────────────────────────────────
// Base size matches the card-stack footprint; --table-scale (set by scaleTable()
// in room.js) stretches both the grid and individual cards to stay in sync with
// the scene transform. Fallback of 1 keeps the fan functional if JS hasn't run.
$card-w: 90px;
$card-h: 60px;
// ─── No-deck warning overlay ──────────────────────────────────────────────
.role-no-deck-warning {
position: fixed;
z-index: 10000;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1rem;
max-width: 11rem;
border-radius: 0.5rem;
background-color: rgba(var(--tooltip-bg), 0.75);
backdrop-filter: blur(6px);
border: 0.1rem solid rgba(var(--secUser), 0.4);
box-shadow: 0 0.25rem 1rem rgba(0, 0, 0, 0.4);
p {
font-size: 0.75rem;
color: rgba(var(--secUser), 0.9);
text-align: center;
margin: 0;
}
.guard-actions {
display: flex;
gap: 0.5rem;
}
}
// ─── Role select modal ─────────────────────────────────────────────────────
.role-select-backdrop {
position: fixed;
inset: 0;
z-index: 200;
display: flex;
justify-content: center;
align-items: center;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
cursor: pointer;
}
#id_role_select {
// Always a 3×2 grid — 6 landscape cards in a row would overflow any viewport.
display: grid;
grid-template-columns: repeat(3, calc(#{$card-w} * var(--table-scale, 1)));
gap: 1rem;
pointer-events: none;
}
// ─── Card component ────────────────────────────────────────────────────────
.card {
width: calc(#{$card-w} * var(--table-scale, 1));
height: calc(#{$card-h} * var(--table-scale, 1));
border-radius: 6px;
cursor: pointer;
pointer-events: auto;
position: relative;
perspective: 600px;
.card-back,
.card-front {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: inherit;
border: 2px solid rgba(var(--terUser), 1);
background: rgba(var(--quiUser), 1);
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
transition: transform 0.35s ease;
}
.card-back {
transform: rotateY(0deg);
font-size: calc(0.66rem * var(--table-scale, 1));
letter-spacing: 0.14em;
color: rgba(var(--quiUser), 1);
background: rgba(var(--terUser), 1);
border: 2px solid rgba(var(--quiUser), 1);
}
.card-front {
transform: rotateY(180deg);
padding: 0.5rem;
text-align: center;
.card-role-name {
font-size: calc(0.66rem * var(--table-scale, 1));
color: rgba(var(--quaUser), 1);
text-transform: uppercase;
letter-spacing: 0.05em;
}
}
&.flipped,
&.face-up {
.card-back { transform: rotateY(-180deg); }
.card-front { transform: rotateY(0deg); }
}
}
// Landscape mobile — aggressively scale down to fit short viewport
@media (orientation: landscape) {
// Sink navbar + footer sidebar below any modal backdrop when open.
// Landscape navbar and footer sidebar are both z-index:100 (_base.scss).
// Gate/role-select/sig backdrops are also z-index:100 — DOM paint-order ties
// let the footer (later in DOM) bleed through. Drop both to 50.
html:has(.gate-backdrop) body .container .navbar,
html:has(.role-select-backdrop) body .container .navbar,
html:has(.sig-backdrop) body .container .navbar {
z-index: 50;
}
html:has(.gate-backdrop) body #id_footer,
html:has(.role-select-backdrop) body #id_footer,
html:has(.sig-backdrop) body #id_footer {
z-index: 50;
}
// Position strip: horizontal row across the top, slots 1-6 in order.
// Offset from both sidebars (5rem each) and centred with gap.
.position-strip {
flex-direction: row;
top: 2.5rem;
left: 5rem;
right: 5rem;
justify-content: center;
gap: round($gate-gap * 0.4);
}
// Small landscape (phones ≤550px tall): strip stays horizontal — no two-column
// trick needed now that the h2 is in the gutter. Just clear any order overrides.
@media (max-height: 550px) {
.position-strip {
.gate-slot { order: 0; }
top: 1rem;
}
}
}
// ─── Seat tray — see _tray.scss ─────────────────────────────────────────────