Files
undercooked/app/static/css/base.css
T
ducoterra 6fd83446ec phase: 01_runnable_service_scaffold
**Phase 01 final verification pass — all criteria green**

- Verified (nothing left to implement; all 4 task files already in `complete/`): toolchain lock, typed `Settings`+`.env.example`, debugpy bootstrap, app factory, CLI (`--host/--port/--reload/--levels-dir`, `python -m undercooked`), static shell + `/static`, security headers, no-CDN/doc guards, `validate.sh`.
- `uv sync --extra dev` → exit 0; `uv.lock` present (untracked → harness commits it with the phase).
- `uv run undercooked --host 127.0.0.1 --port 8000` → `curl /healthz` = 200 `{"status":"ok","version":"0.1.0","rooms":0}` (superset of the criterion's body — now documented in README); `curl /` = 200 shell HTML; static css/js 200; CSP/nosniff/no-referrer/DENY on 200, 404 and 405 too.
- `DEBUGPY=1`: port 5678 is held on this machine by the user's own podman dev-container debugpy adapter — **no live change made/killed**; verified via `DEBUGPY=1 DEBUGPY_PORT=5679` → listener opens, `/healthz` still 200; the occupied-port case degraded gracefully as designed (warning, server serves). Bad config → one line + exit 2 ✓.
- `bash .agents/validate.sh` (root **and** `.agents/`) → exit 0: `ruff check .` 0 errors, `pyright` 0 errors, levels-CLI notice-skip (matches README prose), **284 passed** full suite incl. 10 Playwright e2e, coverage **100.00%** (floor 90), repo hygiene clean. `uv run pytest -m e2e` → 10 passed.
- Docs fixed: README Status → phase 01 complete + next pending 02 (was pointing at `todo/01…`), documented real `/healthz` body, precedence now "CLI flags > env > .env > default", added the PLAN §13-promised **How to play** section (honest about shell-only today), Layout rows marked what exists vs phases 02/07. AGENTS.md: recorded the `data:`-URL exemption in the no-CDN rule (test allows it) and matched the session-token rule to the browser's WebCrypto/base64url implementation. Also corrected a stale "session-wide chromium" docstring in `tests/e2e/conftest.py` (fixtures are function-scoped).
- Deviations: no container build run (needs an image pull; not a phase criterion — `Containerfile`/`compose.yaml` reviewed and consistent with `docs/DEPLOYMENT.md`); `HEAD /` is 405 (routes are GET-only by design; health checks use GET). No git index/history commands run.
- Next pending phase: `02_level_content_and_validation`.
2026-10-06 01:51:20 -04:00

247 lines
5.8 KiB
CSS

/* Undercooked — base stylesheet (phase 01 shell).
*
* Rules that later phases must keep:
* - every byte is same-origin: no @import, no web fonts, no remote images (A12);
* - the palette, spacing and type scale exist only as custom properties below;
* - 44x44 minimum hit targets and >= 4.5:1 text contrast (PLAN.md phase 14);
* - the layout is fluid from a 360px phone to a 4K desktop without breakpoints;
* - motion is decorative: prefers-reduced-motion turns all of it off.
*/
:root {
/* Palette — warm kitchen, dark by default. Text pairs are >= 4.5:1. */
--uc-bg: #1a1713;
--uc-surface: #241f1a;
--uc-surface-2: #2f2820;
--uc-line: #857259; /* 3.1:1 on every surface: UI edges are visible (WCAG 1.4.11) */
--uc-ink: #f6f1e7; /* 13.9:1 on --uc-bg */
--uc-ink-muted: #cfc5b5; /* 8.6:1 on --uc-bg */
--uc-accent: #ff9f1c; /* 7.6:1 on --uc-bg */
--uc-accent-ink: #1a1713; /* 7.6:1 on --uc-accent */
--uc-danger: #ff8a80; /* 7.4:1 on --uc-bg */
--uc-focus: #ffe066;
/* Fluid type scale: 360px phone -> 3840px desktop, no breakpoints needed. */
--uc-step--1: clamp(0.78rem, 0.74rem + 0.2vw, 0.92rem);
--uc-step-0: clamp(1rem, 0.95rem + 0.3vw, 1.2rem);
--uc-step-1: clamp(1.25rem, 1.15rem + 0.6vw, 1.7rem);
--uc-step-2: clamp(1.7rem, 1.4rem + 1.6vw, 3rem);
--uc-space-1: 0.5rem;
--uc-space-2: 0.75rem;
--uc-space-3: 1.25rem;
--uc-space-4: 2rem;
--uc-radius: 0.65rem;
--uc-hit: 44px; /* WCAG 2.5.5 minimum target size */
--uc-content: 74rem;
--uc-font: ui-sans-serif, system-ui, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--uc-font-ui: ui-rounded, "Segoe UI Variable Display", var(--uc-font);
--uc-ease: cubic-bezier(0.2, 0.8, 0.2, 1);
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
min-height: 100svh;
display: grid;
grid-template-rows: 1fr auto;
gap: var(--uc-space-3);
padding: max(var(--uc-space-3), env(safe-area-inset-top))
max(var(--uc-space-2), env(safe-area-inset-right))
max(var(--uc-space-3), env(safe-area-inset-bottom))
max(var(--uc-space-2), env(safe-area-inset-left));
background: radial-gradient(120% 90% at 50% -10%, var(--uc-surface-2), var(--uc-bg) 60%);
color: var(--uc-ink);
font-family: var(--uc-font);
font-size: var(--uc-step-0);
line-height: 1.45;
text-rendering: optimizeLegibility;
}
.shell,
.colophon {
width: 100%;
max-width: var(--uc-content);
margin-inline: auto;
}
.masthead {
text-align: center;
}
.wordmark {
margin: 0;
font-family: var(--uc-font-ui);
font-size: var(--uc-step-2);
font-weight: 800;
letter-spacing: 0.01em;
color: var(--uc-accent);
}
.tagline,
.hint,
.colophon p {
margin: var(--uc-space-1) 0 0;
color: var(--uc-ink-muted);
font-size: var(--uc-step--1);
}
.panel {
display: grid;
gap: var(--uc-space-2);
margin: var(--uc-space-3) auto 0;
padding: var(--uc-space-3);
max-width: 34rem;
background: var(--uc-surface);
border: 1px solid var(--uc-line);
border-radius: var(--uc-radius);
}
.panel h2 {
margin: 0;
font-family: var(--uc-font-ui);
font-size: var(--uc-step-1);
}
.join-row {
display: flex;
flex-wrap: wrap;
gap: var(--uc-space-2);
}
label {
display: block;
margin-bottom: var(--uc-space-1);
font-size: var(--uc-step--1);
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--uc-ink-muted);
}
input[type="text"] {
flex: 1 1 8.5rem;
/* An <input>'s intrinsic width is its default 20 characters, which would widen
the whole fluid layout on a 360px phone. The value is 5 characters. */
min-width: 0;
min-height: var(--uc-hit);
padding: 0 var(--uc-space-2);
background: var(--uc-bg);
color: var(--uc-ink);
border: 1px solid var(--uc-line);
border-radius: var(--uc-radius);
font: inherit;
font-size: var(--uc-step-1);
letter-spacing: 0.35em;
text-transform: uppercase;
}
input[type="text"]::placeholder {
color: #8d8272; /* 4.6:1 on --uc-bg: placeholder text is real text */
letter-spacing: 0.35em;
}
.btn {
min-height: var(--uc-hit);
min-width: var(--uc-hit);
padding: 0 var(--uc-space-3);
background: var(--uc-surface-2);
color: var(--uc-ink);
border: 1px solid var(--uc-line);
border-radius: var(--uc-radius);
font: inherit;
font-size: var(--uc-step-0);
font-weight: 600;
cursor: pointer;
transition:
transform 120ms var(--uc-ease),
background-color 120ms var(--uc-ease);
}
.btn:hover {
background: #3b3226;
}
.btn:active {
transform: translateY(1px);
}
.btn-primary {
background: var(--uc-accent);
color: var(--uc-accent-ink);
border-color: var(--uc-accent);
}
.btn-primary:hover {
background: #ffb445;
}
/* Visible focus for keyboard and low-vision users; never remove this. */
:where(a, button, input, [tabindex]):focus-visible {
outline: 3px solid var(--uc-focus);
outline-offset: 2px;
border-radius: var(--uc-radius);
}
.status {
min-height: calc(var(--uc-hit) - 8px);
padding: var(--uc-space-2);
background: var(--uc-bg);
border: 1px dashed var(--uc-line);
border-radius: var(--uc-radius);
font-size: var(--uc-step--1);
}
.status[data-kind="error"] {
color: var(--uc-danger);
border-color: var(--uc-danger);
}
#kcanvas {
display: block;
width: 100%;
height: auto;
aspect-ratio: 16 / 10;
background: #0f0d0b;
border: 1px solid var(--uc-line);
border-radius: var(--uc-radius);
}
#kcanvas[hidden] {
display: none;
}
code {
font-family: ui-monospace, "Cascadia Mono", "Segoe UI Mono", Menlo, monospace;
font-size: 0.95em;
color: var(--uc-accent);
}
.colophon {
text-align: center;
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
scroll-behavior: auto !important;
}
.btn:active {
transform: none;
}
}