Safer Lighting
Sister company to Safer Electric · ESA Licence #7008996 · Real GTA warehouse · Trade contractor accounts
Safer Lighting wholesales LED commercial fixtures, electrical boxes, PVC conduit, and trade hardware. The store ships from a real GTA warehouse. The brief: build a B2C-friendly storefront that doesn't blow the trust of professional contractors who need wholesale pricing, quantity breaks, and account-tagged discounts — without forcing them through a 72-hour login wall like the established players in the category do.
This isn't a Dawn install with a custom logo. It's a B2B catalog with two-tier pricing, manual contractor approval, classic-customer-accounts kept on the canonical domain (no shopify.com redirect), a full design-system reskin, and a Python pipeline going QuickBooks → CSV → Shopify import.
Custom Shopify 2.0 Theme on a Dawn Fork
We started from Dawn for the accessibility floor and the Shopify 2.0 sectioning, then rebuilt every surface in our own design system. ~68 sections, ~50 snippets, all custom or Dawn-rewritten. JSON section templates so every page is editable from the admin without a code touch. Vanilla JS only — zero jQuery, zero unnecessary frameworks. CSP-friendly.
Two-Tier B2B Pricing — The Hard Part
Retail customers see cost × 2.24. Approved contractors see cost × 2.16 (~3% lower at the same SKU, applied automatically when logged in and tagged). Display compare-at: sale ÷ 0.775, so every product anchors at ~22.5% off. Quantity breaks: −8% at 5 units, −15% at 25, −22% at 100. The custom snippets/price.liquid reads a pricing.b2b_price metafield and swaps the active price + strikethrough automatically when the logged-in customer carries a trade tag.
Manual Contractor Approval Workflow
Application form on /pages/contractor-application collects company name, business email, and ESA licence #. Submissions email the admin, who verifies the licence number against Ontario's public ESA contractor directory and then tags the customer approved-contractor from Shopify admin. No self-grant possible — contractors can't sign up and claim trade pricing themselves.
The Transparency Moat
Arani — the dominant competitor in this category — hides every price behind a 24-72 hour B2B login wall. Public Shopify pricing wins the contractor with a bid due in 4 hours. That's a structural moat against the established players, not just a feature. The site still protects trade pricing for approved accounts; it just doesn't gatekeep the retail catalog.
Custom Semantic Search
<safer-search> is a custom element in assets/safer-search.js. Predictive search with 50+ synonym groups mapping shopper terms to technical product types. A homeowner typing 'pot light' finds the recessed downlight catalog page. A contractor typing 'high bay' finds the UFO highbay. 'Wall pack' resolves to outdoor wallpack fixtures. Mobile uses a persistent horizontal search bar pinned under the header — not Dawn's modal-style search.
Python + Node Data Pipeline
scripts/build-products.mjs generates the canonical catalog. import-products.cjs pushes to Shopify's product import API. create-collections, create-pages, create-redirects, create-metafield-definitions are all idempotent admin scripts. Pricing CSVs flow QuickBooks export → cost-keyed pricing strategy CSV → Shopify-format import CSV. One Python script. Deterministic. Re-runnable any time prices change.
The receipts. Three concrete bug-fix-and-redesign moments. Anyone can stand up a Shopify theme. Diagnosing layered failures and fixing them at root cause is the work.
Pricing display bug — two stacked failures
The storefront was rendering compare-at-price with the same value as sale price, so every product looked struck through with the same number. Two stacked bugs: a CSS override forced strikethrough on every .price-item--regular regardless of sale state, AND the import data had blank Compare-at on every row. Diagnosed via live HTML inspection. Fixed at root cause — CSS gated to .price--on-sale, data regenerated from the pricing CSV with proper compare-at math.
Collection grid orphan card
10 products in a 4-column grid produced an orphan row of 2 — and Dawn's flex-based grid stretched the orphans to 100% via flex-grow: 1. Fixed by forcing CSS Grid (display: grid) at every breakpoint with explicit grid-template-columns: repeat(N, 1fr) so orphans always sit at exactly 1/N of the row.
Mobile drawer overlap with :has() and a JS fallback
The bottom phone-CTA, signup row, and Quick Order FAB all bled through the Shop submenu when it slid in. Root cause: Dawn's stock CSS hides the outer .menu-drawer__menu via .submenu-open with visibility:hidden, but the CTAs are siblings of the menu, not children. Fix: :has(.submenu-open) selectors on the wrapper containers, plus a JS-class-based fallback (body.overflow-hidden-tablet) for browsers that don't yet support :has().
// Same DNA as Safer Electric and Safer Inventory
The Safer Brand Design System lives at assets/brand-typography.css and assets/silicon-valley.css. One system, two production properties, three apps in the inventory monorepo — all consuming the same tokens.
Navy #0c1e33. Gold #d6b059 used sparingly — never as fill. Cream #f0ede6 over navy, never pure white. Cormorant Garamond for headlines (italic for emphasis). Plus Jakarta Sans for body and UI. JetBrains Mono for SKUs, prices, and licence numbers. A 3D ThreeJS hero canvas reads document.documentElement.dataset.theme and swaps particle colors live when the user toggles dark/light.
// The right tool for the job
Liquid + vanilla JS, by design. We could have shipped a headless React storefront. We chose not to — Shopify's sectioning model gives the merchant control without code, the SSR is bulletproof, and the build deploys via Shopify's GitHub integration in ~30 seconds. Right tool, not newest tool.