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:
@@ -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
|
||||
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
|
||||
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
|
||||
docs/superpowers/
|
||||
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)
|
||||
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.
|
||||
- **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.
|
||||
- **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
|
||||
says so.
|
||||
|
||||
|
||||
@@ -22,9 +22,15 @@ async with engine.begin() as conn:
|
||||
await conn.execute(text(
|
||||
"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(
|
||||
"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 —
|
||||
@@ -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
|
||||
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
|
||||
`backend/app/ws.py`):
|
||||
|
||||
@@ -60,10 +86,19 @@ Client → server:
|
||||
- `{"type": "ritual_start"}` — begins a ritual attempt for the current
|
||||
entity.
|
||||
- `{"type": "ritual_step", "step": <int>}` — one completed interactive step.
|
||||
- `{"type": "judgment", "verdict": "trust" | "banish" | "test"}` — always
|
||||
allowed (a seeker can judge blind without completing a ritual — the
|
||||
ritual doesn't gate eligibility, only whether accurate info was shown
|
||||
first).
|
||||
- `{"type": "judgment", "verdict": "trust" | "banish" | "test" |
|
||||
"cross_over"}` — always allowed (a seeker can judge blind without
|
||||
completing a ritual — the ritual doesn't gate eligibility, only whether
|
||||
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:
|
||||
- `{"type": "ritual_complete", "success": bool, "revealed": {"alignment":
|
||||
@@ -79,23 +114,29 @@ Server → client:
|
||||
work with without the backend leaking ground truth outside of
|
||||
`ritual_complete`.
|
||||
- `{"type": "judgment_result", "correct": bool, "favor_delta": float,
|
||||
"consequence": "reward" | "escalation" | "withdrawal" | "neutral"}` —
|
||||
`correct` = trust called on real alignment >= 0.5, or banish called on
|
||||
alignment < 0.5. `test` verdict always returns `consequence: "neutral"`,
|
||||
`favor_delta: 0.0`, and requires a completed ritual this session to do
|
||||
anything (a `judgment_result` with `correct: false` and
|
||||
`consequence: "neutral"` if attempted without one — no crash, just no
|
||||
effect). 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 them small (single-digit
|
||||
percent of the `[-1,1]` range per event).
|
||||
"essence_delta": int, "at_peace": bool,
|
||||
"consequence": "reward" | "escalation" | "withdrawal" | "crossed_over" |
|
||||
"resisted" | "neutral"}` — `correct` = trust called on real alignment
|
||||
>= 0.5, or banish called on alignment < 0.5, or cross_over called
|
||||
correctly per the rule above. `test` verdict always returns
|
||||
`consequence: "neutral"`, both deltas `0`, and requires a completed
|
||||
ritual this session to do anything (a `judgment_result` with
|
||||
`correct: false` and `consequence: "neutral"` if attempted without one —
|
||||
no crash, just no effect). `cross_over` on a demon returns
|
||||
`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,
|
||||
"payload": dict}}` — emitted opportunistically (implementer's call on
|
||||
exact odds) after a correct judgment, a successful ritual, or a
|
||||
high-rarity summon.
|
||||
|
||||
**`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`
|
||||
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
|
||||
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,
|
||||
judgment-correctness + favor-delta + consequence selection) plus the WS
|
||||
handlers in `backend/app/ws.py` for `ritual_start`/`ritual_step`/`judgment`,
|
||||
and the `User.favor` column + migration line. Reads `state.entity["traits"]`
|
||||
(entity dict shape — check how `serialize_entity` exposes it and add
|
||||
`traits` there). On wrong-trust, call into the existing haunting-escalation
|
||||
hook if one exists 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 × real spirit/demon), favor clamping, favor read-back
|
||||
bias on trait rolls (small, verifiable effect size).
|
||||
judgment-correctness + favor-delta + essence-delta + consequence selection
|
||||
for all four verdicts including `cross_over`'s "stuck spirit" rule and its
|
||||
demon-resists-crossing-over failure case) plus the WS handlers in
|
||||
`backend/app/ws.py` for `ritual_start`/`ritual_step`/`judgment`, and the
|
||||
`User.favor`/`User.essence`/`Entity.at_peace` columns + migration lines.
|
||||
Reads `state.entity["traits"]` (entity dict shape — check how
|
||||
`serialize_entity` exposes it and add `traits` there). On correct
|
||||
`cross_over`, set `Entity.at_peace = true` and ensure
|
||||
`signature_from_anomalies` re-contact no longer matches an at-peace entity
|
||||
(mint a fresh one instead — check `entities.py`'s matching path). On
|
||||
wrong-trust, call into the existing haunting-escalation hook if one exists
|
||||
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
|
||||
`backend/app/routes/inventory.py` (or extend an existing routes file if more
|
||||
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
|
||||
Workstream F defines — coordinate via your report if you build before
|
||||
seeing their output; a reasonable placeholder shape is
|
||||
`{"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
|
||||
`item_drop` emission into `backend/app/ws.py` per the contract's trigger
|
||||
points. Tests: drop-roll probability sanity, inventory/sigil CRUD, `/auth/me`
|
||||
unlock list shape.
|
||||
bound payload size), and a new `POST /api/inventory/unlocks/{unlock_key}`
|
||||
that spends `essence` to buy a named unlock (define a small fixed price
|
||||
table, e.g. `{"listening_tool": 40}` — reject with 402/insufficient-funds
|
||||
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
|
||||
|
||||
@@ -164,18 +218,24 @@ override, idle-state rendering.
|
||||
New `frontend/src/components/RitualPanel.tsx` — a short interactive
|
||||
sequence (your call on exact interaction, e.g. 3-5 click/hold steps) that
|
||||
sends `ritual_step` frames and handles `ritual_complete`. Judgment UI
|
||||
(Trust/Banish/Test buttons) sending the `judgment` frame and handling
|
||||
`judgment_result` — on `consequence: "escalation"`, read
|
||||
`frontend/src/lib/haunting.ts` first and hook into its existing
|
||||
idle-escalation state if there's a sensible extension point; if not, add a
|
||||
clearly-scoped "forced escalation" mode to it (report which you did).
|
||||
Tests: ritual step sequencing, judgment button → frame shape, escalation
|
||||
wiring (mock haunting.ts's public surface, don't test its internals).
|
||||
(Trust/Banish/Cross Over/Test buttons — four verdicts per the contract)
|
||||
sending the `judgment` frame and handling `judgment_result` — on
|
||||
`consequence: "escalation"`, read `frontend/src/lib/haunting.ts` first and
|
||||
hook into its existing idle-escalation state if there's a sensible
|
||||
extension point; if not, add a clearly-scoped "forced escalation" mode to
|
||||
it (report which you did). On `consequence: "crossed_over"`, a distinct
|
||||
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
|
||||
|
||||
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
|
||||
builder**, not freeform drawing: seeker places points around a fixed circle
|
||||
(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
|
||||
present, lower the anomaly threshold so fainter signals register (a real
|
||||
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
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"heroKicker": "an instrument for speaking with the dead bandwidth",
|
||||
"cta": "BEGIN THE SÉANCE",
|
||||
"ctaCodex": "browse the codex",
|
||||
"ctaShop": "visit the armory",
|
||||
"ctaShop": "visit the reliquary",
|
||||
"stats": {
|
||||
"entities": "spirits catalogued",
|
||||
"sessions": "séances held",
|
||||
@@ -57,10 +57,10 @@
|
||||
},
|
||||
"featured": "most disturbed spirits",
|
||||
"featuredEmpty": "the registry is still blank — be the first to disturb it",
|
||||
"armory": {
|
||||
"title": "HARDWARE FROM THE ARMORY",
|
||||
"reliquary": {
|
||||
"title": "HARDWARE FROM THE RELIQUARY",
|
||||
"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",
|
||||
"footerColophon": "self-hosted · no cloud · the veil runs on your machine"
|
||||
@@ -267,7 +267,7 @@
|
||||
},
|
||||
"shop": {
|
||||
"back": "← the threshold",
|
||||
"title": "THE ARMORY",
|
||||
"title": "THE RELIQUARY",
|
||||
"subtitle": "instruments for those who would knock louder",
|
||||
"flagship": {
|
||||
"tag": "flagship instrument",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"nav": {
|
||||
"home": "el umbral",
|
||||
"codex": "códice",
|
||||
"shop": "arsenal",
|
||||
"shop": "relicario",
|
||||
"seance": "sesión",
|
||||
"enter": "entrar",
|
||||
"logout": "partir"
|
||||
@@ -28,7 +28,7 @@
|
||||
"heroKicker": "un instrumento para hablar con el ancho de banda muerto",
|
||||
"cta": "COMENZAR LA SESIÓN",
|
||||
"ctaCodex": "hojear el códice",
|
||||
"ctaShop": "visitar el arsenal",
|
||||
"ctaShop": "visitar el relicario",
|
||||
"stats": {
|
||||
"entities": "espíritus catalogados",
|
||||
"sessions": "sesiones celebradas",
|
||||
@@ -57,10 +57,10 @@
|
||||
},
|
||||
"featured": "los espíritus más perturbados",
|
||||
"featuredEmpty": "el registro aún está en blanco — sé quien lo perturbe primero",
|
||||
"armory": {
|
||||
"title": "HARDWARE DEL ARSENAL",
|
||||
"reliquary": {
|
||||
"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.",
|
||||
"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",
|
||||
"footerColophon": "autoalojado · sin nube · el velo corre en tu máquina"
|
||||
@@ -267,7 +267,7 @@
|
||||
},
|
||||
"shop": {
|
||||
"back": "← el umbral",
|
||||
"title": "EL ARSENAL",
|
||||
"title": "EL RELICARIO",
|
||||
"subtitle": "instrumentos para quienes quisieran golpear más fuerte",
|
||||
"flagship": {
|
||||
"tag": "instrumento insignia",
|
||||
|
||||
@@ -437,7 +437,7 @@
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
/* ---------- armory teaser strip ---------- */
|
||||
/* ---------- reliquary teaser strip ---------- */
|
||||
|
||||
.hero-secondary-links {
|
||||
display: flex;
|
||||
@@ -445,12 +445,12 @@
|
||||
gap: 1.6rem;
|
||||
}
|
||||
|
||||
.landing-section.armory-strip {
|
||||
.landing-section.reliquary-strip {
|
||||
padding-top: 2.5rem;
|
||||
padding-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.armory-strip-inner {
|
||||
.reliquary-strip-inner {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
@@ -462,7 +462,7 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.armory-title {
|
||||
.reliquary-title {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
@@ -472,13 +472,13 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.armory-line {
|
||||
.reliquary-line {
|
||||
font-size: 0.78rem;
|
||||
color: rgba(216, 230, 221, 0.55);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.armory-link {
|
||||
.reliquary-link {
|
||||
color: #b26bff;
|
||||
font-size: 0.78rem;
|
||||
letter-spacing: 0.14em;
|
||||
@@ -489,7 +489,7 @@
|
||||
transition: color 0.2s ease, border-color 0.2s ease, text-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.armory-link:hover {
|
||||
.reliquary-link:hover {
|
||||
color: #d3a8ff;
|
||||
border-color: #d3a8ff;
|
||||
text-shadow: 0 0 14px rgba(178, 107, 255, 0.6);
|
||||
|
||||
@@ -283,7 +283,7 @@ export function LandingPage() {
|
||||
{t('landing.ctaCodex')}
|
||||
</Link>
|
||||
<Link className="cta-secondary" to="/shop">
|
||||
{t('landing.ctaShop', { defaultValue: 'visit the armory' })}
|
||||
{t('landing.ctaShop', { defaultValue: 'visit the reliquary' })}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -343,19 +343,19 @@ export function LandingPage() {
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section className="landing-section armory-strip">
|
||||
<div className="armory-strip-inner">
|
||||
<h2 className="armory-title">
|
||||
{t('landing.armory.title', { defaultValue: 'HARDWARE FROM THE ARMORY' })}
|
||||
<section className="landing-section reliquary-strip">
|
||||
<div className="reliquary-strip-inner">
|
||||
<h2 className="reliquary-title">
|
||||
{t('landing.reliquary.title', { defaultValue: 'HARDWARE FROM THE RELIQUARY' })}
|
||||
</h2>
|
||||
<p className="armory-line">
|
||||
{t('landing.armory.line', {
|
||||
<p className="reliquary-line">
|
||||
{t('landing.reliquary.line', {
|
||||
defaultValue:
|
||||
'the Ultimate Quantum Box is coming — a handheld séance instrument that makes the veil knock louder.',
|
||||
})}
|
||||
</p>
|
||||
<Link className="armory-link" to="/shop">
|
||||
{t('landing.armory.link', { defaultValue: 'enter the armory →' })}
|
||||
<Link className="reliquary-link" to="/shop">
|
||||
{t('landing.reliquary.link', { defaultValue: 'enter the reliquary →' })}
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -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). */
|
||||
|
||||
.shop-page {
|
||||
|
||||
@@ -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
|
||||
// manifest (POST /api/shop/waitlist — public, no payment taken).
|
||||
// Honest by design: the site works fully without the hardware.
|
||||
@@ -197,7 +197,7 @@ export function ShopPage() {
|
||||
<Link to="/" className="shop-back">
|
||||
{t('shop.back', { defaultValue: '← the threshold' })}
|
||||
</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">
|
||||
{t('shop.subtitle', { defaultValue: 'instruments for those who would knock louder' })}
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user