rename: The Armory -> The Reliquary; extend spec with essence + cross-over

Renaming: "Armory" read too militaristic for a séance app. Landed on "The
Reliquary" (not "The Threshold" — that name was already taken by the
landing-page back-link).

Spec addendum: added a visible essence currency (earned per summon, spent
on unlocks — distinct from the hidden favor score) and a fourth judgment
verdict, cross_over, for compassionately helping a genuinely benevolent
"stuck" spirit move on rather than just trusting or banishing it. Updates
workstreams B/C/E/F accordingly before any of them are dispatched.
This commit is contained in:
Indiana
2026-07-23 11:06:07 +00:00
parent 291d75c70c
commit 6d8c6f2496
8 changed files with 137 additions and 76 deletions

View File

@@ -67,7 +67,7 @@ the collectible layer sitting on top of the anomaly-detection plumbing:
nothing about a spirit is user-authored, all of it comes from the same nothing about a spirit is user-authored, all of it comes from the same
signature → LLM-mint → normalize pipeline that runs live during a séance. signature → LLM-mint → normalize pipeline that runs live during a séance.
## The Armory ## The Reliquary
`/shop` is a waitlist page for **the Ultimate Quantum Box** — a proposed `/shop` is a waitlist page for **the Ultimate Quantum Box** — a proposed
ESP32-P4/C6 handheld séance instrument (thermal camera, EMF whisker array, ESP32-P4/C6 handheld séance instrument (thermal camera, EMF whisker array,
@@ -158,7 +158,7 @@ frontend/
src/i18n/ react-i18next init + en.json / es.json src/i18n/ react-i18next init + en.json / es.json
docs/superpowers/ docs/superpowers/
specs/ the design spec — note the live app now exceeds it specs/ the design spec — note the live app now exceeds it
(5 modes, not 4; the Codex, the Armory and the (5 modes, not 4; the Codex, the Reliquary and the
haunting layer are all further along than documented) haunting layer are all further along than documented)
plans/ the 7 implementation plans this repo was built from plans/ the 7 implementation plans this repo was built from
``` ```
@@ -380,7 +380,7 @@ Honestly, in order of confidence:
the landing page's marketing grid yet. the landing page's marketing grid yet.
- **Cosmetic.** The frontend build ships one 871 KB JS chunk (Vite warns - **Cosmetic.** The frontend build ships one 871 KB JS chunk (Vite warns
about it); code-splitting would help first paint but hasn't been done. about it); code-splitting would help first paint but hasn't been done.
- **Intentionally unfinished.** The Armory / Ultimate Quantum Box is a - **Intentionally unfinished.** The Reliquary / Ultimate Quantum Box is a
waitlist only — no hardware exists, no payment is taken, and the page waitlist only — no hardware exists, no payment is taken, and the page
says so. says so.

View File

@@ -22,9 +22,15 @@ async with engine.begin() as conn:
await conn.execute(text( await conn.execute(text(
"ALTER TABLE users ADD COLUMN IF NOT EXISTS favor DOUBLE PRECISION NOT NULL DEFAULT 0.0" "ALTER TABLE users ADD COLUMN IF NOT EXISTS favor DOUBLE PRECISION NOT NULL DEFAULT 0.0"
)) ))
await conn.execute(text(
"ALTER TABLE users ADD COLUMN IF NOT EXISTS essence INTEGER NOT NULL DEFAULT 0"
))
await conn.execute(text( await conn.execute(text(
"ALTER TABLE entities ADD COLUMN IF NOT EXISTS traits JSONB NOT NULL DEFAULT '{}'::jsonb" "ALTER TABLE entities ADD COLUMN IF NOT EXISTS traits JSONB NOT NULL DEFAULT '{}'::jsonb"
)) ))
await conn.execute(text(
"ALTER TABLE entities ADD COLUMN IF NOT EXISTS at_peace BOOLEAN NOT NULL DEFAULT false"
))
``` ```
New tables (`unlocks`, `inventory_items`, `sigils`) don't need this — New tables (`unlocks`, `inventory_items`, `sigils`) don't need this —
@@ -53,6 +59,26 @@ legible/less volatile; lower favor → more volatile/deceptive). The read-back
bias is a small effect — a `favor`-scaled adjustment to the volatility and bias is a small effect — a `favor`-scaled adjustment to the volatility and
deceptiveness rolls in the trait-rolling function, not a hard gate. deceptiveness rolls in the trait-rolling function, not a hard gate.
**`User.essence: int`** (new column, default `0`) — a visible, spendable
currency, distinct from the hidden `favor` score. Earned in small amounts
just for summoning (every successful summon, any mode) and larger amounts
for meaningful actions (ritual success, a correct judgment, crossing a
spirit over — see below). This is the currency Workstream C's unlocks/items
are priced in — earning it should feel constant and easy ("the more you
summon"), spending it on unlocks/tools is the sink. Exact amounts are the
implementer's call; keep summon income small (single digits) and
milestone income larger (tens), so unlocks feel reachable within a handful
of sessions, not grindy.
**`Entity.at_peace: bool`** (new column, default `false`) — set when a
seeker successfully helps a genuinely benevolent, "stuck" spirit cross
over (see the `cross_over` verdict below). An at-peace entity stays in the
Codex permanently (memorialized, never deleted) but can no longer be
re-contacted — `signature_from_anomalies` matching it should mint a new
entity instead of re-summoning the retired one. The Codex API/UI marking
it distinctly is a later concern (Workstream D can add a simple "at peace"
badge if time allows, but it's not required for this spec to be complete).
**New WS frames on `/ws/session`** (extends the existing protocol in **New WS frames on `/ws/session`** (extends the existing protocol in
`backend/app/ws.py`): `backend/app/ws.py`):
@@ -60,10 +86,19 @@ Client → server:
- `{"type": "ritual_start"}` — begins a ritual attempt for the current - `{"type": "ritual_start"}` — begins a ritual attempt for the current
entity. entity.
- `{"type": "ritual_step", "step": <int>}` — one completed interactive step. - `{"type": "ritual_step", "step": <int>}` — one completed interactive step.
- `{"type": "judgment", "verdict": "trust" | "banish" | "test"}` — always - `{"type": "judgment", "verdict": "trust" | "banish" | "test" |
allowed (a seeker can judge blind without completing a ritual — the "cross_over"}` — always allowed (a seeker can judge blind without
ritual doesn't gate eligibility, only whether accurate info was shown completing a ritual — the ritual doesn't gate eligibility, only whether
first). accurate info was shown first). `cross_over` is the compassionate
resolution: helping a spirit move on rather than simply continuing
contact (`trust`) or expelling it (`banish`). It's the *correct* call
specifically for a genuinely benevolent entity that reads as "stuck"
(implementer's call on the exact signal — a reasonable rule: alignment
>= 0.5 and volatility above some threshold, since the existing fallback
personas already lean on "died with something unfinished" themes). Used
on a demon, it fails outright — demons resist crossing over — with no
reward and no `at_peace` change, distinct from a wrong `trust`/`banish`
call (it's a naive read, not a reckless one, so no favor penalty either).
Server → client: Server → client:
- `{"type": "ritual_complete", "success": bool, "revealed": {"alignment": - `{"type": "ritual_complete", "success": bool, "revealed": {"alignment":
@@ -79,23 +114,29 @@ Server → client:
work with without the backend leaking ground truth outside of work with without the backend leaking ground truth outside of
`ritual_complete`. `ritual_complete`.
- `{"type": "judgment_result", "correct": bool, "favor_delta": float, - `{"type": "judgment_result", "correct": bool, "favor_delta": float,
"consequence": "reward" | "escalation" | "withdrawal" | "neutral"}` — "essence_delta": int, "at_peace": bool,
`correct` = trust called on real alignment >= 0.5, or banish called on "consequence": "reward" | "escalation" | "withdrawal" | "crossed_over" |
alignment < 0.5. `test` verdict always returns `consequence: "neutral"`, "resisted" | "neutral"}` — `correct` = trust called on real alignment
`favor_delta: 0.0`, and requires a completed ritual this session to do >= 0.5, or banish called on alignment < 0.5, or cross_over called
anything (a `judgment_result` with `correct: false` and correctly per the rule above. `test` verdict always returns
`consequence: "neutral"` if attempted without one — no crash, just no `consequence: "neutral"`, both deltas `0`, and requires a completed
effect). Wrong-trust favor penalty should be larger in magnitude than ritual this session to do anything (a `judgment_result` with
wrong-banish penalty (recklessness costs more than caution) — exact `correct: false` and `consequence: "neutral"` if attempted without one —
numbers are the implementer's call, keep them small (single-digit no crash, just no effect). `cross_over` on a demon returns
percent of the `[-1,1]` range per event). `consequence: "resisted"`, both deltas `0`. `cross_over` correctly called
returns `consequence: "crossed_over"`, `at_peace: true`, and the largest
essence reward of any outcome. Wrong-trust favor penalty should be larger
in magnitude than wrong-banish penalty (recklessness costs more than
caution) — exact numbers are the implementer's call, keep favor deltas
small (single-digit percent of the `[-1,1]` range per event).
- `{"type": "item_drop", "item": {"item_type": str, "item_key": str, - `{"type": "item_drop", "item": {"item_type": str, "item_key": str,
"payload": dict}}` — emitted opportunistically (implementer's call on "payload": dict}}` — emitted opportunistically (implementer's call on
exact odds) after a correct judgment, a successful ritual, or a exact odds) after a correct judgment, a successful ritual, or a
high-rarity summon. high-rarity summon.
**`GET /auth/me`** gains an `unlocks: list[str]` field (unlock keys the **`GET /auth/me`** gains an `unlocks: list[str]` field (unlock keys the
user has earned) so the frontend knows what's enabled — e.g. `"listening_tool"`. user has earned) and an `essence: int` field (current spendable balance)
so the frontend knows what's enabled and affordable.
**New tables** (SQLAlchemy models, follow the existing `Entity`/`User` **New tables** (SQLAlchemy models, follow the existing `Entity`/`User`
style in `backend/app/models/`): style in `backend/app/models/`):
@@ -114,36 +155,49 @@ from persona/LLM input as specified above. Tests: trait ranges, signature
determinism, persona/trait independence (same persona template can pair determinism, persona/trait independence (same persona template can pair
with any alignment). with any alignment).
## Workstream B (backend) — ritual + judgment + favor ## Workstream B (backend) — ritual + judgment + favor + cross-over
New `backend/app/judgment.py` (pure functions: ritual success roll, New `backend/app/judgment.py` (pure functions: ritual success roll,
judgment-correctness + favor-delta + consequence selection) plus the WS judgment-correctness + favor-delta + essence-delta + consequence selection
handlers in `backend/app/ws.py` for `ritual_start`/`ritual_step`/`judgment`, for all four verdicts including `cross_over`'s "stuck spirit" rule and its
and the `User.favor` column + migration line. Reads `state.entity["traits"]` demon-resists-crossing-over failure case) plus the WS handlers in
(entity dict shape — check how `serialize_entity` exposes it and add `backend/app/ws.py` for `ritual_start`/`ritual_step`/`judgment`, and the
`traits` there). On wrong-trust, call into the existing haunting-escalation `User.favor`/`User.essence`/`Entity.at_peace` columns + migration lines.
hook if one exists server-side, or note in your report if escalation is Reads `state.entity["traits"]` (entity dict shape — check how
frontend-only (check `frontend/src/lib/haunting.ts` first — if escalation `serialize_entity` exposes it and add `traits` there). On correct
state lives purely client-side, your job is just to emit `cross_over`, set `Entity.at_peace = true` and ensure
`consequence: "escalation"` correctly and leave the actual effect to `signature_from_anomalies` re-contact no longer matches an at-peace entity
Workstream D). Tests: ritual success/fail paths, judgment correctness (mint a fresh one instead — check `entities.py`'s matching path). On
matrix (trust/banish × real spirit/demon), favor clamping, favor read-back wrong-trust, call into the existing haunting-escalation hook if one exists
bias on trait rolls (small, verifiable effect size). server-side, or note in your report if escalation is frontend-only (check
`frontend/src/lib/haunting.ts` first — if escalation state lives purely
client-side, your job is just to emit `consequence: "escalation"` correctly
and leave the actual effect to Workstream D). Tests: ritual success/fail
paths, judgment correctness matrix (trust/banish/cross_over × real
spirit/demon/stuck spirit), favor clamping, essence crediting per outcome,
at_peace persistence + re-contact behavior, favor read-back bias on trait
rolls (small, verifiable effect size).
## Workstream C (backend) — unlocks, items, sigils, drops ## Workstream C (backend) — unlocks, items, sigils, drops, essence
New models (`unlocks`, `inventory_items`, `sigils` per the contract), a new New models (`unlocks`, `inventory_items`, `sigils` per the contract), a new
`backend/app/routes/inventory.py` (or extend an existing routes file if more `backend/app/routes/inventory.py` (or extend an existing routes file if more
consistent with this codebase's conventions — check `backend/app/routes/`) consistent with this codebase's conventions — check `backend/app/routes/`)
exposing REST endpoints to list a user's unlocks/items/sigils and to save a exposing REST endpoints to list a user's unlocks/items/sigils, to save a
sigil design (`POST`, validates the `design` JSON shape against whatever sigil design (`POST`, validates the `design` JSON shape against whatever
Workstream F defines — coordinate via your report if you build before Workstream F defines — coordinate via your report if you build before
seeing their output; a reasonable placeholder shape is seeing their output; a reasonable placeholder shape is
`{"points": [[x,y],...], "rune": str}`, cap `points` length e.g. at 12 to `{"points": [[x,y],...], "rune": str}`, cap `points` length e.g. at 12 to
bound payload size). Extend `GET /auth/me` with `unlocks: list[str]`. Wire bound payload size), and a new `POST /api/inventory/unlocks/{unlock_key}`
`item_drop` emission into `backend/app/ws.py` per the contract's trigger that spends `essence` to buy a named unlock (define a small fixed price
points. Tests: drop-roll probability sanity, inventory/sigil CRUD, `/auth/me` table, e.g. `{"listening_tool": 40}` — reject with 402/insufficient-funds
unlock list shape. style error if the user's `essence` is below the price, otherwise deduct
and insert into `unlocks`). Extend `GET /auth/me` with `unlocks: list[str]`
and `essence: int`. Wire `item_drop` emission and `essence` crediting
(summon-time trickle + judgment/ritual/cross-over milestones) into
`backend/app/ws.py` per the contract's trigger points and amounts. Tests:
drop-roll probability sanity, inventory/sigil CRUD, essence purchase
success/insufficient-funds paths, `/auth/me` unlock list + essence shape.
## Workstream D (frontend) — Ghost Log HUD + evil meter + tells ## Workstream D (frontend) — Ghost Log HUD + evil meter + tells
@@ -164,18 +218,24 @@ override, idle-state rendering.
New `frontend/src/components/RitualPanel.tsx` — a short interactive New `frontend/src/components/RitualPanel.tsx` — a short interactive
sequence (your call on exact interaction, e.g. 3-5 click/hold steps) that sequence (your call on exact interaction, e.g. 3-5 click/hold steps) that
sends `ritual_step` frames and handles `ritual_complete`. Judgment UI sends `ritual_step` frames and handles `ritual_complete`. Judgment UI
(Trust/Banish/Test buttons) sending the `judgment` frame and handling (Trust/Banish/Cross Over/Test buttons — four verdicts per the contract)
`judgment_result` — on `consequence: "escalation"`, read sending the `judgment` frame and handling `judgment_result` — on
`frontend/src/lib/haunting.ts` first and hook into its existing `consequence: "escalation"`, read `frontend/src/lib/haunting.ts` first and
idle-escalation state if there's a sensible extension point; if not, add a hook into its existing idle-escalation state if there's a sensible
clearly-scoped "forced escalation" mode to it (report which you did). extension point; if not, add a clearly-scoped "forced escalation" mode to
Tests: ritual step sequencing, judgment button → frame shape, escalation it (report which you did). On `consequence: "crossed_over"`, a distinct
wiring (mock haunting.ts's public surface, don't test its internals). calm/peaceful visual beat (your call — e.g. the entity's glyph fading out
gently) rather than reusing the trust "reward" treatment, since it's a
farewell, not a continuation. Tests: ritual step sequencing, judgment
button → frame shape (all four verdicts), escalation wiring (mock
haunting.ts's public surface, don't test its internals).
## Workstream F (frontend) — unlocks/inventory UI + sigil designer + listening tool ## Workstream F (frontend) — unlocks/inventory UI + sigil designer + listening tool
New `frontend/src/components/InventoryPanel.tsx` (list unlocks/items from New `frontend/src/components/InventoryPanel.tsx` (list unlocks/items from
`/auth/me` and the Workstream C endpoints) and `/auth/me` and the Workstream C endpoints, show current `essence` balance,
and a "buy" button per locked unlock that calls Workstream C's purchase
endpoint — disable/gray out unlocks the user can't yet afford) and
`frontend/src/components/SigilDesigner.tsx` — a **constrained geometric `frontend/src/components/SigilDesigner.tsx` — a **constrained geometric
builder**, not freeform drawing: seeker places points around a fixed circle builder**, not freeform drawing: seeker places points around a fixed circle
(cap at 12, matching Workstream C's payload limit), the tool connects them (cap at 12, matching Workstream C's payload limit), the tool connects them
@@ -184,7 +244,8 @@ and overlays a rune choice from a fixed small set. Saves via Workstream C's
`unlocks` list) into `frontend/src/lib/evp.ts`'s detection threshold — when `unlocks` list) into `frontend/src/lib/evp.ts`'s detection threshold — when
present, lower the anomaly threshold so fainter signals register (a real present, lower the anomaly threshold so fainter signals register (a real
gameplay effect, not cosmetic). Tests: sigil point-cap enforcement, rune gameplay effect, not cosmetic). Tests: sigil point-cap enforcement, rune
selection, listening-tool threshold change when unlock present vs. absent. selection, listening-tool threshold change when unlock present vs. absent,
purchase button afford/can't-afford states.
## Explicitly out of scope here ## Explicitly out of scope here

View File

@@ -28,7 +28,7 @@
"heroKicker": "an instrument for speaking with the dead bandwidth", "heroKicker": "an instrument for speaking with the dead bandwidth",
"cta": "BEGIN THE SÉANCE", "cta": "BEGIN THE SÉANCE",
"ctaCodex": "browse the codex", "ctaCodex": "browse the codex",
"ctaShop": "visit the armory", "ctaShop": "visit the reliquary",
"stats": { "stats": {
"entities": "spirits catalogued", "entities": "spirits catalogued",
"sessions": "séances held", "sessions": "séances held",
@@ -57,10 +57,10 @@
}, },
"featured": "most disturbed spirits", "featured": "most disturbed spirits",
"featuredEmpty": "the registry is still blank — be the first to disturb it", "featuredEmpty": "the registry is still blank — be the first to disturb it",
"armory": { "reliquary": {
"title": "HARDWARE FROM THE ARMORY", "title": "HARDWARE FROM THE RELIQUARY",
"line": "the Ultimate Quantum Box is coming — a handheld séance instrument that makes the veil knock louder.", "line": "the Ultimate Quantum Box is coming — a handheld séance instrument that makes the veil knock louder.",
"link": "enter the armory →" "link": "enter the reliquary →"
}, },
"footer": "an interactive horror art installation — the spirits are fiction; the static is real", "footer": "an interactive horror art installation — the spirits are fiction; the static is real",
"footerColophon": "self-hosted · no cloud · the veil runs on your machine" "footerColophon": "self-hosted · no cloud · the veil runs on your machine"
@@ -267,7 +267,7 @@
}, },
"shop": { "shop": {
"back": "← the threshold", "back": "← the threshold",
"title": "THE ARMORY", "title": "THE RELIQUARY",
"subtitle": "instruments for those who would knock louder", "subtitle": "instruments for those who would knock louder",
"flagship": { "flagship": {
"tag": "flagship instrument", "tag": "flagship instrument",

View File

@@ -12,7 +12,7 @@
"nav": { "nav": {
"home": "el umbral", "home": "el umbral",
"codex": "códice", "codex": "códice",
"shop": "arsenal", "shop": "relicario",
"seance": "sesión", "seance": "sesión",
"enter": "entrar", "enter": "entrar",
"logout": "partir" "logout": "partir"
@@ -28,7 +28,7 @@
"heroKicker": "un instrumento para hablar con el ancho de banda muerto", "heroKicker": "un instrumento para hablar con el ancho de banda muerto",
"cta": "COMENZAR LA SESIÓN", "cta": "COMENZAR LA SESIÓN",
"ctaCodex": "hojear el códice", "ctaCodex": "hojear el códice",
"ctaShop": "visitar el arsenal", "ctaShop": "visitar el relicario",
"stats": { "stats": {
"entities": "espíritus catalogados", "entities": "espíritus catalogados",
"sessions": "sesiones celebradas", "sessions": "sesiones celebradas",
@@ -57,10 +57,10 @@
}, },
"featured": "los espíritus más perturbados", "featured": "los espíritus más perturbados",
"featuredEmpty": "el registro aún está en blanco — sé quien lo perturbe primero", "featuredEmpty": "el registro aún está en blanco — sé quien lo perturbe primero",
"armory": { "reliquary": {
"title": "HARDWARE DEL ARSENAL", "title": "HARDWARE DEL RELICARIO",
"line": "la Ultimate Quantum Box se acerca — un instrumento de sesión de mano que hace que el velo golpee más fuerte.", "line": "la Ultimate Quantum Box se acerca — un instrumento de sesión de mano que hace que el velo golpee más fuerte.",
"link": "entrar al arsenal →" "link": "entrar al relicario →"
}, },
"footer": "una instalación de arte de terror interactiva — los espíritus son ficción; la estática es real", "footer": "una instalación de arte de terror interactiva — los espíritus son ficción; la estática es real",
"footerColophon": "autoalojado · sin nube · el velo corre en tu máquina" "footerColophon": "autoalojado · sin nube · el velo corre en tu máquina"
@@ -267,7 +267,7 @@
}, },
"shop": { "shop": {
"back": "← el umbral", "back": "← el umbral",
"title": "EL ARSENAL", "title": "EL RELICARIO",
"subtitle": "instrumentos para quienes quisieran golpear más fuerte", "subtitle": "instrumentos para quienes quisieran golpear más fuerte",
"flagship": { "flagship": {
"tag": "instrumento insignia", "tag": "instrumento insignia",

View File

@@ -437,7 +437,7 @@
font-size: 0.7rem; font-size: 0.7rem;
} }
/* ---------- armory teaser strip ---------- */ /* ---------- reliquary teaser strip ---------- */
.hero-secondary-links { .hero-secondary-links {
display: flex; display: flex;
@@ -445,12 +445,12 @@
gap: 1.6rem; gap: 1.6rem;
} }
.landing-section.armory-strip { .landing-section.reliquary-strip {
padding-top: 2.5rem; padding-top: 2.5rem;
padding-bottom: 2.5rem; padding-bottom: 2.5rem;
} }
.armory-strip-inner { .reliquary-strip-inner {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
@@ -462,7 +462,7 @@
text-align: center; text-align: center;
} }
.armory-title { .reliquary-title {
font-family: 'Cinzel', serif; font-family: 'Cinzel', serif;
font-size: 0.85rem; font-size: 0.85rem;
font-weight: 700; font-weight: 700;
@@ -472,13 +472,13 @@
margin: 0; margin: 0;
} }
.armory-line { .reliquary-line {
font-size: 0.78rem; font-size: 0.78rem;
color: rgba(216, 230, 221, 0.55); color: rgba(216, 230, 221, 0.55);
margin: 0; margin: 0;
} }
.armory-link { .reliquary-link {
color: #b26bff; color: #b26bff;
font-size: 0.78rem; font-size: 0.78rem;
letter-spacing: 0.14em; letter-spacing: 0.14em;
@@ -489,7 +489,7 @@
transition: color 0.2s ease, border-color 0.2s ease, text-shadow 0.2s ease; transition: color 0.2s ease, border-color 0.2s ease, text-shadow 0.2s ease;
} }
.armory-link:hover { .reliquary-link:hover {
color: #d3a8ff; color: #d3a8ff;
border-color: #d3a8ff; border-color: #d3a8ff;
text-shadow: 0 0 14px rgba(178, 107, 255, 0.6); text-shadow: 0 0 14px rgba(178, 107, 255, 0.6);

View File

@@ -283,7 +283,7 @@ export function LandingPage() {
{t('landing.ctaCodex')} {t('landing.ctaCodex')}
</Link> </Link>
<Link className="cta-secondary" to="/shop"> <Link className="cta-secondary" to="/shop">
{t('landing.ctaShop', { defaultValue: 'visit the armory' })} {t('landing.ctaShop', { defaultValue: 'visit the reliquary' })}
</Link> </Link>
</div> </div>
</div> </div>
@@ -343,19 +343,19 @@ export function LandingPage() {
</section> </section>
)} )}
<section className="landing-section armory-strip"> <section className="landing-section reliquary-strip">
<div className="armory-strip-inner"> <div className="reliquary-strip-inner">
<h2 className="armory-title"> <h2 className="reliquary-title">
{t('landing.armory.title', { defaultValue: 'HARDWARE FROM THE ARMORY' })} {t('landing.reliquary.title', { defaultValue: 'HARDWARE FROM THE RELIQUARY' })}
</h2> </h2>
<p className="armory-line"> <p className="reliquary-line">
{t('landing.armory.line', { {t('landing.reliquary.line', {
defaultValue: defaultValue:
'the Ultimate Quantum Box is coming — a handheld séance instrument that makes the veil knock louder.', 'the Ultimate Quantum Box is coming — a handheld séance instrument that makes the veil knock louder.',
})} })}
</p> </p>
<Link className="armory-link" to="/shop"> <Link className="reliquary-link" to="/shop">
{t('landing.armory.link', { defaultValue: 'enter the armory →' })} {t('landing.reliquary.link', { defaultValue: 'enter the reliquary →' })}
</Link> </Link>
</div> </div>
</section> </section>

View File

@@ -1,4 +1,4 @@
/* The Armory — gothic storefront. Tokens: bg #07070d, panel rgba(16,16,26,0.85), /* The Reliquary — gothic storefront. Tokens: bg #07070d, panel rgba(16,16,26,0.85),
phosphor #7cffb2, violet #b26bff, blood #ff3b5c, borders rgba(124,255,178,0.18). */ phosphor #7cffb2, violet #b26bff, blood #ff3b5c, borders rgba(124,255,178,0.18). */
.shop-page { .shop-page {

View File

@@ -1,4 +1,4 @@
// The Armory — storefront for Quantumancy hardware. Flagship 'Ultimate // The Reliquary — storefront for Quantumancy hardware. Flagship 'Ultimate
// Quantum Box' hero, a grid of knock-louder modules, and the reservation // Quantum Box' hero, a grid of knock-louder modules, and the reservation
// manifest (POST /api/shop/waitlist — public, no payment taken). // manifest (POST /api/shop/waitlist — public, no payment taken).
// Honest by design: the site works fully without the hardware. // Honest by design: the site works fully without the hardware.
@@ -197,7 +197,7 @@ export function ShopPage() {
<Link to="/" className="shop-back"> <Link to="/" className="shop-back">
{t('shop.back', { defaultValue: '← the threshold' })} {t('shop.back', { defaultValue: '← the threshold' })}
</Link> </Link>
<h1 className="shop-title">{t('shop.title', { defaultValue: 'THE ARMORY' })}</h1> <h1 className="shop-title">{t('shop.title', { defaultValue: 'THE RELIQUARY' })}</h1>
<p className="shop-subtitle"> <p className="shop-subtitle">
{t('shop.subtitle', { defaultValue: 'instruments for those who would knock louder' })} {t('shop.subtitle', { defaultValue: 'instruments for those who would knock louder' })}
</p> </p>