dc2cdc0a91
The booth is an offline appliance — no webfont CDN — so the font ships from public/fonts/chakra-petch: latin subset (covers en + sq ë/ç), the weights the UI actually uses (400/600/700 + 400 italic, ~40 KB total), SIL OFL license alongside the files. Chakra Petch leads all four family tokens (mono/display/ui/body) with the previous stacks kept as fallback; index.html preloads the two everywhere-weights so first paint doesn't flash the fallback. Not a true monospace — .num/.tabular still request tabular figures and columns verified aligned in the built app. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
493 lines
16 KiB
CSS
493 lines
16 KiB
CSS
@import "tailwindcss";
|
|
|
|
/* Bloomberg-terminal aesthetic: dense, dark, monospace, keyboard-first.
|
|
Tailwind v4 — design tokens live here in @theme (no tailwind.config.js).
|
|
The booth runs on a fixed appliance display; we optimise for a dark room,
|
|
glanceable status colour, and high information density over whitespace.
|
|
|
|
Token vocabulary adopted from the "TRM" design system (Tracking & Race
|
|
Management) — TOKENS ONLY: colours, type scale, spacing, radii, shadows.
|
|
None of TRM's race-timing components are used. The existing `term-*` accents
|
|
are aligned onto TRM's exact values so the whole booth UI inherits the TRM
|
|
palette without renaming a single class. TRM's full vocabulary is also
|
|
exposed as utilities (night-*, ink-*, paper-*, flag/amber/green/blue, the
|
|
spacing/type/shadow scales) for new work.
|
|
|
|
Offline appliance: NO webfont @import (no network at runtime). The primary
|
|
face is Chakra Petch, SELF-HOSTED from public/fonts/chakra-petch (SIL OFL,
|
|
license alongside the files) — latin subset only (covers en + sq ë/ç), the
|
|
weights the UI actually uses (400/600/700 + 400 italic). Not a true
|
|
monospace: it stays FIRST in --font-mono for the look, with the real mono
|
|
stack behind it as fallback; .num/.tabular still request tabular figures. */
|
|
|
|
@font-face {
|
|
font-family: "Chakra Petch";
|
|
font-style: normal;
|
|
font-weight: 400;
|
|
font-display: swap;
|
|
src: url("/fonts/chakra-petch/chakra-petch-latin-400.woff2") format("woff2");
|
|
}
|
|
@font-face {
|
|
font-family: "Chakra Petch";
|
|
font-style: normal;
|
|
font-weight: 600;
|
|
font-display: swap;
|
|
src: url("/fonts/chakra-petch/chakra-petch-latin-600.woff2") format("woff2");
|
|
}
|
|
@font-face {
|
|
font-family: "Chakra Petch";
|
|
font-style: normal;
|
|
font-weight: 700;
|
|
font-display: swap;
|
|
src: url("/fonts/chakra-petch/chakra-petch-latin-700.woff2") format("woff2");
|
|
}
|
|
@font-face {
|
|
font-family: "Chakra Petch";
|
|
font-style: italic;
|
|
font-weight: 400;
|
|
font-display: swap;
|
|
src: url("/fonts/chakra-petch/chakra-petch-latin-400-italic.woff2") format("woff2");
|
|
}
|
|
|
|
@theme {
|
|
/* ============================================================
|
|
TERMINAL ACCENTS — aligned onto TRM's exact values.
|
|
These keep their `term-*` names (used across every screen),
|
|
but now resolve to TRM colours so the palette is unified.
|
|
============================================================ */
|
|
/* Surfaces — TRM "night" (trackside dark) scale. */
|
|
--color-term-bg: #0b0d10; /* TRM --night */
|
|
--color-term-panel: #14171c; /* TRM --night-2 */
|
|
--color-term-panel-2: #1e222a; /* TRM --night-3 */
|
|
--color-term-border: #2a2f38; /* TRM --night-line */
|
|
--color-term-muted: #8a8a82; /* TRM --night-fg-3 / --ink-4 */
|
|
--color-term-text: #f2f2ee; /* TRM --night-fg */
|
|
|
|
/* Status accents — TRM semantic colours. */
|
|
--color-term-amber: #f2a516; /* TRM --amber (caution / accent / focus) */
|
|
--color-term-green: #2e8c4a; /* TRM --green (entry / ok / free) */
|
|
--color-term-red: #e8412b; /* TRM --flag (exit / fault / full) */
|
|
--color-term-cyan: #2563c8; /* TRM --blue (payment / info / live) */
|
|
|
|
/* ============================================================
|
|
TRM FULL VOCABULARY — exposed as Tailwind utilities for new work.
|
|
============================================================ */
|
|
/* Ink & paper (light surfaces — for any light-on-dark inversions). */
|
|
--color-paper: #fafaf7;
|
|
--color-paper-2: #f2f2ee;
|
|
--color-paper-3: #e8e8e2;
|
|
--color-ink: #0e0e0c;
|
|
--color-ink-2: #2a2a26;
|
|
--color-ink-3: #5a5a53;
|
|
--color-ink-4: #8a8a82;
|
|
--color-ink-5: #b8b8b0;
|
|
--color-ink-6: #dcdcd4;
|
|
|
|
/* Night scale (the booth's working surfaces). */
|
|
--color-night: #0b0d10;
|
|
--color-night-2: #14171c;
|
|
--color-night-3: #1e222a;
|
|
--color-night-line: #2a2f38;
|
|
--color-night-fg: #f2f2ee;
|
|
--color-night-fg-2: #b8b8b0;
|
|
--color-night-fg-3: #8a8a82;
|
|
|
|
/* Race accents + semantic. */
|
|
--color-flag: #e8412b;
|
|
--color-flag-2: #c8331f;
|
|
--color-flag-tint: #fbe3de;
|
|
--color-amber: #f2a516;
|
|
--color-amber-2: #c88500;
|
|
--color-amber-tint: #fbefd0;
|
|
--color-green: #2e8c4a;
|
|
--color-green-2: #1f6a36;
|
|
--color-green-tint: #ddefe2;
|
|
--color-blue: #2563c8;
|
|
--color-blue-2: #1a4fa8;
|
|
--color-blue-tint: #dce6f8;
|
|
--color-violet: #6b46c1;
|
|
--color-magenta: #c9296f;
|
|
--color-teal: #188c8a;
|
|
|
|
--color-ok: #2e8c4a;
|
|
--color-warn: #f2a516;
|
|
--color-danger: #e8412b;
|
|
--color-info: #2563c8;
|
|
|
|
/* Data-viz categorical (8). */
|
|
--color-viz-1: #e8412b;
|
|
--color-viz-2: #2563c8;
|
|
--color-viz-3: #2e8c4a;
|
|
--color-viz-4: #f2a516;
|
|
--color-viz-5: #6b46c1;
|
|
--color-viz-6: #188c8a;
|
|
--color-viz-7: #c9296f;
|
|
--color-viz-8: #5a5a53;
|
|
|
|
/* ---------- TYPE — families ---------- */
|
|
/* Chakra Petch (self-hosted, see @font-face above) is the booth's primary
|
|
face everywhere — it leads every stack so headings, body, and the
|
|
`font-mono` chrome all render with it; the stacks behind it are the
|
|
pre-2026-07-05 fallbacks for glyphs outside the latin subset. */
|
|
--font-mono: "Chakra Petch", "JetBrains Mono", "IBM Plex Mono", ui-monospace,
|
|
"SFMono-Regular", "Menlo", "Consolas", monospace;
|
|
--font-display: "Chakra Petch", "Helvetica Neue", Arial, sans-serif;
|
|
--font-ui: "Chakra Petch", "Helvetica Neue", Arial, sans-serif;
|
|
--font-body: "Chakra Petch", "Inter", "Helvetica Neue", Arial, sans-serif;
|
|
|
|
/* ---------- TYPE — scale (TRM, optimised for data density) ---------- */
|
|
--text-overline: 11px;
|
|
--text-micro: 12px;
|
|
--text-small: 13px;
|
|
--text-body: 15px;
|
|
--text-lead: 17px;
|
|
--text-h6: 14px;
|
|
--text-h5: 16px;
|
|
--text-h4: 20px;
|
|
--text-h3: 26px;
|
|
--text-h2: 34px;
|
|
--text-h1: 48px;
|
|
--text-display: 72px;
|
|
--text-jumbo: 120px;
|
|
|
|
/* ---------- SPACING (TRM 4px base) ---------- */
|
|
--spacing-s0: 0;
|
|
--spacing-s1: 2px;
|
|
--spacing-s2: 4px;
|
|
--spacing-s3: 8px;
|
|
--spacing-s4: 12px;
|
|
--spacing-s5: 16px;
|
|
--spacing-s6: 20px;
|
|
--spacing-s7: 24px;
|
|
--spacing-s8: 32px;
|
|
--spacing-s9: 40px;
|
|
--spacing-s10: 48px;
|
|
--spacing-s11: 64px;
|
|
--spacing-s12: 80px;
|
|
--spacing-s13: 96px;
|
|
|
|
/* ---------- RADIUS — TRM is square-edged ---------- */
|
|
--radius-term: 2px; /* existing alias, kept */
|
|
--radius-r0: 0;
|
|
--radius-r1: 2px;
|
|
--radius-r2: 4px;
|
|
--radius-r3: 6px;
|
|
--radius-r4: 10px;
|
|
|
|
/* ---------- ELEVATION — TRM sharp "printed" offset shadows ---------- */
|
|
--shadow-term-1: 0 1px 0 0 #0e0e0c;
|
|
--shadow-term-2: 2px 2px 0 0 #0e0e0c;
|
|
--shadow-term-3: 4px 4px 0 0 #0e0e0c;
|
|
--shadow-soft: 0 1px 2px rgba(14, 14, 12, 0.06), 0 4px 12px rgba(14, 14, 12, 0.04);
|
|
--shadow-pop: 0 8px 24px rgba(14, 14, 12, 0.12);
|
|
|
|
/* ---------- MOTION (TRM) ---------- */
|
|
--ease-snap: cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
|
|
|
|
/* ---------- COMPONENT TOKENS (TRM control heights, table rows) ---------- */
|
|
--control-h-sm: 28px;
|
|
--control-h-md: 36px;
|
|
--control-h-lg: 44px;
|
|
--table-row-h: 36px;
|
|
--table-row-h-dense: 28px;
|
|
}
|
|
|
|
html,
|
|
body,
|
|
#root {
|
|
height: 100%;
|
|
}
|
|
|
|
/* Tell the engine the UI is dark so NATIVE controls — the <select> option popup,
|
|
scrollbars, date pickers, form widgets — render dark too. WebKitGTK (the Tauri
|
|
Linux WebView) otherwise paints the dropdown list with the OS light palette, so a
|
|
dark-theme <select> opened to a WHITE option list. `.theme-light` flips it back. */
|
|
html {
|
|
color-scheme: dark;
|
|
}
|
|
html.theme-light {
|
|
color-scheme: light;
|
|
}
|
|
|
|
body {
|
|
margin: 0;
|
|
background: var(--color-term-bg);
|
|
color: var(--color-term-text);
|
|
font-family: var(--font-mono);
|
|
font-size: 0.8125rem;
|
|
line-height: 1.4;
|
|
-webkit-font-smoothing: antialiased;
|
|
/* Crisp text and no rubber-banding on the fixed appliance display. */
|
|
overscroll-behavior: none;
|
|
}
|
|
|
|
/* Tabular numerics everywhere — counts, money, clocks must not jitter. */
|
|
.num,
|
|
.tabular {
|
|
font-variant-numeric: tabular-nums;
|
|
letter-spacing: -0.01em;
|
|
}
|
|
|
|
/* Terminal scrollbars — thin, dark, unobtrusive. */
|
|
* {
|
|
scrollbar-width: thin;
|
|
scrollbar-color: var(--color-term-border) transparent;
|
|
}
|
|
|
|
/* A visible keyboard-focus ring in the amber accent (keyboard-first UI). */
|
|
:focus-visible {
|
|
outline: 1px solid var(--color-term-amber);
|
|
outline-offset: 1px;
|
|
}
|
|
|
|
/* ============================================================
|
|
COMPONENT LAYER
|
|
The TRM tokens are good, but every screen hand-rolled inputs and
|
|
buttons as bare outlines on near-black panels, so a field, a card,
|
|
and a button were visually indistinguishable. These classes give
|
|
each control a real identity:
|
|
- inputs read as RECESSED slots (lighter fill + inset shadow)
|
|
- the primary button is FILLED (accent body, dark text) — the
|
|
one obvious action; neutrals are filled grey, not bare outlines
|
|
- explicit hover / active / focus / disabled states everywhere
|
|
Use @apply so the classes compose with Tailwind utilities.
|
|
============================================================ */
|
|
@layer components {
|
|
/* ---- Form fields: a recessed slot, clearly an input ---- */
|
|
.input,
|
|
.select,
|
|
.textarea {
|
|
@apply w-full rounded-term border bg-term-bg px-2.5 text-term-text
|
|
placeholder:text-term-muted;
|
|
border-color: #3a414c; /* lighter than panel borders */
|
|
height: var(--control-h-md);
|
|
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.45);
|
|
transition: border-color 120ms var(--ease-snap), box-shadow 120ms var(--ease-snap);
|
|
}
|
|
.textarea {
|
|
height: auto;
|
|
@apply py-2 leading-snug;
|
|
}
|
|
.input::placeholder,
|
|
.textarea::placeholder {
|
|
@apply text-term-muted;
|
|
}
|
|
.input:hover,
|
|
.select:hover,
|
|
.textarea:hover {
|
|
border-color: #4a525f;
|
|
}
|
|
.input:focus,
|
|
.select:focus,
|
|
.textarea:focus {
|
|
outline: none;
|
|
border-color: var(--color-term-amber);
|
|
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.45), 0 0 0 1px var(--color-term-amber);
|
|
}
|
|
.input:disabled,
|
|
.select:disabled,
|
|
.textarea:disabled {
|
|
@apply cursor-not-allowed opacity-50;
|
|
}
|
|
/* Native <option> popup colours. `color-scheme: dark` (on <html>) handles most
|
|
engines, but WebKitGTK (Tauri Linux) needs the option row colours set explicitly
|
|
or the open dropdown list stays white-on-light. The light theme re-lightens below. */
|
|
.select option,
|
|
.select optgroup {
|
|
background-color: var(--color-term-panel);
|
|
color: var(--color-term-text);
|
|
}
|
|
/* Small / dense variant for inline table cells */
|
|
.input-sm {
|
|
height: var(--control-h-sm);
|
|
@apply px-2 text-[0.75rem];
|
|
}
|
|
|
|
.field {
|
|
@apply flex flex-col gap-1;
|
|
}
|
|
.label {
|
|
@apply text-[0.6875rem] uppercase tracking-wider text-term-muted;
|
|
}
|
|
.hint {
|
|
@apply text-[0.6875rem] leading-snug text-term-muted;
|
|
}
|
|
|
|
/* ---- Buttons: a button must look pressable, never like a field ---- */
|
|
.btn {
|
|
@apply inline-flex items-center justify-center gap-1.5 rounded-term border
|
|
px-3 text-[0.75rem] font-semibold uppercase tracking-wider
|
|
transition-colors select-none;
|
|
height: var(--control-h-md);
|
|
/* Neutral default: a filled grey body, not a bare outline. */
|
|
background: var(--color-term-panel-2);
|
|
border-color: #3a414c;
|
|
color: var(--color-term-text);
|
|
}
|
|
.btn:hover:not(:disabled) {
|
|
background: #2a313b;
|
|
border-color: #4a525f;
|
|
}
|
|
.btn:active:not(:disabled) {
|
|
transform: translateY(1px);
|
|
}
|
|
.btn:disabled {
|
|
@apply cursor-not-allowed opacity-40;
|
|
}
|
|
.btn-sm {
|
|
height: var(--control-h-sm);
|
|
@apply px-2.5 text-[0.6875rem];
|
|
}
|
|
.btn-lg {
|
|
height: var(--control-h-lg);
|
|
@apply px-5 text-[0.8125rem];
|
|
}
|
|
|
|
/* Primary: FILLED amber, dark text — the unmistakable main action. */
|
|
.btn-primary {
|
|
background: var(--color-term-amber);
|
|
border-color: var(--color-term-amber);
|
|
color: #0b0d10;
|
|
}
|
|
.btn-primary:hover:not(:disabled) {
|
|
background: #ffb733;
|
|
border-color: #ffb733;
|
|
}
|
|
|
|
/* Semantic filled variants (entry / payment / destructive). */
|
|
.btn-go {
|
|
background: var(--color-term-green);
|
|
border-color: var(--color-term-green);
|
|
color: #f2f2ee;
|
|
}
|
|
.btn-go:hover:not(:disabled) {
|
|
background: #38a85a;
|
|
border-color: #38a85a;
|
|
}
|
|
.btn-pay {
|
|
background: var(--color-term-cyan);
|
|
border-color: var(--color-term-cyan);
|
|
color: #f2f2ee;
|
|
}
|
|
.btn-pay:hover:not(:disabled) {
|
|
background: #2f74e0;
|
|
border-color: #2f74e0;
|
|
}
|
|
.btn-danger {
|
|
background: transparent;
|
|
border-color: var(--color-term-red);
|
|
color: var(--color-term-red);
|
|
}
|
|
.btn-danger:hover:not(:disabled) {
|
|
background: color-mix(in srgb, var(--color-term-red) 14%, transparent);
|
|
}
|
|
|
|
/* Ghost: lowest-emphasis (cancel, secondary nav) — text + hover only. */
|
|
.btn-ghost {
|
|
background: transparent;
|
|
border-color: transparent;
|
|
color: var(--color-term-muted);
|
|
}
|
|
.btn-ghost:hover:not(:disabled) {
|
|
background: var(--color-term-panel-2);
|
|
color: var(--color-term-text);
|
|
border-color: transparent;
|
|
}
|
|
|
|
/* ---- Card: a panel that is clearly a container, not a field ---- */
|
|
.card {
|
|
@apply rounded-term border border-term-border bg-term-panel;
|
|
}
|
|
.card-head {
|
|
@apply flex items-center justify-between border-b border-term-border
|
|
bg-term-panel-2 px-4 py-2 text-[0.75rem] uppercase tracking-wider text-term-muted;
|
|
}
|
|
.card-body {
|
|
@apply p-4;
|
|
}
|
|
}
|
|
|
|
/* ============================================================
|
|
LIGHT THEME
|
|
The booth defaults to dark (dark room), but a user may prefer light; the
|
|
choice is saved to their profile (users.theme) and applied on <html> as
|
|
`.theme-light`. Every screen reads colour through the --color-term-* tokens,
|
|
so re-pointing them here re-skins the whole app. The TRM "paper/ink" scale
|
|
supplies the surfaces; accents are tuned a shade darker for contrast on white.
|
|
A few component-layer values are literal hex (input borders, the inset
|
|
"recessed" shadow, primary-button text) — those are overridden too so fields
|
|
and buttons keep their affordance on a light background.
|
|
============================================================ */
|
|
html.theme-light {
|
|
/* Surfaces — TRM paper scale (light → slightly darker for layering). */
|
|
--color-term-bg: #fafaf7; /* paper */
|
|
--color-term-panel: #f2f2ee; /* paper-2 */
|
|
--color-term-panel-2: #e8e8e2; /* paper-3 */
|
|
--color-term-border: #d2d2c8;
|
|
--color-term-muted: #5a5a53; /* ink-3 — readable secondary text */
|
|
--color-term-text: #14171c; /* near-black ink */
|
|
|
|
/* Accents — a step darker than the dark-theme values for white-bg contrast. */
|
|
--color-term-amber: #b8740a;
|
|
--color-term-green: #1f6a36;
|
|
--color-term-red: #c8331f;
|
|
--color-term-cyan: #1a4fa8;
|
|
}
|
|
|
|
/* Component-layer literals that must flip for light (the rest read tokens). */
|
|
html.theme-light .input,
|
|
html.theme-light .select,
|
|
html.theme-light .textarea {
|
|
border-color: #c2c2b8;
|
|
box-shadow: inset 0 1px 2px rgba(20, 23, 28, 0.08);
|
|
}
|
|
html.theme-light .input:hover,
|
|
html.theme-light .select:hover,
|
|
html.theme-light .textarea:hover {
|
|
border-color: #a8a89e;
|
|
}
|
|
html.theme-light .input:focus,
|
|
html.theme-light .select:focus,
|
|
html.theme-light .textarea:focus {
|
|
box-shadow: inset 0 1px 2px rgba(20, 23, 28, 0.08), 0 0 0 1px var(--color-term-amber);
|
|
}
|
|
html.theme-light .btn {
|
|
border-color: #c2c2b8;
|
|
}
|
|
html.theme-light .btn:hover:not(:disabled) {
|
|
background: #dcdcd4;
|
|
border-color: #a8a89e;
|
|
}
|
|
/* Filled buttons keep light text; primary uses dark-on-amber, kept legible. */
|
|
html.theme-light .btn-primary {
|
|
color: #fafaf7;
|
|
}
|
|
|
|
/* ── Barrier-light blink ────────────────────────────────────────────────────
|
|
The booth Entry/Exit indicator blinks green↔red (~1 Hz) when the radar/presence
|
|
input is active but the camera hasn't confirmed a vehicle yet — mirroring the
|
|
physical button lamp (relay 3). Toggles a CSS var the component maps onto its
|
|
border / tint / glyph, so green and red alternate every 500 ms. */
|
|
@keyframes lane-blink {
|
|
0%, 49% { --lane-c: var(--color-term-green); --lane-tint: color-mix(in srgb, var(--color-term-green) 10%, transparent); }
|
|
50%, 100% { --lane-c: var(--color-term-red); --lane-tint: color-mix(in srgb, var(--color-term-red) 10%, transparent); }
|
|
}
|
|
.lane-blink {
|
|
animation: lane-blink 1s steps(1, end) infinite;
|
|
border-color: var(--lane-c);
|
|
background: var(--lane-tint);
|
|
color: var(--lane-c);
|
|
}
|
|
@media (prefers-reduced-motion: reduce) {
|
|
/* No flashing for motion-sensitive users — hold the "attention" (red) state. */
|
|
.lane-blink {
|
|
animation: none;
|
|
--lane-c: var(--color-term-red);
|
|
--lane-tint: color-mix(in srgb, var(--color-term-red) 10%, transparent);
|
|
border-color: var(--lane-c);
|
|
background: var(--lane-tint);
|
|
color: var(--lane-c);
|
|
}
|
|
}
|