Files
python-tdd/src/static_src/scss/_room.scss

680 lines
19 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;
min-height: 60vh;
}
#id_room_menu {
position: fixed;
bottom: 6.6rem;
right: 0.5rem;
z-index: 202;
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-overlay) {
overflow: hidden;
}
.gate-overlay {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(4px);
z-index: 100;
overflow-y: auto;
overscroll-behavior: contain;
-webkit-overflow-scrolling: touch;
// Prevents backdrop from intercepting clicks on position:fixed elements
// (e.g. #id_kit_btn) in Linux headless Firefox.
// NOTE: may be superfluous — see html:has comment above.
pointer-events: none;
}
.gate-modal {
display: flex;
flex-direction: column;
align-items: center;
pointer-events: auto;
padding: 2rem;
border: 0.1rem solid rgba(var(--terUser), 0.5);
border-radius: 1rem;
background-color: 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)
-0.125rem -0.125rem 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;
margin-bottom: 1rem;
.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;
}
}
}
.gate-slots {
display: flex;
flex-direction: row;
align-items: center;
gap: $gate-gap;
.gate-slot {
position: relative;
width: $gate-node;
height: $gate-node;
border-radius: 50%;
border: $gate-line solid rgba(var(--terUser), 1);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex-shrink: 0;
&.filled,
&.reserved {
background: rgba(var(--terUser), 0.2);
}
&.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;
}
// CARTE drop-target circle — matches .reserved appearance
&:has(.drop-token-btn) {
background: rgba(var(--terUser), 0.2);
&: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),
;
}
}
}
}
}
// Narrow viewport — scale down, 2×3 slot grid (portrait mobile + narrow desktop)
@media (max-width: 700px) {
.gate-modal {
padding: 1.25rem 1.5rem;
.gate-header {
h1 { font-size: 1.5rem; }
.gate-status-wrap { margin-bottom: 0.5rem; }
}
.token-slot { min-width: 150px; }
.gate-slots {
display: grid;
grid-template-columns: repeat(3, 52px);
grid-template-rows: repeat(2, 52px);
gap: 24px;
.gate-slot {
width: 52px;
height: 52px;
&:nth-child(1) { grid-column: 1; grid-row: 1; }
&:nth-child(2) { grid-column: 2; grid-row: 1; }
&:nth-child(3) { grid-column: 3; grid-row: 1; }
&:nth-child(4) { grid-column: 1; grid-row: 2; }
&:nth-child(5) { grid-column: 2; grid-row: 2; }
&:nth-child(6) { grid-column: 3; grid-row: 2; }
}
}
}
}
// ─── Room shell layout ─────────────────────────────────────────────────────
.room-shell {
display: flex;
flex-direction: row;
align-items: stretch;
gap: 2rem;
width: 100%;
max-height: 80vh;
}
// ─── 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 160×185 container gives equal-length sides (height = width × 2/√3).
//
// Seats use absolute positioning from the .room-table centre.
// $seat-r = 130px — radius to seat centroid
// $seat-r-x = round(130px × sin60°) = 113px — horizontal component
// $seat-r-y = round(130px × cos60°) = 65px — vertical component
//
// Clockwise from top: slots 1→2→3→4→5→6.
$seat-r: 130px;
$seat-r-x: round($seat-r * 0.866); // 113px
$seat-r-y: round($seat-r * 0.5); // 65px
.room-table {
flex: 2;
position: relative;
display: flex;
align-items: center;
justify-content: center;
min-height: 300px;
}
.table-hex {
width: 160px;
height: 185px;
clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
background: rgba(var(--priUser), 0.8);
// box-shadow is clipped by clip-path; use filter instead
filter: drop-shadow(0 0 8px rgba(var(--terUser), 0.25));
display: flex;
align-items: center;
justify-content: center;
}
.table-center {
display: flex;
align-items: center;
justify-content: center;
}
.room-inventory {
flex: 1;
display: flex;
flex-direction: column;
gap: 1rem;
min-height: 0;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: rgba(var(--terUser), 0.3) transparent;
}
.table-seat {
position: absolute;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
// Centre the element on its anchor point
transform: translate(-50%, -50%);
// Clockwise from top — slot drop order during ROLE_SELECT
&[data-slot="1"] { left: 50%; top: calc(50% - #{$seat-r}); }
&[data-slot="2"] { left: calc(50% + #{$seat-r-x}); top: calc(50% - #{$seat-r-y}); }
&[data-slot="3"] { left: calc(50% + #{$seat-r-x}); top: calc(50% + #{$seat-r-y}); }
&[data-slot="4"] { left: 50%; top: calc(50% + #{$seat-r}); }
&[data-slot="5"] { left: calc(50% - #{$seat-r-x}); top: calc(50% + #{$seat-r-y}); }
&[data-slot="6"] { left: calc(50% - #{$seat-r-x}); top: calc(50% - #{$seat-r-y}); }
.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: 60px;
height: 90px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
border: 1px solid rgba(var(--secUser), 1);
background: rgba(var(--terUser), 1);
cursor: default;
transition: box-shadow 0.2s ease;
&[data-state="eligible"] {
cursor: pointer;
border-color: rgba(var(--terUser), 1);
box-shadow:
0 0 0.6rem rgba(var(--ninUser), 0.6),
0 0 1.6rem rgba(var(--secUser), 0.25);
}
&[data-state="ineligible"] {
opacity: 0.4;
cursor: not-allowed;
}
}
// ─── Role select modal ─────────────────────────────────────────────────────
.role-select-backdrop {
position: fixed;
inset: 0;
z-index: 100;
display: flex;
justify-content: center;
align-items: center;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
cursor: pointer;
}
#id_role_select {
display: flex;
gap: 1rem;
pointer-events: none;
@media (max-width: 600px) {
display: grid;
grid-template-columns: repeat(3, 80px);
grid-template-rows: repeat(2, 120px);
gap: 0.75rem;
}
}
// ─── Card component ────────────────────────────────────────────────────────
$card-w: 80px;
$card-h: 120px;
.card {
width: $card-w;
height: $card-h;
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: 1.5rem;
color: rgba(var(--quaUser), 1);
background: rgba(var(--quiUser), 1);
border: 1px solid rgba(var(--terUser), 1);
}
.card-front {
transform: rotateY(180deg);
padding: 0.5rem;
text-align: center;
.card-role-name {
font-size: 0.75rem;
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); }
}
}
// ─── Inventory role card hand ───────────────────────────────────────────────
//
// Cards are stacked vertically: only a $strip-height peek of each card below
// the first is visible by default, showing the role name at the top of the
// card face. Hovering any card slides it right to pop it clear of the stack.
$inv-card-w: 100px;
$inv-card-h: 150px;
$inv-strip: 30px; // visible height of each stacked card after the first
#id_inv_role_card {
display: flex;
flex-direction: column;
.card {
width: $inv-card-w;
height: $inv-card-h;
position: relative;
z-index: 1;
flex-shrink: 0;
transition: transform 0.2s ease;
// Every card after the first overlaps the one above it
& + .card {
margin-top: -($inv-card-h - $inv-strip);
}
// Role name pinned to the top of the face so it reads in the strip
.card-front {
justify-content: flex-start;
padding-top: 0.4rem;
}
// Pop the hovered card to the right, above siblings
&:hover {
transform: translateX(1.5rem);
z-index: 10;
}
}
}
// ─── Partner indicator ─────────────────────────────────────────────────────
.partner-indicator {
margin-top: 0.5rem;
font-size: 0.75rem;
opacity: 0.6;
text-align: center;
}
// Landscape mobile — aggressively scale down to fit short viewport
@media (orientation: landscape) and (max-width: 1023px) {
.gate-modal {
padding: 0.6rem 1.25rem;
.gate-header {
h1 { font-size: 1rem; margin: 0 0 0.25rem; }
.gate-status-wrap { font-size: 0.65em; margin-bottom: 0.35rem; }
}
.token-slot {
min-width: 130px;
.token-rails,
button.token-rails { padding: 0.4rem 0.35rem; }
.token-panel {
padding: 0.3rem 0.5rem;
.token-denomination { font-size: 1.1em; }
}
}
.gate-slots {
gap: 14px;
.gate-slot {
width: 40px;
height: 40px;
.slot-number { font-size: 0.6em; }
}
}
.form-container {
h3 { font-size: 0.85rem; margin: 0.25rem 0; }
form { gap: 0.35rem; }
.form-control-lg {
--_pad-v: 0.4rem;
font-size: 0.9rem;
}
}
}
}