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

623 lines
20 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.

// ── Fluid root rem: 1rem scales with viewport ─────────────────────────────
// All sidebar/h2/font sizes downstream are in rem, so redefining root
// font-size against `vmin` (smaller of vh/vw) gives us a single sliding
// scale that's invariant under phone rotation: rotating swaps width/height
// but vmin stays the same → 1rem stays the same → navbar/footer/h2 hold
// their size. Floor 14px on cramped viewports, ceiling 22px on huge ones.
// 2.4vmin hits 16px (browser default) at vmin=667 (iPhone SE landscape).
html {
font-size: clamp(14px, 2.4vmin, 22px);
}
// Layout custom properties — single source of truth for the landscape
// sidebar width (navbar/footer) + the rotated-h2 column slot to the right
// of the navbar. Container margin-left in landscape adds these so applets
// can't bleed under the wordmark.
:root {
--sidebar-w: 5rem;
--h2-col-w: 3rem;
}
body {
display: flex;
flex-direction: column;
background-color: rgba(var(--priUser), 1);
color: rgba(var(--secUser), 1);
font-family: Georgia, serif;
height: 100vh;
a {
text-decoration: none;
font-weight: 700;
color: rgba(var(--terUser), 1);
&:hover {
color: rgba(var(--ninUser), 1);
text-shadow: 0 0 0.5rem rgba(var(--terUser), 1);
}
}
.container {
max-width: 960px;
width: 100%;
margin: 0 auto;
// padding: 0 1rem;
flex: 1;
.navbar {
padding: 0.75rem 0;
border-bottom: 0.1rem solid rgba(var(--secUser), 0.4);
.navbar-brand {
margin-left: 1rem;
h1 {
font-size: 2rem;
}
}
.container-fluid {
display: flex;
align-items: center;
gap: 1rem;
margin-right: 0.5rem;
.navbar-user {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
justify-content: center;
gap: 0.25rem;
.navbar-text { flex: none; } // prevent expansion; BYE abuts the spans
> form { flex-shrink: 0; order: -1; } // BYE left of spans
}
> #id_cont_game { flex-shrink: 0; }
}
.navbar-text,
.navbar-link {
flex: 1;
min-width: 0;
text-align: center;
.navbar-label {
display: block;
color: rgba(var(--secUser), 0.7);
font-size: 0.75rem;
}
.navbar-identity {
display: block;
color: rgba(var(--quaUser), 1);
font-size: 0.875rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
.input-group {
position: fixed;
left: 0;
right: 0;
top: 50%;
transform: translateY(-50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
z-index: 50;
// Match .sky-field label — small, gold, uppercase, tracked.
label {
font-size: 0.6rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: rgba(var(--quaUser), 0.8);
}
.form-control {
width: 24rem;
text-align: center;
}
}
.form-control {
background-color: rgba(var(--priUser), 1);
color: rgba(var(--secUser), 1);
border: 0.1rem solid rgba(var(--secUser), 0.5);
--_pad-v: 0.5rem;
padding: var(--_pad-v) 0.75rem;
border-radius: calc((var(--_pad-v) * 2 + 1em) / 3);
width: 100%;
font-family: inherit;
&.is-invalid {
border-color: rgba(var(--priRd), 1);
}
&.form-control-lg {
--_pad-v: 0.75rem;
padding: var(--_pad-v) 1rem;
font-size: 1.125rem;
}
&.is-invalid ~ .invalid-feedback {
display: block;
}
&:focus {
border-color: rgba(var(--terUser), 0.75);
box-shadow: 0 0 0.75rem rgba(var(--terUser), 0.5);
}
}
.invalid-feedback {
display: none;
color: rgba(var(--priRd), 1);
font-size: 0.875rem;
margin-top: 0.25rem;
}
.alert {
padding: 0.75rem 1rem;
margin: 0.75rem;
border-radius: 0.5rem;
border: 0.1rem solid rgba(var(--priYl), 0.5);
color: rgba(var(--priYl), 1);
&.alert-success {
border-color: rgba(var(--priGn), 0.5);
color: rgba(var(--priGn), 1);
}
&.alert-warning {
border-color: rgba(var(--priOr), 0.5);
color: rgba(var(--priOr), 1);
}
}
.row {
padding: 2rem 0;
.col-md-12 {
width: 100%;
justify-content: center;
}
.col-lg-6 {
max-width: inherit;
margin: 0 1rem;
// Two-span title: <span>BILL</span><span>POST</span>. First
// word (always 4 letters: BILL/DASH/GAME/etc.) gets 45% of
// the title width; the variable second word fills the
// remaining 55%. Letters within each span spread via
// text-align: justify + text-justify: inter-character. The
// first-span colour shifts to --quaUser so the two-tone
// heading reads "Bill | Post" / "Dash | Sky".
h2 {
display: flex;
font-size: 3rem;
color: rgba(var(--secUser), 0.75);
margin-bottom: 1rem;
text-transform: uppercase;
text-shadow:
var(--title-shadow-offset) var(--title-shadow-offset) 0 rgba(0, 0, 0, 0.8)
;
// Each word-span hosts per-letter <span>s injected by the
// h2-letter-split script in base.html — display: flex +
// justify-content: space-between distributes those letters
// across the slot's width (or height in landscape's
// writing-mode: vertical-rl). text-justify: inter-character
// would do the same in pure CSS, but iOS Safari + Firefox
// silently fall back to inter-word for Latin scripts, which
// can't split a single word — letters end up clustered at
// the slot's start with empty space trailing. The flex
// approach works everywhere.
> span {
display: flex;
justify-content: space-between;
align-items: center;
box-sizing: border-box;
}
// Padding-inline (logical) creates the natural visual gap
// between the two words at the 45/55 boundary — works for
// both portrait (horizontal) AND the landscape rotated
// wordmark (vertical-rl writing mode).
> span:first-child {
flex: 0 0 45%;
padding-inline-end: 0.4em;
color: rgba(var(--quaUser), 0.75);
}
> span:last-child {
flex: 0 0 55%;
padding-inline-start: 0.4em;
}
}
}
}
}
}
@media (min-width: 1200px) {
body .container {
max-width: 1200px;
}
}
@media (orientation: landscape) {
// ── Sidebar layout: navbar ← left, footer → right ────────────────────────────
body {
flex-direction: row;
}
// Navbar → fixed left sidebar (width derives from --sidebar-w which is
// fluid via the rem-redefine above; no per-breakpoint width jumps).
body .container .navbar {
position: fixed;
left: 0;
top: 0;
height: 100vh;
width: var(--sidebar-w);
padding: 0.5rem 0;
border-bottom: none;
border-right: 0.1rem solid rgba(var(--secUser), 0.4);
background-color: rgba(var(--priUser), 1);
z-index: 100;
overflow: hidden;
.container-fluid {
flex-direction: column;
height: 100%;
max-height: 700px;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0 0.25rem;
margin: 0; // reset portrait margin-right: 0.5rem so container fills full sidebar width
> #id_cont_game { flex-shrink: 0; order: -1; } // cont-game above brand
.navbar-user {
flex-direction: column;
align-items: center;
gap: 0.25rem;
.navbar-text { margin: 0; } // cancel landscape margin:auto so BYE abuts
> form { order: 0; .btn { margin-top: 0; } } // abut spans
}
}
.navbar-brand h1 {
writing-mode: vertical-rl;
transform: rotate(180deg);
font-size: 1.2rem;
line-height: 1.2;
white-space: nowrap;
// margin-right: 3.25rem;
}
.navbar-brand {
order: 1; // brand at bottom
width: 100%;
margin-left: 0; // reset portrait margin-left: 1rem
display: flex;
justify-content: center;
}
.navbar-link { display: none; }
.navbar-text {
writing-mode: vertical-rl;
transform: rotate(180deg);
font-size: 0.65rem;
white-space: nowrap;
margin: auto 0;
.navbar-label { opacity: 0.7; }
}
// .btn-primary {
// width: 4rem;
// height: 4rem;
// font-size: 0.875rem;
// border-width: 0.21rem;
// }
// Login form: offset from fixed sidebars in landscape
.input-group {
left: var(--sidebar-w);
right: var(--sidebar-w);
.navbar-text {
writing-mode: horizontal-tb;
transform: none;
font-size: 0.75rem;
white-space: normal;
margin: 0 0 0.25rem;
text-align: center;
}
}
}
// Container: fill center, compensate for fixed sidebars on both sides
// AND for the rotated-h2 column on the left (so applets can't bleed
// under the wordmark — true aperture clipping).
// max-width: none overrides the @media (min-width: 1200px) rule above
// so the container fills all available space between the sidebars.
body .container {
flex: 1;
min-width: 0;
max-width: none;
margin-left: calc(var(--sidebar-w) + var(--h2-col-w));
margin-right: var(--sidebar-w);
padding: 0 0.5rem;
}
// Header row: h2 rotates into the dedicated --h2-col-w slot just right
// of the navbar. position:fixed takes h2 out of flow; .row collapses
// to zero height automatically. Resets portrait flex so the rotated
// wordmark renders as one continuous title (not split 45/55 here).
body .container .row {
padding: 0;
margin: 0;
}
body .container .row .col-lg-6 h2 {
position: fixed;
left: var(--sidebar-w);
width: var(--h2-col-w);
top: 50%;
height: 80vh; // explicit height so the flex 45/55 % basis resolves
transform: translateY(-50%) rotate(180deg);
writing-mode: vertical-rl; // inline axis becomes top-to-bottom; flex stacks on it
// Per-letter flex spread (justify-content: space-between on each word
// span) fills the slot regardless of font-size, so we only need to
// cap font-size by vh so each letter glyph stays smaller than slot /
// letter-count. Worst case: HOWDY STRANGER (8ch second word) in 55%
// of 80vh on a 375-tall iPhone SE landscape → 165px slot ÷ 8 ≈ 20px
// max glyph height; clamp's 4.4vh + 1.2rem floor gives 16.516.8px
// at that viewport, well under 20.
font-size: clamp(1.2rem, 4.4vh, 2.75rem);
margin: 0;
z-index: 85;
pointer-events: none;
// Inherits display: flex + the per-span flex 45/55 + padding-inline
// boundary from the portrait base. With writing-mode: vertical-rl the
// flex axis runs vertically, so first-span (BILL) takes 45% of the
// height (becomes bottom 45% after rotate(180deg)) and second-span
// (POST) takes the upper 55%. padding-inline-end resolves to the
// bottom edge of the first span — natural break between words.
}
// 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.
body #id_footer {
position: fixed;
right: 0;
top: 0;
width: var(--sidebar-w);
height: 100vh;
flex-direction: column;
justify-content: center;
align-items: center;
border-top: none;
border-left: 0.1rem solid rgba(var(--secUser), 0.3);
background-color: rgba(var(--priUser), 1); // opaque: masks tray sliding behind it
padding: 1rem 0;
gap: 0;
z-index: 100;
#id_footer_nav {
flex-direction: column-reverse;
width: auto;
max-width: none;
gap: 1.5rem !important;
margin-bottom: 4rem;
a {
font-size: 1.75rem;
display: flex;
justify-content: center;
align-items: center;
}
}
// ©2026 Dis Co. — single-line vertical strip at the right edge of
// the right sidebar (= the very right edge of the viewport in
// landscape). Reads bottom-to-top via writing-mode: vertical-rl +
// rotate(180deg) — same pattern as the navbar's rotated brand
// wordmark. Tucks into the empty 0.875rem gutter between the
// viewport edge and the centred icon/btn column, no overlap.
.footer-container {
position: absolute;
right: 0.125rem;
top: auto;
line-height: 1 !important;
color: rgba(var(--secUser), 1);
writing-mode: vertical-rl;
transform: rotate(180deg);
white-space: nowrap;
br { display: none; }
small {
font-size: 0.75rem !important;
}
}
}
}
// Footer typography refinements that only kick in once the viewport is
// wide enough to clear the cramped phone-landscape regime. Sidebar
// dimensions themselves are now fluid via rem and don't need a per-
// breakpoint width override (the old ≥1800px doubling block is gone).
@media (orientation: landscape) and (min-width: 700px) {
body #id_footer {
#id_footer_nav {
gap: 3rem !important;
a {
font-size: 1.75rem;
display: flex;
justify-content: center;
align-items: center;
}
}
.footer-container {
line-height: 1;
// margin-top vestige of the absolute-top-anchored layout —
// dropped now that the rotated text is bottom-anchored.
small {
font-size: 1rem;
}
}
}
}
@media (orientation: portrait) and (max-width: 500px) {
body .container {
.navbar {
padding: 0 0 0.25rem 0;
.navbar-brand h1 {
font-size: 1.2rem;
}
}
// Per-letter flex spread fills each 45/55 slot regardless of font-size,
// so we just need to cap the glyph size by viewport width to keep the
// worst-case title (HOWDY STRANGER, 8ch second word) from clipping at
// tiny mobile widths. clamp picks max(1.3rem, 5vw) capped at 2rem —
// at 320w → 18.2px, 390w → 19.5px, 430w → 21.5px.
// padding-inline boundary bumped from 0.4em → 0.6em (each side) so the
// last letter of word 1 (H) and first letter of word 2 (B) don't run
// together at the cramped portrait font-size.
.row .col-lg-6 h2 {
margin: 0;
font-size: clamp(1.3rem, 5vw, 2rem);
> span:first-child { padding-inline-end: 0.6em; }
> span:last-child { padding-inline-start: 0.6em; }
}
}
}
#id_footer {
flex-shrink: 0;
height: 6rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
align-items: center;
padding: 1rem 1rem;
border-top: 0.1rem solid rgba(var(--secUser), 0.3);
// background: linear-gradient(
// to top,
// rgba(var(--priUser), 1) 25%,
// transparent 100%
// );
#id_footer_nav {
display: flex;
justify-content: space-evenly;
width: 80%;
max-width: 500px;
a {
font-size: 1.75rem;
color: rgba(var(--secUser), 0.6);
text-shadow:
0 0 0.25rem rgba(0, 0, 0, 0.25),
;
&.active {
color: rgba(var(--quaUser), 1);
text-shadow:
0 0 0.5rem rgba(0, 0, 0, 0.5),
;
}
&:hover {
color: rgba(var(--quaUser), 1);
text-shadow:
0 0 1rem rgba(0, 0, 0, 0.25),
0 0 0.5rem rgba(var(--ninUser), 0.25)
;
}
}
}
.footer-container {
br { display: none; }
small {
font-size: 0.75rem;
opacity: 1;
}
}
}
.forthcoming {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
font-style: italic;
opacity: 0.6;
}
// Ordinal superscript: 21st, 2nd, 3rd etc. — matches .tt-ord but globally available.
.ord {
font-size: 0.6em;
vertical-align: 0.25em;
line-height: 0;
margin-left: -0.1em;
letter-spacing: 0;
}
#id_guard_portal {
display: none;
position: fixed;
z-index: 10000;
padding: 0.75rem 1rem;
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);
&.active {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}
.guard-message {
font-size: 0.85rem;
color: rgba(var(--secUser), 0.9);
text-align: center;
white-space: nowrap;
}
.guard-actions {
display: flex;
gap: 0.5rem;
}
}
.card-ref {
color: rgba(var(--terUser), 1) !important;
font-weight: 600 !important;
}