Protocol Raw Feed Calculator v7.0 - Final Documentation¶
Created: November 10, 2025 Updated: April 10, 2026 Status: ✅ Production Ready Version: 7.2 FINAL
Table of Contents¶
- Executive Summary
- What Changed (v6.4 → v7.0)
- What Changed (v6.1 → v6.4)
- What Changed (v6.0 → v6.1)
- What Changed (v5.5 → v6.0)
- Technical Specifications
- Delivery Frequency Logic
- Box Recommendation Logic
- Alternative Box Display Logic
- Smaller First Delivery Option
- Pricing Display Strategy
- Freezer Guidance
- Visual Identity Alignment
- Testing & Validation
- Implementation Checklist
- Files & Code Reference
- Version History
Executive Summary¶
What This Is¶
The Protocol Raw Feed Calculator v7.0 is a warm, personalised, step-by-step feeding plan calculator that converts premium upgraders into committed Protocol Raw customers. v7.0 replaces the single-screen form with a progressive 5-step guided flow, captures the dog's name for end-to-end personalisation, and redesigns the results section into a warm commercial layout.
It uses:
- One-step-at-a-time input flow — only the current step is visible, with fade-in animation and browser back button integration
- Dog name personalisation throughout headers, results, CTAs, and product page
- Server-side computation — all calculation logic (RER, MER, box recommendation, pricing, treat adjustments) runs in the calculator-compute-plan Edge Function, not in the browser
- Veterinary-standard RER × MER methodology for adult/senior dogs
- Senior-specific MER factors (~10% reduction reflecting metabolic slowdown)
- Puppy-specific multipliers (3.0×, 2.0×, 1.8× based on age)
- Lab-verified 1900 kcal/kg formula for precise gram calculations
- Warm commercial results layout with plan card, price card, trust grid, and "How It Works at Home"
- Smart box recommendations with value-optimised alternatives
- 5 frequency options (2, 3, 4, 5, 6 weeks) ensuring optimal delivery timing
- Storage guidance personalised with dog name
- Freezer guidance addressing pre-purchase anxiety without creating friction
Key Metrics¶
- 15kg adult anchor: £24/week ✅
- Box-2 retention target: >70%
- Calculator completion rate: >60%
v7.2 Highlights (latest)¶
- ✅ Server-side computation — all calculation constants, formulas, and recommendation logic moved from client-side JavaScript to the
calculator-compute-planSupabase Edge Function - ✅ Formulas no longer exposed in browser — MER multipliers, BCS multipliers, puppy multipliers, treat multipliers, pricing constants, box recommendation algorithm, and frequency algorithm are all server-side
- ✅ Pre-computed treat variants — the Edge Function returns all three treat level results (none/some/lots) in a single response, so treat toggle is instant with no additional API call
- ✅ Input-only client —
feed-calculator.liquidcollects form inputs and POSTs to the Edge Function; no calculation occurs in the browser - ✅ Server-validated inputs — weight bounds (0.5–120kg), valid enum values for life stage, activity, body condition, and puppy age group are validated server-side
- ✅ Token generation uses server-computed values —
calculator-generate-token-multipetnow receives plan data computed by the Edge Function rather than client-computed values
v7.1 Highlights¶
- ✅ One-step-at-a-time flow — only the current step is visible (replaced progressive disclosure where all completed steps stayed visible)
- ✅ "← Back" links on Steps 1-4 for discoverable backward navigation
- ✅ Clickable completed progress dots — navigate to any previously completed step
- ✅ Browser back button navigates between calculator steps instead of leaving the page (history.pushState/popstate)
- ✅ Results edit button — "Bella's plan. Edit" pill above results returns to step flow with all inputs pre-filled
- ✅ Session persistence — sessionStorage preserves calculator results across page navigation (30-minute TTL); back from product page restores results instantly
- ✅ Pre-filled inputs on edit — dog name, life stage, weight, activity, body condition, and neutered toggle all restored when returning from results
- ✅ Price context simplified — "First box £XX · then £XX per box" (removed delivery frequency)
- ✅ Product page alt box sizing fix — correct alternative box shown (e.g. 12kg for 8kg, not hardcoded 16kg)
- ✅ Treat adjustment frequency fix —
recalculateWithTreats()recalculates delivery frequency viagetOptimalFrequency()instead of passing stale weeks - ✅ Results header update — header shows "YOUR FEEDING PLAN / Bella's feeding plan" when results display, restores step-specific text on edit
- ✅ Removed weight dial locking — no longer needed since only one step is visible at a time
v7.0 Highlights¶
- ✅ Progressive 5-step input flow replacing single-screen form
- ✅ Dog name capture as Step 0 with personalisation throughout
- ✅ Dynamic header that reacts to dog name and current step
- ✅ Sticky progress dots with step completion tracking
- ✅ Personalised loading screen ("Calculating Bella's plan")
- ✅ Results section redesign: plan card with dark header, price card, trust grid
- ✅ Personalised CTAs: "Start Bella's Plan" throughout
- ✅ "How It Works at Home" section with storage/serving/verify steps
- ✅ Storage guidance with dog name: "Keep Bella's trays frozen"
- ✅ Alternative box display redesigned as prominent/subtle cards
- ✅ "Switching is Simple" reassurance section on product page (transition plan → human support → safety net)
v6.4 Highlights¶
- ✅ "Smaller first delivery" option for 12kg customers (start with 8kg, move to 12kg from Box 2)
- ✅ New URL parameters:
optimal_boxandfirst_order_downsize - ✅ Treat adjustment correctly hides/restores first delivery option
v6.1 Highlights¶
- ✅ Added buffer/flexibility line to product page calculator banner
- ✅ Shows calculated days supply with subscription flexibility messaging
v6.0 Highlights¶
- ✅ Added freezer guidance as integrated line in box details
- ✅ Changed "pouches" to "trays" throughout
- ✅ Fixed Cream colour (#FEFDF7 → #F9F7F4) per Visual Identity Guide v2.2
- ✅ Fixed Warm Gray → Taupe for three text elements (better readability)
What Changed (v6.4 → v7.0)¶
1. Progressive Step-by-Step Input Flow¶
Purpose: Replace the single-screen form (all inputs visible at once) with a guided 5-step flow that feels conversational and builds commitment incrementally.
Step structure:
| Step | Content | Advance Trigger |
|---|---|---|
| 0 — Dog Name | Text input (placeholder "e.g. Kai") | "Continue with [name]" button, Enter key, or "Skip this step" link |
| 1 — Life Stage | Three radio cards: Puppy (under 24 months) / Adult / Senior | Auto-advance 400ms after non-default selection, or "Continue" button |
| 1b — Puppy Age (puppies only) | Three radio cards: 0–4 months / 4–12 months / 12–24 months | Auto-advance 400ms after selection |
| 2 — Weight | Weight dial (slider + minus/plus buttons) | "[weight] kg, that's right" confirm button |
| 3 — Activity Level | Card-button grid (4 options for adult, 2 for puppy) | Auto-advance 400ms after selection. For puppies, Calculate button appears here |
| 4 — Body Condition + Neutered | Three SVG dog silhouette cards + Yes/No neutered toggle | Calculate button (adult/senior dogs) |
Puppy step sequence: When "Puppy" is selected at Step 1, the step sequence becomes 0 → 1 → 1b (puppy age) → 2 → 3. Step 4 (BCS + Neutered) is skipped for puppies. The puppy age step uses the same radio-card styling as the life stage step and determines the MER multiplier: 3.0x (0–4 months), 2.0x (4–12 months), 1.8x (12–24 months).
CSS mechanics (v7.1 — one-step-at-a-time):
.step-wrapper {
display: none;
opacity: 0;
transform: translateY(16px);
}
.step-wrapper.active {
display: block;
opacity: 1;
transform: translateY(0);
animation: stepFadeIn 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards;
margin-bottom: 40px;
}
@keyframes stepFadeIn {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
Only the current step is visible at any time. display: none hides inactive steps completely (no accidental interaction). Controlled by currentStep integer and advanceStep() function, which toggles the .active class: el.classList.toggle('active', stepNum === currentStep).
Back navigation (v7.1): Each step (1-4) has a "← Back" text link below the Continue/confirm button. Taupe colour (#6B6360), Inter 14px, underline on hover. Completed progress dots are also clickable to jump to any previous step.
.step-back-link {
font-family: 'Inter', sans-serif;
font-size: 14px;
color: #6B6360;
background: none;
border: none;
cursor: pointer;
padding: 8px 0;
margin-top: 12px;
display: block;
text-align: center;
}
.step-back-link:hover { text-decoration: underline; }
.progress-dot.completed { cursor: pointer; }
.progress-dot.completed:hover { opacity: 0.8; }
Browser back button (v7.1): advanceStep() pushes a history.pushState({ calcStep: N }) entry on each step transition. A popstate listener catches the browser back button and calls advanceStep() with the previous step number, using an isPopState flag to prevent duplicate history entries. On page load, history.replaceState({ calcStep: 0 }) sets the initial state. Results push { calcStep: 'results' } — back from results returns to the last input step (Step 4 for adult/senior, Step 3 for puppy).
Weight dial locking: Removed in v7.1. No longer needed since only one step is visible at a time — the weight slider is completely hidden (display: none) when the user is on any other step.
Multipet trigger: "Have multiple dogs? Calculate for the whole pack" appears from Step 1 onwards as a dashed-border prompt, only for single-pet adult/senior flows.
2. Dog Name Capture (Step 0)¶
Purpose: Capture the dog's name upfront so the entire calculator experience — headers, results, CTAs, and the downstream product page — can be personalised.
Implementation:
- White card with text input (id="dog-name-input")
- If name entered: "Continue with [name]" button
- If no name: "Skip this step" ghost link
- Enter key also advances
- Name stored in dogName variable, passed to product page as dog_name URL parameter
Fallback: All personalised copy gracefully falls back to generic when dogName is empty ("Your dog" / "Your plan" / etc.).
3. Dynamic Header Personalisation¶
updateHeader() function updates the main heading and subtitle on every step advance:
| Step | Title (no name) | Title (with "Bella") |
|---|---|---|
| 0 | "Let's find the right plan." | "Let's find the right plan." |
| 1 | "Tell us about your dog." | "Nice to meet Bella." |
| 2 | "How much does your dog weigh?" | "How much does Bella weigh?" |
| 3 | "What's your dog's day like?" | "What's Bella's day like?" |
| 4 | "Almost done." | "Almost done." |
Subtitle uses possessive: "Every answer helps us get Bella's portion exactly right."
4. Progress Dots¶
Five pill-shaped dots rendered above the input area:
<div class="progress-dots">
<div class="progress-dot current"></div>
<div class="progress-dot"></div>
<!-- ... -->
</div>
| State | Style |
|---|---|
| Default | 8×8px circle, rgba(0,0,0,0.12) |
| Current | 24×8px pill, burnt sienna (#B85C3A) |
| Completed | 8×8px circle, burnt sienna at 50% opacity |
Sticky on scroll: position: sticky; top: 80px; z-index: 10; background: var(--col-warm-linen);
Hidden on Calculate: progressDots.style.display = 'none' called before showing the loading screen. Note: the .progress-dots CSS must not use display: flex !important — !important overrides inline style changes from JS.
5. Personalised Loading Screen¶
When Calculate is clicked, the loading title adapts:
let loadingText = "Calculating your dog's plan";
if (petCount > 1) {
loadingText = "Calculating your dogs' plan";
} else if (dogName) {
loadingText = `Calculating ${dogName}'s plan`;
}
6. Results Section Redesign — Warm Commercial Layout¶
What was replaced: Three stat cards (grams / kcal / tray count) + text list of box details.
New results structure (in order):
A. Personalised Results Header¶
- Small uppercase label (burnt sienna): "[Name]'s feeding plan" or "Your Dog's Personalised Feeding Plan"
- Large animated counter: "[grams]g per day" (counts up from 0)
- Supporting line: "Calculated from [Name]'s weight, activity level and life stage."
- Meta line: "[kcal] kcal daily · ¾ tray per serving"
B. Plan Card (dark header + white body)¶
- Dark espresso header: "[Name]'s Plan" (left) + box size badge e.g. "12kg Box" (right)
- Card body: Visual tray row (SVG icon + "24 trays of 500g" + duration/delivery line + "Pause, skip or cancel anytime")
- 2-column info grid: Freezer icon + "Fits 1.5 freezer drawers" / QR icon + "Batch tested safe · Scan QR for lab results"
- Storage bar: "No preservatives. Keep [Name]'s trays frozen. Once defrosted, use within 4 days. Once opened, use within 3 days."
C. Price Card¶
- Large price hero: "£24" (burnt sienna, Montserrat 48px) + "per week"
- Context line: "£109 per box · delivered every 4 weeks"
- First box offer pill: "[First box] £89 · that's £19/week to start"
- Primary CTA button (burnt sienna, full width): "Start [Name]'s Plan →"
D. Treat Adjustment (after primary CTA, before trust grid)¶
- Compact section: "Do [Name]'s daily calories include treats?"
- Three radio options: No treats most days / Some treats / Lots of treats
- Delta feedback: "Adjusted to 340g/day to leave room for treats"
- Title personalised with
singleDogName
E. Multipet / Puppy Education (conditional)¶
- Multipet: Individual dog breakdown grid
- Puppies: FEDIAF/Ca:P/protein/DHA education cards + growth plan reminder
F. Trust Grid (2×2 compact)¶
- FEDIAF verified nutrition
- Every batch lab tested
- 10-day transition plan included
- UKAS accredited testing
G. "How It Works at Home" Section¶
Three-column grid:
- Store frozen — "Stack trays in your freezer. Move a day's worth to the fridge the night before."
- Serve fresh — "Scoop [Name]'s [grams]g portion into the bowl. That's it." (personalised via
id="hiw-serve-desc") - Scan & verify — "Scan the QR code on the box to see your batch's lab results."
H. "Switching is Simple" Reassurance (Product Page Only)¶
Centred, containerless reassurance section on the product page addressing commitment anxiety. Three escalating levels — transition plan, human support, soft safety net — designed as calm prose, not a feature list.
- Label: "SWITCHING IS SIMPLE" (Burnt Sienna, 11px Montserrat, uppercase, letter-spaced)
- Body: Three lines in Taupe at 14px Inter, narrower max-width (460px) for visual intimacy
- Personalised: "...portions calculated for [Name]" / "If [Name] needs more time to adjust..."
- No background, card, border, or icons — breathes between What's in the Box and the CTA
Design rationale: Not a money-back guarantee. Layered reassurance that addresses commitment anxiety without cheapening the brand. Reads as a conversation, not a checklist.
I. Results Edit Summary (v7.1)¶
Displayed above results: "Bella's plan. Edit" (or "Your plan. Edit" if no name). The Edit button is a pill-shaped element with a 1px border (#D5CEC9, 6px radius) for visibility.
.results-edit-summary { font-size: 13px; color: #918A85; text-align: center; margin: 0 0 24px 0; }
.results-edit-link { font-size: 13px; font-weight: 500; color: #6B6360; border: 1px solid #D5CEC9; border-radius: 6px; padding: 4px 12px; }
Clicking Edit:
1. Clears sessionStorage results
2. Hides results div, shows progress dots
3. Calls restoreInputStates() — pre-fills all DOM inputs from JS state variables (dog name, life stage radio, weight slider, activity card, body condition card, neutered toggle)
4. Calls advanceStep(0) — returns to Step 0 with "Continue with Kai" already showing
The restoreInputStates() function synchronises JS state → DOM for all steps so the user is editing their previous answers, not starting over.
J. Secondary CTA¶
Link-style button: "Start [Name]'s Plan →"
7. Session Persistence (v7.1)¶
Calculator results are persisted in sessionStorage so that navigating to the product page and pressing back restores results instantly instead of showing a blank calculator.
Data saved (after Calculate):
sessionStorage.setItem('calculator_results', JSON.stringify({
dogName, lifeStage: currentLifeStage,
weight: parseFloat(weightDialSlider.value),
activity: stepActivity, bodyCondition: stepBodyCondition, neutered: stepNeutered,
plan: fullPlan, treatLevel: currentTreatLevel,
timestamp: Date.now()
}));
Restore logic (on DOMContentLoaded):
- Reads calculator_results from sessionStorage
- Checks 30-minute TTL (Date.now() - timestamp < 30 * 60 * 1000)
- Restores state variables (dogName, lifeStage, weight, activity, bodyCondition, neutered)
- Calls restoreInputStates() to pre-fill all DOM inputs
- Hides step flow, shows results with saved plan data
- Restores treat level selection if changed
- Sets history.replaceState({ calcStep: 'results' }) so back button leaves the page
Cleared when: User clicks Edit from results, or sessionStorage is cleared by the browser (tab close).
8. Alternative Box Display Redesign¶
Alternative boxes are now displayed as styled card elements rather than text links:
- Prominent alternative (
id="alternative-box-prominent"): "Better Value Option" card with "Switch to 16kg Box · £17/week" CTA - Subtle alternative (
id="alternative-box-subtle"): Text link — "Want better value? View 8kg option (£21/week, every 2 weeks)" - First delivery option (
id="first-delivery-option"): "Smaller first delivery? Start with 8kg, then move to 12kg from Box 2"
Shown/hidden by updateResults() based on box recommendation logic (unchanged from v6.4).
8. Personalised CTAs Throughout¶
Primary and secondary CTAs update in updateResults():
Dog name passed to product page URL:
9. Warm Design System Variables¶
All components use consistent CSS custom properties:
| Variable | Value | Usage |
|---|---|---|
--col-espresso |
#2B2523 |
Dark headers, body text |
--col-burnt-sienna |
#B85C3A |
CTAs, active states, labels |
--col-burnt-sienna-dark |
#9F4D2F |
CTA hover state |
--col-forest-green |
#2D5144 |
Trust icons, success states |
--col-cream |
#F9F7F4 |
Section backgrounds |
--col-warm-linen |
#EBE8E3 |
Card borders, inactive dots, progress bar background |
--col-stone |
#C4BCB0 |
Subtler text |
--col-warm-gray |
#918A85 |
Placeholder, meta text |
--col-taupe |
#6B6360 |
Body text, descriptions |
What Was Not Changed (v7.0 → v7.1)¶
The following remain untouched:
- Step content (HTML inside each step)
- The {% schema %} block
- The {% for block in section.blocks %} loop logic
- The Seal Subscriptions frequency-setting logic (setSealFrequency)
- The token/claimed button override flow
- The modal for unclaimed tokens
- All Liquid template logic
- The underlying calculation engine (RER × MER methodology)
- Box recommendation algorithm
- Delivery frequency logic (getOptimalFrequency)
- Pricing constants
- Auto-advance on Steps 1 (non-adult 400ms) and 3 (activity 400ms)
- Loading screen animation
- Treat adjustment UI and recalculation logic (only frequency passing fixed)
What Changed (v6.1 → v6.4)¶
1. Smaller First Delivery Option¶
Purpose: Allow 12kg customers to start with an 8kg first delivery to ease freezer space anxiety, then transition to their optimal 12kg box from Box 2 onwards.
Trigger: Only appears when recommendedBox === '12kg' && alternativeBox === '16kg' (medium-consumption dogs, roughly 250-470g/day).
Implementation: Four code changes to feed-calculator.liquid:
-
HTML (line 411-418): New
first-delivery-optiondiv reusing existingalternative-option-subtleclass. Zero new CSS required. -
calculateBoxRecommendation(): Calculates first delivery data (box, days supply, weeks, weekly price) only when the trigger condition is met. Returns five new properties in the plan object.
-
updateResults(): Shows/hides the first delivery element and builds the product page URL with
optimal_box=12kgandfirst_order_downsize=trueparameters. -
recalculateWithTreats(): Hides the first delivery option when treat adjustment flips the alternative from 16kg value to 8kg convenience (avoids duplicate 8kg links). Restores it when the original alternative remains valid.
Display: Subtle text link below the existing alternative box option:
Copy rationale: "Smaller first delivery?" is a question (not a recommendation). Shows both ongoing (strikethrough) and first-box weekly prices so the customer sees the real cost. Never framed as "trial" or "sample".
Design principle applied: Practical logistics, not emotional hand-holding. Same philosophy as freezer guidance — answer the concern without creating a decision problem.
2. New URL Parameters¶
Two new parameters added to the first delivery CTA link:
| Parameter | Value | Purpose |
|---|---|---|
optimal_box |
12kg |
Calculator's actual recommendation |
first_order_downsize |
true |
Flags this as a downsized first order |
These are consumed by the product page (implemented in v7.0 warm product page experience) and stored in the subscription record to trigger the post-Box-1 upgrade email.
What Changed (v6.0 → v6.1)¶
1. Product Page Buffer/Flexibility Line¶
Purpose: Show customers how long their box lasts and that they have full subscription flexibility, reducing purchase hesitation at the point of commitment.
Implementation: Single line added below the data grid in the product page calculator banner (main-product.liquid):
BOX SIZE 16KG
DAILY FEED 475g
DELIVERY Every 4 weeks
Lasts approximately 33 days · skip, pause or change frequency anytime
Calculation: Math.floor(boxGrams[boxSize] / parseInt(dailyGrams))
Design decisions:
- Information only, no link or decision point
- Uses Taupe colour (#6B6360) at 13px — same visual weight as freezer guidance in calculator
- Separated from data grid by subtle 1px border-top
- Hidden by default (display:none), shown only when calculator URL params are present
- Displays for both single-pet and multi-pet views
Copy rationale: "Lasts approximately X days" makes the buffer self-evident (e.g. 33 days vs 28-day cadence = 5 days spare). "Skip, pause or change frequency anytime" gives three concrete actions that neutralise lock-in anxiety. No mention of "portal" (customers don't know what that is pre-purchase).
Design principle applied: Information without friction. Same philosophy as calculator freezer guidance.
CSS:
.calc-buffer-line {
font-family: 'Inter', sans-serif;
font-size: 13px;
color: var(--col-taupe, #6B6360);
margin: 12px 0 0 0;
padding-top: 12px;
border-top: 1px solid rgba(43, 37, 35, 0.08);
line-height: 1.4;
}
What Changed (v5.5 → v6.0)¶
1. Freezer Guidance Added¶
Purpose: Address pre-purchase anxiety about freezer space without creating a decision point.
Implementation: Simple third line in box details block:
Design decisions:
- Information only, no link or alternative suggestion
- Same visual weight as duration line (secondary info)
- Uses Taupe colour (#6B6360) - not highlighted
- Drawer estimates: 8kg = 1, 12kg = 1.5, 16kg = 2
Rationale: Customers who can't accommodate the freezer space aren't our customers. This line answers the question calmly without framing it as a problem or offering a workaround that adds friction.
Design principle applied: Information without friction. Freezer guidance answers the question without creating a decision point.
2. Terminology: "Pouches" → "Trays"¶
All references to packaging updated to reflect actual product format:
| Location | Old | New |
|---|---|---|
| Example anchor | "24 pouches" | "24 trays" |
| Box contents | "24 pouches (500g each)" | "24 trays (500g each)" |
| Portion card | "pouch per day" | "tray per day" |
| PRICING constant | pouches: 24 |
trays: 24 |
3. Visual Identity Alignment¶
Cream colour corrected:
/* Was (incorrect) */
--col-cream: #FEFDF7;
/* Now (per Visual Identity Guide v2.2) */
--col-cream: #F9F7F4;
Warm Gray → Taupe for better readability:
Per Visual Identity Guide: "Warm Gray #918A85 only for meta/secondary elements. Espresso #2B2523 for body text (NOT Warm Gray)."
Changed three instances where Warm Gray was used for readable content:
| Element | Old | New |
|---|---|---|
.portion-unit |
--col-warm-gray |
--col-taupe |
.badge-content p |
--col-warm-gray |
--col-taupe |
.price-period-hero |
--col-warm-gray |
--col-taupe |
Technical Specifications¶
Server-Side Computation (v7.2)¶
As of v7.2, all calculation logic lives in the calculator-compute-plan Supabase Edge Function. The client-side JavaScript in feed-calculator.liquid collects form inputs only and POSTs them to the Edge Function. No formulas, multiplier tables, or pricing constants exist in the browser.
Why: Prevents exposure of proprietary multiplier calibrations, pricing strategy, and recommendation algorithm in the page source. The underlying methodology (RER × MER) is published veterinary science and is not secret — but the specific multiplier values, box thresholds, and frequency algorithm are business decisions that benefit from server-side encapsulation.
Methodology (unchanged)¶
The calculator uses veterinary-standard Resting Energy Requirement (RER) × Maintenance Energy Requirement (MER) methodology:
- RER — basal metabolic rate derived from body weight using the standard allometric formula
- MER — RER adjusted by activity level, neutered status, body condition, and life stage (adult/senior/puppy)
- Daily grams — MER converted to grams using Protocol Raw's lab-verified energy density (1900 kcal/kg), rounded to nearest 25g
- Box recommendation — optimal box size selected based on daily grams, with delivery frequency optimised for longest viable cadence with a minimum 3-day buffer
- Treat adjustment — daily portion reduced by 10% (some) or 20% (lots) to leave caloric room for treats
All multiplier values, pricing constants, and algorithm parameters are defined in the Edge Function source code at supabase/functions/calculator-compute-plan/index.ts.
API Contract¶
Endpoint: POST /functions/v1/calculator-compute-plan
Request payload:
{
"pets": [
{
"pet_number": 1,
"pet_name": "Bella",
"weight_kg": 15,
"life_stage": "adult",
"activity_level": "moderate",
"body_condition": "ideal",
"neutered": true,
"puppy_age_group": null
}
]
}
| Field | Type | Required | Values |
|---|---|---|---|
pet_number |
integer | Yes | Sequential (1, 2, 3...) |
pet_name |
string | Yes | Dog's name or "Dog N" |
weight_kg |
number | Yes | 0.5–120 |
life_stage |
string | Yes | puppy, adult, senior |
activity_level |
string | Yes | Adults: low, moderate, high. Puppies: normal, very-active |
body_condition |
string | Adults only | underweight, ideal, overweight |
neutered |
boolean | Adults only | true / false |
puppy_age_group |
string | Puppies only | puppy_0_4, puppy_4_12, puppy_12_24 |
Response payload:
{
"success": true,
"plan": {
"petData": [
{
"pet_number": 1,
"pet_name": "Bella",
"life_stage": "adult",
"weight_kg": 15,
"calculated_rer": 534,
"calculated_mer": 710,
"base_mer": 710,
"daily_grams": 375,
"monthly_price_gbp": 45,
"age_range": null,
"neutered": true,
"activity_level": "moderate",
"body_condition": "ideal"
}
],
"householdDailyGrams": 375,
"householdDailyCalories": 710,
"householdTotalGbp": 45,
"recommendedBox": "12kg",
"alternativeBox": "16kg",
"alternativeType": "value",
"showProminently": false,
"daysSupply": 32,
"weeks": 4,
"trays": 24,
"boxPrice": 109,
"firstDeliveryPrice": 89,
"weeklyPrice": 24,
"firstWeeklyPrice": 19,
"altWeeklyPrice": 22,
"altFirstWeeklyPrice": 18,
"altWeeks": 5,
"altDaysSupply": 42,
"altURL": "/products/protocol-raw-complete-16kg-box",
"showFirstDeliveryOption": true,
"firstDeliveryBox": "8kg",
"firstDeliveryDaysSupply": 21,
"firstDeliveryWeeks": 2,
"firstDeliveryWeeklyPrice": 23,
"firstDeliveryRegularWeekly": 30,
"firstDeliveryFirstWeekly": 23,
"freezerInfo": { "trays": 24, "drawers": "1.5" },
"treatVariants": {
"none": { "treatLevel": "none", "householdDailyGrams": 375, "householdDailyCalories": 710, "pets": [...], "recommendation": {...} },
"some": { "treatLevel": "some", "householdDailyGrams": 325, "householdDailyCalories": 639, "pets": [...], "recommendation": {...} },
"lots": { "treatLevel": "lots", "householdDailyGrams": 300, "householdDailyCalories": 568, "pets": [...], "recommendation": {...} }
}
}
}
Key response properties:
treatVariants— only included for single non-puppy dogs. Contains pre-computed plan for each treat level (none/some/lots), each with its own fullrecommendationobject (box, frequency, pricing, alternatives, freezer info). The UI toggles between these instantly without re-calling the API.freezerInfo— included at top level and within each treat variant's recommendation. Containstraysanddrawersfor the recommended box.base_mer— stored on each pet for the token generator; represents unadjusted MER before treat multiplier.
Input Validation¶
The Edge Function validates all inputs server-side:
- Weight: 0.5–120kg
- Life stage: puppy, adult, senior
- Activity level: low, moderate, high (adults) or normal, very-active (puppies)
- Body condition: underweight, ideal, overweight (adults/seniors only)
- Puppy age group: puppy_0_4, puppy_4_12, puppy_12_24 (puppies only)
- Maximum 10 pets per request
Delivery Frequency Logic¶
Note (v7.2): The frequency selection algorithm now runs server-side in
calculator-compute-plan. The product page independently recalculates frequency from box size and daily grams as a consistency check (using its own copy ofgetOptimalFrequency()). The logic described below is unchanged — only its execution location has moved.
Available Frequencies¶
| Weeks | Days | Typical Use Case |
|---|---|---|
| 6 | 42 | Very small dogs (Chihuahuas, toy breeds) |
| 5 | 35 | Small dogs (Cavaliers, small Cockapoos) |
| 4 | 28 | Medium dogs (Spaniels, Beagles) - default |
| 3 | 21 | Large dogs (Labradors, Goldens) |
| 2 | 14 | Giant dogs (Great Danes, Mastiffs) |
Selection Algorithm¶
function getOptimalFrequency(daysSupply) {
const frequencies = [6, 5, 4, 3, 2]; // weeks, longest first
const minBuffer = 3; // minimum days buffer before running out
for (const weeks of frequencies) {
const deliveryDays = weeks * 7;
if (daysSupply >= deliveryDays + minBuffer) {
return weeks;
}
}
return 2; // fallback to minimum frequency
}
Selection Logic Explained¶
The algorithm picks the longest viable frequency (to minimise delivery frequency and handling) while ensuring the customer never runs out (minimum 3-day buffer).
Example: 200g/day dog with 8kg box - Days supply: 8000 ÷ 200 = 40 days - Check 6 weeks: 40 ≥ 42 + 3? No (40 < 45) - Check 5 weeks: 40 ≥ 35 + 3? Yes (40 ≥ 38) ✓ - Result: 5 weeks
Complete Frequency Mapping¶
| Days Supply | 6wk (45+) | 5wk (38+) | 4wk (31+) | 3wk (24+) | 2wk (17+) | Selected |
|---|---|---|---|---|---|---|
| 53 days | ✓ | - | - | - | - | 6 weeks |
| 45 days | ✓ | - | - | - | - | 6 weeks |
| 40 days | ✗ | ✓ | - | - | - | 5 weeks |
| 38 days | ✗ | ✓ | - | - | - | 5 weeks |
| 35 days | ✗ | ✗ | ✓ | - | - | 4 weeks |
| 32 days | ✗ | ✗ | ✓ | - | - | 4 weeks |
| 25 days | ✗ | ✗ | ✗ | ✓ | - | 3 weeks |
| 17 days | ✗ | ✗ | ✗ | ✗ | ✓ | 2 weeks |
Box Recommendation Logic¶
Note (v7.2): The box recommendation algorithm now runs server-side in
calculator-compute-plan. The logic described below is unchanged — only its execution location has moved.
Algorithm¶
function calculateBoxRecommendation(dailyGrams, totalCalories) {
const boxOptions = [
{ size: '8kg', grams: 8000, price: 89 },
{ size: '12kg', grams: 12000, price: 109 },
{ size: '16kg', grams: 16000, price: 129 }
];
// Find smallest box that lasts at least 2 weeks with 3-day buffer
for (const box of boxOptions) {
const daysSupply = Math.floor(box.grams / dailyGrams);
if (daysSupply >= 17) { // 14 days + 3 day buffer
return {
recommendedBox: box.size,
daysSupply: daysSupply,
frequency: getOptimalFrequency(daysSupply),
trays: box.grams / 500,
// ... other properties
};
}
}
// Fallback: largest box with 2-week frequency
return { recommendedBox: '16kg', frequency: 2, ... };
}
Box Selection Matrix¶
| Daily Grams | 8kg Days | 12kg Days | 16kg Days | Recommended |
|---|---|---|---|---|
| ≤200g | 40+ | 60+ | 80+ | 8kg |
| 201-300g | 26-39 | 40-59 | 53-79 | 8kg or 12kg |
| 301-470g | 17-26 | 25-39 | 34-52 | 12kg |
| 471-940g | <17 | 12-25 | 17-33 | 16kg |
| >940g | <17 | <17 | <17 | 16kg (2wk) |
Alternative Box Display Logic¶
The Rules¶
if (recommendedBox === '8kg') {
// Check if 12kg alternative has acceptable buffer (≤7 days)
const alt12kgFrequency = getOptimalFrequency(daysSupply12kg);
const alt12kgBuffer = daysSupply12kg - (alt12kgFrequency * 7);
if (alt12kgBuffer <= 7) {
alternativeBox = '12kg';
alternativeType = 'value';
showProminently = true;
} else {
// 12kg creates too much excess food — no alternative for smallest box
alternativeBox = null;
alternativeType = null;
showProminently = false;
}
} else if (recommendedBox === '12kg') {
// Check if 16kg alternative has acceptable buffer (≤7 days)
const alt16kgFrequency = getOptimalFrequency(daysSupply16kg);
const alt16kgBuffer = daysSupply16kg - (alt16kgFrequency * 7);
if (alt16kgBuffer <= 7) {
alternativeBox = '16kg';
alternativeType = 'value';
showProminently = false;
} else {
// 16kg creates too much excess food — show 8kg as convenience
alternativeBox = '8kg';
alternativeType = 'convenience';
showProminently = false;
}
} else {
// Large dogs (16kg): show 12kg subtly as convenience
alternativeBox = '12kg';
alternativeType = 'convenience';
showProminently = false;
}
Alternative Box Buffer Rule¶
An alternative box is only shown if its buffer (days supply minus cadence days) is ≤7 days. This prevents recommending boxes that create excessive freezer accumulation.
If buffer > 7 days: - 12kg recommended → 16kg alternative hidden, show 8kg convenience instead - 8kg recommended → 12kg alternative hidden, no alternative shown
This rule applies consistently in:
1. Initial calculateBoxRecommendation()
2. recalculateWithTreats() re-evaluation
3. Replaces the previous daysSupply16kg <= 56 hard ceiling and the altMaxDays = 45 treat re-evaluation threshold
Display Rationale¶
| Recommended | Alternative | Type | Prominent? | Condition |
|---|---|---|---|---|
| 8kg | 12kg | Value | Yes | Buffer ≤ 7 days |
| 8kg | (none) | — | — | If 12kg buffer > 7 days |
| 12kg | 16kg | Value | No | Buffer ≤ 7 days |
| 12kg | 8kg | Convenience | No | If 16kg buffer > 7 days |
| 16kg | 12kg | Convenience | No | Always shown |
Product Page Buffer Validation¶
The product page (main-product.liquid) applies the same buffer validation when displaying alternative box options. This ensures consistency between calculator recommendations and product page alternatives.
// Product page alternative box validation
const altGramsMap = { '8kg': 8000, '12kg': 12000, '16kg': 16000 };
const altDaysSupply = Math.floor(altGramsMap[altBox] / parseInt(dailyGrams));
const altFrequency = getOptimalFrequency(altDaysSupply);
const altBuffer = altDaysSupply - (altFrequency * 7);
if (altBuffer > 7) {
// Hide alternative — too much excess food
productAltBox.style.display = 'none';
}
This prevents the product page from showing an alternative box that the calculator would not have recommended.
Product Page Frequency Recalculation¶
The product page recalculates delivery frequency from box and household_grams session data (fetched via the calculator-get-session edge function) rather than trusting any URL parameter. This ensures the displayed frequency is always correct for the current box size.
function getOptimalFrequency(ds) {
const frequencies = [6, 5, 4, 3, 2];
const minBuffer = 3;
for (const w of frequencies) {
if (ds >= w * 7 + minBuffer) return w;
}
return 2;
}
const boxGramsLookup = { '8kg': 8000, '12kg': 12000, '16kg': 16000 };
const currentDaysSupply = Math.floor(boxGramsLookup[boxSize] / parseInt(dailyGrams));
const calculatedWeeks = getOptimalFrequency(currentDaysSupply);
Why not trust the URL weeks parameter?
The session stores household_grams from the calculator. When a customer clicks "View 16kg option" from a 12kg product page, the alt box link passes &box=16kg as a URL override. The product page recalculates frequency from grams, ensuring the banner card, Seal subscription frequency, and buildAltUrl() all use the correct frequency for the box being viewed.
buildAltUrl() generates token-only URLs:
function buildAltUrl(altBox) {
var params = new URLSearchParams();
params.set('token', token);
params.set('box', altBox);
if (treatLevel !== 'none') params.set('treat_level', treatLevel);
return '/products/protocol-raw-complete-' + altBox + '-box?' + params.toString();
}
This creates a self-correcting chain: each product page fetches session data, recalculates its own frequency, and generates clean token-only URLs for alternative boxes.
Smaller First Delivery Option¶
Eligibility¶
Only available when all conditions are met: - Calculator recommends 12kg box - Alternative box is 16kg (value type) — which requires the 16kg buffer to be ≤ 7 days
Logic in calculateBoxRecommendation()¶
let showFirstDeliveryOption = false;
let firstDeliveryBox = null;
if (recommendedBox === '12kg' && alternativeBox === '16kg') {
showFirstDeliveryOption = true;
firstDeliveryBox = '8kg';
firstDeliveryDaysSupply = daysSupply8kg;
firstDeliveryWeeks = getOptimalFrequency(daysSupply8kg);
firstDeliveryWeeklyPrice = Math.round(PRICING.box8kg.firstDelivery / (daysSupply8kg / 7));
}
Treat Adjustment Interaction¶
| Treat Level | 16kg Days Supply | Alt Still Valid? | First Delivery Shown? |
|---|---|---|---|
| None | ≤ 45 days | Yes (16kg value) | ✅ Yes |
| Some | May exceed 45 days | Depends | Depends |
| Lots | Likely > 45 days | No → flips to 8kg convenience | ⌠Hidden |
When treats cause the alternative box buffer to exceed 7 days, the alternative flips from value to convenience (smaller box). The first delivery option hides because the convenience link already serves the same purpose.
const altFreq = getOptimalFrequency(altAdjustedDaysSupply);
const altBuffer = altAdjustedDaysSupply - (altFreq * 7);
if (altBuffer > 7 && basePlanData.alternativeType === 'value') {
// Flip to convenience
}
When the customer changes back to a treat level where the buffer is acceptable, the value alternative and first delivery option reappear.
Display Matrix (Updated)¶
| Recommended | Alternative | Type | Prominent? | First Delivery? | Reasoning |
|---|---|---|---|---|---|
| 8kg | 12kg | Value | Yes | No | Already smallest box |
| 12kg | 16kg | Value | No | Yes (8kg) | Practical freezer step-down |
| 12kg | 8kg | Convenience | No | No | Alt already points to 8kg |
| 16kg | 12kg | Convenience | No | No | 8kg would be two steps down |
Pricing Display Strategy¶
The Core Insight¶
The phrasing determines which mental calculation customers perform:
| Display | Customer Calculates | Result |
|---|---|---|
| "£89 every 4 weeks" | 4 weeks × £18/week = £72 | ≠ £89 → doubt |
| "£89 per box" | £89 ÷ 5 weeks = £18/week | ✓ correct |
Price Anchoring Strategy¶
Calculator: Hero shows first-box weekly price (discounted), with ongoing weekly in Taupe below: "£[ongoing]/week from Box 2". Context line shows per-box total unchanged.
Product Page: Flex row with crossed-out ongoing weekly + first-box weekly hero. Context line: "First box £89 · then £109 per box" (delivery frequency removed in v7.1 — customers don't need to think about cadence at point of purchase).
Alternative boxes: All alt box links show strikethrough ongoing weekly + highlighted first-box weekly (e.g. "~~£17~~ £14/week").
Pricing Constants¶
Note (v7.2): Pricing constants are now defined server-side in
calculator-compute-plan/index.ts. They are no longer present in the Shopify theme. The values below are for reference.
| Box | Regular Price | First Delivery Price | Trays |
|---|---|---|---|
| 8kg | £89 | £69 | 16 |
| 12kg | £109 | £89 | 24 |
| 16kg | £129 | £109 | 32 |
Tray size: 500g.
Freezer Guidance¶
Data Structure¶
Note (v7.2): The freezer drawer map is now defined server-side in
calculator-compute-plan. ThefreezerInfoobject (containingtraysanddrawers) is included in the API response atplan.freezerInfoand within each treat variant's recommendation.
| Box | Trays | Drawers |
|---|---|---|
| 8kg | 16 | 1 |
| 12kg | 24 | 1.5 |
| 16kg | 32 | 2 |
Display Implementation¶
// FREEZER GUIDANCE - uses server-computed value from plan response
const freezerInfo = plan.freezerInfo;
document.getElementById('box-freezer').textContent =
`Fits approximately ${freezerInfo.drawers} freezer drawers`;
HTML Structure¶
<div class="box-details">
<p class="box-contents" id="box-contents">24 trays (500g each) - 12kg Box</p>
<p class="box-duration" id="box-duration">Lasts approximately 32 days</p>
<p class="box-freezer" id="box-freezer">Fits approximately 1.5 freezer drawers</p>
</div>
CSS¶
.box-freezer {
font-family: 'Inter', sans-serif;
font-size: 14px;
color: var(--col-taupe);
margin: 4px 0 0 0;
}
/* Mobile */
@media (max-width: 768px) {
.box-freezer {
font-size: 13px;
}
}
Visual Identity Alignment¶
Colour Corrections (v6.0)¶
| Variable | Old Value | New Value | Reference |
|---|---|---|---|
--col-cream |
#FEFDF7 | #F9F7F4 | Visual Identity Guide v2.2 |
.portion-unit |
--col-warm-gray |
--col-taupe |
Readability fix |
.badge-content p |
--col-warm-gray |
--col-taupe |
Readability fix |
.price-period-hero |
--col-warm-gray |
--col-taupe |
Readability fix |
Colour Reference (v7.0 — Full Warm Palette)¶
| Name | Hex | Usage |
|---|---|---|
| Espresso | #2B2523 | Dark headers, body text |
| Burnt Sienna | #B85C3A | CTAs, active states, progress dots, labels |
| Burnt Sienna Dark | #9F4D2F | CTA hover state |
| Forest Green | #2D5144 | Trust icons, success states |
| Cream | #F9F7F4 | Section backgrounds |
| Warm Linen | #EBE8E3 | Card borders, inactive progress dots, step backgrounds |
| Stone | #C4BCB0 | Subtler text |
| Warm Gray | #918A85 | Meta/timestamps, placeholder text |
| Taupe | #6B6360 | Secondary text (readable), descriptions |
Testing & Validation¶
Test 1: 15kg Adult Anchor (MUST PASS)¶
Input: Adult, 15kg, neutered, moderate, ideal
Expected: - Daily grams: 375g ✅ - Recommended box: 12kg ✅ - Days supply: 32 days ✅ - Frequency: 4 weeks ✅ - Weekly price: £24 ✅ - Freezer guidance: "Fits approximately 1.5 freezer drawers" ✅
Test 2: 200g/day Dog (5-Week Cadence)¶
Input: Adult, ~9kg dog, neutered, moderate, ideal → 200g/day
Expected: - Daily grams: 200g ✅ - Recommended box: 8kg ✅ - Days supply: 40 days ✅ - Frequency: 5 weeks ✅ (NOT 4 weeks) - Buffer: 5 days ✅ - Weekly price: £16 ✅ (89 ÷ 5.7 weeks) - Freezer guidance: "Fits approximately 1 freezer drawers" ✅
Test 3: 8kg Cavalier (225g/day)¶
Input: Adult, 8kg, neutered, moderate, ideal
Expected: - Daily grams: 225g ✅ - Recommended box: 8kg ✅ - Days supply: 35 days ✅ - Frequency: 4 weeks ✅ (35 < 38 threshold for 5wk) - Buffer: 7 days ✅ - Weekly price: £18 ✅
Test 4: Very Small Dog (150g/day)¶
Input: Adult, 5kg, neutered, low activity, ideal → ~150g/day
Expected: - Daily grams: 150g ✅ - Recommended box: 8kg ✅ - Days supply: 53 days ✅ - Frequency: 6 weeks ✅ - Buffer: 11 days ✅ - Weekly price: £12 ✅
Test 5: 30kg Golden (Large Dog)¶
Input: Adult, 30kg, neutered, moderate, ideal
Expected: - Daily grams: 625g ✅ - Recommended box: 16kg ✅ - Days supply: 25 days ✅ - Frequency: 3 weeks ✅ - Buffer: 4 days ✅ - Weekly price: £36 ✅ - Freezer guidance: "Fits approximately 2 freezer drawers" ✅
Test 6: 50kg Giant Dog¶
Input: Adult, 50kg, neutered, moderate, ideal
Expected: - Daily grams: 925g ✅ - Recommended box: 16kg ✅ - Days supply: 17 days ✅ - Frequency: 2 weeks ✅ - Buffer: 3 days ✅ - Weekly price: £53 ✅
Test 7: All Frequencies Have Adequate Buffer¶
| Frequency | Min Days Supply | Buffer |
|---|---|---|
| 6 weeks | 45 days | ≥3 days ✅ |
| 5 weeks | 38 days | ≥3 days ✅ |
| 4 weeks | 31 days | ≥3 days ✅ |
| 3 weeks | 24 days | ≥3 days ✅ |
| 2 weeks | 17 days | ≥3 days ✅ |
Complete Frequency Test Matrix¶
| Daily Grams | Box | Days Supply | Frequency | Buffer | Weekly £ |
|---|---|---|---|---|---|
| 150g | 8kg | 53 | 6 weeks | 11 days | £12 |
| 175g | 8kg | 45 | 6 weeks | 3 days | £14 |
| 200g | 8kg | 40 | 5 weeks | 5 days | £16 |
| 225g | 8kg | 35 | 4 weeks | 7 days | £18 |
| 275g | 12kg | 43 | 6 weeks | 1 day | £18 |
| 300g | 12kg | 40 | 5 weeks | 5 days | £19 |
| 375g | 12kg | 32 | 4 weeks | 4 days | £24 |
| 500g | 16kg | 32 | 4 weeks | 4 days | £28 |
| 625g | 16kg | 25 | 3 weeks | 4 days | £36 |
| 925g | 16kg | 17 | 2 weeks | 3 days | £53 |
v7.0 Specific Testing Checklist¶
One-Step-at-a-Time Flow (v7.1): - [ ] Page load: only Step 0 visible - [ ] Step 0 (Dog Name): text input appears, "Continue with [name]" shown when name entered - [ ] Step 0: "Skip this step" link visible when no name entered - [ ] Step 0: Enter key advances to Step 1 - [ ] Step 1 (Life Stage): three radio cards, auto-advance on non-default selection after 400ms - [ ] Step 1b (Puppy Age): appears only when Puppy selected. Three radio cards (0-4m, 4-12m, 12-24m), auto-advance 400ms - [ ] Step 1b: dynamic header "How old is [name]?" or "How old is your puppy?" - [ ] Step 2 (Weight): dial + confirm button "[weight] kg, that's right" - [ ] Step 3 (Activity): card grid, auto-advance after 400ms. Calculate button visible for puppies - [ ] Step 3: Continue button visible when revisiting with activity already selected - [ ] Step 4 (BCS + Neutered): SVG silhouettes + toggle. Calculate button for adult/senior - [ ] Only one step visible at a time — previous steps fully hidden (display: none) - [ ] Each step fades in with slide-up animation - [ ] Multipet trigger appears from Step 1 onwards for single-pet adult/senior flow
Back Navigation (v7.1): - [ ] "← Back" link visible on Steps 1-4, below Continue/confirm button - [ ] Back link navigates to previous step - [ ] Completed progress dots are clickable — navigate to that step - [ ] Future (uncompleted) progress dots are NOT clickable - [ ] Browser back button goes to previous step (not away from page) - [ ] Browser back from results goes to last input step (Step 4 adult/senior, Step 3 puppy) - [ ] Browser back from Step 0 navigates away from page (normal behaviour) - [ ] All previously entered values preserved when navigating back
Progress Dots: - [ ] Five dots visible above inputs, current step highlighted as burnt sienna pill - [ ] Dots stick to top on scroll (sticky at 80px) - [ ] Dots hidden when Calculate is clicked - [ ] Completed dots show burnt sienna at 50% opacity
Dynamic Header: - [ ] Header title changes at each step - [ ] With dog name "Bella": Step 1 shows "Nice to meet Bella." - [ ] With dog name "Bella": Step 2 shows "How much does Bella weigh?" - [ ] Without name: generic copy used - [ ] Results show: "YOUR FEEDING PLAN" / "Bella's feeding plan" - [ ] Header restores step-specific text when returning from results via Edit
Personalised Loading: - [ ] Single dog with name: "Calculating Bella's plan" - [ ] Single dog without name: "Calculating your dog's plan" - [ ] Multipet: "Calculating your dogs' plan"
Results Section: - [ ] Animated gram counter counts up from 0 - [ ] Plan card: dark espresso header with "[Name]'s Plan" + box badge - [ ] Plan card: tray count, duration, delivery, pause/skip/cancel line - [ ] Plan card: freezer drawer info + QR/lab test info in 2-column grid - [ ] Storage bar: personalised "Keep [Name]'s trays frozen..." text - [ ] Price card: first-box weekly hero + "from Box 2" ongoing line + context - [ ] Price context line: "First box £XX · then £XX per box" (no delivery frequency) - [ ] Primary CTA: "Start [Name]'s Plan →" (or "Start Your Plan →") - [ ] Treat adjustment: title personalised with dog name - [ ] Treat adjustment recalculates delivery frequency via getOptimalFrequency() - [ ] Trust grid: 4 items in 2×2 layout - [ ] "How It Works at Home": 3-column grid with personalised serve description - [ ] Secondary CTA: "Start [Name]'s Plan →" - [ ] Edit summary: "Bella's plan. Edit" pill visible above results
Results Edit Flow (v7.1): - [ ] Clicking Edit returns to Step 0 with dog name pre-filled in input - [ ] Continue button shows "Continue with [name]" on return - [ ] Life stage radio pre-selected on return - [ ] Weight dial shows previously entered weight on return - [ ] Activity card shows previous selection on return - [ ] Body condition card shows previous selection on return - [ ] Neutered toggle shows previous selection on return - [ ] Re-calculating produces updated results
Session Persistence (v7.1): - [ ] Navigate to product page, press browser back → results restore instantly - [ ] All CTA URLs, treat level, and plan data restored correctly - [ ] Results expire after 30 minutes (fresh calculator shown) - [ ] Clicking Edit clears sessionStorage (no stale restore on next visit)
Alternative Boxes: - [ ] Prominent alternative card renders with strikethrough ongoing + first-box weekly (8kg → 12kg upsell) - [ ] Subtle alternative text link renders with strikethrough ongoing + first-box weekly (12kg → 16kg value) - [ ] First delivery option shows strikethrough ongoing + first-box weekly (12kg recommended + 16kg value alt)
Product Page (v7.1):
- [ ] Alt box link shows correct box size (12kg for 8kg recommended, not hardcoded 16kg)
- [ ] Price context shows "First box £XX · then £XX per box" (no delivery frequency)
- [ ] 16kg branch uses correct container for alt box display
- [ ] Frequency displayed on banner card matches recalculated frequency (not URL weeks param)
- [ ] Alternative box link navigates to correct product URL with recalculated weeks param
- [ ] Alternative box hidden when buffer > 7 days (e.g. small dog on 8kg page, 12kg alt has excessive buffer)
- [ ] Seal subscription frequency dropdown syncs to calculatedWeeks, not URL weeks
v6.4 Specific Testing Checklist¶
- [ ] 12kg recommended + 16kg value alt → "Smaller first delivery?" link appears
- [ ] 8kg recommended → first delivery option hidden
- [ ] 16kg recommended → first delivery option hidden
- [ ] 12kg recommended + 8kg convenience alt → first delivery option hidden
- [ ] First delivery link points to 8kg product page with
optimal_box=12kg&first_order_downsize=true - [ ] Treat "Lots" flips alt to 8kg convenience → first delivery option hides
- [ ] Treat "None" restores 16kg value alt → first delivery option reappears
- [ ] First delivery option uses
alternative-option-subtlestyling (consistent with existing alt) - [ ] Multi-pet calculation: first delivery option still works correctly
v6.0 Specific Testing Checklist¶
- [ ] 15kg anchor dog shows "Fits approximately 1.5 freezer drawers"
- [ ] 8kg box shows "Fits approximately 1 freezer drawers"
- [ ] 16kg box shows "Fits approximately 2 freezer drawers"
- [ ] All "pouch" references now say "tray"
- [ ] Cream background matches
#F9F7F4 - [ ] Portion unit text is readable (Taupe, not Warm Gray)
- [ ] Method badge description is readable (Taupe, not Warm Gray)
- [ ] "per week" text is readable (Taupe, not Warm Gray)
v6.1 Specific Testing Checklist¶
- [ ] 15kg anchor shows buffer line: "Lasts approximately 32 days · skip, pause or change frequency anytime"
- [ ] 475g/day (16kg box) shows: "Lasts approximately 33 days"
- [ ] 200g/day (8kg box) shows: "Lasts approximately 40 days"
- [ ] 925g/day (16kg box) shows: "Lasts approximately 17 days"
- [ ] Buffer line hidden when no calculator URL params present
- [ ] Buffer line displays correctly in multi-pet view
- [ ] Buffer line uses Taupe colour (#6B6360) at 13px
Implementation Checklist¶
Calculator Code (v6.0)¶
- [ ] Add
FREEZER_DRAWER_MAPconstant - [ ] Update
PRICINGconstant to usetraysinstead ofpouches - [ ] Add freezer guidance DOM update in
updateResults() - [ ] Update all "pouch" text to "tray"
- [ ] Fix
--col-creamto#F9F7F4 - [ ] Change
.portion-unitcolour to Taupe - [ ] Change
.badge-content pcolour to Taupe - [ ] Change
.price-period-herocolour to Taupe - [ ] Add
.box-freezerCSS styles
Seal Subscriptions¶
- [ ] Verify 5-week interval exists (Seal ID:
712350990711) - [ ] Verify interval appears in customer portal
- [ ] Test subscription creation with all frequencies
Product Page¶
- [ ] Verify frequency dropdown includes all options (2-6 weeks)
- [ ] Test checkout flow with each frequency
- [ ] Verify Seal frequency sync from URL parameter
- [ ] Buffer line displays correct days supply for all box/grams combinations
- [ ] Buffer line hidden when visiting product page without calculator params
Files & Code Reference¶
Files Changed (v7.2)¶
| File | Changes |
|---|---|
sections/feed-calculator.liquid |
Removed all calculation constants (PROTOCOL_RAW_V3_1, TREAT_MULTIPLIERS) and formulas (calculateRER, calculateMER, calculateDailyGrams, calculateBoxRecommendation, getOptimalFrequency). Renamed collectPetData() → collectPetInputs() (input-only). handleCalculateClick() POSTs to calculator-compute-plan Edge Function. recalculateWithTreats() reads from server-computed treatVariants. updateResults() uses plan.freezerInfo. Session storage includes treatVariants for back-navigation restore. |
supabase/functions/calculator-compute-plan/index.ts |
New. All calculation constants, formulas, box recommendation, frequency algorithm, and treat variant computation. Returns full plan object with all three treat variants. Input validation for weight, enums, pet count. |
supabase/functions/calculator-compute-plan/config.toml |
New. verify_jwt = false (public endpoint called from Shopify storefront). |
Files Changed (v7.1)¶
| File | Changes |
|---|---|
sections/feed-calculator.liquid |
One-step-at-a-time CSS (display:none/block + animation), removed weight locking (CSS + HTML + JS), back links on Steps 1-4, clickable progress dots, browser back button (pushState/popstate), results edit button with pre-fill, restoreInputStates() function, session persistence (sessionStorage save/restore), activity step Continue on revisit, results header update, price context simplified, treat adjustment frequency fix |
sections/main-product.liquid |
Price context simplified (removed delivery frequency), alt box sizing fix (dynamic text), 16kg branch container fix, frequency recalculation from box/grams (independent of URL weeks param), buildAltUrl() recalculates frequency per alt box, buffer validation on alternative box display, Seal frequency sync uses calculated frequency |
Files Changed (v7.0)¶
| File | Changes |
|---|---|
sections/feed-calculator.liquid |
Progressive step flow, dog name capture, dynamic header, progress dots, personalised loading, results redesign, warm commercial layout, plan card, price card, trust grid, How It Works section, storage guidance, personalised CTAs, alternative box cards |
sections/main-product.liquid |
Warm product page experience for calculator visitors — calc-mode layout transformation, personalised banner card, photo grid, personalised buy buttons (weekly pricing), What's in the Box section, alternative box cards, Seal widget hiding |
templates/product.json |
Removed "You may also like" recommendations section |
Files Changed (v6.4)¶
| File | Changes |
|---|---|
sections/feed-calculator.liquid |
First delivery HTML, JS logic, treat interaction |
sections/feed-calculator-minimal.liquid |
Same changes (keep in sync — pending) |
Files Changed (v6.1)¶
| File | Changes |
|---|---|
sections/main-product.liquid |
Added buffer/flexibility line (HTML, CSS, JS) |
Files Changed (v6.0)¶
| File | Changes |
|---|---|
sections/feed-calculator.liquid |
All v6.0 changes (freezer, trays, colours) |
sections/feed-calculator-minimal.liquid |
Same changes (keep in sync) |
Files Changed (v5.5)¶
| File | Changes |
|---|---|
sections/feed-calculator.liquid |
JS frequency logic (5-week) |
sections/feed-calculator-minimal.liquid |
JS frequency logic (5-week) |
sections/main-product.liquid |
Seal frequency sync |
Seal Subscriptions Frequency IDs¶
const sealFrequencyMap = {
'2': '712286208375',
'3': '712286241143',
'4': '712286175607',
'5': '712350990711',
'6': '712286273911',
'7': '712351023479',
'8': '712286306679'
};
Seal Frequency Sync Code¶
// ============================================================
// SET SEAL SUBSCRIPTIONS FREQUENCY FROM URL PARAMETER
// ============================================================
function setSealFrequency() {
const sealSelect = document.querySelector('select.sls-select');
if (!sealSelect || !weeks) return;
const sealFrequencyMap = {
'2': '712286208375',
'3': '712286241143',
'4': '712286175607',
'5': '712350990711',
'6': '712286273911',
'7': '712351023479',
'8': '712286306679'
};
const targetValue = sealFrequencyMap[weeks];
if (targetValue && sealSelect.value !== targetValue) {
sealSelect.value = targetValue;
sealSelect.dispatchEvent(new Event('change', { bubbles: true }));
console.log(`[Protocol Raw] Set Seal frequency to ${weeks} weeks (ID: ${targetValue})`);
}
}
if (document.querySelector('select.sls-select')) {
setSealFrequency();
} else {
const sealObserver = new MutationObserver((mutations, obs) => {
if (document.querySelector('select.sls-select')) {
setSealFrequency();
obs.disconnect();
}
});
sealObserver.observe(document.body, { childList: true, subtree: true });
setTimeout(setSealFrequency, 2000);
}
Version History¶
v7.2 - April 10, 2026 (CURRENT)¶
Changes:
✅ All calculation logic moved server-side to new calculator-compute-plan Supabase Edge Function
✅ Removed from client-side JS: PROTOCOL_RAW_V3_1 constants object (MER_FACTORS, SENIOR_MER_FACTORS, PUPPY_MER_FACTORS, PUPPY_ACTIVITY_MULTIPLIERS, BCS_MULTIPLIERS, PRICING, FREEZER_DRAWER_MAP, FORMULA_ME), TREAT_MULTIPLIERS, calculateRER(), calculateMER(), calculateDailyGrams(), calculateBoxRecommendation(), getOptimalFrequency()
✅ collectPetData() renamed to collectPetInputs() — now gathers form values only, no computation
✅ handleCalculateClick() POSTs pet inputs to Edge Function, receives full computed plan
✅ Edge Function returns all three treat variants (none/some/lots) in a single response for instant client-side toggle
✅ recalculateWithTreats() reads from pre-computed treatVariants object instead of applying client-side multipliers
✅ updateResults() reads freezerInfo from plan response instead of client-side constant lookup
✅ treatVariants persisted to sessionStorage alongside plan for back-navigation restore
✅ Server-side input validation: weight bounds (0.5–120kg), valid enums, max 10 pets
✅ Token generation (calculator-generate-token-multipet) now receives server-computed plan data — no code changes needed, data shape is compatible
Purpose: Move all feeding calculation logic server-side so formulas, multiplier calibrations, and pricing constants are not exposed in the browser source code. The UI remains purely an input form and results renderer.
Design principle applied: Client collects, server computes, client renders. No business logic in the browser. Treat adjustment stays instant via pre-computed variants — no UX regression from the architecture change.
Files Modified:
- sections/feed-calculator.liquid (removed constants + formulas, added API call)
- supabase/functions/calculator-compute-plan/index.ts (new Edge Function)
- supabase/functions/calculator-compute-plan/config.toml (new, verify_jwt = false)
v7.1 - February 28, 2026¶
Changes:
✅ One-step-at-a-time flow — only current step visible (display: none/display: block + fade-in animation), replacing progressive disclosure where all completed steps stayed visible
✅ "← Back" text links on Steps 1-4 (Taupe, Inter 14px, underline on hover)
✅ Clickable completed progress dots for step navigation
✅ Browser back button navigates calculator steps (history.pushState/popstate)
✅ Results edit button — "Bella's plan. Edit" pill above results, returns to step flow with all inputs pre-filled
✅ restoreInputStates() function pre-fills all DOM inputs from JS state (name, life stage, weight, activity, body condition, neutered)
✅ Session persistence via sessionStorage — back-navigation from product page restores results instantly (30-minute TTL)
✅ Additional state saved: weight, activity, bodyCondition, neutered alongside existing dogName/lifeStage
✅ Activity step Continue button shown when revisiting Step 3 with activity already selected
✅ Results header: "YOUR FEEDING PLAN / Bella's feeding plan" replaces step text; restores on Edit
✅ Price context simplified to "First box £XX · then £XX per box" (delivery frequency removed from calculator and product page)
✅ Treat adjustment delivery frequency fix — recalculateWithTreats() uses getOptimalFrequency() for primary CTA, alt box, and smaller convenience box URLs
✅ Product page alt box sizing — correct alternative box shown dynamically (e.g. "View 12kg option" for 8kg box, not hardcoded "View 16kg option")
✅ Product page 16kg branch fixed to use correct container (productAltBox, not productFirstDelivery)
✅ Removed all weight dial locking (CSS, HTML, JS) — unnecessary with one-step-at-a-time
✅ Product page frequency recalculation — calculatedWeeks derived from box/grams instead of trusting URL weeks param
✅ Product page buildAltUrl() recalculates frequency per alternative box size and uses correct product URL path
✅ Product page buffer validation — alternative box hidden if buffer > 7 days (consistent with calculator)
✅ Product page Seal frequency sync uses calculatedWeeks instead of URL weeks param
Purpose: Refine the calculator UX from a growing accordion to a focused single-step conversation. Eliminate accidental interactions, add discoverable navigation, and create a seamless edit flow where returning to the calculator always preserves the user's previous answers.
Design principle applied: One decision at a time. The user is never overwhelmed with previous choices. Back navigation (link, dots, browser button) is always available. Editing is non-destructive — every value is preserved.
Files Modified:
- sections/feed-calculator.liquid
- sections/main-product.liquid
v7.0 - February 27, 2026¶
Changes:
✅ Progressive 5-step input flow replacing single-screen form
✅ Dog name capture as Step 0 — personalisation throughout headers, results, CTAs, product page
✅ Dynamic header that adapts title/subtitle to current step and dog name
✅ Sticky progress dots with completion tracking, hidden on Calculate
✅ Personalised loading screen ("Calculating Bella's plan")
✅ Results section redesigned: plan card (dark header + white body), price card, trust grid (2×2)
✅ "How It Works at Home" section (store frozen / serve fresh / scan & verify)
✅ Storage guidance personalised with dog name
✅ Personalised CTAs: "Start Bella's Plan" primary and secondary
✅ Alternative box display redesigned as prominent card / subtle link
✅ First-box weekly price as hero (strikethrough ongoing weekly) on calculator and product page
✅ Alternative box links show both strikethrough ongoing + first-box weekly prices
✅ First delivery option shows strikethrough ongoing + first-box weekly prices
✅ Removed redundant "First box" pill from calculator price card and product page badge
✅ Warm design system CSS variables applied throughout
✅ "Switching is Simple" reassurance section on product page — layered commitment anxiety relief
✅ Weight dial locking — Step 2 locks after confirmation to prevent accidental slider changes on mobile, "Edit" link to unlock (removed in v7.1)
✅ Fix: progress dots now correctly hidden after Calculate (removed CSS !important that overrode JS display: none)
Purpose: Transform the calculator from a clinical data-entry form into a warm, conversational, personalised experience that builds emotional connection before showing results. Dog name personalisation creates continuity from calculator through product page to checkout.
Design principle applied: Warmth through personalisation. Every step feels like it's being built for this specific dog, not a generic tool. Progressive disclosure reduces cognitive load while maintaining scientific precision.
Files Modified:
- sections/feed-calculator.liquid
- sections/main-product.liquid
v6.4 - February 3, 2026 (SUPERSEDED)¶
Changes:
✅ "Smaller first delivery" option for 12kg customers
✅ Calculates 8kg first-delivery data when 12kg recommended + 16kg value alternative
✅ New URL parameters: optimal_box and first_order_downsize passed to product page
✅ Treat adjustment hides first delivery when alternative flips to 8kg convenience
✅ Treat adjustment restores first delivery when original 16kg alternative remains valid
Purpose: Reduce freezer anxiety for medium-dog customers by offering a practical one-step-down first delivery, with clear messaging about the Box-2 transition to their optimal size.
Design principle applied: Practical logistics, not emotional hand-holding. Framed as a freezer convenience choice, never as a trial.
Files Modified:
- sections/feed-calculator.liquid
v6.1 - February 3, 2026 (SUPERSEDED)¶
Changes:
✅ Added buffer/flexibility line to product page calculator banner
✅ Shows "Lasts approximately X days · skip, pause or change frequency anytime"
Purpose: Reduce purchase hesitation by making the delivery buffer visible and subscription flexibility explicit at the point of commitment.
Design principle applied: Information without friction. Same philosophy as calculator freezer guidance.
Files Modified:
- sections/main-product.liquid
v6.0 - January 27, 2026 (SUPERSEDED)¶
Changes:
✅ Added freezer guidance as integrated line in box details
✅ Changed "pouches" to "trays" throughout
✅ Fixed Cream colour (#FEFDF7 → #F9F7F4)
✅ Fixed Warm Gray → Taupe for three text elements
Design principle applied: Information without friction. Freezer guidance answers the question without creating a decision point.
Files Modified:
- sections/feed-calculator.liquid
- sections/feed-calculator-minimal.liquid
v5.5 - November 29, 2025 (SUPERSEDED)¶
Changes:
✅ Added 5-week delivery cadence option
✅ Updated getOptimalFrequency to include 5-week threshold
✅ Small/medium dogs now get optimal delivery timing
✅ All test cases passing including new 5-week scenarios
Impact: - 15kg anchor unchanged (£24/week) ✅ - 200g/day dogs now get 5-week delivery (was 4-week) - Reduces customer "paying too early" friction - Better alignment between food consumption and billing
Files Modified:
- sections/feed-calculator.liquid (JS frequency logic)
- sections/feed-calculator-minimal.liquid (JS frequency logic)
- sections/main-product.liquid (Seal frequency sync)
- Seal Subscriptions settings (5-week interval already exists)
v5.4 - November 28, 2025 (SUPERSEDED)¶
Changes:
✅ Fixed pricing display: "£X per box" instead of "£X every Y weeks"
✅ Fixed missing DOM updates (box-contents, box-duration, human-readable)
✅ Fixed product page weekly price calculation (consumption-based)
✅ Restructured product page to lead with regular price, discount as bonus
v5.3 - November 28, 2025 (SUPERSEDED)¶
Changes:
✅ Fixed alternative box logic (small dogs get prominent 12kg value option)
✅ Fixed frequency fallback (return 2, not 4)
✅ Removed puppy email optin (handled by Customer.io post-purchase)
✅ Added showProminently and alternativeType to box recommendation
v5.2 - November 27, 2025 (SUPERSEDED)¶
- Mobile optimizations v2.0
- Multipet support refinements
- Various bug fixes
v3.1 - November 24, 2025 (SUPERSEDED)¶
- Senior-specific MER factors implemented
- calculateMER accepts lifeStage parameter
v3.0 - November 10, 2025 (SUPERSEDED)¶
- Life stage system (Puppy/Adult/Senior)
- Puppy age-based multipliers
- Educational content
- Email opt-in system (removed in v5.3)
v2.1 - October 30, 2025 (SUPERSEDED)¶
- MER multipliers corrected
- 15kg anchor = £24/week established
Documentation complete. v7.0 deployed to live theme. 🚀¶
Warm progressive flow with dog name personalisation. Scientific precision wrapped in emotional connection. £24/week anchor preserved.
END OF DOCUMENTATION