Corrects §6.3's link-resolution table, which named three website paths the site
does not serve (/site/news/<idOrSlug>, /page/<slug>, /contact) and missed two it
does (/site/atlas/:slug, /site/market/vendors/:serial), and records the four
drawer decisions taken before code: the /<slug> CMS catch-all with the site's
reserved segments excluded, the static section header, the hand-off icon, and
reusing LocalAssetResolver for the Custom Tab's absolute URL.
Adds "Phase 6 as landed" and ticks the phase in §8.
Co-Authored-By: Claude <noreply@anthropic.com>
§6.2 corrected and amended, §6.1 given the Home decision, and a "Phase 5 as
landed" section added with the ✅ in §8.
**§6.2's table was in the wrong order, and the order is now load-bearing.** The
three news categories were listed Five on Friday / Newsletter / Screenshots; the
website's array has them Screenshots / Five on Friday / Newsletter. That did not
matter while the table was only a mapping, but a stored `order` is an index into
the site's nav, so a row the admin never moved takes its sort key from this list —
and a key from the wrong list scrambles a partially-overridden nav. The rows are
now numbered 0-15 and the order is called out as verbatim.
Also written into §6.2: the `feature` values are deliberately **not** mirrored
into the table (`APP_MENU` stays the app's own source of truth for gating), and
the "no `APP_MENU` row" rule covers **seven** entries rather than four — the
three news categories as well as the hub boards.
Two decisions the org lead settled before code, both recorded where the rule
lives rather than only in the phase notes:
- **The news categories are ignored for the drawer**, on the same rule as the hub
four. The mapping still exists for phase 6's added links, where a category tab
is a perfectly good destination because the admin named it by path. The
alternative — surfacing such a row only when overridden — keeps AC-1 but lets an
override introduce navigation after all, so it was rejected.
- **An admin may hide the Home row**, mirroring the website, where `/` is hideable
from the public header. Home stays the start destination and stays reachable by
back-press. The contrast that makes it safe is with `/admin/navigation`, which
has three guards because hiding it would strip the only way to undo an override;
nothing about a hidden Home row is unrecoverable.
"Phase 5 as landed" records the sort-key finding above as the thing the spec did
not settle, that an untouched instance gets `APP_MENU` back by **identity** so
AC-1's drawer claim is an `assertSame`, that the app's own rows are partitioned
off rather than sorted (and what that would mean for a future interleaved row),
that `group`/`section` are read and dropped, and why `Routes.NEWS_ROUTE` is
declared beside `Routes.NEWS` rather than replacing it — with the consequence that
`destination.route` now carries a query and is compared on `substringBefore('?')`.
Android-app: RunicGateway/Android-app#38.
Co-Authored-By: Claude <noreply@anthropic.com>
§5.6 gains the three decisions the drafted section left open — the top bar's
text fallback, the hero's fixed cropped band, and the accessibility split
between a decorative logo and a named one — each with the reasoning that
settled it, so none is re-litigated from scratch. Adds the 32dp/24dp logo
heights, the six-times-height width cap, and the note that the app takes no
fallback hero image where the website substitutes its own emblem.
"Phase 4 as landed" records how the empty-slot rule is enforced by layout
rather than by a conditional at each call site, why the blank check runs on
both sides of the resolver, and the honest limit of the phase's tests: the
drawing cannot be tested here at all, since the app has no Robolectric and no
androidTest source set, so only the decision of whether to draw is pure.
Phase 4 ticked in §8.
Co-Authored-By: Claude <noreply@anthropic.com>
§5.3 rewritten against what the build measured rather than what the plan
estimated, plus a "Phase 3 as landed" section and the ✅ in §8.
Three corrections to the drafted section:
- The APK estimate was wrong by more than 3×. "Roughly 1.5-2.5 MB" becomes a
measured before/after table: 5,031,411 B → 13,574,703 B, +8.15 MiB. The
section now records that Merriweather is 6.08 MiB of that on its own, and the
two cheaper options the org lead costed and declined, so the number is not
re-litigated from scratch later.
- Family lookup must be global, never scoped to the role's option list. Two of
the three presets publish a font their own role's dropdown does not offer,
because resolveThemeTokens copies preset tokens verbatim — the same bypass
§5.4 already records for --shadow-card.
- Italics mirror the website's four rather than being skipped, and every
bundled family supplies the four weights the scale asks for (with IM Fell
English's single weight and Cinzel's untouched 500/600/700 as the two
documented exceptions), because a family is not confined to the role its
dropdown lives in.
Android-app: RunicGateway/Android-app#TBD
Co-Authored-By: Claude <noreply@anthropic.com>
The radii-and-shadow phase, shipped as Android-app#35. Seven notes, of which
three are amendments to the spec rather than records of it.
Section 5.4's elevation map is not a no-op and now says so, with section 2 and
AC-1 amended to match. Material3's filled Card is Level0 and FeatureCard drew
none of the shadow its own KDoc claimed, so the app has been flat since M5 -
while the runic-gateway preset it was drawn from selects the "Default" shadow.
The org lead chose to apply the map as written rather than rebase it on the flat
baseline, because rebasing would have collapsed none/Soft/Default onto 0dp and
left only Deep doing anything on the phone. The radius half is untouched by this
and remains a provable no-op.
Section 5.4's exact-string match is corrected to a nearest-blur match. The
fantasy preset's own --shadow-card is not one of SHADOW_OPTIONS' four values,
because resolveThemeTokens copies a preset's tokens verbatim and they never pass
through the admin form's dropdown.
Section 5.2 gains the rule the pill needed: CircleShape is a percentage, so it
has no shipped dp for a ratio to scale and the resolved px is taken as dp below
the 500px floor. It is a literal because there is no app scale to preserve, not
as an exception to the ratio rule.
Also recorded: why all 24 Card( call sites became ShardCard( (Material takes
elevation as a default argument, not from the theme), that --radius-pill reaches
only StatusPill, and that Shapes - unlike ColorScheme - does implement equals,
so the structural no-op proof is one assertion rather than a reflection walk.
PLAN.md 9's M12 entry records phase 2 landed.
Co-Authored-By: Claude <noreply@anthropic.com>
The colors phase, shipped as Android-app#34. Seven notes, of which three are
departures the build forced rather than choices it made:
ColorScheme has no equals in material3 1.3.0, so AC-1's "full equality, not a
spot check" is a field-by-field compare by reflection over all 36 color roles,
against a verbatim copy of the pre-M12 scheme held in the test.
ShardPillFg is derived from --accent-bright rather than being a sixteenth
token, under the rule 5.1 already states for ShardOnCta.
An instance with an env accent and no theme_visual row loses its accented CTA
buttons, because brand.accent now seeds --accent alone. That is 5.1's "wrong
twice over" being corrected, and it is the one thing on screen that moves.
PLAN.md 9's M12 entry records phase 1 landed.
Co-Authored-By: Claude <noreply@anthropic.com>
Two records, one milestone.
Phase 7 (reading nav_admin / nav_player) is cancelled. Section 6.4 already
made the case against it and scheduled it last so the call could be taken on
its merits with the rest working: the two authenticated navs reach four app
rows between them, which does not pay for a new authenticated fetch, a
session-keyed cache and its teardown. The app therefore makes no authenticated
settings call at all, and the player and staff drawer rows keep their coded --
and so localized -- labels, which is the one thing given up. Section 4's locked
decision, section 3's endpoint row and section 5.5's lifecycle paragraph are
amended to match so the doc does not contradict itself, and section 8 gains
the standing "Phase 7, cancelled" note.
Phase 0 (the contract and the appearance store) landed as Android-app#33, and
its "as landed" notes record two departures from what section 8 specified:
- `theme` is modeled as a raw JsonElement rather than Map<String,String>?.
kotlinx fails the decode of the whole object on a value of an unexpected
kind, and `theme` shares its payload with `brand` and `push` -- so one odd
token would have blanked the branding and dropped the push relay URL, the
opposite of section 2. It is coerced field-by-field instead.
- A failed refresh keeps the last good appearance rather than falling back to
NONE. Section 5.5 said best-effort; the distinction it did not draw is that
a moment of no connectivity on resume must not repaint a themed shard back
to the shipped defaults.
Co-Authored-By: Claude <noreply@anthropic.com>
The website merged runtime admin theming, brand assets and nav overrides to
main (website#126 / docs#109). The app reads exactly one field of it --
brand.accent -- and renders a hardcoded APP_MENU, so an admin who re-skins the
site and restructures the header sees none of it on the phone.
Adds docs/android/THEMING_AND_NAV.md as the design of record for M12, and the
PLAN.md §9 entry that anchors it. Plan only: no app code, no backend work.
Everything consumed is already live on website/main.
The points that shaped it:
- The app's ui/theme/Color.kt palette is already, value for value, the
runic-gateway preset -- M5 was drawn from the same theme.css the preset was
later extracted from. So "an untouched instance is unchanged" carries over as
a testable ColorScheme equality assertion, not an approximation.
- Radii apply as a ratio against that baseline, not as literal dp. The app's
Shapes came from the M5 mockup and genuinely differ (medium 12dp vs
--radius-card 10px); a literal mapping would restyle the untouched app the
day this ships, and copying the app's scale into the server would be a second
source of truth.
- Fonts are bundled, not downloadable: the Play Store font provider makes a
de-Googled device fall back silently. Seven families join the bundled Cinzel.
- Nav overrides are keyed by website paths, so the app needs a path -> route
table -- the one new cross-repo coupling here. An override for a path the app
does not surface in its menu is ignored: a nav override may never introduce
navigation.
- The gates are untouched. MenuAccess and MenuEntry.feature still run after the
merge, so hidden:false cannot un-hide what a role or the shard's visibility
config withholds.
- Read the resolved theme/brand fields, never the raw theme_visual/brand_assets
rows that ride along in the same payload -- re-deriving a palette from them
would be a second resolveThemeTokens in Kotlin, guaranteed to drift.
Nine phases into a fresh edge in both repos, reaching main as one edge -> main
merge, the same shape the website side used. Phase 0 must change nothing on
screen. Phase 7 (the authenticated navs) is marked optional: nav_player reaches
two app rows and nav_admin two, which is a thin return for a new authenticated
fetch and its cache teardown.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TgfKv5cz5pbY3dPeofSE5a