Files
recflare/apps/www/src/client/styles.css
T
2026-08-10 10:26:39 -04:00

896 lines
18 KiB
CSS
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.
/*
* RecFlare — a poster for a game that's up right now.
*
* The page's job is to show that real people are playing on this server, then get you
* into the game or the Discord. So the surface is warm-dark (the room the
* screenshots are lit against), and the logo orange (#FE7101) is spent only on actions
* and the brand mark — never as decoration, or it stops reading as "click this". Green
* means one thing throughout, healthy: the server is up, or the change saved.
*
* Type: Archivo for the nameplate, IBM Plex Sans for everything else. There is
* deliberately no monospace anywhere — this is a game, and mono type made the whole
* page read like a status dashboard.
*/
:root {
color-scheme: dark light;
--bg: #14100c;
--surface: #1e1813;
--surface-hi: #26201a;
--line: #33291f;
--text: #f5ede1;
--muted: #a8927c;
--accent: #fe7101;
--accent-hover: #ff8c33;
/* Dark ink on orange: white on this orange is only ~2.9:1 and fails AA. */
--accent-text: #17110a;
--live: #35d07f;
--error: #ff6b5e;
--ok: #35d07f;
--display: Archivo, system-ui, sans-serif;
--body: 'IBM Plex Sans', system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
--radius: 10px;
}
@media (prefers-color-scheme: light) {
:root {
/* Off-white with just enough warmth to sit under the orange — not a cream wash. */
--bg: #f7f5f2;
--surface: #ffffff;
--surface-hi: #fbf9f6;
--line: #e2ddd6;
--text: #201a14;
--muted: #736656;
--accent: #e05f00;
--accent-hover: #c65300;
--accent-text: #fffaf4;
--live: #12855a;
--error: #c62f22;
--ok: #12855a;
}
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--bg);
color: var(--text);
font: 400 16px/1.6 var(--body);
-webkit-font-smoothing: antialiased;
}
/* Keyboard focus stays visible everywhere, including on the dark surfaces. */
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
border-radius: 4px;
}
/* ---- Containers --------------------------------------------------------- */
.shell {
max-width: 440px;
margin: 0 auto;
padding: 40px 20px 80px;
}
/* The account dashboard: room for the tab rail without stranding the forms. */
.shell.wide {
max-width: 880px;
}
/* The homepage: the stage above brings its own top space and shares this width. */
.shell.home {
max-width: 1040px;
padding-top: 0;
}
/* ---- Navigation --------------------------------------------------------- */
.nav {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
max-width: 1040px;
margin: 0 auto;
padding: 20px;
border-bottom: 1px solid var(--line);
}
.brand {
display: inline-flex;
align-items: center;
gap: 9px;
font-family: var(--display);
font-weight: 800;
font-size: 1.2rem;
letter-spacing: -0.015em;
color: var(--text);
text-decoration: none;
}
.brand::before {
content: '';
width: 9px;
height: 9px;
border-radius: 2px;
background: var(--accent);
}
.nav-links {
display: flex;
align-items: center;
gap: 20px;
}
.nav-links a,
.linkish {
font-family: var(--body);
font-size: 0.95rem;
color: var(--muted);
text-decoration: none;
}
.linkish {
background: none;
border: none;
padding: 0;
cursor: pointer;
}
.nav-links a:hover,
.nav-links a.active,
.linkish:hover {
color: var(--text);
}
/* ---- The stage (hero) --------------------------------------------------- */
/*
* Split down the middle: what this is and the way in on the left, a photo somebody
* actually took in game on the right. The photo is proof and never the payload — with
* no photo the frame holds its space and the left half carries the same words.
*/
.stage {
display: grid;
grid-template-columns: 1fr 1fr;
align-items: center;
gap: 48px;
max-width: 1040px;
margin: 0 auto;
padding: 56px 20px 16px;
}
/* min-width: 0 on both halves, or the split isn't one: a `1fr` track's automatic
minimum is its content's min-content width, so a wide child (the slideshow controls,
a long unbroken credit) grows its column past 50% and takes the space out of the
other one — which is how this last read as 30/70 with the buttons crushed. */
.stage-show {
display: flex;
flex-direction: column;
gap: 12px;
min-width: 0;
}
/* Fixed shape, filled by whatever lands: screenshots arrive at any aspect ratio, and a
frame that resized per photo would jog the headline beside it on every rotation. The
ratio is landscape rather than 4:3 so the photo doesn't tower over the column beside
it — equal columns still read unequal when one is half again as tall. */
.stage-frame {
position: relative;
aspect-ratio: 3 / 2;
overflow: hidden;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--surface-hi);
}
.stage-photo {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
animation: photo-in 0.7s ease;
}
@keyframes photo-in {
from {
opacity: 0;
}
}
.stage-body {
min-width: 0;
}
/* No width cap here, unlike the headings below: this one shares a row with the photo,
and a headline that stops short of its column makes the split read as 30/70. */
.stage-title {
font-family: var(--display);
font-weight: 800;
font-size: clamp(2rem, 5vw, 3.5rem);
line-height: 1;
letter-spacing: -0.03em;
margin: 0 0 16px;
text-wrap: balance;
}
/* The one place the orange carries meaning in the headline: the year it restores. */
.stage-title em {
font-style: normal;
color: var(--accent);
}
.stage-lede {
font-size: 1.05rem;
color: var(--muted);
margin: 0 0 28px;
}
/* The signup offer under the hero buttons. Deliberately quieter than a CTA — it sits
below the downloads without competing with them — but the link itself carries the
accent so it reads as the action it is. */
.stage-alt {
font-size: 0.925rem;
color: var(--muted);
margin: 18px 0 0;
}
.stage-alt a {
color: var(--accent);
font-weight: 600;
text-decoration: none;
}
.stage-alt a:hover {
text-decoration: underline;
}
/* Credit line and slideshow controls, under the photo rather than on it. */
.stage-foot {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
gap: 8px 20px;
/* Reserved even while the feed is in flight, so nothing shifts when it lands. */
min-height: 28px;
font-size: 0.8rem;
color: var(--muted);
}
/* Long usernames and room names wrap instead of widening the column. */
.credit {
min-width: 0;
overflow-wrap: anywhere;
}
/* Back / forward, with the position between them. Fixed width whatever the feed
length is — see the note on .stage-show for what a per-photo control did here. */
.steer {
display: flex;
align-items: center;
gap: 4px;
flex: none;
}
.steer button {
display: grid;
place-content: center;
width: 28px;
height: 28px;
padding: 0;
border: 1px solid var(--line);
border-radius: 8px;
background: transparent;
color: var(--muted);
cursor: pointer;
transition:
color 0.15s ease,
border-color 0.15s ease;
}
.steer button:hover {
color: var(--text);
border-color: var(--muted);
}
/* Tabular figures so the frame doesn't twitch as the index rolls 9 → 10. */
.count {
font-variant-numeric: tabular-nums;
padding: 0 6px;
}
/* ---- What it is (below the stage) --------------------------------------- */
.about {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 32px 48px;
align-items: start;
padding: 56px 0 8px;
}
.about-title {
font-family: var(--display);
font-weight: 700;
font-size: clamp(1.35rem, 2.6vw, 1.75rem);
line-height: 1.15;
letter-spacing: -0.02em;
margin: 0 0 12px;
max-width: 34ch;
text-wrap: balance;
}
.about-lede {
font-size: 1.05rem;
color: var(--muted);
margin: 0;
max-width: 56ch;
}
/* Emphasis in the lede lifts to full text colour — bolder alone barely reads
against the muted grey it sits in. */
.about-lede strong {
font-weight: 600;
color: var(--text);
}
.about-side {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 20px;
}
.about-links {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.status-block {
display: flex;
flex-direction: column;
gap: 7px;
}
.status-quip {
margin: 0;
font-size: 0.8rem;
font-style: italic;
color: var(--muted);
opacity: 0.8;
}
/* Status of the actual server, not decoration — see About. Set in plain sentence
case: uppercase mono made a friendly "we're up" read like a monitoring alert. */
.status {
display: inline-flex;
align-items: center;
gap: 9px;
margin: 0;
font-size: 0.95rem;
font-weight: 500;
color: var(--muted);
}
.status .dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--muted);
flex: none;
}
.status.online .dot {
background: var(--live);
box-shadow: 0 0 0 4px color-mix(in srgb, var(--live) 22%, transparent);
}
.status.online {
color: var(--text);
}
.status.down .dot {
background: var(--error);
}
.cta {
display: inline-block;
font-family: var(--body);
font-weight: 600;
font-size: 0.975rem;
text-decoration: none;
padding: 13px 30px;
border-radius: var(--radius);
border: 1px solid var(--accent);
background: var(--accent);
color: var(--accent-text);
transition:
background 0.15s ease,
border-color 0.15s ease;
}
.cta:hover {
background: var(--accent-hover);
border-color: var(--accent-hover);
}
.stage-actions {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
/* Discord blurple, so the button is recognisable before the label is read. Fixed
colours rather than tokens: it sits on a photo, so it can't follow the theme. */
.cta.discord {
background: #5865f2;
border-color: #5865f2;
color: #fff;
}
.cta.discord:hover {
background: #4752c4;
border-color: #4752c4;
}
/* Secondary action: same shape, outlined instead of filled. */
.cta.ghost {
background: transparent;
border-color: var(--line);
color: var(--text);
}
.cta.ghost:hover {
background: var(--surface);
border-color: var(--muted);
}
/* ---- Footer ------------------------------------------------------------- */
.footer {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
gap: 12px 24px;
max-width: 1040px;
margin: 0 auto;
padding: 24px 20px 40px;
border-top: 1px solid var(--line);
font-size: 0.875rem;
color: var(--muted);
}
.footer nav {
display: flex;
flex-wrap: wrap;
gap: 20px;
}
.footer a {
color: var(--muted);
text-decoration: none;
}
.footer a:hover {
color: var(--text);
}
/* The licence link sits inside a sentence rather than the link row, so it needs an
underline to be findable at all. */
.footer span a {
text-decoration: underline;
text-underline-offset: 2px;
text-decoration-color: color-mix(in srgb, var(--muted) 50%, transparent);
}
.footer span a:hover {
text-decoration-color: currentcolor;
}
/* ---- Cards and the account workspace ------------------------------------ */
h1 {
font-family: var(--display);
font-weight: 700;
font-size: 1.7rem;
letter-spacing: -0.02em;
margin: 0 0 24px;
}
h2 {
font-family: var(--display);
font-weight: 700;
font-size: 1.05rem;
letter-spacing: -0.01em;
margin: 0 0 14px;
}
.card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 22px;
margin-bottom: 16px;
}
.workspace {
display: grid;
grid-template-columns: 190px 1fr;
gap: 16px;
align-items: start;
}
.vtabs {
display: flex;
flex-direction: column;
gap: 4px;
}
.vtabs button {
text-align: left;
background: transparent;
border: 1px solid transparent;
color: var(--muted);
padding: 10px 13px;
border-radius: 8px;
cursor: pointer;
font-family: var(--body);
font-size: 0.9rem;
}
.vtabs button:hover {
color: var(--text);
background: var(--surface);
}
.vtabs button.active {
background: var(--surface);
border-color: var(--line);
color: var(--text);
font-weight: 600;
}
/* The active section's card carries its own margin; drop it inside the panel. */
.panel .card {
margin-bottom: 0;
}
.identity .handle {
font-size: 0.9rem;
color: var(--muted);
}
/* Sign in / create account, at the top of the auth card. Two of a kind, so they read as
one control rather than as two buttons competing with the orange submit below. */
.tabs {
display: flex;
gap: 6px;
margin-bottom: 20px;
padding: 4px;
background: var(--bg);
border: 1px solid var(--line);
border-radius: 8px;
}
.tabs button {
flex: 1;
background: transparent;
border: none;
color: var(--muted);
padding: 8px;
border-radius: 6px;
cursor: pointer;
font-family: var(--body);
font-size: 0.9rem;
}
.tabs button:hover {
color: var(--text);
}
.tabs button.active {
background: var(--surface-hi);
color: var(--text);
font-weight: 600;
}
/* ---- My rooms ----------------------------------------------------------- */
/*
* The owner's own room list. Rows rather than a grid of tiles: a room is identified by
* its name, and the counts underneath are the reason to look — both read left-to-right,
* which a tile would stack into a column of tiny type.
*/
.rooms {
list-style: none;
margin: 18px 0 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 12px;
}
.room {
display: grid;
grid-template-columns: 112px minmax(0, 1fr);
gap: 14px;
align-items: start;
padding-top: 12px;
border-top: 1px solid var(--line);
}
/* The first row sits directly under the intro copy, which already separates it. */
.room:first-child {
padding-top: 0;
border-top: none;
}
/* Fixed 3:2 frame — room thumbnails are screenshots and arrive at any ratio, and a
per-room height would leave the names in a ragged column. */
.room-thumb {
width: 100%;
aspect-ratio: 3 / 2;
object-fit: cover;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--surface-hi);
}
.room-body {
min-width: 0;
}
.room-head {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
/* Room names have no spaces to break at, so let a long one wrap anywhere rather than
widen the row past the panel. */
.room-name {
font-family: var(--display);
font-weight: 700;
font-size: 1rem;
letter-spacing: -0.01em;
overflow-wrap: anywhere;
}
/* Whether the room is visible to anyone else — the one piece of state an owner can't
see anywhere but here. Public gets the same green "healthy" reading as the server
status; every other value stays neutral, since Private isn't a fault. */
.badge {
flex: none;
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
padding: 2px 8px;
border-radius: 999px;
border: 1px solid var(--line);
color: var(--muted);
}
.badge.live {
color: var(--live);
border-color: color-mix(in srgb, var(--live) 45%, transparent);
}
.room-desc {
margin: 6px 0 0;
font-size: 0.875rem;
color: var(--muted);
/* Two lines: enough to tell rooms apart, not enough for one to own the list. */
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
overflow-wrap: anywhere;
}
.room-stats {
margin: 8px 0 0;
font-size: 0.8rem;
color: var(--muted);
font-variant-numeric: tabular-nums;
}
/* ---- Forms -------------------------------------------------------------- */
label {
display: block;
margin-bottom: 14px;
font-size: 0.85rem;
color: var(--muted);
}
input,
textarea {
display: block;
width: 100%;
margin-top: 6px;
padding: 10px 12px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--bg);
color: var(--text);
font-size: 0.95rem;
font-family: inherit;
}
textarea {
resize: vertical;
min-height: 76px;
}
/* Marks a field the form will submit without. Quiet, but next to the label rather than
inside the input, so it survives the field being filled in. */
.optional {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--muted);
opacity: 0.7;
margin-left: 4px;
}
/* Why a field is worth filling in, under the input it belongs to. Sits inside the label,
so it's read out with the field rather than as loose text after it. */
.hint {
display: block;
margin-top: 6px;
font-size: 0.8rem;
line-height: 1.45;
color: var(--muted);
}
input:focus,
textarea:focus {
outline: 2px solid var(--accent);
outline-offset: 0;
border-color: transparent;
}
/* Where the Turnstile iframe mounts. It brings its own chrome, so this only reserves the
space (the widget is 300×65 at normal size) — otherwise the submit button jumps down
the moment the check finishes loading. */
.turnstile {
min-height: 65px;
margin-bottom: 14px;
}
button[type='submit'] {
border: none;
border-radius: 8px;
padding: 11px 18px;
font-family: var(--body);
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
background: var(--accent);
color: var(--accent-text);
/* Hugs its label: a full-width orange bar for "save email" outshouts the hero. */
width: auto;
}
button[type='submit']:not(:disabled):hover {
background: var(--accent-hover);
}
button[type='submit']:disabled {
opacity: 0.55;
cursor: default;
}
/* The cross-link under an auth form ("Don't have an account? Create one"). Sits under
the submit button, which hugs its label, so it needs its own separation from it. */
.swap {
margin: 16px 0 0;
}
.swap a {
color: var(--accent);
font-weight: 600;
text-decoration: none;
}
.swap a:hover {
text-decoration: underline;
}
/* ---- Utilities ---------------------------------------------------------- */
.big {
font-family: var(--display);
font-size: 1.2rem;
font-weight: 700;
letter-spacing: -0.01em;
margin: 3px 0;
}
.muted {
color: var(--muted);
font-size: 0.875rem;
}
.error,
.ok {
font-size: 0.875rem;
margin: 0 0 12px;
}
.error {
color: var(--error);
}
.ok {
color: var(--ok);
}
/* ---- Responsive --------------------------------------------------------- */
@media (max-width: 860px) {
/* One column: the words lead, the photo follows. */
.stage {
grid-template-columns: 1fr;
gap: 32px;
padding: 40px 20px 8px;
}
}
@media (max-width: 760px) {
/* One column: the copy first, then the links and the status under it. */
.about {
grid-template-columns: 1fr;
gap: 26px;
padding-top: 40px;
}
}
@media (max-width: 620px) {
.stage-actions .cta {
flex: 1 1 auto;
text-align: center;
}
.about-links .cta {
flex: 1 1 auto;
text-align: center;
}
.workspace {
grid-template-columns: 1fr;
}
.vtabs {
flex-direction: row;
flex-wrap: wrap;
}
.vtabs button {
border-color: var(--line);
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}