The row already opened the event; the tick sat on top of it, so the two smallest targets on the page did different things a few pixels apart. "Done" was never the only thing a reader wants to say about an event either — status, effort and notes all live in the detail sheet. The tick becomes a chevron: decorative, aria-hidden, and leaning towards where the row is about to take you on hover. The full-bleed button is the only control, so there is no second stop for anyone tabbing or using a screen reader. Undo on a revealed ignored row stays a real button — it is a real action with nowhere better to sit. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
241 lines
5.9 KiB
CSS
241 lines
5.9 KiB
CSS
@import "tailwindcss";
|
||
|
||
/*
|
||
* Two orthogonal colour systems.
|
||
*
|
||
* hue → WHICH GAME (identity; lives in games.ts, applied inline)
|
||
* heat → HOW URGENT (state; the ramp below)
|
||
*
|
||
* Keeping them separate is what lets one glance answer "whose event is this?"
|
||
* and "how long have I got?" at the same time. Never colour an urgency element
|
||
* with a game hue, or vice versa.
|
||
*/
|
||
@theme {
|
||
/* Ground is deep blue-black, not neutral near-black — the whole surface
|
||
should feel like a lit instrument panel rather than a void. */
|
||
--color-ground: #12141c;
|
||
--color-surface: #1b1e2a;
|
||
--color-raised: #232739;
|
||
--color-hairline: #2c3145;
|
||
--color-ink: #e9ebf3;
|
||
--color-muted: #888fa6;
|
||
--color-faint: #5a6178;
|
||
|
||
/* Heat ramp: monotonic in salience, dim → blue → amber → red. */
|
||
--color-calm: #55607f;
|
||
--color-near: #5b8def;
|
||
--color-soon: #e8b33c;
|
||
--color-critical: #ff4d6a;
|
||
|
||
--font-display: "Chakra Petch", ui-sans-serif, system-ui, sans-serif;
|
||
--font-body: "Public Sans", ui-sans-serif, system-ui, sans-serif;
|
||
}
|
||
|
||
:root {
|
||
color-scheme: dark;
|
||
}
|
||
|
||
html,
|
||
body,
|
||
#root {
|
||
height: 100%;
|
||
}
|
||
|
||
body {
|
||
margin: 0;
|
||
background: var(--color-ground);
|
||
color: var(--color-ink);
|
||
font-family: var(--font-body);
|
||
-webkit-font-smoothing: antialiased;
|
||
/* Faint vertical gradient so the page reads as a panel with a light source
|
||
rather than a flat fill. Subtle enough not to fight the data. */
|
||
background-image: radial-gradient(
|
||
120% 70% at 50% -10%,
|
||
#1a1e2c 0%,
|
||
var(--color-ground) 60%
|
||
);
|
||
background-attachment: fixed;
|
||
}
|
||
|
||
/* Countdowns tick. Tabular figures keep them from reflowing every second. */
|
||
.tnum {
|
||
font-variant-numeric: tabular-nums;
|
||
font-feature-settings: "tnum" 1;
|
||
}
|
||
|
||
/* Eyebrow labels: the utility voice of the interface. */
|
||
.eyebrow {
|
||
font-family: var(--font-display);
|
||
font-weight: 600;
|
||
font-size: 0.6875rem;
|
||
letter-spacing: 0.14em;
|
||
text-transform: uppercase;
|
||
color: var(--color-faint);
|
||
}
|
||
|
||
/* ---- Signature: the segmented depletion meter -------------------------- */
|
||
/* Discrete ticks, not a smooth bar — lifted from in-game stamina meters.
|
||
Spent ticks recede; remaining ticks carry the urgency colour. */
|
||
.meter {
|
||
display: flex;
|
||
gap: 2px;
|
||
height: 10px;
|
||
align-items: stretch;
|
||
}
|
||
.meter-tick {
|
||
flex: 1 1 0;
|
||
border-radius: 1px;
|
||
background: var(--color-hairline);
|
||
transition: background-color 240ms ease;
|
||
}
|
||
.meter-tick[data-live="true"] {
|
||
background: var(--tick);
|
||
box-shadow: 0 0 6px -1px var(--tick);
|
||
}
|
||
.meter-tick[data-unknown="true"] {
|
||
background: repeating-linear-gradient(
|
||
90deg,
|
||
var(--color-hairline) 0 2px,
|
||
transparent 2px 4px
|
||
);
|
||
box-shadow: none;
|
||
}
|
||
|
||
@media (prefers-reduced-motion: no-preference) {
|
||
.meter-tick {
|
||
animation: tick-in 320ms cubic-bezier(0.2, 0.7, 0.3, 1) backwards;
|
||
}
|
||
@keyframes tick-in {
|
||
from {
|
||
opacity: 0;
|
||
transform: scaleY(0.3);
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ---- Event rows -------------------------------------------------------- */
|
||
/*
|
||
* Hover should feel instant. Everything here is fast (110–160ms) and eased out,
|
||
* so the row responds on the way in and settles on the way out — a slow
|
||
* symmetric fade is what makes an interface feel laggy even at 60fps.
|
||
*
|
||
* All hover states are behind `hover: hover` so a tap on a touch device does
|
||
* not leave a row stuck in its hover appearance.
|
||
*/
|
||
.event-row {
|
||
--row-bg: transparent;
|
||
background: var(--row-bg);
|
||
transition: background-color 130ms ease-out;
|
||
}
|
||
.event-row.is-complete {
|
||
opacity: 0.4;
|
||
}
|
||
|
||
.row-rail {
|
||
background: var(--hue);
|
||
opacity: 0.55;
|
||
transform: scaleY(0.82);
|
||
transform-origin: center;
|
||
transition:
|
||
transform 160ms cubic-bezier(0.2, 0.7, 0.3, 1),
|
||
opacity 130ms ease-out,
|
||
box-shadow 160ms ease-out;
|
||
}
|
||
|
||
.row-title,
|
||
.row-count,
|
||
.row-summary {
|
||
transition: color 130ms ease-out;
|
||
}
|
||
|
||
.row-check {
|
||
transition:
|
||
border-color 130ms ease-out,
|
||
color 130ms ease-out,
|
||
transform 120ms cubic-bezier(0.2, 0.7, 0.3, 1);
|
||
}
|
||
|
||
@media (hover: hover) {
|
||
.event-row:hover {
|
||
--row-bg: color-mix(in srgb, var(--color-raised) 62%, transparent);
|
||
}
|
||
.event-row:hover .row-rail {
|
||
opacity: 1;
|
||
transform: scaleY(1);
|
||
box-shadow: 0 0 10px -2px var(--hue);
|
||
}
|
||
/* The meter is the signature element, so it is the thing that visibly
|
||
wakes up: live ticks gain a little light under the cursor. */
|
||
.event-row:hover .meter-tick[data-live="true"] {
|
||
filter: brightness(1.28) saturate(1.08);
|
||
}
|
||
.event-row:hover .row-summary {
|
||
color: var(--color-ink);
|
||
}
|
||
.event-row:hover .row-check {
|
||
border-color: var(--color-faint);
|
||
color: var(--color-muted);
|
||
}
|
||
/* The chevron leans towards where the row is about to take you. It is the
|
||
only thing left on the right of a row, so it has to carry "this opens"
|
||
on its own. */
|
||
.event-row:hover .row-open {
|
||
transform: translateX(2px);
|
||
color: var(--color-ink);
|
||
}
|
||
.row-check:hover {
|
||
transform: scale(1.08);
|
||
border-color: var(--color-muted);
|
||
color: var(--color-ink);
|
||
}
|
||
}
|
||
|
||
/* Press feedback lands on both mouse and touch — the tactile half of
|
||
"responsive". */
|
||
.event-row:active {
|
||
--row-bg: color-mix(in srgb, var(--color-raised) 82%, transparent);
|
||
transition-duration: 40ms;
|
||
}
|
||
.row-check:active {
|
||
transform: scale(0.9);
|
||
transition-duration: 60ms;
|
||
}
|
||
|
||
.meter-tick {
|
||
transition:
|
||
background-color 240ms ease,
|
||
filter 130ms ease-out;
|
||
}
|
||
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.event-row,
|
||
.row-rail,
|
||
.row-title,
|
||
.row-count,
|
||
.row-summary,
|
||
.row-check,
|
||
.meter-tick {
|
||
transition: none;
|
||
}
|
||
.row-check:hover,
|
||
.row-check:active,
|
||
.event-row:hover .row-open,
|
||
.event-row:hover .row-rail {
|
||
transform: none;
|
||
}
|
||
}
|
||
|
||
/* Focus ring: visible, and never removed. */
|
||
:where(a, button, [tabindex]):focus-visible {
|
||
outline: 2px solid var(--color-near);
|
||
outline-offset: 2px;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
/* Horizontal scrollers keep their gutter but hide the bar chrome. */
|
||
.scroll-x {
|
||
overflow-x: auto;
|
||
scrollbar-width: thin;
|
||
scrollbar-color: var(--color-hairline) transparent;
|
||
}
|