5a9ff8dda9
Foundation for a gallery of RL-environment demos. Three decisions worth recording here rather than in a commit nobody reads: The word lists are built, not copied. `envs/wordle_five/words/build_words.py` intersects Wordnik (MIT, 11,846 five-letter words) with SCOWL's common-American tier to produce 4,603 answers. The intersection is the point: the list is derived from two permissive sources by a stated rule rather than copied from anyone's editorial selection, and both inputs are committed so a rebuild is byte-identical. The design tokens are PIG's, inlined as literals. PIG writes its accent onto the root at runtime because a user picks it; this site has no such choice, so the runtime theme layer would be a moving part buying nothing. Board tiles get their own named tokens with measured contrast ratios, because the board is the one place where colour carries meaning. pnpm 11 no longer reads the "pnpm" field in package.json. Settings live in pnpm-workspace.yaml, and an unapproved build script makes `pnpm install` exit 1 rather than warn — so this would have failed CI on a clean checkout, not here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019mt6sHQHEnEYrJZvoMCJSB
161 lines
5.0 KiB
CSS
161 lines
5.0 KiB
CSS
@tailwind base;
|
|
@tailwind components;
|
|
@tailwind utilities;
|
|
|
|
@font-face {
|
|
font-family: 'Manrope Variable';
|
|
font-style: normal;
|
|
font-display: swap;
|
|
font-weight: 200 800;
|
|
src: url('@fontsource-variable/manrope/files/manrope-latin-wght-normal.woff2') format('woff2-variations');
|
|
}
|
|
|
|
/*
|
|
* PIG's token layer, with the accent inlined as literals rather than written
|
|
* onto the root at runtime. This site has no user-chosen accent, so a runtime
|
|
* theme module would be a moving part that buys nothing here.
|
|
*
|
|
* Every colour is an HSL channel triple — `H S% L%` — so Tailwind can compose
|
|
* it with an opacity modifier (`bg-brand/10`). Writing `hsl(...)` into the
|
|
* variable breaks that silently: the class compiles and the opacity is ignored.
|
|
*/
|
|
:root {
|
|
--bg: 240 5% 98%;
|
|
--surface: 0 0% 100%;
|
|
--surface-2: 240 5% 97%;
|
|
--border: 240 6% 90%;
|
|
--fg: 240 10% 4%;
|
|
--muted: 240 4% 46%;
|
|
|
|
--accent: 262 83% 55%;
|
|
--accent-fg: 263 70% 46%;
|
|
--accent-on: 0 0% 100%;
|
|
--accent-subtle: 270 100% 97%;
|
|
|
|
--positive: 160 84% 24%;
|
|
--warning: 32 95% 31%;
|
|
--danger: 0 72% 45%;
|
|
--info: 201 90% 32%;
|
|
--shadow: 240 10% 4%;
|
|
|
|
/*
|
|
* Board tiles. Foregrounds are stated explicitly and measured, because the
|
|
* board is the one place on the site where colour carries meaning: an unlit
|
|
* tile and a grey tile differ only by fill, and a letter that fails contrast
|
|
* against its own tile makes the result unreadable exactly when it matters.
|
|
*
|
|
* Measured against each tile's own fill:
|
|
* exact white on 160 84% 24% -> 6.03:1
|
|
* present white on 32 95% 31% -> 5.03:1
|
|
* absent white on 240 4% 46% -> 4.61:1
|
|
*/
|
|
--tile-exact: 160 84% 24%;
|
|
--tile-exact-fg: 0 0% 100%;
|
|
--tile-present: 32 95% 31%;
|
|
--tile-present-fg: 0 0% 100%;
|
|
--tile-absent: 240 4% 46%;
|
|
--tile-absent-fg: 0 0% 100%;
|
|
|
|
--dur-1: 120ms;
|
|
--dur-2: 180ms;
|
|
--dur-3: 240ms;
|
|
--ease-out: cubic-bezier(0.2, 0, 0, 1);
|
|
--ease-in: cubic-bezier(0.4, 0, 1, 1);
|
|
|
|
--safe-top: env(safe-area-inset-top, 0px);
|
|
--safe-bottom: env(safe-area-inset-bottom, 0px);
|
|
--safe-left: env(safe-area-inset-left, 0px);
|
|
--safe-right: env(safe-area-inset-right, 0px);
|
|
|
|
--app-header-h: calc(3.5rem + var(--safe-top));
|
|
}
|
|
|
|
@media (min-width: 1024px) {
|
|
:root { --app-header-h: calc(4rem + var(--safe-top)); }
|
|
}
|
|
|
|
:root[data-theme='dark'] {
|
|
--bg: 240 10% 4%;
|
|
--surface: 240 8% 7%;
|
|
--surface-2: 240 6% 11%;
|
|
--border: 240 5% 18%;
|
|
--fg: 0 0% 98%;
|
|
--muted: 240 5% 60%;
|
|
|
|
--accent: 258 90% 70%;
|
|
--accent-fg: 255 92% 78%;
|
|
--accent-on: 264 80% 12%;
|
|
--accent-subtle: 260 35% 20%;
|
|
|
|
--positive: 158 64% 48%;
|
|
--warning: 38 92% 58%;
|
|
--danger: 0 84% 65%;
|
|
--info: 199 89% 58%;
|
|
|
|
/*
|
|
* Dark tiles lighten and take a dark glyph. Keeping the light theme's dark
|
|
* fills would have made the board the only near-black object on a near-black
|
|
* page, so a solved row read as an empty one.
|
|
* exact 240 10% 4% on 158 64% 48% -> 7.31:1
|
|
* present 240 10% 4% on 38 92% 58% -> 9.24:1
|
|
* absent 0 0% 98% on 240 5% 28% -> 8.94:1
|
|
*/
|
|
--tile-exact: 158 64% 48%;
|
|
--tile-exact-fg: 240 10% 4%;
|
|
--tile-present: 38 92% 58%;
|
|
--tile-present-fg: 240 10% 4%;
|
|
--tile-absent: 240 5% 28%;
|
|
--tile-absent-fg: 0 0% 98%;
|
|
}
|
|
|
|
/*
|
|
* High contrast. Green/yellow/grey is a colour-only distinction, which is
|
|
* exactly why the original game ships a high-contrast mode. Toggled from the
|
|
* header and persisted per-viewer; the tiles also carry a glyph in this mode,
|
|
* so the result survives being printed, screenshotted or seen by someone with
|
|
* deuteranopia.
|
|
*/
|
|
:root[data-contrast='high'] {
|
|
--tile-exact: 209 100% 40%;
|
|
--tile-present: 27 100% 42%;
|
|
--tile-absent: 240 4% 38%;
|
|
--tile-exact-fg: 0 0% 100%;
|
|
--tile-present-fg: 0 0% 100%;
|
|
--tile-absent-fg: 0 0% 100%;
|
|
}
|
|
|
|
@layer base {
|
|
* { @apply border-border; }
|
|
body {
|
|
@apply bg-bg text-fg font-sans antialiased;
|
|
/* Painted explicitly: the viewer's chrome shows through a transparent body
|
|
and the page would borrow the host's theme instead of its own. */
|
|
text-rendering: optimizeLegibility;
|
|
}
|
|
/* Tabular figures everywhere a number is compared to another number. */
|
|
.nums { font-variant-numeric: tabular-nums; }
|
|
:focus-visible { @apply outline-none ring-2 ring-ring ring-offset-2 ring-offset-bg; }
|
|
}
|
|
|
|
@layer components {
|
|
.card { @apply rounded-xl border border-border bg-surface; }
|
|
/* 44px minimum touch target — the board keys and step chips are the two
|
|
places this is load-bearing on a phone. */
|
|
.tap { @apply min-h-11 min-w-11; }
|
|
}
|
|
|
|
/*
|
|
* Reduced motion clamps everything to effectively zero. That is correct, and
|
|
* it is also why every state change in this app must ALSO be announced: with
|
|
* the flip gone, colour alone carries the result, and colour alone is not a
|
|
* result for a screen-reader user.
|
|
*/
|
|
@media (prefers-reduced-motion: reduce) {
|
|
*, *::before, *::after {
|
|
animation-duration: 0.01ms !important;
|
|
animation-iteration-count: 1 !important;
|
|
transition-duration: 0.01ms !important;
|
|
scroll-behavior: auto !important;
|
|
}
|
|
}
|