docs: move PRODUCT and DESIGN into docs/ and list them in AGENTS
CI / Typecheck, test, feed (push) Successful in 17s
CI / Build site (push) Successful in 8s
CI / Container image (push) Successful in 17s

They are reasoning for whoever touches the interface next, which is
what docs/ holds, and the table in AGENTS.md is how an agent finds it.
The design tooling resolves both from docs/ without configuration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Opus 5.5 committed 2026-10-01 04:09:57 +02:00
1 parent a27548e42b
commit 8a6110396a
3 files changed
+2

No files matched your search

+484
View File
@@ -0,0 +1,484 @@
---
name: Event Clock
description: Live and upcoming gacha events, sorted by what expires next.
colors:
ground: "#12141c"
surface: "#1b1e2a"
raised: "#232739"
hairline: "#2c3145"
ink: "#e9ebf3"
ink-strong: "#ffffff"
muted: "#888fa6"
faint: "#5a6178"
scrim: "rgb(18 20 28 / 0.8)"
glow: "#1a1e2c"
calm: "#55607f"
near: "#5b8def"
soon: "#e8b33c"
critical: "#ff4d6a"
ground-light: "#edf0f7"
surface-light: "#ffffff"
raised-light: "#dfe4f0"
hairline-light: "#c9d1e2"
ink-light: "#151824"
ink-strong-light: "#000000"
muted-light: "#535b73"
faint-light: "#767e94"
scrim-light: "rgb(26 31 48 / 0.42)"
glow-light: "#ffffff"
calm-light: "#8a92a8"
near-light: "#2b62c6"
soon-light: "#8a6008"
critical-light: "#c81e3c"
typography:
numeral:
fontFamily: "Chakra Petch, ui-sans-serif, system-ui, sans-serif"
fontSize: "2.75rem"
fontWeight: 700
lineHeight: 1
letterSpacing: "-0.025em"
fontFeature: "\"tnum\" 1"
display:
fontFamily: "Chakra Petch, ui-sans-serif, system-ui, sans-serif"
fontSize: "1.75rem"
fontWeight: 600
lineHeight: 1.15
letterSpacing: "-0.025em"
wordmark:
fontFamily: "Chakra Petch, ui-sans-serif, system-ui, sans-serif"
fontSize: "0.9375rem"
fontWeight: 700
letterSpacing: "0.02em"
reading:
fontFamily: "Chakra Petch, ui-sans-serif, system-ui, sans-serif"
fontSize: "0.875rem"
fontWeight: 600
fontFeature: "\"tnum\" 1"
title:
fontFamily: "Public Sans, ui-sans-serif, system-ui, sans-serif"
fontSize: "0.9375rem"
fontWeight: 500
lineHeight: 1.375
body:
fontFamily: "Public Sans, ui-sans-serif, system-ui, sans-serif"
fontSize: "0.875rem"
fontWeight: 400
lineHeight: 1.625
summary:
fontFamily: "Public Sans, ui-sans-serif, system-ui, sans-serif"
fontSize: "0.8125rem"
fontWeight: 400
lineHeight: 1.375
control:
fontFamily: "Public Sans, ui-sans-serif, system-ui, sans-serif"
fontSize: "0.75rem"
fontWeight: 500
label:
fontFamily: "Chakra Petch, ui-sans-serif, system-ui, sans-serif"
fontSize: "0.6875rem"
fontWeight: 600
letterSpacing: "0.14em"
tag:
fontFamily: "Public Sans, ui-sans-serif, system-ui, sans-serif"
fontSize: "0.625rem"
fontWeight: 500
rounded:
tick: "1px"
tag: "3px"
bar: "5px"
tab: "6px"
control: "8px"
card: "12px"
sheet: "16px"
pill: "9999px"
spacing:
hair: "2px"
xs: "4px"
sm: "6px"
md: "8px"
lg: "12px"
gutter: "16px"
section: "20px"
components:
button-commit:
backgroundColor: "{colors.ink}"
textColor: "{colors.ground}"
typography: "{typography.control}"
rounded: "{rounded.control}"
padding: "10px 16px"
button-commit-hover:
backgroundColor: "{colors.ink-strong}"
button-quiet:
textColor: "{colors.muted}"
typography: "{typography.control}"
rounded: "{rounded.control}"
padding: "6px 12px"
button-quiet-hover:
textColor: "{colors.ink}"
tab:
textColor: "{colors.faint}"
typography: "{typography.control}"
rounded: "{rounded.tab}"
padding: "6px 10px"
tab-selected:
backgroundColor: "{colors.raised}"
textColor: "{colors.ink}"
input:
backgroundColor: "{colors.ground}"
textColor: "{colors.ink}"
rounded: "{rounded.control}"
padding: "8px 12px"
hue-chip:
textColor: "{colors.muted}"
typography: "{typography.control}"
rounded: "{rounded.pill}"
padding: "6px 12px"
tag:
backgroundColor: "{colors.hairline}"
textColor: "{colors.muted}"
typography: "{typography.tag}"
rounded: "{rounded.tag}"
padding: "1px 6px"
event-row:
textColor: "{colors.ink}"
typography: "{typography.title}"
padding: "14px 16px"
row-check:
textColor: "{colors.faint}"
rounded: "{rounded.tab}"
size: "28px"
timeline-bar:
textColor: "{colors.ink}"
rounded: "{rounded.bar}"
height: "36px"
padding: "0 12px"
now-chip:
backgroundColor: "{colors.critical}"
textColor: "{colors.ground}"
rounded: "{rounded.tag}"
padding: "1px 4px"
sheet:
backgroundColor: "{colors.surface}"
rounded: "{rounded.sheet}"
padding: "20px"
toast:
backgroundColor: "{colors.raised}"
textColor: "{colors.ink}"
rounded: "{rounded.card}"
padding: "12px 16px"
---
# Design System: Event Clock
## Overview
**Creative North Star: "The Lit Instrument Panel"**
Event Clock is a cockpit at night. The ground is a deep blue-black, never a neutral void, lit faintly from
above so the page reads as a panel with a light source. Against that calm, only two things are
allowed to glow: **whose game this is** (the game's hue) and **how long is left** (the heat ramp).
Everything else (chrome, borders, labels, controls) stays muted until a hand reaches for it. The
reader should be able to tell at a glance which events need them, because those are the only things
that are lit.
The character is **calm, precise and honest**. Calm, because the surfaces are quiet, the type is
small and exact, and motion is quick and then still. Precise, because every number uses tabular
figures, every countdown resolves on the game's own clock, and labels read like instrument
legends. Honest, because uncertainty has its own look and is never drawn as a full reading. An
unannounced end is a hatched meter or a frayed bar, a bar that started before the view shows a
faded edge, and an event that has not started has a dashed edge. "We don't know" never looks like
"plenty of time".
Density sits at checklist level, not dashboard level. Rows are full-bleed with a 16px gutter and
hairline rules between them, with no cards around each event. The one place the panel raises its
voice is the headline deadline. There the event title is set in display type, the countdown in a
44px numeral, and an urgency-coloured wash warms the whole section as the deadline closes in.
**Key Characteristics:**
- Deep blue-black ground (dark is the default theme), with a light theme that swaps in different
values rather than inverting the dark ones.
- Two colour axes that never cross: game hue for identity, the heat ramp for urgency.
- Two typefaces: Chakra Petch for anything that is a reading (numbers, labels, the wordmark),
Public Sans for anything that is language.
- Flat, tonal surfaces. The only light comes from meaning.
- The segmented depletion meter as the signature element.
- Quiet controls that answer within 110–160ms of being touched.
## Colors
A cool, near-monochrome slate panel whose only saturated colours are game hues (who) and the heat
ramp (how urgent).
### Primary: the heat ramp
The ramp gets more salient at every step (dim → blue → amber → red), and the steps are cut on
absolute time remaining, not on how far through its run an event is. A 90-day event with three hours
left is exactly as urgent as a 3-day event with three hours left.
- **calm**: 7 days or more left. Deliberately dim, almost the colour of a resting control.
- **near**: under 7 days. Also the system's one functional accent: focus rings and the "CLOCK" half
of the wordmark.
- **soon**: under 3 days.
- **critical**: under 24 hours. Also the "now" rule and chip on the timeline, and the "running
out of time" tag.
Heat colours also tint state tags: a 15–18% wash of the step with text in the same step (`soon`
for "tight" and for dailies still to do, `critical` for "running out of time", `near` for "done
today"). The step is chosen by meaning, never by decoration.
### Secondary: game hues (data, not tokens)
Each game has one hue, stored in `src/shared/games.ts`. A reader's own games carry a hue the reader
picked. Hues are **identity**: Wuthering Waves is the green one in every view and in both themes.
Because they are data, no stylesheet can re-strike them per theme. Instead, `readableHue`
(`src/client/state/theme.ts`) adjusts each one on its way to the screen. On light it darkens a hue
along the same hue until it clears 4.5:1. On dark it leaves a hue untouched if it already clears
4.5:1 and lifts the lightness of those that don't (Fate/Grand Order's navy is one). Hues appear as:
a 3px row rail, a chip's border, text and 14% wash, a timeline bar's 22% wash (11% for events that
haven't started) and its 3px left edge, the game name under the headline, and a dot in the "Then"
list.
### Neutral
- **ground**: the page itself; in dark, a deep blue-black. Also the fill of text inputs.
- **glow**: the top of the body's radial gradient (120% × 70% at 50% −10%). It is the light source
that turns the ground into a lit panel.
- **surface**: sheets, dialogs and the base that timeline bars are tinted from.
- **raised**: the selected tab, toasts, and the hover and press tint of a row (mixed at 62% / 82%).
- **hairline**: every border and rule, spent meter ticks, and plain tags.
- **ink**: primary text, and the fill of the one solid commit button.
- **ink-strong**: the far end of ink. The commit button and the headline title move to it on hover.
It is a token and not `white`, because on light "brighter" means darker.
- **muted**: secondary text, summaries, unpressed chips, quiet buttons.
- **faint**: eyebrows, metadata, unselected tabs, idle checkboxes.
- **scrim**: the veil behind a modal, which carries its own alpha because the two themes need very
different amounts.
### The light theme
Light is a set of new token values under `:root[data-theme="light"]`. Every `-light` key in the
frontmatter replaces its dark counterpart. The ground is daylight paper with the same cool blue cast
as the dark ground, not white. Cards go *up* to white, and `raised` becomes a shade rather than a
lift, because on paper you mark a surface as interactive by tinting it. Every step of the heat ramp
is darkened to clear 4.5:1 as small text on the light ground (dark `soon` is 1.9:1 there).
### Named Rules
**The Two Axes Rule.** Hue answers *whose event*; heat answers *how long*. Never colour an urgency
element with a game hue, or a game element with a heat colour. Keeping them apart is what lets one
glance answer both questions.
**The Two Answers Rule.** Every UI colour is a token with a dark value and a light value. A
component names `ink`, `hairline` or `soon` and never a literal. `bg-white`, a hex value or a
hard-coded scrim looks right in one theme and wrong in the other, and nothing will warn you which.
**The Moves-Only-Where-It-Failed Rule.** The dark theme changes only where a colour failed
contrast. A hue that already reads is left exactly as it shipped.
## Typography
**Display Font:** Chakra Petch (500/600/700), with a fallback of ui-sans-serif and system-ui
**Body Font:** Public Sans (400/500/600, and 400 italic), with the same fallback
Both are loaded from Google Fonts with `display=swap`.
**Character:** Chakra Petch is the instrument face. Its squared-off, slightly technical forms are
HUD lettering of the kind these games use, and it is reserved for things the reader *reads off*:
countdowns, counts, eyebrow legends and the wordmark. Public Sans is the plain civic sans used for
everything read as language, such as event titles, summaries, settings and prose.
### Hierarchy
- **Numeral** (Chakra Petch 700, 2.75rem, line-height 1, tight tracking, tabular): the headline
countdown, and nothing else. It is the largest thing on the page.
- **Display** (Chakra Petch 600, 1.75rem, 1.15, tight tracking): the title of the event that
expires next.
- **Wordmark** (Chakra Petch 700, 0.9375rem → 1.125rem past `lg`, +0.02em): `EVENT` in ink and
`CLOCK` in `near`.
- **Reading** (Chakra Petch 600, 0.875rem, tabular): the countdown on each row, and the "Then"
list at 0.75rem.
- **Title** (Public Sans 500, 0.9375rem, 1.375): row titles, wrapping to at most two lines.
- **Body** (Public Sans 400, 0.875rem, 1.625): prose, empty states and notices, held to
`max-w-sm` or `max-w-md`.
- **Summary** (Public Sans 400, 0.8125rem, 1.375, muted): an event's one-line description, clamped
to two lines.
- **Control** (Public Sans 500, 0.75rem): tabs, chips, quiet buttons.
- **Label / Eyebrow** (Chakra Petch 600, 0.6875rem, 0.14em tracking, uppercase, faint): the utility
voice for section legends ("Next to expire", "Focus", "Then") and the game name over each row.
- **Tag** (Public Sans 500, 0.625rem; 0.5625rem for the smallest): type, status and provenance
badges.
### Named Rules
**The Two Voices Rule.** If a reader reads it *off* the panel (a number, a legend, a count), it is
Chakra Petch. If they read it as language, it is Public Sans. Never set a paragraph in Chakra Petch.
**The Tabular Rule.** Anything that ticks or lines up in a column uses tabular figures (`.tnum`).
A countdown that reflows every second is a broken instrument.
## Layout
The page is a single centred shell with hairline side borders from `sm` up. It is `max-w-2xl` on a
phone and small screens, then widens to `5xl` / `6xl` / `7xl` at `lg` / `xl` / `2xl`. The page gutter
is 16px (`px-4`) everywhere, and sections are separated by hairline rules rather than gaps or cards.
**Below `lg`** the page is one column in the order of instruction first: the focus bar, the headline
deadline, today's dailies, then the lists.
**Past `lg`** it splits into a pinned rail (20rem; 22rem past `xl`) and a scrolling main column.
What the page *tells* the reader to do (the next deadlines, tonight's dailies, and the focus bar on
top) pins to the rail and scrolls independently. What it *shows* them (the lists) scrolls beside it.
The rail's border belongs to the rail's own panel, not a full-height divider.
The **timeline** is a board, not a stretch of page. It is its own pane, scrolls in both directions,
pins the date axis to the top and pins every name (lane and event) to the left. Bars are 36px tall,
and day width steps through a zoom ladder up to 108px a day.
Spacing follows Tailwind's 4px grid, with small steps doing most of the work. Gaps are mostly 6, 8
and 12px. Rows use 14px of vertical padding. Section rhythm comes from 8–20px top margins (`mt-2` to
`mt-5`).
### Named Rules
**The Tells-Left, Shows-Right Rule.** Instructions pin, information scrolls. On a wide screen the
headline deadline must never be pushed below the fold by a full-width control row.
**The Rule-Not-Card Rule.** Lists are full-bleed rows divided by hairlines. Use a bordered, rounded
container only for something that is genuinely a separate object (a form, a daily checklist, a
sheet, a toast), never as a wrapper around each event.
## Elevation & Depth
The system is flat. Depth comes from tonal steps (ground → surface → raised) and hairlines, never
from lifting things off the page. Light, where there is any, is **emitted by meaning**:
- **The body glow:** the radial gradient that lights the panel from above.
- **Heat wash:** a blurred radial gradient of the urgency colour at 16% opacity, behind the
headline. The section warms up as the deadline approaches.
- **Live meter ticks:** a 6px glow in the tick's own urgency colour
(`box-shadow: 0 0 6px -1px var(--tick)`), which brightens under the cursor.
- **Row rail on hover:** a 10px glow in the game's hue.
- **Chip ring on hover:** a 1px ring of the game's hue at 45%.
### Shadow Vocabulary
- **Floating notice** (Tailwind `shadow-lg`): toasts and the update notice only. These are the
only objects that float above the page, because they are the only ones that are temporary.
### Named Rules
**The Emitted-Light Rule.** Nothing glows unless the glow carries information about urgency or
identity. A decorative glow, gradient or drop shadow on a static surface breaks the panel.
## Shapes
Corners are small and precise, and they get rounder as an object gets bigger and more separate from
the page:
- 1px on meter ticks.
- 3px on tags and the "now" chip.
- 5px on timeline bars.
- 6px on tabs and row checkboxes.
- 8px on buttons and inputs.
- 12px on cards, forms, toasts and first-run options.
- 16px on sheets (top corners only on a phone, where the sheet rises from the bottom edge).
- Fully round on game chips and status dots.
Borders are always 1px hairline, except for meaningful edges. A row rail is 3px, rounded and in the
game's hue. A timeline bar has a 3px left edge, solid when the event has started and dashed when it
hasn't. The "not started" boundary and clump rules are 1px dashed in faint ink.
Masks carry uncertainty. A bar whose end is unannounced fades out from 60% of its width. A bar that
started before the board's window fades in over its first 14%.
## Components
### Buttons
*Quiet until touched.* Buttons are hairline-bordered at rest and answer fast when touched.
- **Shape:** gently rounded (8px); first-run's primary button uses 12px.
- **Commit:** the only solid button on a screen. Solid ink with ground text, 10px × 16px, and it
moves to ink-strong on hover. "Mark done" and first-run's "Continue" are commit buttons. Its
disabled state is `raised` with faint text, or 40% opacity inside forms.
- **Quiet:** transparent with a hairline border and muted text, 6px × 12px at control size.
Text goes to ink on hover, or to critical when the action destroys something (delete).
- **Focus:** a 2px `near` outline at 2px offset, on every interactive element without exception.
`summary` gets the same rule explicitly, because the shared selector doesn't reach it.
### Tabs (the view switch)
A segmented pair (Checklist / Timeline) inside a hairline-bordered 8px tray with 2px padding. The
selected tab fills with `raised` and turns ink; unselected tabs are faint and go muted on hover.
### Chips (game focus and dailies)
- **Style:** a pill. Its border, text and a 14% wash are mixed from the game's hue when pressed.
When not pressed it is muted text on a transparent background. Each chip carries a small tabular
count at 70% opacity.
- **State:** `aria-pressed` carries the state. On hover a chip lifts 1px and gains a 1px hue ring,
and an unpressed chip's label goes to ink. On press it scales to 0.96. A chip with nothing waiting
sits at 50% opacity: still reachable, but not competing for attention.
### Event row (signature)
The unit of the checklist. It is one full-bleed target that opens the event and does nothing else.
- From left to right: a 3px **rail** in the game's hue (55% opacity, 82% height, which grows to
full height with a glow on hover); then an eyebrow with the game name and tags; then the title,
with its **reading** (the countdown in the urgency colour) on the right; then a one-line summary;
then the depletion meter.
- **Hover:** the background tints to `raised` at 62% and the summary goes to ink. The meter's live
ticks brighten, and the open chevron leans 2px toward where the row is about to take you. Pressing
deepens the tint to 82% within 40ms.
- **Completed:** 40% opacity and a struck-through title. It stays visible but steps back.
- **Unknown end:** the reading drops to faint and the meter is hatched.
- All hover styles sit behind `@media (hover: hover)`, so a tap never leaves a row stuck in its
hover state.
### Depletion meter (signature)
A strip of discrete ticks 10px tall with 2px gaps, lifted from the in-game stamina meters these
games all use. Ticks drain from the right, so what is left sits at the left edge and stacked rows
line up into a readable ramp. Remaining ticks carry the urgency colour and its glow; spent ticks
recede to hairline. **An unannounced end is hatched, never full.** Ticks fill in with a staggered
scale-in animation (320ms, at most a 14ms delay per tick) unless reduced motion is requested.
### Headline deadline
"Next to expire" in eyebrow type, then the event title in display type, then the game name in its
hue, then the 44px numeral countdown with the exact end beside it, then the meter, and behind it all
the heat wash. A "Then" list follows: a hue dot, a muted title and a small tabular reading.
### Timeline bar
A 36px bar with 5px corners. Its fill is the game's hue mixed into `surface` at 22% (11% if the event
hasn't started). It has a 3px hue left edge (dashed if not started), ink text, and a sticky name that
can never leave its own bar. In the merged "Ending soonest" view, a bar also shows the game's short
name in an eyebrow in its hue, which is dropped first when space runs out. Completed bars sit at 35%
opacity. **Now** is a 1px critical rule with a small critical chip on the axis.
### Inputs
A ground-filled field with a hairline border, 8px corners, 8px × 12px padding and body-size ink
text. On focus the border moves to faint. The field gets no glow, because a form is not a reading.
### Sheets and notices
- **Detail sheet:** `surface` fill with a hairline border and 16px corners. It rises from the bottom
on a phone, is centred at `max-w-lg` from `sm` up, and sits over the scrim.
- **Toast / update notice:** `raised` fill, hairline border, 12px corners and the only shadow in the
system. Each offers one action.
### Daily burst
A small spray of 3px sparks that fires once, when the last daily of the day is ticked. It is purely
decorative and ignores pointer input. With reduced motion requested it does not render at all,
rather than being toned down.
## Do's and Don'ts
### Do:
- **Do** name colours by token (`ink`, `hairline`, `soon`) and add a light-theme value whenever you
add a token.
- **Do** pass every game hue through `readableHue` for the active theme before it reaches the
screen.
- **Do** use tabular figures (`.tnum`) on every countdown, count and date that sits in a column.
- **Do** draw uncertainty as uncertainty: a hatched meter, a frayed bar end, a faded clipped start,
a dashed not-yet-started edge, and the words "end date unknown".
- **Do** keep hover and press feedback fast and eased out (110–160ms in, 40–60ms on press), behind
`@media (hover: hover)`, and remove the transforms under `prefers-reduced-motion: reduce`.
- **Do** show the 2px `near` focus outline on every interactive element, `summary` included.
- **Do** keep the board's controls out of the list rows: status, effort and notes live in the
detail sheet.
### Don't:
- **Don't** use literal colours (`bg-white`, hex values in a component, a hard-coded scrim). They
break one of the two themes silently.
- **Don't** colour an urgency element with a game hue, or a game element with a heat step.
- **Don't** replace the segmented meter with a smooth progress bar, or draw an unknown end as a full
one.
- **Don't** add glows, gradients or drop shadows to static surfaces. Light has to mean something.
- **Don't** wrap individual events in cards. Rows are full-bleed and divided by hairlines.
- **Don't** set prose in Chakra Petch, or numbers that tick in a proportional face.
- **Don't** default the theme from `prefers-color-scheme`. Dark is the default until the reader
chooses.
- **Don't** put a second control inside a row, or a drag target on the focus bar or the dailies
strip.
+183
View File
@@ -0,0 +1,183 @@
# Product
<!-- impeccable:product-schema 1 -->
## Platform
web
An installable single-page web app (PWA with a service worker and manifest) that works offline. Native
mobile apps are out of scope: installing to a home screen is enough (`docs/PRD.md` § Out of scope).
## Users
**The repository owner comes first.** Event Clock is the owner's own tool for keeping track of the
gacha games they play. When the owner's use and a public reader's request conflict, the owner's use
wins.
**Public readers come second, and they are welcome.** The app has been posted to r/gachagaming and is
served publicly. The typical public reader matches the PRD persona: someone playing 2–5 gacha games
who checks in a few times a week, usually on a phone. Their situation is a dozen overlapping,
time-limited events spread across as many in-game calendars, and the thing they fear is missing a
limited event by a day. A smaller group plays 6–10 games or more. For those readers, custom games and
events (F13) cover what no list of sources ever will.
The desktop layout is a real layout, not the phone layout stretched, but mobile is the main use case.
## Product Purpose
Event Clock answers three questions in one screen, across every game the reader plays:
1. What is running right now?
2. What ends soonest?
3. Which of these have I already finished, or am I partway through?
It collects live and upcoming events from community wikis, sorts them by end date, and plots them on
a timeline. Readers can mark an event done or in progress, estimate how much effort it takes, and tick
off daily progress on events that repeat.
Success is measured as follows (`docs/PRD.md` § Success criteria):
- A reader can find their next expiring event within 5 seconds of load, on mobile.
- At least 99% of published end dates are correct.
- A new game can be added without a schema migration or a client change.
## Positioning
**One screen for every game, ordered by what runs out next.** Each game publishes its own calendar,
and none of them talk to each other. paimon.moe, Game8 and in-game timers each answer the question
for one game. Event Clock answers it across all of them in a single deadline order. Cross-game
coverage is the core of the product, so the number and range of supported games matter directly.
The most common reason the public thread gave for not using the app was a missing game.
Date discipline is how the product is built rather than the headline claim, but it is never traded
away for more coverage (see Product Principles).
## Operating Context
- Readers check in a few times a week, usually on a phone, often between other things. The dailies
strip is meant to be answerable in about ten seconds.
- A reader has a game open, sees its in-game timer, and compares it with ours. Countdowns therefore
resolve on the game's own server reset for the reader's region (Asia, America or Europe), not on
UTC.
- Readers either work down a checklist of deadlines, or look at a game-by-game timeline in the style
of paimon.moe, which the public thread named as the reference.
- Some readers are catching up on games they set aside rather than busy with all of them. The app
serves them through the in-progress status and effort fields, and never nags them about a game they
stopped playing.
- Data is refreshed twice a day from community wikis. Some sources (the nine Game8 pages) cannot be
fetched by the scheduled runner and are refreshed by hand. The data's age is therefore a fact the
reader is shown, not one they have to assume.
## Capabilities and Constraints
**Capabilities.** The full feature set is specified as F1–F16 in `docs/PRD.md`:
- A timeline: one lane per game, or a single queue sorted by soonest end.
- A checklist of everything running, sorted by soonest end.
- Done and in-progress marks, effort estimates and notes.
- Daily checklists, with past days that can still be filled in.
- Ignoring an event, and filters by game or type.
- Focusing on one game at a time.
- Region selection.
- JSON export and import.
- The reader's own games and events, one-off or repeating.
- First-run setup.
- Offline use.
- A notice when a new version of the app is ready.
- Dark or light mode, plus a "follow the system" option.
- The reader's own game order.
The app currently covers twenty games from twenty-two sources.
**Hard constraints** (`AGENTS.md` § Three constraints):
- **No accounts, logins or user records.** All reader state lives in `localStorage`. Moving to
another device works through export and import, never a server-side user.
- **No LLM in the pipeline.** Event data comes only from deterministic parsers.
- **A Bun server owns fetching and parsing.** The client only ever calls this app's own `/api/*`.
- **Some stored keys must never move.** Event IDs, `dailies:<game>`, game-day keys and the
reader-authored `mygame:` / `myevent:` IDs are `localStorage` keys. Changing how any of them is
built silently wipes readers' progress, and there is no server copy to restore from.
- **Scraping conduct is binding:** honour `robots.txt`, refresh each source at most once every six
hours, space requests to the same host, and never get around an access control.
**Not the product** (`docs/PRD.md`):
- A wiki or event guide
- A notification or push service
- A pull tracker, damage calculator or build planner
- Localised beyond English
**Terminology:**
- The user is a **reader**.
- The two views are **Checklist** (stored id `"soon"`) and **Timeline**.
- An event's states are **untouched**, **doing it** and **done**.
- **Ignore** is not the same as done.
- A **lane** is one game's row or source.
- The daily **reset** is the boundary between game-days.
**Undecided:**
- Whether a month grid is needed in addition to the timeline.
- The formal accessibility standard (see below).
- The SQLite layer and the review queue for quarantined events are specified but not built.
## Brand Commitments
- **Name:** Event Clock.
- **Tagline:** "Live and upcoming gacha events, sorted by what expires next."
- **Unofficial and unaffiliated.** The app says so plainly. It credits the source wikis and the game
studios on the same screen as the data, and treats the source page as the authority when the two
disagree (F11).
- **Every event links to its source.** A date the reader typed in is visibly theirs and never
credited to a source (F13).
- **Game hues are identity.** A game keeps its colour across views and themes (F15).
- **Dark is the default theme.** Light mode and a "follow the system" option are available (F15).
- **Voice:** plain, specific and honest about uncertainty. The app says "end date unknown" rather
than implying a date, and states what it is holding back ("N live · N upcoming") instead of leaving
the reader to infer it. Do not use the "AI slop" self-description in promotion (`docs/FEEDBACK.md`
P3).
## Evidence on Hand
- `docs/FEEDBACK.md`: a reading of the r/gachagaming release thread (26 comments from 15 accounts),
with verbatim quotes. Readers called the app useful without being asked, one bookmarked it, and no
one disputed a date. The one design criticism was formed from the promo screenshot alone.
- The promo screenshot is outdated. FEEDBACK.md calls for a reshoot from a wide window, in timeline
view.
- There are no usage metrics, testimonials beyond that thread, press mentions or user counts. Do not
invent any.
- Pinned fixtures (`fixtures/<game>/`) and live snapshots (`snapshots/`) hold real event data for
every supported game.
## Product Principles
1. **Deadline order is the product.** Every sort and grouping falls back to soonest end first.
Truncating a list hides rows but never reorders them, and "next to expire" always means the
earliest end date.
2. **Breadth, never at the expense of a true date.** More games is the main growth lever, but a
wrong end date is worse than a missing event. Unknown ends stay unknown, a stale source is
disclosed, and a page that is out of date but still parses is treated as a failure.
3. **Showing is not telling.** What the reader can look at is their choice. The headline deadlines
and the dailies are instructions, so they never point at work that is already done or ignored.
4. **The reader's answers are theirs.** Their view, game order, region and choice of games are asked
for once and remembered. A game added later arrives switched off, and nothing the reader set is
overridden by a heuristic.
5. **Nothing is held back silently.** If something is hidden, stale or out of date, the reader is told
in words, including whose games the notice covers.
## Accessibility & Inclusion
**No formal standard has been chosen yet.** The following rules are already in force and must be
kept:
- Game hues and the urgency colours clear 4.5:1 contrast against the background in both themes.
- No interaction is drag-only. Reordering has arrow buttons, so it works on touch, by keyboard and
with a screen reader.
- Native `<details>` and `<button>` elements are used, so keyboard and screen-reader access needs no
second implementation.
- Focus outlines are visible on `summary` elements.
- A list row is a single tap target.
- The interface is in English only. Dates and countdowns are formatted with `Intl`.