Skip to content

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

  1. Executive Summary
  2. What Changed (v6.4 → v7.0)
  3. What Changed (v6.1 → v6.4)
  4. What Changed (v6.0 → v6.1)
  5. What Changed (v5.5 → v6.0)
  6. Technical Specifications
  7. Delivery Frequency Logic
  8. Box Recommendation Logic
  9. Alternative Box Display Logic
  10. Smaller First Delivery Option
  11. Pricing Display Strategy
  12. Freezer Guidance
  13. Visual Identity Alignment
  14. Testing & Validation
  15. Implementation Checklist
  16. Files & Code Reference
  17. 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-plan Supabase 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 clientfeed-calculator.liquid collects 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 valuescalculator-generate-token-multipet now 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 fixrecalculateWithTreats() recalculates delivery frequency via getOptimalFrequency() 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_box and first_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:

  1. Store frozen — "Stack trays in your freezer. Move a day's worth to the fridge the night before."
  2. Serve fresh — "Scoop [Name]'s [grams]g portion into the bowl. That's it." (personalised via id="hiw-serve-desc")
  3. 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():

btnText.textContent = singleDogName
  ? `Start ${singleDogName}'s Plan`
  : 'Start Your Plan';

Dog name passed to product page URL:

if (singleDogName) {
  params.append('dog_name', singleDogName);
}

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:

  1. HTML (line 411-418): New first-delivery-option div reusing existing alternative-option-subtle class. Zero new CSS required.

  2. 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.

  3. updateResults(): Shows/hides the first delivery element and builds the product page URL with optimal_box=12kg and first_order_downsize=true parameters.

  4. 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:

Smaller first delivery? Start with 8kg (~~£18~~ £14/week for first box)

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:

24 trays (500g each) - 12kg Box
Lasts approximately 32 days
Fits approximately 1.5 freezer drawers

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:

  1. RER — basal metabolic rate derived from body weight using the standard allometric formula
  2. MER — RER adjusted by activity level, neutered status, body condition, and life stage (adult/senior/puppy)
  3. Daily grams — MER converted to grams using Protocol Raw's lab-verified energy density (1900 kcal/kg), rounded to nearest 25g
  4. Box recommendation — optimal box size selected based on daily grams, with delivery frequency optimised for longest viable cadence with a minimum 3-day buffer
  5. 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 full recommendation object (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. Contains trays and drawers for 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 of getOptimalFrequency()). 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.

Buffer = daysSupply - (optimalFrequency × 7)

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. The freezerInfo object (containing trays and drawers) is included in the API response at plan.freezerInfo and 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-subtle styling (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_MAP constant
  • [ ] Update PRICING constant to use trays instead of pouches
  • [ ] Add freezer guidance DOM update in updateResults()
  • [ ] Update all "pouch" text to "tray"
  • [ ] Fix --col-cream to #F9F7F4
  • [ ] Change .portion-unit colour to Taupe
  • [ ] Change .badge-content p colour to Taupe
  • [ ] Change .price-period-hero colour to Taupe
  • [ ] Add .box-freezer CSS 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