# Bigscreen VR — Shopify Store Theme

This repo is the **Shopify theme** for store.bigscreenvr.com (a Dawn 5.0 fork,
originally "BeyonDark"). It replaced Builder.io as the store's rendering
layer in July 2026. The marketing site (bigscreenvr.com) is a separate repo
(`bigscreenvr-site`); this repo follows its design system.

## Design system (auto-loaded, binding)

The full brand + visual system is imported below — same files as the site
repo. **Where the site docs and this file disagree, THIS FILE WINS**: the
store inverts several site rules (light surfaces instead of the black stage,
Roman-only weights, contained store rhythm except the configurator split).
The "Design rules" section further down holds those store deltas.

@PRODUCT.md
@DESIGN.md

## Golden safety rule

**The GitHub-connected theme (`bigscreenvr-store-theme/main`) IS the published
live theme** — since the Aug 2026 cutover, every push to `main` deploys to
customers within minutes. Treat a push to `main` like a production deploy:
verify locally first (`shopify theme dev`), and sequence data/theme changes so
no intermediate state ships. `shopify theme publish` and `shopify theme push
--allow-live` remain forbidden unless Max explicitly asks in that moment.
`theme dev`, `theme share`, and work on other branches stay invisible to
customers.

**So work on `dev`** (added 2026-08-26), which has its own GitHub-connected but
*unpublished* Shopify theme — Shopify's integration binds one theme to one
branch, so a second theme tracking `dev` gives a real preview URL. Committing
and pushing `dev` are safe; reaching customers is a deliberate `dev` -> `main`
merge Max approves.

Two consequences that are easy to forget:

- **The customizer writes back to git.** Editing theme settings in the admin
  makes Shopify commit to that theme's branch, so `main` drifts on its own and
  `dev` needs `main` merged into it regularly. (It already has: `main` gained
  six commits during one session on `dev`.)
- **Translate & Adapt theme content does NOT survive a theme swap**, and is
  scoped to one theme. JP entered against the *dev* theme's JSON-template
  settings does not appear on the live theme and is lost when a different theme
  is published. Locale files and metaobjects carry across; JSON-template
  settings do not. One more reason translatable copy belongs in
  `locales/*.json` or a metaobject, never in a section setting.

## Workflow

- Source of truth: this repo. Never edit in Shopify's online code editor.
- Dev loop: `shopify theme dev --store bigscreenvr.myshopify.com` → live
  preview at localhost:9292 (auth expires after a few hours; restart on
  "access token expired").
- Coworker previews: `shopify theme share` (mints an unpublished theme +
  link). The GitHub-connected theme `bigscreenvr-store-theme/main` is NOT a
  preview — it is the live theme (see the golden safety rule).
- Remote: https://github.com/BigscreenVR/bigscreenvr-store-theme — work lands
  on `main` (unlike the site repo's dev-branch protocol), and pushing it
  deploys live; use a branch for anything not ready to ship.
- If anyone edits theme settings in the admin customizer, `shopify theme
  pull` the change back (until the GitHub connection handles it both ways).

## Store data via the Admin API

Metaobjects, metafields, product properties, and ja/en translations are
managed from this repo over the Admin GraphQL API — full instructions,
canned queries, and the binding write protocol (mutations need human confirm +
`--write`) live in **`SHOPIFY-API.md`**; vocabulary in **`CONTEXT.md`**.
Runner: `node scripts/shopify-api.mjs`; one-time setup:
`bash scripts/setup-shopify-app.sh`.

## Architecture

- **Owned sections (the rebuild):** `bs-product-configurator.liquid` (Beyond 2
  configurator + render stage), `bs-product-story.liquid` (below-fold bands +
  spec table), `bs-item-grid.liquid` (storefront grid). Styles:
  `assets/bs-chrome.css` (tokens/nav/marquee/footer — loads on every page),
  `assets/bs-product.css`, `assets/bs-store.css`.
- **Data lives in Shopify, not code:** the configurator reads per-variant
  metafields (`store.subtitle`, `store.ship_date/ship_msg`,
  `store.addon_groups` metaobjects with checked/disabled/**ephemeral** variant
  lists), cart rules (`cart.qty_limit`, `cart.qty_count_products`), specs
  (`store.specs` JSON), and localized strings (`shop.metaobjects.product_misc`
  / `header` / `footer` / `cart`). Ephemeral = selectable but never added to
  cart or priced (e.g. the included Soft Strap). `price_footnote` is a
  per-currency JSON map.
- **Never hardcode customer-facing copy in a section** — the store ships
  ~40 locales and JP is fully translated. A string only becomes translatable
  if Translate & Adapt can see it, which means one of: a metafield/metaobject,
  a locale file (`locales/*.json` + `t`), or a section setting **whose value is
  stored in the JSON template**. The trap: a `default:` in `{% schema %}`
  renders fine in English and is invisible to the translator, so the page looks
  done and ships English to every other market. `bs-product-story` shipped that
  way (24 strings), as did the `bs-in-action` / `bs-discord-cta` headings — all
  now mirrored into `product.shop-bs2.json`. If you add a setting with copy in
  it, write the English into the template too. Config-ish settings (`handles`,
  `collection_handle`) are deliberately NOT stored: a translated product handle
  silently breaks the gate.
- **Two translation surfaces, and they don't behave the same.** Metaobject and
  metafield translations attach to the DATA, so they survive a theme publish and
  are visible in Translate & Adapt right now. Theme content (locale files, JSON
  template settings) is scoped to ONE theme: not visible in Translate & Adapt
  until that theme is published, and not carried over when a future theme
  replaces it. Everything the live Builder page translates goes through
  metaobjects — the product template holds no translated copy at all, and the
  Builder sections have no locale handling. The story/FAQ/in-action/Discord copy
  in `product.shop-bs2.json` is therefore the store's first theme-scoped copy.
  Max chose that knowingly (July 2026) for customizer editing; the price is that
  JP for those strings can only be entered after cutover, and a later theme
  swap means redoing them. Prefer the `product_misc` metaobject for any NEW
  product-page copy.
- **Templates share sections:** most products (t-shirts included) are
  assigned to `product.shop-bs2.json`. The configurator degrades for
  non-Beyond products (no kicker/rule/note, own photo, single-variant picker
  hidden) and the story's hardware bands are gated to
  `product.handle == 'bigscreen-beyond-2'`. The no-buy template hides the
  order button via `template.suffix`.
- **Render stage:** `dyn-bs2--{shell}-{halo-}{strap}-{cushion}.webp` matrix
  (24 self-hosted renders). VRChat Edition = purple shell + art backdrop +
  VRC_white.svg mark. Beware: "Nuclear Orange" contains the substring
  "clear" — shell tokens use ordered matching.
- **Builder legacy:** `*-builder-io.liquid` sections, `content.*.builder`
  snippets and `page.*.preview.builder` templates are Builder's dead compiled
  output — no template references them anymore. Keep until the Builder app
  is uninstalled (post-cutover; uninstalling earlier could break the LIVE
  theme, which still runs Builder sections). Raw Builder source is archived
  in `builder-source/`.
- **The nav is a twin.** `sections/header.liquid` mirrors the marketing site's
  `src/site/partials/nav.html` (repo `website`, **dev** branch, locally at
  `C:\Users\Max\Documents\GitHub\website`). Any change to nav structure, labels,
  or dropdown contents MUST be made in both files in the same sitting — there is
  no build step keeping them honest, only this rule. Deliberate differences that
  are NOT drift: store links are locale-aware (`sroot`) with metaobject labels,
  site links are absolute store URLs with `target="_blank"`; the site carries
  the blog badge + mobile note + adaptive nav-color script, the store carries
  `is-active` states + the ticker; the store resolves the 2e variant id in
  Liquid, the site hardcodes it; the site's right-hand action is the violet
  "Order now" button, the **store's is the Cart pill** (icon + label + count, at
  every breakpoint — a store that has a cart should not spend its one chrome
  action sending a shopper back to the product page).
- **Apps to never break:** LensAdvizor (prescriptions — the hidden
  `<product-form>` + `#la-select-lenses-btn` in the configurator section are
  its hooks), Yotpo (reviews/Q&A app sections; only shallow CSS skinning),
  Klaviyo, the eg-cart-drawer. Cart bubble = `#cart-icon-bubble` +
  offcanvas attrs.

## Design rules (hard-won, do not relearn)

- **The store is LIGHT**: white pages, #F4F3F3 frames, #DFDFDF hover, black
  text. Never put store surfaces on the marketing site's black stage.
- **Type: Neue Haas Grotesk Display Pro 55 Roman everywhere — no bold, no
  medium.** Fragment Mono only for labels/prices-in-chrome, ALWAYS uppercase.
  Dawn's font variables are overridden in bs-chrome.css (kills Assistant
  theme-wide); Yotpo's "Neue Haas New" is aliased to the real file.
- **px only, never rem** — legacy CSS sets html font-size 62.5% (10px base),
  so rem values render at 62.5% of intent.
- One secondary grey: **#70777A**. One violet: **#4D1EF7** (CTA/selection
  only). Bevel corner language: `border-radius: 3px 3px Npx 3px` +
  `corner-shape: round round bevel round`.
- Product page register: near-full-width Polestar split (config rail
  `clamp(460px, 46%, 690px)`, slim outer gutters, sticky beveled stage
  locked under the chrome). Do not contain it to the site width — tried and
  reverted (reasoning in bs-product.css).
- **On mobile the config panel scrolls INTO the plate.** One column means the
  plate and the sticky order footer sandwich the options; at full height they
  leave ~130px to read ~5000px of configurator. So the plate's height is
  recomputed every frame as the gap between the chrome and the panel's top
  edge: the panel rising eats the plate 1:1 down to a 120px floor, product
  scaling with it (`--bsp-h` / `--bsp-p`, set by the rAF scroll driver in
  bs-product-configurator.liquid). Rules, all learned the hard way:
  - **Measure the gap, never count scrolled pixels.** The plate's bottom edge
    and the panel's top edge are then the same line by construction, so copy
    can never slide under the plate — which is exactly what a fixed 44vh plate
    did, clipping the product title against its own bottom edge.
  - **Never a class toggle.** A boolean means two sizes, so the change can
    only arrive as a jump, at a point the visitor can't predict. Tried with an
    IntersectionObserver at several trip lines; every version read as jerky and
    random. Continuous is what makes it feel attached to the scroll.
  - **No CSS transition on the plate height** — the value is rewritten per
    frame and a transition makes each frame chase the last one.
  - The mobile `.bsp-split` grid pins the stage ROW, so resizing the plate
    changes no document height (otherwise the page jumps mid-scroll) and the
    panel's measured position stays stable.
  - Everything that isn't the product (rating, model caption, add-on chip)
    fades out over the first ~45% of the travel. The strip is the product
    alone; anything else in a 120px band is too loud for what it's worth.
- **Never edit CSS with regex/scripted sweeps** — a sweep once silently ate
  neighbouring rules three times over. Targeted edits only.
- **Never key behavior on `variant.title` or `data-title`** — titles are
  translated per-locale (JA broke the halo tree, cushion interlock, render
  matrix, and deep links in Aug 2026). Behavior keys on **SKU**: inputs carry
  `data-sku`, the configurator's identity helpers (`isHaloOpt`, `shellToken`,
  …) match SKU first with the old title regex only as an EN fallback. Titles
  are for DISPLAY. New behavior = extend the SKU map in the helpers, and note
  that changing a SKU in the admin silently disarms its behavior.

## Verifying

Use the theme dev preview + browser tools. Known headless-pane artifacts
(fine on real displays): CSS transitions freeze at t=0, rAF never fires,
lazy images don't load, scroll events don't fire. Judge geometry and computed
styles, not animation states. After UI changes, audit computed font families
and weights — Dawn/app CSS reintroduces Assistant/faux-bold easily.
