Style guide. Copy from here.
Every component of the OBALUS store in the v2-B language: black, white, orange; Archivo caps; Figtree body at 19–20px; hard black borders; radius 4px; no gray text. The contract lives in analysis/hdzbold-revamp/DESIGN-SYSTEM.md.
v1.2 · 01.10.2026 (wave 4, owner feedback) — additive; v1.1 and v1 still work.
New in v1.2
wave 4 · owner feedback 01.10 · additive- No eyebrows. The small caps line above a heading is retired everywhere (owner). Product pages hide any left over (
.hdzb-eyebrowunder[data-hdzb-tpl="product"]). Don't add new ones. - Header: one icon size (24px glyph, 44px target on phones, 48px on desktop; cart glyph 22px in the orange circle). Nav ≤ 6: Reels · Rods · Line & Lures · Gear · Learn · Build a setup. Dropdowns = sub-category tiles with the orange collection images + ONE tile (the "Build a setup" and quiz promos once each, the others a product with its price) + at most one help link that says where it goes. The drawer shows the same sub-categories with thumbnails and one product row; "Build a setup" appears once (the big button).
- Header height contract: the header never changes height on scroll (compact = logo scale only, on > 96px / off < 24px).
--hdzb-header-h= its rendered height, border included, kept exact by a ResizeObserver. Sticky bars:top: var(--hdzb-header-h)→ flush.
- Product card rhythm: media 1:1 → title (2 lines reserved) → price → offer line → rating or "30-day guarantee" → CTA pinned to the bottom. Every card in a row/rail has the same height.
- Offer line (
snippets/hdzb-card-offer): "Buy 1, get 1" on BOGO products, live deals ("2 reels + free rod"), the bundle saving once the codes exist, else one verified fact (≤ 20 characters). - Low in stock chip on the card media (and on the product page's buy box): real stock "Only N left", the owner's manual list, and the per-visitor interest label (
HDZB.interest). Settings: Theme settings → Product cards (HDZB).
New in v1.1
additive — every v1 class and snippet parameter still works- Motion system:
data-hdzb-revealanddata-hdzb-reveal="stagger", button press, card lift, image fade-in, "Added ✓" on quick add, cart badge bump, accordion height, drawer/search/dropdown slide, sticky bar, toast,.hdzb-skel. All off under reduced motion. - 17 fishing icons on the Lucide grid (reel, spinning-reel, rod, spool, lure, hook, tackle-box, pliers, sunglasses, shirt, boat, bobber, net, fish, waves, hand-right, hand-left).
- Header: desktop dropdowns for Reels / Rods / Line & Lures / Gear from the store menus + a featured tile; mobile menu rows with category icons and expandable sub-links; search quick links answer "saltwater" and "left hand".
- Product card: 1:1 light panel with
contain(studio and lifestyle shots line up), rating slot keeps titles aligned, CTA never wraps on 2-column phones. - Fixes:
hdzb-mt-*now win over heading margins; rail "previous" arrow hidden at the start; the logo is never an H1; breadcrumbs cut at 60 characters and show Home › Shop › Collection; sticky bar out of the tab order when hidden. - New helpers:
.hdzb-shead(section heading + link),.hdzb-mt-12/64,HDZB.initReveal(),HDZB.bump(el),HDZB.reducedMotion(), footer "Back to top".
Tokens
:root --hdzb-*--hdzb-orange#FF5A00 · CTA, accent word, numbers, check icons
--hdzb-orange-600#E64F00 · hover
--hdzb-ink#000000 · text, borders, black panels
--hdzb-white#FFFFFF
--hdzb-peach#FFF3EC · upsell panels, selected tier
--hdzb-grey#F1F1EF · light-grey sections
--hdzb-grey-2#F8F8F7
--hdzb-line#E4E4E4 · 1px borders (never text)
--hdzb-star#FFB800
--hdzb-ok#2E9E5B · verified, free shipping reached
--hdzb-red#D62828 · errors, strike on "their" price
--hdzb-r4px radius. Only avatars and the header cart button are round.
--hdzb-tap56px — minimum height of primary controls (inputs, buttons, segmented options).
--hdzb-gutter16px / 20px / 32px side gutter (mobile / ≥600 / ≥990).
--hdzb-max1200px page width; .hdzb-wrap--text = 720px reading column.
--hdzb-header-hthe header's rendered height, border included (62px phone / 74px desktop), kept exact by JS — top: var(--hdzb-header-h) sits flush under it. Never use it to size the header itself.
--hdzb-header-rowthe header row's design height (60px / 72px) — only for the header.
px onlyEnterprise sets html { font-size: 62.5% }, so never use rem in HDZB CSS.
Type
.hdzb-display .hdzb-h1 … .hdzb-h4 .hdzb-lead .hdzb-body .hdzb-smallEyebrows are retired (v1.2, owner): no small caps line above headings. Headings ≤ 6 words, one short sub-line.
Buy one reel. Get one free.
H2 — Two reels for $59.99
H3 — Casts clean out of the box
H4 — What's in the box
Lead 21/23px — Same components, same smoothness, none of the markup you pay for at the big brands.
Body 19/20px, Figtree, line-height 1.5. Text is black on light backgrounds and white on black. Grey is for borders and backgrounds only — never for words. Links are underlined, never orange on white.
Small 15/16px — fine print, captions under photos, policy notes.
Show HTML
<h1 class="hdzb-h1">Buy one reel. <em>Get one free.</em></h1> <p class="hdzb-lead">Two reels for $59.99.</p> <p class="hdzb-body">Body copy…</p>
Buttons
.hdzb-btn --primary --outline --white --full --lg --sm --xsNote under a button: 30-day money-back · secure checkout
Primary = orange, one per screen. Full width on mobile, 56–60px tall. Labels are verbs: "Add to cart", "Get the complete setup". Never "Buy now"-style pressure.
Show HTML
<a class="hdzb-btn hdzb-btn--primary hdzb-btn--full hdzb-btn--lg" href="/cart">Add to cart</a> <button type="button" class="hdzb-btn hdzb-btn--outline hdzb-btn--sm">Choose hand</button> <!-- declarative add to cart (opens the UpCart drawer): --> <button type="button" class="hdzb-btn hdzb-btn--primary" data-hdzb-add data-id="42858033381582" data-qty="1" data-busy-text="Adding…">Add the rod</button>
Badges, tags, pills
.hdzb-badge --black --white --peach --ok --red --lg · .hdzb-tag --ours · .hdzb-pill.hdzb-low chip (v1.2): real stock count / manual list / per-visitor interest. Lives on the card media and the product page's buy box only.
Show HTML
<span class="hdzb-badge">Buy 1 get 1 free</span> <span class="hdzb-badge hdzb-badge--black">Best value</span> <div class="hdzb-ratio hdzb-ratio--4x3"><img …><span class="hdzb-tag">✗ Their reel</span></div> <div class="hdzb-ratio hdzb-ratio--4x3"><img …><span class="hdzb-tag hdzb-tag--ours">✓ The Obalus</span></div> <a class="hdzb-pill is-on" href="/collections/reels">Reels</a>
Price
{% render 'hdzb-price', product: product, size: 'lg' %}Rules. Prices come from Shopify (product/variant), never typed in. Every element carries data-cents so JS never parses text. Money is formatted by Shopify for the visitor's currency — see Money & currency. Products tagged Buy 1 Get 1 get the two-for-one note automatically.
{% render 'hdzb-price', product: product, size: 'lg' %}
{% render 'hdzb-price', product: rod, variant: rod_v, size: 'sm', show_save: false, show_note: false %}
<!-- raw money from cents, Conversion-Bear-safe: -->
{% render 'hdzb-money', cents: 5999 %} → $59.99
{% render 'hdzb-money', cents: 6000, trim: true %} → $60
{{ product.price | money | strip_html }} → same thing inline
Rating (real only)
{% render 'hdzb-rating', product: product, size: 'lg', link: '#reviews' %}Source: product.metafields.alireviews.seo_rating_review_key_script (fallback …seo_rating_review_key). The Ali Reviews app is uninstalled, so these numbers are a snapshot — still the only verified aggregate we have. 7 of 49 active products carry one (the flagship family 4.8/183, fish finder 4.8/103, bait life support 4.9/165, tackle box 4.9/38). Never type "4.9/5 by 12,999 customers" anywhere.
Lists: checks, icon rows, numbered cards, accordion
.hdzb-checks · .hdzb-icons · .hdzb-nums · .hdzb-acc- Casts clean out of the box. Even on your first baitcaster.
- Free video course included. You are set up in ten minutes.
- Fresh and saltwater rated. 12+1 ball bearings.
- No brand tax. No sponsored pros in the price.
- 30-day money-back
- Free course included
- Ships tomorrow
- Saltwater ready
- Cast where you're looking.Under the dock. Past the weed line. Right on the money.
- Comfortable all day.7.8 oz. Light enough that your hand isn't cooked by noon.
- Confidence on the hookset.18 lb of drag. When something big hits, the drag holds.
Does it work in saltwater?
Yes. Both reels have 12+1 ball bearings and a SIC ceramic line guide. Rinse with fresh water after a salt trip, like any reel.
Which line should I spool?
15–30 lb braid for bass and most freshwater. 40–65 lb for heavy cover, catfish, pike and inshore salt. Never under 15 lb braid.
What if I don't like it?
You have 30 days from delivery to ask for a return. Send it back unused in its original packaging and we refund you. Damaged or defective? We replace it free.
Show HTML
<ul class="hdzb-checks">
<li>{% render 'hdzb-icon', name: 'check', size: 22 %}<span>Casts clean out of the box.</span></li>
</ul>
<div class="hdzb-icons" style="--cols:3">
<div class="hdzb-icons__i"><span class="hdzb-icons__ic">{% render 'hdzb-icon', name: 'target', size: 30 %}</span><div><div class="hdzb-icons__t">Cast 1</div><div class="hdzb-icons__s">Lands clean.</div></div></div>
</div>
<ol class="hdzb-nums" style="--cols:3"><li class="hdzb-nums__i"><div class="hdzb-nums__t">Title</div><div class="hdzb-nums__b">Text</div></li></ol>
<div class="hdzb-acc">
<details class="hdzb-acc__i"><summary class="hdzb-acc__q">Question{% render 'hdzb-icon', name: 'plus', size: 26, stroke: 3 %}</summary><div class="hdzb-acc__a"><p>Answer</p></div></details>
</div>Quote cards, boxes, comparison
.hdzb-quote · .hdzb-box · .hdzb-vs · .hdzb-mcardI'm 54 and just got mine. First baitcaster I ever used without throwing it away.
I have two and they are truly amazing. Easy long casts and no backlashes.
I bought 2 of them. Great reels, as good if not better than the $250 reels. That's a fact.
.hdzb-box
Hard 2px black border. The buy box, the summary panel, anything that has to look like a decision.
.hdzb-box--peach
Upsell, bonus, "in every order".
.hdzb-box--black
Sparingly. Rustam: simple light blocks beat designer dark cards.



Show HTML
<div class="hdzb-quote">
{% render 'hdzb-stars', value: 5, size: 16 %}
<p class="hdzb-quote__t">First baitcaster I ever used without throwing it away.</p>
<div class="hdzb-quote__who">{% render 'hdzb-avatar', name: 'Frank' %}<div><b>Frank</b><span class="hdzb-verified">{% render 'hdzb-icon', name: 'badge-check', size: 16 %}Verified buyer</span></div></div>
</div>
<div class="hdzb-vs">
<div class="hdzb-vs__col hdzb-vs__col--them"><span class="hdzb-vs__tag">Their reel</span><div class="hdzb-vs__price">$250</div><div class="hdzb-vs__label">…</div></div>
<div class="hdzb-vs__col hdzb-vs__col--us"><span class="hdzb-vs__tag">The Obalus</span><div class="hdzb-vs__price">$60</div><div class="hdzb-vs__label">…</div></div>
<span class="hdzb-vs__mid">vs</span>
</div>Buy box parts: segmented control, tiers, upsells, sticky bar
.hdzb-seg · .hdzb-tiers · .hdzb-upsell · .hdzb-sticky · .hdzb-qty
-45%
- 30-day money-back
- Free course included
- Ships tomorrow
- Saltwater ready
The tier cards and the Right/Left switch are role="radio" buttons inside a data-hdzb-radios group: hdzb.js handles selection, arrow keys and fires hdzb:change with detail.value. The add button here really adds two reels and opens UpCart. The sticky bar (bottom of this page) appears once this box scrolls out of view — data-hdzb-sticky data-target="#sg-buybox-demo".
<div class="hdzb-seg" role="radiogroup" aria-label="Reel handle" data-hdzb-radios>
<button type="button" class="hdzb-seg__o is-on" role="radio" aria-checked="true" data-value="right">Right</button>
<button type="button" class="hdzb-seg__o" role="radio" aria-checked="false" data-value="left">Left</button>
</div>
<div class="hdzb-tiers" role="radiogroup" data-hdzb-radios>
<button type="button" class="hdzb-tier is-on" role="radio" aria-checked="true" data-value="2">
<span class="hdzb-badge hdzb-badge--black hdzb-tier__badge">Best value</span>
<span class="hdzb-tier__dot"></span>
<span><span class="hdzb-tier__name">Complete setup</span><span class="hdzb-tier__what">Two reels + rod</span><span class="hdzb-tier__ship">Ships free</span></span>
<span class="hdzb-tier__price">$109.98</span>
</button>
</div>
<div class="hdzb-qty" data-hdzb-qty><button type="button" class="hdzb-qty__b" data-dir="-1">−</button><input class="hdzb-qty__in" type="number" min="1" value="1"><button type="button" class="hdzb-qty__b" data-dir="1">+</button></div>
<div class="hdzb-sticky" data-hdzb-sticky data-target="#buybox">
<img class="hdzb-sticky__img" src="…" alt="">
<div class="hdzb-sticky__t"><div class="hdzb-sticky__name">Two reels · buy 1 get 1 free</div><div class="hdzb-sticky__price">$59.99 <s>$119.99</s></div></div>
<a class="hdzb-btn hdzb-btn--primary" href="#buybox">Add to cart</a>
</div>
group.addEventListener('hdzb:change', e => console.log(e.detail.value));
Trust rows, payment icons, alerts, toast
{% render 'hdzb-trust-row', style: 'grid|band|inline|col' %} · {% render 'hdzb-payment-icons' %}- Free shipping on orders $100+
- 30-day money-back guarantee
- Ships tomorrow
- 94,000+ orders shipped
- Free shipping on orders $100+
- 30-day money-back guarantee
- Ships tomorrow
- 94,000+ orders shipped
- Free shipping on orders $100+
- 30-day money-back guarantee
- Ships tomorrow
- 94,000+ orders shipped
Product cards: grid and rail
{% render 'hdzb-product-card', product: product %} · .hdzb-grid · .hdzb-rail- Same height, same rhythmMedia 1:1 · title 2 lines (reserved) · price · offer line · rating row · CTA at the bottom. Rails and grids stretch every card to the tallest.
- Rating below the priceReal Ali Reviews rating only. No rating → the same row says "30-day guarantee" (setting). Never a typed number.
- One offer line
snippets/hdzb-card-offer: BOGO → "Buy 1, get 1"; deals; bundle saving (only when the codes exist); else one fact. ≤ 20 characters.
data-hdzb-reveal="stagger"Show HTML
<div class="hdzb-grid">{% for product in collection.products %}{% render 'hdzb-product-card', product: product, eager: forloop.first %}{% endfor %}</div>
<div class="hdzb-rail hdzb-rail--3" data-hdzb-rail>
<button type="button" class="hdzb-rail__arrow hdzb-rail__arrow--prev" aria-label="Previous">{% render 'hdzb-icon', name: 'chevron-left', size: 24, stroke: 3 %}</button>
<div class="hdzb-rail__track">{% render 'hdzb-product-card', product: p %} …</div>
<button type="button" class="hdzb-rail__arrow hdzb-rail__arrow--next" aria-label="Next">{% render 'hdzb-icon', name: 'chevron-right', size: 24, stroke: 3 %}</button>
</div>Media: ratio boxes, images, video
{% render 'hdzb-img', … %} · {% render 'hdzb-video', … %} · .hdzb-ratio--1x1|4x5|4x3|3x2|16x9|9x16





Real customer photos (bogo-19/20/24/22) only with a "Real Obalus owner(s)" caption. AI lifestyle images are staged — never captioned as customers.
Show HTML
{% render 'hdzb-img', img: section.settings.image, alt: 'Angler with a bass', ratio: '4x5', eager: true, sizes: '(min-width: 990px) 560px, 100vw' %}
{% render 'hdzb-img', asset: 'bogo-19.jpg', alt: '…', ratio: '1x1' %}
{% assign vid = 'bogo-big-one.mp4' | file_url %} ← filters do NOT work inside render arguments; assign first
{% render 'hdzb-video', src: vid, poster: section.settings.poster, ratio: '4x5', mode: 'click', caption: 'The big one' %}
{% render 'hdzb-video', src: section.settings.video_url, ratio: '9x16', mode: 'auto' %}
<div class="hdzb-ratio hdzb-ratio--16x9"><img src="…" alt="…" loading="lazy"></div>Forms, table, pagination, breadcrumbs
.hdzb-input .hdzb-select .hdzb-textarea .hdzb-check · .hdzb-table · .hdzb-pager · {% render 'hdzb-breadcrumbs' %}Show HTML
<label class="hdzb-field"><span class="hdzb-label">Email</span><input class="hdzb-input" type="email"></label>
<label class="hdzb-check"><input type="checkbox"><span>Email me when the next deal starts.</span></label>
<div class="hdzb-table"><div class="hdzb-table__row hdzb-table__head"><div class="hdzb-table__c"></div><div class="hdzb-table__c">Their reel</div><div class="hdzb-table__c hdzb-table__c--us">Obalus</div></div>…</div>
<nav class="hdzb-pager"><span class="is-cur">1</span><a href="?page=2">2</a></nav>
{% render 'hdzb-breadcrumbs' %}Section wrapper and rhythm
.hdzb › .hdzb-sec --white|grey|grey2|peach|black --rule --tight › .hdzb-wrap --text|wide|flushEvery new section: <div class="hdzb"><section class="hdzb-sec …"><div class="hdzb-wrap">. Vertical padding 56px (72px desktop) comes from --pt/--pb; the standard schema settings bg, pad_top, pad_bottom, rule, anchor are read by hdzb-section-open. Alternate white / light-grey / peach. Black only for a short, loud block.
{% render 'hdzb-section-open', s: section.settings, id: section.id, wrap: 'text' %}
<h2 class="hdzb-h2">{{ section.settings.heading }}</h2>
…
{% render 'hdzb-section-close' %}
{% schema %}
{ "settings": [
{ "type": "select", "id": "bg", "label": "Background", "default": "white", "options": [
{ "value": "white", "label": "White" }, { "value": "grey", "label": "Light grey" }, { "value": "peach", "label": "Peach" }, { "value": "black", "label": "Black" } ] },
{ "type": "range", "id": "pad_top", "label": "Top padding", "min": 0, "max": 96, "step": 8, "default": 56 },
{ "type": "range", "id": "pad_bottom", "label": "Bottom padding", "min": 0, "max": 96, "step": 8, "default": 56 },
{ "type": "checkbox", "id": "rule", "label": "2px black top rule", "default": false },
{ "type": "text", "id": "anchor", "label": "Anchor id" }
] }
{% endschema %}
Money & currency (the "$1322000" bug)
{{ cents | money | strip_html }} · HDZB.money(cents)cart.currencyUSD ($)
localization.countryVereinigte Staaten · US
reel.price (cents)5999
price | money | strip_html$59.99
price | money_with_currency | strip_html$59.99 USD
price | money (raw)<span class="money conversion-bear-money">$59.99</span>
shop.money_format<span class="money conversion-bear-money">${{amount}}</span>
JS HDZB.money(5999)…
JS HDZB.money(6000,{trim:true})…
JS Shopify.currency…
JS HDZB.usd(10000)…
What actually happens: there is no currency-converter script on the page. Shopify Markets renders the whole store in the visitor's currency server-side (Uzbekistan → UZS, rate ≈12,000). Liquid prices, /cart.js and add-to-cart all speak that currency. The old bug was JS writing '$' + cents/100. The store's USD money format still carries the Conversion Bear <span class="money conversion-bear-money"> from a past install — strip_html removes it.
The rule: Liquid → {{ cents | money | strip_html }} (or hdzb-money). JS → HDZB.money(cents). Never hardcode "$", never put the money class on our elements, always keep raw cents in data-cents. USD thresholds (free shipping $100) → HDZB.usd(10000).
Preview in USD like a customer: add &country=US to the preview URL once; Shopify remembers it for the session.
Motion
data-hdzb-reveal · data-hdzb-reveal="stagger" · .hdzb-skelCalm and short: 150–450 ms, ease-out, once. It shows where to look next; it never grabs attention. Everything is off under "reduce motion", and content is fully visible without JavaScript. Never on the hero / LCP image or the buy box.
Section title blocks fade up 12px.
Children follow 60 ms apart (max 8 steps).
Stats, quotes, trust rows.
Press = scale .98 · quick add → "Added ✓" 1.8 s → back · cart count bumps when it grows · cards lift 3px on desktop hover.
.hdzb-skel (+ --line, --block)Show HTML
<div class="hdzb-shead" data-hdzb-reveal>…h2 · sub · link…</div> <div class="hdzb-grid" data-hdzb-reveal="stagger">…cards…</div> <div class="hdzb-skel hdzb-skel--block"></div> HDZB.initReveal(scope) // done by HDZB.init(scope) after a re-render HDZB.bump(el) · HDZB.reducedMotion() → true|false <button data-hdzb-add … data-done-text="Added" data-done="false"> // done-state label / switch it off
JS API
window.HDZBHDZB.money(5999) // "$59.99" (formatted by Shopify's rules for the active currency)
HDZB.money(6000, { trim: true }) // "$60"
HDZB.usd(10000) // $100 in active-currency cents (uses Shopify.currency.rate)
HDZB.cart.get() // → cart JSON, fires 'hdzb:cart'
HDZB.cart.add(42858033381582) // one item, opens UpCart
HDZB.cart.add([{ id: 42646296854734, quantity: 2 }, { id: 42858033381582, quantity: 1 }], { open: true, toast: 'Added' })
HDZB.cart.change(line, qty) / .remove(line) / .update({ note: '…' }) / .clear()
HDZB.cart.setDiscount('SETUP10') // → { applied: true|false, code, cart } (POST /cart/update.js { discount })
HDZB.cart.clearDiscount()
HDZB.cart.open() / .close() / .refresh() / .count() / .state
HDZB.setup.code / .amountCents / .apply() / .clear() // thin alias of window.HDZS (bundle engine, Theme settings → Bundle deals)
HDZB.checkout({ discount: 'SETUP10' }) // fallback: /checkout?discount=CODE
HDZB.toast('Added to cart', { kind: 'ok' | 'err', ms: 2600 })
HDZB.lockScroll(true|false) · HDZB.trapFocus(el, { onEscape, initial, restore })
HDZB.menu.open() / .close() · HDZB.search.open() / .close()
HDZB.init(scope) // rails, steppers, radios, videos, sticky bars, [data-hdzb-add] buttons, image fade, reveal in new DOM
HDZB.initReveal(scope) · HDZB.bump(el) · HDZB.reducedMotion() · HDZB.dropdown.close() // v1.1
HDZB.interest.get() / .score(handle) / .flagged() / .apply(scope) / .reset() // v1.2 per-visitor "Low in stock" (owner decision 01.10)
<article data-hdzb-lowable data-handle="…"> … <div data-hdzb-low-slot></div> // v1.2: your own card gets the chip too
HDZB.on('hdzb:cart', ({ cart, source }) => …) // also window events: hdzb:cart, hdzb:cart:added, hdzb:cart:error, hdzb:discount
// Declarative:
<button data-hdzb-add data-id="VARIANT_ID" data-qty="1" data-open="false" data-toast="Added" data-busy-text="Adding…" data-fallback="/products/x">
<button data-hdzb-add data-items='[{"id":42646296854734,"quantity":2},{"id":42858033381582,"quantity":1}]'>
<div data-hdzb-rail> · <div data-hdzb-qty> · <div data-hdzb-radios> · <div data-hdzb-video data-src data-mode="auto|click"> · <div data-hdzb-sticky data-target="#buybox">
Fishing icons (17)
{% render 'hdzb-icon', name: 'reel', size: 24 %}Drawn for OBALUS on the Lucide grid: 24px, 2px stroke, round caps and joins, currentColor. Orange when an icon stands for a product; black inside text.
Interface icons (88)
{% render 'hdzb-icon', name: 'truck', size: 22, stroke: 2 %}


