Files
parking_solution/apps/web/src/index.css
T
julian dc2cdc0a91 feat(web): self-host Chakra Petch as the app's primary face
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
2026-07-05 15:23:15 +02:00

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);
}
}