# Crust — AI Agent Instructions

Crust is a premium Shopify Online Store 2.0 theme built on theme blocks (nestable blocks
composed inside sections and inside each other). This file teaches an AI coding assistant the
theme's architecture and conventions so its edits behave like first-party code.

Setup: place this file in your theme's root directory as `CLAUDE.md` (Claude Code) or
`AGENTS.md` (Codex, Cursor, and most other agents — same content, different filename).
Recommended companion: the Shopify AI toolkit (`claude plugin install
shopify-ai-toolkit@claude-plugins-official`) for Shopify docs search and Liquid validation.

Kit version 1.3.0, written for Crust 1.0.x — compare `config/settings_schema.json` >
`theme_info.theme_version`. If the theme is newer than this kit, trust what the code shows
over this file and suggest fetching the current kit from elade.io/ai-kit/crust.

## Workflow safety (read first)

- **Always work on a duplicate.** In Shopify admin: Themes > ... > Duplicate. Never edit the
  published theme's code directly.
- Use the Shopify CLI: `shopify theme list` (find the duplicate's id), `shopify theme pull
  --theme <id>`, `shopify theme dev --theme <id>` (live preview), `shopify theme push
  --theme <id>`.
- **Keep the pulled theme in git.** After the first pull: `git init && git add -A &&
  git commit -m "Crust as pulled"`. Commit before every pull and before every push — a pull
  overwrites local files, and without commits there is no diff to re-apply and no undo.
- **Pull before push.** If anyone edits the theme in the online theme editor while you hold a
  local copy, a push overwrites their JSON changes. Commit your local work, re-pull
  `templates/`, `sections/`, and `config/`, then re-apply your changes from the git diff if
  the pull overwrote them.
- **`shopify theme push` exits 0 even when files fail.** Read the full output for
  `pushed with errors`; never trust the exit code or the last line alone.
- Run `shopify theme check` after every change; fix offenses before moving on.
- **Publish only when the merchant asks, and confirm first.** Never publish on your own
  initiative, and never use `shopify theme push --publish`: it skips the preview. When asked,
  give the duplicate's preview link, say that publishing makes it the live theme, and wait for
  a yes. Then run `shopify theme publish --theme <id>`. Afterwards, tell them the previous
  theme is still in the library and that publishing it again is the rollback.

## Store notes — STORE.md (shared memory across AI tools)

Maintain a `STORE.md` file next to this one. Tool-specific memory (Claude Code memory, etc.)
is per-user and invisible to other assistants; this file is the shared, portable memory.
**Read it at the start of every session; append to it whenever the merchant decides
something.** It records:

- **Store profile**: primary storefront language, other selling languages, brand name, niche.
- **Language policy**: what language visitor-facing strings are written in (the store's
  primary language) and what language editor labels use — ask the merchant once, record here,
  never re-ask.
- **Brand voice**: tone adjectives, banned words, key selling points (fed by interviews).
- **Customizations log**: every new `custom-*` file, every edit to a shipped file, and every
  key added to `locales/*.json` (file + reason + summary). After a theme update reports
  "code edits could not be included", this log is the reapply checklist.

If STORE.md doesn't exist yet, create it with exactly this skeleton (every tool that touches
this store fills in the same structure):

```markdown
# STORE.md — shared AI memory for this store

## Store profile

## Language policy

## Brand voice

## Customizations log
<!-- one line per entry: YYYY-MM-DD — file — reason — summary -->
```

## File map

```
assets/     CSS + JS (flat; no subfolders allowed on Shopify)
blocks/     theme blocks; _-prefixed = private (composed by sections, hidden from pickers)
config/     settings_schema.json (theme settings), settings_data.json (current values)
layout/     theme.liquid, password.liquid
locales/    6 languages; *.json = storefront strings, *.schema.json = editor labels
sections/   sections; main-* = template main content
snippets/   shared partials ({% render %} targets)
templates/  JSON templates wiring sections per page type
```

## Architecture

- **Sections own structure; the block tree owns content.** Section headers and content come
  from nested theme blocks (see `_section-content`), not from section settings. Container
  blocks (slides, panels, cards) accept child blocks via `{% content_for 'blocks' %}`.
- **Every theme block** has `"tag": null` in its schema and `{{ block.shopify_attributes }}`
  on its root element. Removing either breaks the theme editor (block overlay / layout).
- A block needs at least one `preset` in its schema to appear in the editor's add-block
  picker. The `blocks: [...]` whitelist in a section schema governs the picker only — a
  `{% content_for 'block', type: '...' %}` static block renders without being whitelisted.
- **Static blocks**: `"static": true` in the schema preset AND in every JSON template
  instance; static block ids must NOT appear in `block_order`. Omitting the JSON flag
  produces misleading cascade errors.
- The top-level section root carries `class="section"` alongside its BEM class — dropping it
  removes the section from the scroll-reveal continuum.
- Copy an existing section/block of the same shape before writing a new one from scratch;
  match its idioms (schema layout, class naming, snippet usage).

## The composition system (read before building anything new)

Most "sections" a merchant sees in the editor are NOT separate section files — they are presets
of **`sections/section.liquid`**, a generic canvas that provides color scheme, background media,
width, flex layout, paddings, and a reveal-animation wrapper, then renders
`{% content_for 'blocks' %}`. Before writing any new code, check whether the design is just a
new arrangement of existing blocks inside this canvas — that is the intended way to build
custom layouts, it needs zero code, and it survives theme updates by definition.

Layout primitives (container blocks; all accept nested `@theme` blocks):

- `group` — flex container (direction, gap, alignment, width fit/fill/custom).
- `styled-group` — group plus its own surface: color scheme, background media, border,
  corner radius, shadow.
- `proportional-group` + `_proportional-part` — ratio-based columns (e.g. 5:2).
- `horizontal-content` + `_column` — responsive column grid driven by `min_column_width`.
- `_collage` — featured-left/right media grids of `media-card` blocks.
- `_parallax-pane` — full-bleed media pane with parallax and overlay.

Content primitives: `text`, `rich-text`, `accent-text` (accented `<em>` treatments), `eyebrow`
(kicker with optional flanking lines), `button`, `separator`, `image`, `illustration` (one mark in
three media: `ink` masks an SVG so `currentColor` drives the fill, `watercolor` and `custom`
are rasters that carry their own color), `stamp` (short decorative text), `media-card`, `popup` (static `_popup-trigger` + `_popup-dialog` children),
`email-signup`, `countdown`, `marquee`. Study a few presets in `sections/section.liquid` to see
how deeply these nest — that is the house style for composition.

Recurring settings idioms (reuse them in any new schema so the editor feels consistent):

- Layout fields: `direction`, `gap`, `align_horizontal_*` / `align_vertical_*`, `wrap_elements`.
- Mobile mirrors gated by a checkbox: `different_layout_on_mobile`,
  `different_paddings_on_mobile`, `different_font_size_on_mobile` + `mobile_*` twins.
- Width: `fit` / `fill` / `page` / `custom` (+ `max_width`, `max_width_mobile`).
- Color: `inherit_color_scheme` checkbox + `color_scheme` picker on blocks that own a surface.
- Text: `preset` (typography preset), `font` (`heading`/`body`), `emphasis` (`normal`/`muted`/`faded`/`faint`),
  `max_width` (`narrow`/`medium`/`custom`).

### How settings become styles (the style-snippet pattern)

Sections and blocks never build style strings ad hoc. They `{% capture %}` the output of shared
`snippets/*-style.liquid` helpers into a `style=""` attribute:

- `layout-style` (flex props), `padding-style`, `margin-style`, `border-style`, `height-style`,
  `spacing-style`, `typography-style`, `text-styles`, `background-overlay-style`,
  `overlap-choreography-style`, `block-height-properties`.
- These emit **CSS custom properties, and only when the value differs from the default** —
  keeping inline styles minimal and letting the stylesheet's defaults win elsewhere.
- Mobile variants emit `--mobile-*` twins, activated by a class on the same element
  (`layout-mobile`, `paddings-mobile`, `margins-mobile`). Follow the same "emit only when it
  differs from the resolved desktop value" rule.
- Class lists come from `*-class` fragment snippets (`button-class`, `color-scheme-class`,
  `visibility-class`, `dark-header-class`) rendered inline inside `class="..."` — each returns
  its classes with a leading space.
- Every snippet has a `{% doc %}` header documenting its parameters — **read it before
  calling**; parameter objects are usually `section.settings` or `block.settings` passed whole.

A new block that wants configurable layout/spacing should reuse these snippets and setting IDs
verbatim rather than inventing its own `style` plumbing.

### Decorative ornament fields

Five sections carry a hairline contour field behind their content: `faq`, `vertical-tabs`,
`image-compare`, `staggered-content`, `decorated-statement`. Each renders its own
`snippets/ornament-field-<section>.liquid`, and all five are the same shape — one
`aria-hidden` div, `class="ornament-field <section>__ornament"`, a `--ornament-field-weight`
custom property, and `data-ornament-desktop` pointing at `assets/ornament-<section>.svg`,
the whole thing wrapped in `{% if settings.enable_ornaments %}`.

`assets/lazy-ornament.js` fetches that SVG and inlines it above 950px; below 950px the field
is a pure-CSS grain and nothing is fetched. It is inlined rather than referenced from
`mask-image` because an SVG used as an image renders in its own viewport and drops
`vector-effect: non-scaling-stroke` — the hairlines would thicken with the section.

A new section that wants one adds `assets/ornament-<name>.svg` plus a matching
`snippets/ornament-field-<name>.liquid` copied from any of the five, and renders it. Never
hand-roll a decorative layer, and never render one unconditionally: `settings.enable_ornaments`
is the merchant's single off switch for all of it.

Decoration that is content rather than texture goes through blocks instead — `illustration`
and `stamp` are addable marks. `decorated-statement` shows the placement idiom: a private
`_decor-field` container whose `_decor-image` children take their anchor slots from list
order via `:nth-child`, so no child owns a position setting.

## Typography

Ten typography presets are the public type API: `display`, `display-sm`, `h1`, `h2`, `h3`,
`lg`, `md`, `base`, `sm`, `xs` — each merchant-tunable in theme settings (size, weight,
line height, letter spacing, transform, mobile size). Text-bearing blocks expose them through
their `preset` setting plus `font` (`heading` / `body`); `preset: custom` unlocks per-block
overrides via `typography-style`. In CSS, consume the emitted `--font-size-*` tokens. Never
invent an eleventh preset, hardcode a px font size, or re-style a preset class inside a
section's CSS.

## Custom sections and blocks (update-safe customization)

Shopify theme updates carry over theme-editor customizations (settings, templates, section
JSON) automatically, and **non-conflicting code edits are included in the updated theme**. A
brand-new file cannot conflict with anything an update changes — so custom work belongs in NEW
files, never in edits to shipped ones:

1. **Prefer zero code**: compose the design as blocks inside a `Section` (section.liquid)
   instance in the editor or a JSON template. Survives updates, merchant-editable.
2. **Need real code?** Create `sections/custom-<name>.liquid` (copy the closest shipped
   section as a starting point — `sections/section.liquid` for a generic canvas) and
   `assets/custom-<name>.css`. Same for blocks: `blocks/custom-<name>.liquid`.
3. A custom section/block MUST include at least one `preset` in its schema or it never appears
   in the editor's picker. A custom block with a preset automatically becomes addable inside
   every container that accepts `@theme` blocks.
4. **Visitor-facing text in custom code — the ladder:**
   1. Expose it as a `text`/`richtext` setting with a default. The merchant's entered value
      becomes store data (template JSON) — update-safe, merchant-editable, and translatable
      with Shopify's Translate & Adapt (which translates entered VALUES; setting labels are
      editor-only schema and are not its concern).
   2. For fixed microcopy (aria labels, generic UI verbs): reuse an existing theme key —
      grep `locales/en.default.json` for the string and use `{{ 'that.key' | t }}`.
   3. Otherwise hardcode it in the store's primary language (see STORE.md).
   4. Adding NEW keys to `locales/*.json` is the last resort: theme updates rewrite locale
      files, and merchant-added keys can be dropped in a conflict. If you must, log every
      added key in STORE.md's customizations log so it can be restored.
5. If you must edit a shipped file, keep the diff minimal and record it (file + reason +
   summary) in STORE.md's customizations log.

## Schema (the `{% schema %}` JSON in sections and blocks)

- Edit the JSON directly, then validate with `shopify theme check` and a test push.
- `visible_if` on a block setting can reference `block.settings.*` and `section.settings.*`,
  never a parent block's settings. `visible_if` is rejected on list-type settings.
- `enabled_on` is valid on sections only — Shopify rejects it on theme block schemas.
- `range`: every stored value must sit exactly on a step, and `(max - min) / step + 1` must
  be <= 101, or the push silently keeps the old file deployed.
- **Never rename a shipped setting `id`** — saved merchant values are keyed by it. Change the
  `label` instead.
- `info:` text is for facts a merchant cannot guess (a format, a constraint, an admin
  pointer). If it restates the label or narrates the preview, delete it.

## Liquid

- `{% render %}`, never `{% include %}`.
- The theme's own strings go through `{{ 'key' | t }}`; a key must exist in ALL six locale
  files (`locales/*.json` storefront, `locales/*.schema.json` editor labels) or it renders as
  the raw key. For NEW visitor-facing text in custom code, do NOT default to adding keys —
  follow the ladder in "Custom sections and blocks" (settings first; new keys last resort).
- Plain `text` / `*_label` / `heading` settings rendered with `{{ }}` need `| escape`.
  Raw output is only for `richtext` / `inline_richtext`. Never `| escape` an `image_tag`
  result or a value passed as a filter argument.
- **Never pipe a filter inside `image_tag` arguments** — it silently swallows every following
  argument (`class`, `width`, `loading` vanish; no error anywhere). Assign first
  (`{% assign alt = image.alt | default: '' %}`), then pass the bare variable. The same trap
  applies to filters piped inside `{% render %}` arguments.
- List-type settings (`product_list`, `collection_list`) are drops: `drop[i]` is silently
  nil. Iterate with `{% for %}`, or apply `| compact` before indexing.
- Iterating `section.blocks` exposes only `block.id` and `section.blocks.size` for theme
  blocks — `block.type` and `block.settings.x` come back blank.
- `{% javascript %}` and `{% stylesheet %}` must sit at a section's top level, never inside
  `{% if %}` or `{% for %}`.
- `inline_asset_content` silently drops files over 15 KB.

## Templates and JSON files

- `rich_text` values in JSON templates allow only `<p>`, `<ul>`, `<ol>`, `<h1>`-`<h6>` as
  top-level nodes (inline tags belong inside `<p>`). `inline_richtext` allows no `<br>`.
- A template or section-group JSON top-level `name` must be 50 characters or fewer — longer
  bounces the push for that file while everything else deploys.
- Every section inside a `*-group.json` needs a `block_order`, or its blocks silently vanish.

## CSS

- **Consume the tokens, never redeclare them.** Colors and type come from custom properties
  (`--color-*`, `--font-size-*`) generated from theme settings. Hardcoding a hex or a px font
  size in a section stylesheet breaks merchant control.
- Font weights are 400 / 500 / 700 only — never 600.
- Color schemes carry 11 roles (background, text, accent, accent_secondary, separator,
  overlay_text, overlay_background, and the four button roles). Apply schemes via
  `{% render 'color-scheme-class' %}`; keep the scheme class on a visible painted surface.
- BEM naming, low specificity. **Never style a theme block's selectors from a section's CSS
  file** — context styling belongs on the block itself via a modifier class, a setting, or an
  exposed custom property.
- Custom-property defaults go on the consuming class (`.card { --card-gap: 1rem; }`), not
  into the `var()` fallback — an inherited value silently beats the fallback.
- Per-section styles live in `assets/<section>.css`, loaded with `{% render 'load-css' %}`
  (deferred) or `stylesheet_tag`. Never hand-roll a `media="print"` deferred link — it breaks
  theme-editor re-renders. `critical.css`, `deferred.css`, and `product.css` are global
  bundles; put new styles in per-section files instead of appending there.
- Use `overflow: clip`, not `overflow: hidden`, on ancestors of scroll-driven animations —
  `hidden` freezes them.
- Buttons: the label must be wrapped in `<span class="btn__label">`, or the text paints under
  the radial hover fill.
- Design language: no upward `translateY` on `:hover` (scale / zoom / arrow-nudge are fine);
  equal-height cards in a row need `height: 100%` on the card, not just parent stretch.
- Images and media always reserve space (`width`/`height` attributes or `aspect-ratio`),
  lazy-load below the fold, and use `alt=""` when decorative.
- Solutions must hold for any merchant content — long titles, many blocks, RTL locales,
  short viewports. Never "fix" layout by constraining content length.

## JavaScript

- Modern syntax only: `const`/`let`, arrow class fields, `async/await`, `for...of`, early
  returns. No `var`, no `console.log`, no `static` class members (iOS Safari < 14.1).
- Prefer native HTML primitives (`<details>`, `<dialog>`, `[popover]`) and CSS over custom
  JS. Per-section behavior ships as a `<script type="module">` loaded by that section.
- Shared utilities live on window globals loaded in `theme.liquid`'s head — reuse them, do
  not reimplement: `EladeStandardEvents` (Shopify standard storefront events — dispatch cart
  updates through it so apps and other listeners stay in sync), `EladeScrollLock` (body
  scroll locking), `EladeButtonHover`, `EladeCapabilities` (device/perf tier),
  `EladeQuickView`, `EladeCart`.
- After inserting DOM that contains a `.btn`, call `window.EladeButtonHover.refresh(scope)`
  or the radial hover fill renders from center instead of the cursor.
- No layout reads (`getBoundingClientRect`, `offset*`, `getComputedStyle`) in a custom
  element's `connectedCallback` or at module top level — use `IntersectionObserver`.
- Performance budget: LCP <= 2.5 s, INP <= 200 ms, CLS <= 0.05. Think twice before adding
  scroll/resize listeners or `requestAnimationFrame` loops; prefer observers and CSS.

## Animation

- Reveal and parallax are theme-level toggles (`settings.enable_reveal_animation`,
  `settings.enable_parallax`) — never add per-section animation switches, and gate any new
  motion behind the theme toggles plus `prefers-reduced-motion`.
