Skip to content

Commit ebcd802

Browse files
committed
atlas: convert all specimens to the season model; retire ::phase
Roll the season-selector rework out to every specimen and remove the old mechanic it replaces. - All 15 field notes now read: a general introduction to the specimen, then the ::almanac dial, then one ::season-note per Glass-Year season (six, in year order). Turning the dial fully swaps the note; each ::sighting quotes a real dated observation and nests in the season that contains its date. - Retire the ::phase component (Phase.vue) and its CSS — the subtle ink-fade it provided is what the feedback found illegible; the season notes replace it. - Drop the now-unread `engaged`/`engage()` state from the Almanac contract (only ::phase read it; the season notes use their own post-mount collapse instead), and the calls to it in the wheel and Season.vue. - Fix ::almanac: it is a block component and needs its closing `::` (like the empty ::sighting); the bare form broke the following ::season-note parse so the notes rendered as plain prose. Corrected in every file and in the skill. - Update the /atlas-specimen authoring skill to teach the new essay structure instead of ::phase, and refresh the almanac composable header + the e2e comment. Verified: full `pnpm gate` green (lint, typecheck, validate:content, unit, build, 29/29 e2e). A Playwright probe across one specimen per biome confirms six season notes render, one shows at a time, PageUp fully switches the note, and a sighting-tick tap flares its quote — plus the landing roster tracking the needle. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KD1GSj3Ljh79fK5EX9PKW3
1 parent abe22c2 commit ebcd802

24 files changed

Lines changed: 494 additions & 200 deletions

.agents/skills/atlas-specimen/SKILL.md

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -166,9 +166,24 @@ illustration: |
166166
</g>
167167
---
168168

169-
The field-note essay, in the naturalist's voice (§1). Prose, not bullets. How it
170-
was found, its habits, one anecdote. No leading `#` — the binomial comes from the
171-
frontmatter. Three or four short paragraphs is plenty.
169+
A general introduction to the specimen, in the naturalist's voice (§1) — how it
170+
was found, its character, one anecdote. Prose, not bullets. No leading `#` (the
171+
binomial comes from the frontmatter). One or two short paragraphs, ending with a
172+
nudge to the dial ("… Turn the almanac to move among them.").
173+
174+
::almanac
175+
::
176+
177+
:::season-note{of="long-damp"}
178+
Then one season-note per Glass-Year season, IN YEAR ORDER (radiator-months,
179+
lamp-lengthening, great-airing, long-damp, small-dry, settling) — each a short
180+
paragraph of what the creature does in that season. A `::sighting{date}` that
181+
quotes a real dated observation nests inside the season whose span contains its
182+
date. The dial is the essay's season selector: it shows one note at a time.
183+
184+
::sighting{date="2026-06-20"}
185+
::
186+
:::
172187
```
173188

174189
**`phenology.phases`** (#279) is the annual sibling of `activity`: where `activity`
@@ -179,7 +194,9 @@ invented one. Each phase is free-form and per-creature (unlike the six Glass-Yea
179194
don't restate or invent season names here; a phase is a different, creature-scoped
180195
thing that happens to sit on the same wheel):
181196

182-
- `name` — a slug a `::phase` component binds to (later ticket).
197+
- `name` — the phase's slug; it drives the dial's hatched inner arcs. (The
198+
essay is structured by the six Glass-Year *seasons* via `::season-note`, not by
199+
these free-form phases — the phases are the dial's own annual ornament.)
183200
- `label` — in-voice, the way `activity.label` is: "the lantern swell", not
184201
"winter dormancy".
185202
- `span``[startDayOfYear, endDayOfYear]`, 0–365. **May wrap the year**

layers/atlas/app/assets/theme.css

Lines changed: 6 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -480,10 +480,11 @@
480480
.atlas-rarity-legend .m { font-family: var(--atlas-data); color: var(--biome-accent); }
481481
.atlas-rarity-legend .d { color: var(--atlas-muted); font-style: italic; }
482482

483-
/* ── Dial-driven prose (#283): :season / ::phase / ::sighting ──────────────
484-
The MDC components that thread the essay onto the almanac dial. Engraved
485-
register throughout: currentColor line, fill none; the pencil is a LEGIBLE
486-
lighter ink (contrast-checked), never a ghost. */
483+
/* ── Dial-driven prose: :season / ::almanac / ::season-note / ::sighting ────
484+
The MDC components that thread the essay onto the almanac dial. The dial is
485+
the essay's season selector: it shows one ::season-note at a time and turning
486+
it swaps the prose. Engraved register throughout: currentColor line, fill
487+
none; the one accent is the needle head. */
487488

488489
/* :season — a season's name as a letterpress small-caps handle. */
489490
.atlas-season {
@@ -608,39 +609,8 @@
608609
to { opacity: 1; transform: none; }
609610
}
610611

611-
/* ::phase — the passage's ink follows the needle, once the dial is engaged.
612-
Pencil = 40% ink into muted: measurably legible on every biome's paper
613-
(≈6.9:1 on the light papers, ≈9:1 on the dark), never a ghost. */
614-
.atlas-phase { position: relative; transition: color 0.45s ease; }
615-
.atlas-phase.is-penciled { color: color-mix(in srgb, var(--atlas-ink) 40%, var(--atlas-muted)); }
616-
/* pencil the whole passage uniformly — links and emphasis grey with the text */
617-
.atlas-phase.is-penciled :is(a, em, strong, code) { color: inherit; }
618-
.atlas-phase .phase-glyph {
619-
position: absolute;
620-
top: 0.25rem;
621-
left: -2.7rem;
622-
width: 1.5rem;
623-
height: 1.5rem;
624-
padding: 0.15rem;
625-
border: 0;
626-
background: none;
627-
color: var(--atlas-muted);
628-
cursor: pointer;
629-
transition: color 0.15s ease;
630-
}
631-
.atlas-phase .phase-glyph:hover,
632-
.atlas-phase .phase-glyph:focus-visible { color: var(--biome-accent); }
633-
.atlas-phase .phase-glyph svg { display: block; width: 100%; height: 100%; }
634-
.atlas-phase .phase-glyph .year { fill: none; stroke: currentColor; stroke-width: 0.9; opacity: 0.4; }
635-
.atlas-phase .phase-glyph .arc { fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }
636-
.atlas-phase.is-now .phase-glyph .arc { stroke-width: 2.8; }
637-
/* no room in the margin on a narrow page: seat the glyph above the passage */
638-
@media (max-width: 62rem) {
639-
.atlas-phase .phase-glyph { position: static; display: inline-flex; margin: 0 0 0.1rem -0.15rem; }
640-
}
641612
/* the essay's drop-cap belongs to the essay's opening paragraph only — a
642-
passage's own first paragraph must not sprout one */
643-
.atlas-phase .phase-body p:first-of-type::first-letter,
613+
season-note or a quote's own first paragraph must not sprout one */
644614
.atlas-sighting .s-quote p::first-letter,
645615
.atlas-almanac .wcap p::first-letter {
646616
float: none;

layers/atlas/app/components/atlas/PhenologyWheel.vue

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ const props = defineProps<{
4141
// or the biome landing); a wheel seated without a provider (e.g. a gallery)
4242
// makes its own.
4343
const almanac = useAlmanac() ?? provideAlmanac({ phases: () => props.phases ?? [] })
44-
const { day, setDay, today, marks, engage, focusDay } = almanac
44+
const { day, setDay, today, marks, focusDay } = almanac
4545
4646
// SSR-stable ids for the <defs> this instance owns (patterns, label paths).
4747
const uid = useId()
@@ -185,7 +185,6 @@ function pointerAngle(e: PointerEvent): number {
185185
186186
function onPointerDown(e: PointerEvent) {
187187
if (!svgEl.value || (e.pointerType === 'mouse' && e.button !== 0)) return
188-
engage() // the reader has the needle — the essay may follow it now
189188
svgEl.value.setPointerCapture(e.pointerId)
190189
scrubbing.value = true
191190
moved = false
@@ -251,7 +250,6 @@ function onKeydown(e: KeyboardEvent) {
251250
return
252251
}
253252
e.preventDefault()
254-
engage() // a keyboard scrub engages the dial exactly as a pointer grab does
255253
setDay(next)
256254
commitDayToUrl()
257255
}

layers/atlas/app/components/content/Phase.vue

Lines changed: 0 additions & 65 deletions
This file was deleted.

layers/atlas/app/components/content/Season.vue

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ const glyphArc = computed(() => (season.value ? ringArcPath(season.value.span, 5
3939
4040
function swing() {
4141
if (!almanac || !season.value) return
42-
almanac.engage() // a deliberate needle-swing engages the dial (#279 decision 4)
4342
almanac.setDay(spanMidpoint(season.value.span))
4443
}
4544
</script>

layers/atlas/app/components/content/SeasonNote.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
// wholly swaps the prose from season to season (the reader's own words: "a
66
// paragraph for each specific season … use the wheel to fully switch").
77
//
8-
// Progressive enhancement, done the same way ::phase does it, so SSR stays
9-
// mismatch-free: every season-note server-renders EXPANDED, and the client's
8+
// Progressive enhancement kept SSR-mismatch-free by collapsing only after
9+
// hydration: every season-note server-renders EXPANDED, and the client's
1010
// first (hydrating) paint matches — all notes visible. Only in `onMounted`,
1111
// after hydration, does it collapse to the one the needle is on. So crawlers
1212
// and no-JS readers get the whole year top-to-bottom (each block headed by its

layers/atlas/app/composables/almanac.ts

Lines changed: 18 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
// The Almanac — the shared reactive state under the specimen page's almanac
22
// dial (#282, map #279), and THE contract the dial-driven MDC components
3-
// (#283: `:season`, `::phase`, `::sighting`) build against. Auto-imported
4-
// layer-wide (docs/agents/tenant-layers.md §1).
3+
// (`::almanac`, `:season`, `::season-note`, `::sighting`) build against. The
4+
// dial is the essay's season selector: the shared `day` decides which season
5+
// the needle rides, and each `::season-note` shows only in its own season, so
6+
// turning the dial swaps the prose. Auto-imported layer-wide
7+
// (docs/agents/tenant-layers.md §1).
58
//
69
// ── The contract ─────────────────────────────────────────────────────────────
710
// The specimen page owns the state and provides it once in its setup:
@@ -20,22 +23,20 @@
2023
// state, so an MDC component pasted into a non-specimen Document degrades
2124
// gracefully instead of crashing the page — always null-check.
2225
//
23-
// Typical #283 usage: a `::phase{of="x"}` finds its span via
24-
// `almanac.phases.value.find(p => p.name === of)` and lights up when
25-
// `inSpan(almanac.day.value, span)`; a `::sighting{date}` does
26-
// `register({ id: useId(), day: dateToDay(date), kind: 'sighting' })` in setup
27-
// and `unregister(id)` in `onUnmounted`. Marks registered by components that
28-
// render after the wheel (everything inside the essay does) appear on the dial
29-
// reactively — client-side, post-hydration — by design, so the server-rendered
30-
// markup stays mismatch-free.
26+
// Typical usage: a `::season-note{of="x"}` shows while `seasonOf(day.value)` is
27+
// its season (collapsing to the needle's season on mount — see SeasonNote); a
28+
// `::sighting{date}` does `register({ id: useId(), day: dateToDay(date),
29+
// kind: 'sighting' })` in setup and `unregister(id)` in `onUnmounted`. Marks
30+
// registered by components that render after the wheel (everything inside the
31+
// essay does) appear on the dial reactively — client-side, post-hydration — by
32+
// design, so the server-rendered markup stays mismatch-free.
3133
//
32-
// ── #283 extensions ──────────────────────────────────────────────────────────
33-
// `engaged`/`engage()` — progressive-enhancement gate (map #279 decision 4):
34-
// false until the reader first takes hold of the needle (the wheel flips it on
35-
// first pointer grab or keyboard scrub; the prose handles that swing the
36-
// needle flip it too). `::phase` gates its ink↔pencil register on it, so the
37-
// essay server-renders fully inked and STAYS inked for a reader who never
38-
// touches the dial.
34+
// ── Extensions ───────────────────────────────────────────────────────────────
35+
// `focusPulse`/`focusDay(day)` — the clickable-tick path: tapping a dial tick
36+
// moves the needle to that exact day AND bumps `focusPulse`, and the matching
37+
// `::sighting` watches the pulse to scroll its quote into view. Distinct from
38+
// the silent `setDay` so a descendant reacts only to a deliberate "take me
39+
// there", not to every drag that crosses its day.
3940
// `observations`/`findSighting(date)` — the biome's dated ledger, provided by
4041
// the page alongside `phases`, so a `::sighting{date}` can quote the
4142
// observation's own note without retyping it (single-homed; lookup semantics
@@ -74,14 +75,6 @@ export interface Almanac {
7475
register: (mark: AlmanacMark) => void
7576
/** Remove a mark by id — call on unmount. Unknown ids are a no-op. */
7677
unregister: (id: string) => void
77-
/** True once the reader has taken hold of the needle — false on every fresh
78-
* render, server and client alike (#283 extension; see header). `::phase`
79-
* reads it so the essay stays fully inked until the dial is engaged. */
80-
engaged: Readonly<Ref<boolean>>
81-
/** Flip `engaged` (idempotent). Called by whichever interaction first hands
82-
* the reader the needle — the wheel's pointer/keyboard scrub, or a prose
83-
* handle's deliberate needle-swing. */
84-
engage: () => void
8578
/** A monotonic pulse bumped by `focusDay` — the "take me to this exact day
8679
* AND draw the reader's eye there" signal the clickable dial ticks fire. A
8780
* `::sighting` watches it and scrolls its quote into view when its own day
@@ -128,7 +121,6 @@ export function provideAlmanac(options: ProvideAlmanacOptions = {}): Almanac {
128121
const today = normalizeDay(Math.round(options.today ?? useGlassToday()))
129122
const day = ref(normalizeDay(Math.round(options.initialDay ?? today)))
130123
const markList = ref<AlmanacMark[]>([])
131-
const engaged = ref(false)
132124
const focusPulse = ref(0)
133125
const observations = computed(() => toValue(options.observations) ?? [])
134126

@@ -146,13 +138,8 @@ export function provideAlmanac(options: ProvideAlmanacOptions = {}): Almanac {
146138
unregister: (id: string) => {
147139
markList.value = withoutAlmanacMark(markList.value, id)
148140
},
149-
engaged,
150-
engage: () => {
151-
engaged.value = true
152-
},
153141
focusPulse,
154142
focusDay: (d: number) => {
155-
engaged.value = true
156143
day.value = normalizeDay(Math.round(d))
157144
focusPulse.value++
158145
},

layers/atlas/content/canopy/pages/aranea-patiens.md

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,38 @@ illustration: |
9292

9393
*Aranea patiens* is common, and we set that word down with respect, because commonness on this scale is a kind of mastery. There is scarcely a fork of the upper boughs without its wheel of silk, hung at an angle to catch whatever the dusk shakes loose, and at the hub of each wheel the same small brown proprietor, head-down, legs at ease, doing the one thing it does supremely well, which is nothing at all.
9494

95-
The web is built in darkness and built entire — begun after the last light fails and finished, so far as we can tell, before we have finished remarking that it has begun. What the day damages the night replaces without comment. By dawn the new wheel is strung with dew, and this is when the weaver is at its handsomest: the silk goes to silver, every spoke beaded, and the animal sits amid its own accounting like a clerk who has balanced the books and sees no reason to say so.
95+
The visitor inclined to pity the prey should sit with the weaver through one whole night, as we have: eight hours motionless in the cold, the web trembling with every false alarm, and no certainty at all that the canopy owes it breakfast. Everything up here waits; the weaver has merely gone further, and made of waiting a livelihood. We could not do it, and we have the advantage of knowing the kitchen is stocked.
9696

97-
Its diet the ledger gives as *whatever the web keeps*, and the phrase was chosen for its honesty. The weaver does not hunt; it has arranged matters so that hunting comes to it. We have seen the web keep a lantern moth — the lamp burning in the silk for a long minute, then banked, then dark — and we confess the company recorded that entry in silence and went early to supper. We have also seen it keep a false-leaf, which had made, that night, its second mistake.
97+
That livelihood keeps a calendar of its own — first strung, then wheeled high, then quietly taken in — and we have set it down season by season below. Turn the almanac to follow the year around the hub.
9898

99-
The visitor inclined to pity the prey should sit with the weaver through one whole night, as we have. Eight hours motionless in the cold, the web trembling with every false alarm, and no certainty at all that the canopy owes it breakfast. Everything up here waits; the weaver has merely gone further, and made of waiting a livelihood. We could not do it, and we have the advantage of knowing the kitchen is stocked.
99+
::almanac
100+
::
101+
102+
:::season-note{of="radiator-months"}
103+
Through the dry-warm months the weaver spins next to nothing. It sits flat against the bark, legs drawn in, and waits — which, having made a livelihood of waiting, it does supremely well and with no visible sign of hardship. A wheel strung now would catch only dust and dry air, and the weaver, who is nothing if not economical, declines to string one. We mark the empty forks and pass on.
104+
:::
105+
106+
:::season-note{of="lamp-lengthening"}
107+
As the light lengthens the first wheels of the year go up, strung low and small, as though the weaver were testing the air's opinion before committing to it. They are modest things, easily missed, taken down some mornings and not replaced — the returning silk spent cautiously, on the chance the season is not yet in earnest. By the last of these weeks the forks are furnished again, if sparely.
108+
:::
109+
110+
:::season-note{of="great-airing"}
111+
With the lid open an hour a day the draught worries at the new webs, and the weaver answers by building lower and anchoring harder rather than by giving up the fork. The wheels widen as the season turns; what began as a cautious test becomes, by degrees, the great work of the damp months ahead. The draught takes a wheel now and then, and the weaver replaces it by the following dark without comment.
112+
:::
113+
114+
:::season-note{of="long-damp"}
115+
This is the high wheeling, the great wheels of the damp season, wider each night than we would credit and replaced entire by dawn — what the day damages the night makes good, so that by first light the new wheel stands strung with dew, every spoke beaded, the animal amid its own accounting like a clerk who has balanced the books and sees no reason to say so.
116+
117+
::sighting{date="2026-06-26"}
118+
::
119+
120+
It hunts, in these weeks, only in the sense that it has arranged for hunting to come to it. We have seen the web keep a lantern moth — the lamp burning in the silk for a long minute, then banked, then dark — and we confess the company recorded that entry in silence and went early to supper. We have also seen it keep a false-leaf, which had made, that night, its second mistake.
121+
:::
122+
123+
:::season-note{of="small-dry"}
124+
As the misting lapses the great wheels come down, a bough at a time, and the silk is not discarded but eaten and carried inward, as savings against the lean months. The weaver reads the drying air a fortnight before we do, and begins its economies while the canopy still calls the season summer. A web taken in this week will not be restrung until the damp comes back.
125+
:::
126+
127+
:::season-note{of="settling"}
128+
After the great cleaning the taking-in goes on in earnest, wheel by wheel, until the forks that hummed with silk all summer stand bare and the whole season's spinning has been carried inward as store. The weaver does this without visible regret. By the season's close it has spun down almost to nothing and sits closer to the bark each night, settling toward the long, flat patience of the cold.
129+
:::

0 commit comments

Comments
 (0)