
Magnificent Monsters Update
An important update regarding Magnificent Monsters Unfortunately, we need to share an update we never expected to have…
Internal reference
Design tokens, components and patterns for this theme. Every value below is read from theme settings, so it always matches the live storefront.
Read live from theme settings. Use the CSS custom property, never a hex value — hardcoding a colour breaks merchant theming.
var(--text-color)
#616161
var(--heading-color)
#16134a
var(--accent-color)
#16134a
var(--link-color)
#c8e9f2
var(--border-color)
#e8e8e8
var(--background)
#f5f5f5
var(--secondary-background)
#ffffff
Headings use var(--heading-font-family), body copy uses
var(--text-font-family). Base size is
16px.
Heading 1 — Figtree
Heading 2
Heading 3
Heading 4
Heading 5
Body copy — Figtree. The quick brown fox jumps over the lazy dog.
Small print and captions.
Standard heading block for content sections. Pass heading_tag: 'h2' below a hero.
{%- render 'section-header',
eyebrow: section.settings.eyebrow,
heading: section.settings.heading,
subheading: section.settings.subheading,
heading_tag: 'h2',
alignment: 'left' -%}
Produces an accessible name for an image-only link, so it does not fail WCAG 2.4.4.
{%- capture promo_label -%}
{%- render 'promo-link-label', title: block.settings.title, image: block.settings.image, link: block.settings.link -%}
{%- endcapture -%}
Always use image_tag: it generates a srcset automatically. Lazy-load
below the fold only — never above it, since that directly harms LCP.
{{ block.settings.image
| image_url: width: 600
| image_tag:
loading: 'lazy',
sizes: '(min-width: 741px) 280px, 45vw',
widths: '200,280,400,560,600',
alt: block.settings.image.alt | default: block.settings.name }}
| Do | Don't |
|---|---|
var(--accent-color) | #17A0EC hardcoded |
Text in section settings or locales/ | Hardcoded strings in markup |
{% render %} | {% include %} (deprecated) |
One h1 per page | Multiple h1 across sections |
Edits in custom.css | Edits in theme.css (vendor) |
| Every link has an accessible name | Image-only links with alt="" |
Vertical rhythm comes from .section, not from a section's own
padding. Measured on the live storefront:
| Class | Measured on this page | Use |
|---|---|---|
.section | — | The default gap between any two sections |
.section--tight | — | A denser band, e.g. the trust icons row |
.container | — | Every section's inner wrapper. Never add page-level side padding of your own |
Both are responsive, which is why they are measured rather than written
down: .section is 50px below the 641px breakpoint and 60px from there up,
and .container's gutter widens with it. Resize the window and the table
follows.
Custom sections still expose padding_top and padding_bottom
range settings, but they default to 0. A section root carries both
.section and its own padding, so a non-zero default double-spaces the page:
the condition guide shipped at 90–98px between sections against the
site's 50px until this was fixed. Reach for padding only when you want space
inside a section, not between them.
{ "type": "range", "id": "padding_top", "label": "Padding top",
"min": 0, "max": 120, "step": 4, "unit": "px", "default": 0 }
The one exception is a tinted band. When
use_secondary_background is on, the background would otherwise hug the
text, so those sections floor their padding at 32px regardless of the setting:
padding:
{%- if section.settings.use_secondary_background -%}
{{ section.settings.padding_top | at_least: 32 }}px 0 {{ section.settings.padding_bottom | at_least: 32 }}px
{%- else -%}
{{ section.settings.padding_top }}px 0 {{ section.settings.padding_bottom }}px
{%- endif -%};
All six are weight 700, letter-spacing normal. The classes are visual only — pick the element from the document outline and the class from here. The scale is responsive, so the sizes below are read from the page you are looking at rather than written down: resize the window and they change.
.h1 —
.h2 —
.h3 —
.h4 —
.h5 —
.h6 —
Section titles across the whole site are .h3 (21px), inside
a .section__header with margin-bottom: 15px.
snippets/section-header.liquid matches that — it renders an
<h2> element with the .h3 class, so the outline stays
correct while the size agrees with the rest of the storefront. It used
.h2 (23px) before, which read as a mistake rather than a decision.
Inherited from the Warehouse base theme and used by every product grid. Classes read
as fraction--breakpoint and are passed to snippets as
grid_classes. This syntax cannot be guessed, so it is documented here.
| Class | Meaning |
|---|---|
1/2--tablet | Half width from the tablet breakpoint up |
1/3--lap-and-up | One third from laptop up |
1/4--lap | One quarter at the laptop breakpoint |
1/5--desk | One fifth at desktop |
1/6--wide | One sixth on wide screens |
Combine them to change per breakpoint — they are additive, not exclusive.
snippets/product-item.liquid — the most reused component in the theme.
Always render products through it rather than building card markup, so price,
badges, membership labels and quick-add behave consistently.
{% render 'product-item',
product: product,
grid_classes: '1/2--tablet 1/3--lap-and-up',
show_add_to_cart: true %}
horizontal: true switches to the list layout used on search results.
Live example — rendered from real store data:


Built for editorial pages. All carry presets, so they can be added to any
page from the theme editor — not only the templates they were written for.
| Section | Blocks | Use it for |
|---|---|---|
page-hero | meta | Page h1 plus label/value pairs and a CTA |
spec-table | row | Structured facts as a label/value table |
grade-detail | grade | Paired cards with criteria and front/back photos |
scale-comparison | segment | Comparing scales on one aligned track |
grade-crosswalk | mapping | Equivalence table across systems |
checklist-steps | step | Numbered walkthrough where order matters |
rule-list | rule | Set of short titled rules |
pre-order-hero | meta, trust | Pre-order page h1, release facts, CTA and reassurance row |
release-countdown | — | Release date plus a live countdown |
product-lineup | group | The few products a launch page is actually selling |
release-timeline | — | Ordered dates with passed/upcoming states; steps with no date are skipped |
rarity-breakdown | — | Proportional bar plus counts and percentages, from plain "Label: count" lines |
set-overview | — | Long-form copy with a <details> read-more, for indexable text |
_blocks | @theme, @app | Upstream, from Warehouse 7.0.1. A container for Theme Blocks and app blocks on any page. With no blocks/ directory in this theme, only @app blocks resolve today |
tcg-news | — | Blog-driven news rail with per-game tag filter. Reads a native blog on purpose: Liquid cannot fetch external feeds, so ingestion happens upstream (Flow/backend) and the articles stay crawlable on our domain |
release-calendar | — | Upcoming releases across all games, from the shop's pre_order.release_calendar collection list, dated by each collection's release_date. Undated collections are skipped by design |
page-nav | item | Jump list for a long page. Targets the id section-header generates from each heading |
glossary | term | Term and definition pairs as a real <dl>, with a synonyms line |
worked-example | — | One real item photographed front and back, beside the reasoning. Also works as a titled callout with no photos |
styleguide | — | This page |
Every heading rendered through section-header is linkable.
The snippet emits an id handleized from the heading, so
“The TierZero Grade mapping” is reachable at
#the-tierzero-grade-mapping with no per-section setting.
page-nav targets exactly those ids.
Only one section per page may render the h1. page-hero
and pre-order-hero have a Use as page title toggle; everything
below them uses h2. Watch the stock faq section: its
separator block renders an h1, so a page that already has
a hero must use question blocks only.
Added in PRs #28–#33. Each entry says where the data comes from and the exact steps to put the component on a page, because "it exists in the theme" and "a merchant can use it" are different claims.
Any Collection list section can now render as a grid instead of the
horizontal scroller: Layout → Grid, then columns for desktop
(2–9) and mobile (2–4). Tablet is capped at 4 automatically. From 5 desktop columns
the tile titles drop 2px so rows stay aligned. An optional
release status pill reads each collection's
pre_order.release_date.
Theme editor → section "Collection list" → Layout: Grid → Columns (desktop): 9, Columns (mobile): 3Live example — real collections, grid mode, 4 columns with the status pill on:
Why grid mode kills the carousel: from 1000px up, theme.js turns
every .collection-list into a Flickity carousel via
watchCSS. Grid instances opt out by overriding the
:after hook — scoped per section, so scroller instances elsewhere keep
their carousel. Verify grids at ≥1000px after JS settles.
Shows a live figure ("1,204,551 cards sold this year") from
shop.metafields.tierzero.<key>. The theme never computes the
number — a scheduled job writes the metafield; the block renders it verbatim and
renders nothing if the metafield is empty (never "0"). In the theme
editor an unset metric shows a bracketed hint instead.
1. Job writes shop.metafields.tierzero.cards_sold_2026 (any text, pre-formatted)
2. Theme editor → Announcement banner → Add block → Live metric
3. Key: cards_sold_2026 · Text: {value} cards sold this year
Live state:shop.metafields.tierzero.cards_sold_2026 is empty, so a metric block renders
nothing at all on the storefront. That is the intended fail-safe — a dead
job must never show “0 cards sold”.
Blog-driven news rail. Reads a native blog on purpose — Liquid cannot fetch external
feeds; ingestion happens upstream (person or Flow/backend job) and stories stay
crawlable on our domain. One blog serves every game via article tags
(yugioh, mtg, pokemon, riftbound).
1. Tag articles in the News blog per game
2. Add section "TCG news" → pick blog → set "Only show articles tagged": yugioh
3. On the shared tcg-game template: enable "take the tag from the collection"
and set each collection's pre_order.news_tag metafield insteadLive example — the first two articles the section would render from the News blog:

An important update regarding Magnificent Monsters Unfortunately, we need to share an update we never expected to have…

International Express Shipping Update: DHL Express Returns as Our Exclusive Express Carrier At Tier Zero Games, we're constantly…
Cross-game upcoming releases. Source is ONE shop metafield —
pre_order.release_calendar, a list of collections — and every date
comes from each collection's own pre_order.release_date, so the
calendar can never disagree with a set page. Rows sort by date; undated collections
are skipped by design (the set page already renders "To confirm").
Adding a release (no deploy):
1. Admin → Settings → Custom data → Shop → pre_order.release_calendar → add the collection
2. Make sure the collection has pre_order.release_date
Done — every calendar instance updates.Live example — the calendar's current contents, rendered exactly as the section renders them:
Product cards link to the card's other printings when the product's
tierzero.related_rarities_alt_arts metafield is non-empty — the same
list the PDP printings selector reads, so the two can never disagree. It is a link
to the PDP, never a picker: quick-add on a grid must not switch products silently.
Toggle: Theme settings → Products grid → Card printings. No metafield, no link.
{%- assign other = product.metafields.tierzero.related_rarities_alt_arts.value -%}
{%- if other.size > 0 -%}
+{{ other.size }} other printings
{%- endif -%}
Live example — how the link renders: +5 other printings
Live demo of both sections: the page.releases template
(/pages/releases once the page is created in Admin).
The constraint to understand first: a JSON page template shares its section settings with every page that uses it. Type a release date into a section and all pre-order pages show that date. So anything that varies per page cannot live in a setting — it has to come from data.
The split this theme uses: layout, labels and copy stay in section
settings, and per-release facts come from the collection's
pre_order metafields. Sections name the fact they want; they never hold
the value.
| Step | What happens |
|---|---|
| 1. Resolve the collection | Page's pre_order.collection metafield, else collections[page.handle], else the section's fallback setting. The page metafield wins, because it is the only per-page one. |
| 2. Name a fact | Each block has a Value from select — release_date, price, formats… |
| 3. Resolve it | snippets/pre-order-value.liquid maps the name to the metafield and outputs text, or nothing if unset. |
| 4. Fall back | Metafield → the block's written value → a visible “To confirm” flag. Never a blank cell. |
{%- liquid
assign po_collection = page.metafields.pre_order.collection.value
if po_collection == blank
assign po_collection = collections[page.handle]
endif
if po_collection == blank
assign po_collection = section.settings.collection
endif
-%}
{%- capture row_value -%}
{%- render 'pre-order-value',
po_collection: po_collection,
source: block.settings.source,
date_format: section.settings.date_format -%}
{%- endcapture -%}
{%- assign row_value = row_value | strip -%}
render has isolated scope, so a snippet cannot assign variables for its
caller. That is why the resolver outputs a value and the caller captures it —
and why the eight-line collection lookup is repeated in each section rather than
shared.
Adding a new fact is two edits: a when branch in the resolver and one
option in each schema that should offer it. spec-table
defaults every row to Written below, so existing pages are unaffected.
Do not use featured-collection on a shared template.
Its collection is a section setting, so every page using the template would show the
same collection. Sections that need the page's own collection must resolve it as above.
Repeatable per-page content cannot be blocks either. Blocks live in the
template, so they are shared too. A per-set rarity list or overview has to arrive as
data: rarity-breakdown parses plain Label: count lines and
set-overview splits a multi-line metafield into paragraphs. Plain text
beats JSON or metaobjects here — someone copying four numbers off a publisher page
should not have to learn a syntax.
Check for structured data the theme already emits.
snippets/microdata-schema.liquid puts a BreadcrumbList in the
document head on every page. A second one from a section produced two competing trails
for one URL, so pre-order-hero renders visible breadcrumbs by default and
keeps its JSON-LD behind an off-by-default setting.
snippets/pill.liquid — badge for grade codes, region tags and short
status labels. Variants tie a pill to the scale it belongs to.
{%- render 'pill', code: 'NM', label: 'Near Mint', variant: 'na' -%}
A component is not done until every state is handled. Check these before shipping.
| State | Requirement |
|---|---|
| Default | Meets 4.5:1 contrast for text |
| Hover | Visible change that is not colour alone |
| Focus | Never remove the outline without an equally visible replacement |
| Active / pressed | Reflect it in aria-pressed where it is a toggle |
| Disabled | Use the real disabled attribute, not just styling |
| Empty | Say what is missing and how to fix it — never render blank |
| Error | Message next to the field, not only a red border |
Every input needs a <label for> pointing at its id.
A placeholder is not a label. Build ids from something stable — an id built from an
out-of-scope Liquid variable renders empty and silently breaks the association.
{%- render 'icon', icon: 'cart' -%}
Icons are decorative by default. If an icon is the only content of a button or link,
the control still needs an accessible name via aria-label.
Names must match the when cases in snippets/icon.liquid.
An unknown name renders nothing at all — silently, with no error. Available include:
cart, big-cart, search, close,
close-2, minus, check, heart,
account, filter, grid, list,
lock, package, address, play,
timer, zoom, hamburger,
arrow-left, arrow-right, tail-left,
tail-right, plus the social icons.
Named anti-patterns from hallmark,
kept here because we shipped two of them ourselves and only found out
by running its checklist over our own sections. See
docs/core/design-review.md for how to run it.
| Tell | Why it reads as generated | Do instead |
|---|---|---|
| Side-stripe card | A 3–6px coloured border on one edge only. Very recognisable 2018-SaaS-AI | Hairline all round, and put the accent on the eyebrow or a small marker |
| Card-in-card | A bordered container holding bordered children — visual nesting with no semantic reason | Pick one containment layer. Separate the inner items by ground colour |
| Three equal feature columns | Icon over two-line heading over three-line body, ×3, 24px gap. Every model emits it | Vary widths, drop one card, or use typographic rhythm instead of cards |
| Gradient headline or hero | background-clip: text on a purple-to-pink fill; gradient hero backgrounds | Solid ink. Reach for weight, italic or scale for emphasis |
| Full-viewport centred hero | min-height: 100vh, everything centred, one sentence, one button | Let the hero be as tall as its content and bias it left |
| Pure #000 / #fff | Reads flat and synthetic | Use the theme tokens, which are merchant-set and already tinted |
The last three are largely decided by the base theme rather than by our sections, so treat them as a check on anything new rather than a licence to edit vendor files.
CI fails on link-name and aria-allowed-attr. These are the
rules behind that, each one written from a defect actually found in this theme.
| Rule | Why |
|---|---|
| Every link has an accessible name | 15 image-only links shipped with empty text and alt="" |
aria-expanded only on interactive elements | It was on a plain div, which is invalid |
Table row labels use <th scope="row"> | <td> loses the label/value pairing |
| Never carry meaning by colour alone | Add a border, icon or text as well — WCAG 1.4.1 |
Respect prefers-reduced-motion | Keep the state change, drop the movement |
| Contrast: 4.5:1 normal, 3:1 large text | The brand blue currently fails at 2.89:1 |