content:
   700→    IF discount.value_type = 'percent':
   701→        totalDiscount = qualifyingSubtotal * discount.value_amount / 100
   702→    ELSE IF discount.value_type = 'fixed':
   703→        totalDiscount = MIN(discount.value_amount, qualifyingSubtotal)
   704→
   705→    remainingDiscount = totalDiscount
   706→
   707→    FOR EACH qualifyingLine (except last):
   708→        lineDiscount = ROUND(totalDiscount * lineSubtotal / qualifyingSubtotal)
   709→        remainingDiscount -= lineDiscount
   710→        SET line.line_discount_amount = lineDiscount
   711→
   712→    // Last line gets the remainder (largest-remainder method)
   713→    SET lastLine.line_discount_amount = remainingDiscount
   714→
   715→    RECALCULATE line_total_amount for each affected line
   716→```
   717→
   718→**Storage of allocations:**
   719→- During checkout: update `cart_lines.line_discount_amount`.
   720→- On order creation: store in `order_lines.discount_allocations_json` as an array of `{discount_id, amount}`.
   721→
   722→### 7.7 Usage Tracking
   723→
   724→When an order is successfully created with a discount code:
   725→- Increment `discounts.usage_count` by 1.
   726→- This happens within the order creation transaction (see Section 6.2, completeCheckout procedure, step 7).
   727→
   728→---
   729→
   730→## 8. Tax Calculation
   731→
   732→### 8.1 Tax Provider Architecture
   733→
   734→**Interface:** `App\Contracts\TaxProvider`
   735→
   736→- Must implement a `calculate(request)` method that accepts a TaxCalculationRequest and returns a TaxCalculationResult.
   737→- The TaxCalculationRequest contains: line items (with discounted amounts), shipping amount, shipping address, store tax settings.
   738→- The TaxCalculationResult contains: array of TaxLine value objects, total tax amount.
   739→
   740→**Service:** `App\Services\TaxCalculator` - orchestrates provider selection and delegates to the appropriate implementation.
   741→
   742→**Implementations:**
   743→- `App\Services\Tax\ManualTaxProvider` - default, uses manually configured rates
   744→- `App\Services\Tax\StripeTaxProvider` - stub for Stripe Tax API integration
   745→
   746→### 8.2 Manual Tax Mode
   747→
   748→**Rate resolution:**
   749→1. Match shipping address against shipping zones (same zone matching algorithm as shipping - see Section 9.1).
   750→2. Use the tax rate defined in `tax_settings.config_json` for the matched zone, or the global default rate.
   751→3. Rate stored as integer basis points (e.g., 1900 = 19.00%).
   752→
   753→**Calculation per line:**
   754→
   755→```
   756→line_tax = ROUND(line_total_after_discount * rate / 10000)
   757→```
   758→
   759→Round each line's tax independently, then sum for the total tax amount.
   760→
   761→### 8.3 Prices Include Tax
   762→
   763→Controlled by `tax_settings.prices_include_tax` (boolean).
   764→
   765→#### Tax-inclusive pricing (prices_include_tax = true)
   766→
   767→- Displayed prices on the storefront are gross (include tax).
   768→- Subtotal shown to customer is gross.
   769→- Tax is "extracted" from the gross amount:
   770→
   771→```
   772→net_amount = intdiv(gross_amount * 10000, 10000 + rate_bps)
   773→tax = gross_amount - net_amount
   774→```
   775→
   776→> **Note:** Integer division (truncation toward zero) is used rather than rounding to ensure deterministic tax extraction. This matches PHP's `intdiv()` function.
   777→
   778→**Example:** Price = 1190 cents, rate = 1900 (19%).
   779→- net = intdiv(1190 * 10000, 11900) = intdiv(11900000, 11900) = 1000
   780→- tax = 1190 - 1000 = 190
   781→
   782→#### Tax-exclusive pricing (prices_include_tax = false)
   783→
   784→- Displayed prices are net (excluding tax).
   785→- Tax added on top:
   786→
   787→```
   788→tax = ROUND(net_amount * rate / 10000)
   789→total = net_amount + tax
   790→```
   791→
   792→**Example:** Price = 1000 cents, rate = 1900.
   793→- tax = ROUND(1000 * 1900 / 10000) = 190
   794→- total = 1190
   795→
   796→### 8.4 Stripe Tax Provider (Stub)
   797→
   798→**Implementation:** `App\Services\Tax\StripeTaxProvider`
   799→
   800→- Sends line items and shipping address to Stripe Tax API.
   801→- Stores the full response in `checkouts.tax_provider_snapshot_json` for audit.
   802→- Fallback behavior (configurable per store in `tax_settings.config_json`):
   803→
   804→| Fallback Setting | Behavior |
   805→|---|---|
   806→| `"block"` | Block checkout progression if Stripe Tax API errors |
   807→| `"allow"` | Allow checkout to proceed without tax (log a warning) |
   808→
   809→### 8.5 Discount-Before-Tax Semantics
   810→
   811→Discounts are always applied before tax calculation. Tax is calculated on the discounted amount. This aligns with standard accounting practice and Stripe Tax behavior.
   812→
   813→For tax-inclusive pricing (`prices_include_tax = true`): The discount is subtracted from the gross (tax-inclusive) subtotal. Tax is then extracted from the post-discount gross amount: `tax = post_discount_gross * rate / (1 + rate)`. This means the tax portion decreases proportionally when a discount is applied.
   814→
   815→**Calculation order within the pricing pipeline:**
   816→1. Calculate line subtotals.
   817→2. Apply discount allocations to lines.
   818→3. Calculate tax on discounted line amounts.
   819→4. Sum everything for the total.
   820→
   821→---
   822→
   823→## 9. Shipping Calculation
   824→
   825→### 9.1 Zone Matching
   826→
   827→**Service:** `App\Services\ShippingCalculator`
   828→
   829→**Zone matching algorithm:**
   830→
   831→```
   832→PROCEDURE getMatchingZone(address, store):
   833→    zones = load all shipping zones for store
   834→
   835→    bestMatch = null
   836→    bestSpecificity = -1
   837→
   838→    FOR EACH zone IN zones:
   839→        countryMatch = address.country_code IN zone.countries_json
   840→        regionMatch = address.province_code IN zone.regions_json
   841→
   842→        IF countryMatch AND regionMatch:
   843→            specificity = 2  // Most specific
   844→        ELSE IF countryMatch:
   845→            specificity = 1  // Country-only match
   846→        ELSE:
   847→            CONTINUE  // No match
   848→
   849→        IF specificity > bestSpecificity:
   850→            bestMatch = zone
   851→            bestSpecificity = specificity
   852→        ELSE IF specificity = bestSpecificity AND zone.id < bestMatch.id:
   853→            bestMatch = zone  // Tie-break: lowest ID wins
   854→
   855→    RETURN bestMatch  // null if no zone matches
   856→```
   857→
   858→**When no zone matches:** The address is unserviceable. Block checkout progression with error "Cannot ship to this address".
   859→
   860→### 9.2 Rate Types
   861→
   862→For the matched zone, return all active shipping rates (`is_active = 1`). Calculate cost for each:
   863→
   864→#### Flat rate
   865→
   866→Config structure: `{"amount": 500}`
   867→
   868→- Cost = fixed amount regardless of cart contents.
   869→
   870→#### Weight-based rate
   871→
   872→Config structure: `{"ranges": [{"min_g": 0, "max_g": 1000, "amount": 500}, {"min_g": 1001, "max_g": 5000, "amount": 1000}]}`
   873→
   874→```
   875→PROCEDURE calculateWeightRate(config, cart):
   876→    totalWeight = 0
   877→    FOR EACH line IN cart.lines:
   878→        IF line.variant.requires_shipping = true:
   879→            totalWeight += line.variant.weight_g * line.quantity
   880→
   881→    FOR EACH range IN config.ranges:
   882→        IF range.min_g <= totalWeight AND totalWeight <= range.max_g:
   883→            RETURN range.amount
   884→
   885→    RETURN null  // Rate not available for this weight
   886→```
   887→
   888→#### Price-based rate
   889→
   890→Config structure: `{"ranges": [{"min_amount": 0, "max_amount": 5000, "amount": 500}, {"min_amount": 5001, "amount": 0}]}`
   891→
   892→```
   893→PROCEDURE calculatePriceRate(config, cartSubtotal):
   894→    FOR EACH range IN config.ranges:
   895→        IF range.min_amount <= cartSubtotal:
   896→            IF range.max_amount IS NOT SET OR cartSubtotal <= range.max_amount:
   897→                RETURN range.amount
   898→
   899→    RETURN null  // Rate not available for this subtotal
   900→```
   901→
   902→A range without `max_amount` matches all subtotals above `min_amount` (useful for "free shipping over X" rules).
   903→
   904→#### Carrier-calculated rate (stub)
   905→
   906→Config structure: `{"carrier": "ups", "service": "ground"}`
   907→
   908→- Placeholder for future carrier API integration.
   909→- Returns a hardcoded rate or error until implemented.
   910→
   911→### 9.3 Requires Shipping Check
   912→
   913→- If no line in the cart has `requires_shipping = true` on its variant, the shipping step is skipped entirely.
   914→- shipping_amount = 0 in the pricing result.
   915→- The checkout transitions directly past `shipping_selected` (set status to `shipping_selected` with shipping_method_id = null).
   916→
   917→**Mixed carts** (some physical, some digital):
   918→- Shipping is calculated only for physical items' weight.
   919→- Digital items are excluded from weight calculations.
   920→
   921→### 9.4 Free Shipping Discount Override
   922→
   923→If a validated discount has `value_type = 'free_shipping'`:
   924→- Override the calculated shipping amount to 0.
   925→- The shipping method is still selected (for fulfillment routing), but the customer is not charged.
   926→
   927→---
   928→
   929→## 10. Payment Processing (Mock PSP)
   930→
   931→### 10.1 Mock Payment Provider Architecture
   932→
   933→The platform uses a **Mock Payment Service Provider (PSP)** instead of real payment gateways. All payment processing happens in-process with no external API calls, no webhooks, and no redirects. This enables a fully functional checkout flow for development, testing, and demonstration purposes.
   934→
   935→**Interface:** `App\Contracts\PaymentProvider`
   936→
   937→Must implement:
   938→- `charge(checkout, paymentMethodData)` - returns a PaymentResult value object (success/failure, reference ID)
   939→- `refund(payment, amount)` - initiates a mock refund, returns a RefundResult
   940→
   941→**Service:** `App\Services\PaymentService` - orchestrates payment processing, delegates to MockPaymentProvider.
   942→
   943→**Implementation:** `App\Services\Payment\MockPaymentProvider`
   944→
   945→### 10.2 Supported Payment Methods
   946→
   947→**Enum:** `App\Enums\PaymentMethod` - values: `CreditCard` (`credit_card`), `Paypal` (`paypal`), `BankTransfer` (`bank_transfer`)
   948→
   949→| Method | Behavior | Capture Timing |
   950→|---|---|---|
   951→| `credit_card` | Simulates card authorization and immediate capture. Uses magic card numbers for test scenarios. | Auto-capture on checkout (instant) |
   952→| `paypal` | Simulates PayPal approval and immediate capture. Always succeeds (no magic numbers). | Auto-capture on checkout (instant) |
   953→| `bank_transfer` | No charge at checkout. Order created with `financial_status = pending`. Admin manually confirms payment receipt. | Deferred - admin confirms payment |
   954→
   955→### 10.3 Magic Card Numbers (Credit Card Testing)
   956→
   957→When payment_method is `credit_card`, the mock provider inspects the card number submitted in the checkout form to determine the outcome:
   958→
   959→| Card Number | Outcome | PaymentResult |
   960→|---|---|---|
   961→| `4242 4242 4242 4242` | Success | `success = true`, status = `captured` |
   962→| `4000 0000 0000 0002` | Decline | `success = false`, error = `card_declined` |
   963→| `4000 0000 0000 9995` | Insufficient funds | `success = false`, error = `insufficient_funds` |
   964→| Any other valid-looking number | Success | `success = true`, status = `captured` |
   965→
   966→The mock provider does **not** validate card expiry, CVC, or cardholder name. These fields exist in the UI for realism but are ignored by the backend.
   967→
   968→### 10.4 Mock Payment Flow
   969→
   970→```mermaid
   971→sequenceDiagram
   972→    participant Customer
   973→    participant Storefront
   974→    participant Backend
   975→    participant MockPSP
   976→
   977→    Customer->>Storefront: Select payment method + enter details
   978→    Storefront->>Backend: POST /checkouts/{id}/pay (payment_method, card details if applicable)
   979→    Backend->>Backend: Reserve inventory (if not already reserved)
   980→    Backend->>MockPSP: charge(checkout, paymentMethodData)
   981→
   982→    alt Credit Card / PayPal (instant capture)
   983→        MockPSP-->>Backend: PaymentResult (success/failure)
   984→        alt Success
   985→            Backend->>Backend: Create order (financial_status = paid)
   986→            Backend->>Backend: Commit inventory, mark cart converted
   987→            Backend-->>Storefront: 200 OK (order confirmation)
   988→        else Failure (decline / insufficient funds)
   989→            Backend->>Backend: Release reserved inventory
   990→            Backend-->>Storefront: 422 (error code + message)
   991→        end
   992→    else Bank Transfer (deferred)
   993→        MockPSP-->>Backend: PaymentResult (success = true, status = pending)
   994→        Backend->>Backend: Create order (financial_status = pending)
   995→        Backend->>Backend: Inventory remains reserved (not committed)
   996→        Backend-->>Storefront: 200 OK (order confirmation with bank transfer instructions)
   997→    end
   998→```
   999→

content:
    IF discount.value_type = 'percent':
        totalDiscount = qualifyingSubtotal * discount.value_amount / 100
    ELSE IF discount.value_type = 'fixed':
        totalDiscount = MIN(discount.value_amount, qualifyingSubtotal)

    remainingDiscount = totalDiscount

    FOR EACH qualifyingLine (except last):
        lineDiscount = ROUND(totalDiscount * lineSubtotal / qualifyingSubtotal)
        remainingDiscount -= lineDiscount
        SET line.line_discount_amount = lineDiscount

    // Last line gets the remainder (largest-remainder method)
    SET lastLine.line_discount_amount = remainingDiscount

    RECALCULATE line_total_amount for each affected line
```

**Storage of allocations:**
- During checkout: update `cart_lines.line_discount_amount`.
- On order creation: store in `order_lines.discount_allocations_json` as an array of `{discount_id, amount}`.

### 7.7 Usage Tracking

When an order is successfully created with a discount code:
- Increment `discounts.usage_count` by 1.
- This happens within the order creation transaction (see Section 6.2, completeCheckout procedure, step 7).

---

## 8. Tax Calculation

### 8.1 Tax Provider Architecture

**Interface:** `App\Contracts\TaxProvider`

- Must implement a `calculate(request)` method that accepts a TaxCalculationRequest and returns a TaxCalculationResult.
- The TaxCalculationRequest contains: line items (with discounted amounts), shipping amount, shipping address, store tax settings.
- The TaxCalculationResult contains: array of TaxLine value objects, total tax amount.

**Service:** `App\Services\TaxCalculator` - orchestrates provider selection and delegates to the appropriate implementation.

**Implementations:**
- `App\Services\Tax\ManualTaxProvider` - default, uses manually configured rates
- `App\Services\Tax\StripeTaxProvider` - stub for Stripe Tax API integration

### 8.2 Manual Tax Mode

**Rate resolution:**
1. Match shipping address against shipping zones (same zone matching algorithm as shipping - see Section 9.1).
2. Use the tax rate defined in `tax_settings.config_json` for the matched zone, or the global default rate.
3. Rate stored as integer basis points (e.g., 1900 = 19.00%).

**Calculation per line:**

```
line_tax = ROUND(line_total_after_discount * rate / 10000)
```

Round each line's tax independently, then sum for the total tax amount.

### 8.3 Prices Include Tax

Controlled by `tax_settings.prices_include_tax` (boolean).

#### Tax-inclusive pricing (prices_include_tax = true)

- Displayed prices on the storefront are gross (include tax).
- Subtotal shown to customer is gross.
- Tax is "extracted" from the gross amount:

```
net_amount = intdiv(gross_amount * 10000, 10000 + rate_bps)
tax = gross_amount - net_amount
```

> **Note:** Integer division (truncation toward zero) is used rather than rounding to ensure deterministic tax extraction. This matches PHP's `intdiv()` function.

**Example:** Price = 1190 cents, rate = 1900 (19%).
- net = intdiv(1190 * 10000, 11900) = intdiv(11900000, 11900) = 1000
- tax = 1190 - 1000 = 190

#### Tax-exclusive pricing (prices_include_tax = false)

- Displayed prices are net (excluding tax).
- Tax added on top:

```
tax = ROUND(net_amount * rate / 10000)
total = net_amount + tax
```

**Example:** Price = 1000 cents, rate = 1900.
- tax = ROUND(1000 * 1900 / 10000) = 190
- total = 1190

### 8.4 Stripe Tax Provider (Stub)

**Implementation:** `App\Services\Tax\StripeTaxProvider`

- Sends line items and shipping address to Stripe Tax API.
- Stores the full response in `checkouts.tax_provider_snapshot_json` for audit.
- Fallback behavior (configurable per store in `tax_settings.config_json`):

| Fallback Setting | Behavior |
|---|---|
| `"block"` | Block checkout progression if Stripe Tax API errors |
| `"allow"` | Allow checkout to proceed without tax (log a warning) |

### 8.5 Discount-Before-Tax Semantics

Discounts are always applied before tax calculation. Tax is calculated on the discounted amount. This aligns with standard accounting practice and Stripe Tax behavior.

For tax-inclusive pricing (`prices_include_tax = true`): The discount is subtracted from the gross (tax-inclusive) subtotal. Tax is then extracted from the post-discount gross amount: `tax = post_discount_gross * rate / (1 + rate)`. This means the tax portion decreases proportionally when a discount is applied.

**Calculation order within the pricing pipeline:**
1. Calculate line subtotals.
2. Apply discount allocations to lines.
3. Calculate tax on discounted line amounts.
4. Sum everything for the total.

---

## 9. Shipping Calculation

### 9.1 Zone Matching

**Service:** `App\Services\ShippingCalculator`

**Zone matching algorithm:**

```
PROCEDURE getMatchingZone(address, store):
    zones = load all shipping zones for store

    bestMatch = null
    bestSpecificity = -1

    FOR EACH zone IN zones:
        countryMatch = address.country_code IN zone.countries_json
        regionMatch = address.province_code IN zone.regions_json

        IF countryMatch AND regionMatch:
            specificity = 2  // Most specific
        ELSE IF countryMatch:
            specificity = 1  // Country-only match
        ELSE:
            CONTINUE  // No match

        IF specificity > bestSpecificity:
            bestMatch = zone
            bestSpecificity = specificity
        ELSE IF specificity = bestSpecificity AND zone.id < bestMatch.id:
            bestMatch = zone  // Tie-break: lowest ID wins

    RETURN bestMatch  // null if no zone matches
```

**When no zone matches:** The address is unserviceable. Block checkout progression with error "Cannot ship to this address".

### 9.2 Rate Types

For the matched zone, return all active shipping rates (`is_active = 1`). Calculate cost for each:

#### Flat rate

Config structure: `{"amount": 500}`

- Cost = fixed amount regardless of cart contents.

#### Weight-based rate

Config structure: `{"ranges": [{"min_g": 0, "max_g": 1000, "amount": 500}, {"min_g": 1001, "max_g": 5000, "amount": 1000}]}`

```
PROCEDURE calculateWeightRate(config, cart):
    totalWeight = 0
    FOR EACH line IN cart.lines:
        IF line.variant.requires_shipping = true:
            totalWeight += line.variant.weight_g * line.quantity

    FOR EACH range IN config.ranges:
        IF range.min_g <= totalWeight AND totalWeight <= range.max_g:
            RETURN range.amount

    RETURN null  // Rate not available for this weight
```

#### Price-based rate

Config structure: `{"ranges": [{"min_amount": 0, "max_amount": 5000, "amount": 500}, {"min_amount": 5001, "amount": 0}]}`

```
PROCEDURE calculatePriceRate(config, cartSubtotal):
    FOR EACH range IN config.ranges:
        IF range.min_amount <= cartSubtotal:
            IF range.max_amount IS NOT SET OR cartSubtotal <= range.max_amount:
                RETURN range.amount

    RETURN null  // Rate not available for this subtotal
```

A range without `max_amount` matches all subtotals above `min_amount` (useful for "free shipping over X" rules).

#### Carrier-calculated rate (stub)

Config structure: `{"carrier": "ups", "service": "ground"}`

- Placeholder for future carrier API integration.
- Returns a hardcoded rate or error until implemented.

### 9.3 Requires Shipping Check

- If no line in the cart has `requires_shipping = true` on its variant, the shipping step is skipped entirely.
- shipping_amount = 0 in the pricing result.
- The checkout transitions directly past `shipping_selected` (set status to `shipping_selected` with shipping_method_id = null).

**Mixed carts** (some physical, some digital):
- Shipping is calculated only for physical items' weight.
- Digital items are excluded from weight calculations.

### 9.4 Free Shipping Discount Override

If a validated discount has `value_type = 'free_shipping'`:
- Override the calculated shipping amount to 0.
- The shipping method is still selected (for fulfillment routing), but the customer is not charged.

---

## 10. Payment Processing (Mock PSP)

### 10.1 Mock Payment Provider Architecture

The platform uses a **Mock Payment Service Provider (PSP)** instead of real payment gateways. All payment processing happens in-process with no external API calls, no webhooks, and no redirects. This enables a fully functional checkout flow for development, testing, and demonstration purposes.

**Interface:** `App\Contracts\PaymentProvider`

Must implement:
- `charge(checkout, paymentMethodData)` - returns a PaymentResult value object (success/failure, reference ID)
- `refund(payment, amount)` - initiates a mock refund, returns a RefundResult

**Service:** `App\Services\PaymentService` - orchestrates payment processing, delegates to MockPaymentProvider.

**Implementation:** `App\Services\Payment\MockPaymentProvider`

### 10.2 Supported Payment Methods

**Enum:** `App\Enums\PaymentMethod` - values: `CreditCard` (`credit_card`), `Paypal` (`paypal`), `BankTransfer` (`bank_transfer`)

| Method | Behavior | Capture Timing |
|---|---|---|
| `credit_card` | Simulates card authorization and immediate capture. Uses magic card numbers for test scenarios. | Auto-capture on checkout (instant) |
| `paypal` | Simulates PayPal approval and immediate capture. Always succeeds (no magic numbers). | Auto-capture on checkout (instant) |
| `bank_transfer` | No charge at checkout. Order created with `financial_status = pending`. Admin manually confirms payment receipt. | Deferred - admin confirms payment |

### 10.3 Magic Card Numbers (Credit Card Testing)

When payment_method is `credit_card`, the mock provider inspects the card number submitted in the checkout form to determine the outcome:

| Card Number | Outcome | PaymentResult |
|---|---|---|
| `4242 4242 4242 4242` | Success | `success = true`, status = `captured` |
| `4000 0000 0000 0002` | Decline | `success = false`, error = `card_declined` |
| `4000 0000 0000 9995` | Insufficient funds | `success = false`, error = `insufficient_funds` |
| Any other valid-looking number | Success | `success = true`, status = `captured` |

The mock provider does **not** validate card expiry, CVC, or cardholder name. These fields exist in the UI for realism but are ignored by the backend.

### 10.4 Mock Payment Flow

```mermaid
sequenceDiagram
    participant Customer
    participant Storefront
    participant Backend
    participant MockPSP

    Customer->>Storefront: Select payment method + enter details
    Storefront->>Backend: POST /checkouts/{id}/pay (payment_method, card details if applicable)
    Backend->>Backend: Reserve inventory (if not already reserved)
    Backend->>MockPSP: charge(checkout, paymentMethodData)

    alt Credit Card / PayPal (instant capture)
        MockPSP-->>Backend: PaymentResult (success/failure)
        alt Success
            Backend->>Backend: Create order (financial_status = paid)
            Backend->>Backend: Commit inventory, mark cart converted
            Backend-->>Storefront: 200 OK (order confirmation)
        else Failure (decline / insufficient funds)
            Backend->>Backend: Release reserved inventory
            Backend-->>Storefront: 422 (error code + message)
        end
    else Bank Transfer (deferred)
        MockPSP-->>Backend: PaymentResult (success = true, status = pending)
        Backend->>Backend: Create order (financial_status = pending)
        Backend->>Backend: Inventory remains reserved (not committed)
        Backend-->>Storefront: 200 OK (order confirmation with bank transfer instructions)
    end
```


userType: external

gitBranch: claude-subagents

slug: wise-shimmying-pretzel

type: message

role: assistant

service_tier: standard

type: assistant


type: progress

hookEvent: PostToolUse

command: callback


type: user

role: user

content:
     1→# 03 - Admin Panel UI/UX Specification
     2→
     3→This document defines every view, component, and interaction for the admin panel. The admin is entirely server-rendered using Laravel 12 + Livewire v4 + Flux UI Free components + Tailwind CSS v4. There is no SPA or client-side router.
     4→
     5→**Flux UI Free components available:** avatar, badge, brand, breadcrumbs, button, callout, checkbox, dropdown, field, heading, icon, input, modal, navbar, profile, radio, select, separator, switch, text, textarea, tooltip
     6→
     7→Anything not covered by Flux UI Free uses plain Tailwind CSS v4 markup with Livewire directives.
     8→
     9→---
    10→
    11→## 1. Admin Layout Shell
    12→
    13→The admin layout is a persistent shell that wraps every admin page. It consists of a left sidebar, a top bar, and a main content area.
    14→
    15→### 1.1 Route Prefix
    16→
    17→All admin routes live under `/admin` and are protected by authentication middleware plus store-scoping middleware. The store is resolved from the session or a store-selector in the top bar.
    18→
    19→### 1.2 Sidebar Navigation
    20→
    21→**Livewire component:** `App\Livewire\Admin\Layout\Sidebar`
    22→
    23→**Blade view:** `resources/views/livewire/admin/layout/sidebar.blade.php`
    24→
    25→#### Component Requirements
    26→
    27→| Property | Type | Purpose |
    28→|----------|------|---------|
    29→| `collapsed` | `bool` | Whether the sidebar is collapsed (mobile state) |
    30→| `currentRoute` | `string` | The current route name for active highlighting |
    31→
    32→| Action | Behavior |
    33→|--------|----------|
    34→| `toggle()` | Toggles the `collapsed` state |
    35→
    36→#### Wireframe
    37→
    38→```
    39→+-------------------------------+
    40→|  [BRAND LOGO]  Platform Name  |
    41→|-------------------------------|
    42→|  [separator]                  |
    43→|                               |
    44→|  [chart-bar]    Dashboard     |
    45→|                               |
    46→|  PRODUCTS                     |
    47→|  [cube]         Products      |
    48→|  [rect-stack]   Collections   |
    49→|  [archive-box]  Inventory     |
    50→|                               |
    51→|  [separator]                  |
    52→|  ORDERS                       |
    53→|  [shopping-bag] Orders        |
    54→|                               |
    55→|  [separator]                  |
    56→|  CUSTOMERS                    |
    57→|  [users]        Customers     |
    58→|                               |
    59→|  [separator]                  |
    60→|  DISCOUNTS                    |
    61→|  [tag]          Discounts     |
    62→|                               |
    63→|  [separator]                  |
    64→|  CONTENT                      |
    65→|  [doc-text]     Pages         |
    66→|  [bars-3]       Navigation    |
    67→|  [paint-brush]  Themes        |
    68→|                               |
    69→|  [separator]                  |
    70→|  [chart-pie]    Analytics     |
    71→|  [cog-6-tooth]  Settings      |
    72→|  [squares-2x2]  Apps          |
    73→|  [code-bracket] Developers    |
    74→+-------------------------------+
    75→```
    76→
    77→#### Navigation Structure
    78→
    79→```
    80→Dashboard                        -> admin.dashboard
    81→Products (group label)
    82→  Products                       -> admin.products.index
    83→  Collections                    -> admin.collections.index
    84→  Inventory                      -> admin.inventory.index
    85→Orders
    86→  Orders                         -> admin.orders.index
    87→Customers
    88→  Customers                      -> admin.customers.index
    89→Discounts
    90→  Discounts                      -> admin.discounts.index
    91→Content (group label)
    92→  Pages                          -> admin.pages.index
    93→  Navigation                     -> admin.navigation.index
    94→  Themes                         -> admin.themes.index
    95→Analytics                        -> admin.analytics.index
    96→Settings                         -> admin.settings.index
    97→Apps                             -> admin.apps.index
    98→Developers                       -> admin.developers.index
    99→```
   100→
   101→Each nav item displays an icon (via Flux icon component) and a text label. The active item receives a highlighted background and bold text. All navigation links use Livewire SPA-style page transitions. Groups are visually separated with Flux separator components.
   102→
   103→#### Flux Component Mapping
   104→
   105→| UI Element | Flux Component |
   106→|-----------|----------------|
   107→| Platform logo and name | `brand` |
   108→| Navigation icons | `icon` |
   109→| Group dividers | `separator` |
   110→
   111→#### Responsive Behavior
   112→
   113→- **Desktop (lg and above):** Sidebar is always visible, fixed left, 256px wide. Main content has a left margin of 256px.
   114→- **Mobile (below lg):** Sidebar is hidden off-screen. A hamburger button (icon: bars-3) appears in the top bar. When user clicks it, the sidebar slides in as an overlay with a semi-transparent backdrop. Clicking the backdrop or a nav item closes it.
   115→
   116→### 1.3 Top Bar
   117→
   118→**Livewire component:** `App\Livewire\Admin\Layout\TopBar`
   119→
   120→**Blade view:** `resources/views/livewire/admin/layout/top-bar.blade.php`
   121→
   122→#### Component Requirements
   123→
   124→| Property | Type | Purpose |
   125→|----------|------|---------|
   126→| `currentStoreName` | `string` | Name of the active store |
   127→| `stores` | `Collection` | List of stores the user has access to |
   128→| `unreadNotificationCount` | `int` | Count of unread notifications |
   129→
   130→| Action | Behavior |
   131→|--------|----------|
   132→| `switchStore(string $storeId)` | Switches active store in session, redirects to dashboard |
   133→
   134→#### Wireframe
   135→
   136→```
   137→+--------------------------------------------------------------------+
   138→| [hamburger*]  [Store Name v]                  [bell(3)]  [avatar v] |
   139→+--------------------------------------------------------------------+
   140→  * hamburger visible on mobile only
   141→
   142→  Store Name dropdown:
   143→    - Store A
   144→    - Store B
   145→    - Store C
   146→
   147→  Avatar dropdown:
   148→    - Settings (links to /admin/settings)
   149→    - [separator]
   150→    - Log out (POST /logout with CSRF)
   151→```
   152→
   153→#### Interaction Flows
   154→
   155→- When user clicks the store name, a dropdown appears listing all stores. Clicking a store calls `switchStore()` which updates the session and redirects to the dashboard.
   156→- Notification bell shows an unread count badge (positioned absolutely over the icon) when `unreadNotificationCount > 0`.
   157→- Profile dropdown is triggered by a Flux profile component showing the user's avatar and name.
   158→
   159→#### Flux Component Mapping
   160→
   161→| UI Element | Flux Component |
   162→|-----------|----------------|
   163→| Store selector | `dropdown` |
   164→| Hamburger (mobile) | `button` (icon: bars-3) |
   165→| Notification bell | `button` (icon: bell, variant: ghost) |
   166→| Notification count | `badge` |
   167→| User profile trigger | `profile` |
   168→| Profile menu | `dropdown` |
   169→| Menu divider | `separator` |
   170→
   171→### 1.4 Main Content Area
   172→
   173→The main content area sits to the right of the sidebar and below the top bar. Every page renders within this area.
   174→
   175→**Breadcrumbs:** Every page includes a Flux breadcrumbs component at the top of the content area. The first item is always "Home" linking to `/admin`. Subsequent items reflect the page hierarchy. Example for product edit: Home > Products > "Blue T-Shirt".
   176→
   177→**Layout wrapper:** The content area has consistent padding (larger on desktop, smaller on mobile) and a max-width container centered horizontally.
   178→
   179→### 1.5 Toast Notifications
   180→
   181→A global toast notification system lives in the layout shell. It uses Alpine.js to listen for Livewire dispatched events.
   182→
   183→| Property | Value |
   184→|----------|-------|
   185→| Event name | `toast` |
   186→| Payload | `{ type: 'success' / 'error' / 'info', message: string }` |
   187→| Position | Top-right corner |
   188→| Auto-dismiss | 5 seconds |
   189→| Stacking | Supports multiple toasts |
   190→| Styling | Rounded card, shadow, colored left border (green=success, red=error, blue=info) |
   191→
   192→Components dispatch toasts after successful actions (e.g., "Product saved successfully.") or on errors ("Something went wrong. Please try again.").
   193→
   194→### 1.6 Dark Mode
   195→
   196→The admin supports dark mode via Tailwind's `dark:` variant. The mode follows the user's system preference by default. A toggle can be added in the profile dropdown later. All components and custom markup must include dark mode variants.
   197→
   198→Dark mode preference is persisted in `localStorage` under the key `theme` with values `light` or `dark`. On page load, the stored preference is applied before first paint to avoid flash of incorrect theme. If no preference is stored, the system preference (`prefers-color-scheme`) is used as default.
   199→
   200→---

type: text

content:
# 03 - Admin Panel UI/UX Specification

This document defines every view, component, and interaction for the admin panel. The admin is entirely server-rendered using Laravel 12 + Livewire v4 + Flux UI Free components + Tailwind CSS v4. There is no SPA or client-side router.

**Flux UI Free components available:** avatar, badge, brand, breadcrumbs, button, callout, checkbox, dropdown, field, heading, icon, input, modal, navbar, profile, radio, select, separator, switch, text, textarea, tooltip

Anything not covered by Flux UI Free uses plain Tailwind CSS v4 markup with Livewire directives.

---

## 1. Admin Layout Shell

The admin layout is a persistent shell that wraps every admin page. It consists of a left sidebar, a top bar, and a main content area.

### 1.1 Route Prefix

All admin routes live under `/admin` and are protected by authentication middleware plus store-scoping middleware. The store is resolved from the session or a store-selector in the top bar.

### 1.2 Sidebar Navigation

**Livewire component:** `App\Livewire\Admin\Layout\Sidebar`

**Blade view:** `resources/views/livewire/admin/layout/sidebar.blade.php`

#### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `collapsed` | `bool` | Whether the sidebar is collapsed (mobile state) |
| `currentRoute` | `string` | The current route name for active highlighting |

| Action | Behavior |
|--------|----------|
| `toggle()` | Toggles the `collapsed` state |

#### Wireframe

```
+-------------------------------+
|  [BRAND LOGO]  Platform Name  |
|-------------------------------|
|  [separator]                  |
|                               |
|  [chart-bar]    Dashboard     |
|                               |
|  PRODUCTS                     |
|  [cube]         Products      |
|  [rect-stack]   Collections   |
|  [archive-box]  Inventory     |
|                               |
|  [separator]                  |
|  ORDERS                       |
|  [shopping-bag] Orders        |
|                               |
|  [separator]                  |
|  CUSTOMERS                    |
|  [users]        Customers     |
|                               |
|  [separator]                  |
|  DISCOUNTS                    |
|  [tag]          Discounts     |
|                               |
|  [separator]                  |
|  CONTENT                      |
|  [doc-text]     Pages         |
|  [bars-3]       Navigation    |
|  [paint-brush]  Themes        |
|                               |
|  [separator]                  |
|  [chart-pie]    Analytics     |
|  [cog-6-tooth]  Settings      |
|  [squares-2x2]  Apps          |
|  [code-bracket] Developers    |
+-------------------------------+
```

#### Navigation Structure

```
Dashboard                        -> admin.dashboard
Products (group label)
  Products                       -> admin.products.index
  Collections                    -> admin.collections.index
  Inventory                      -> admin.inventory.index
Orders
  Orders                         -> admin.orders.index
Customers
  Customers                      -> admin.customers.index
Discounts
  Discounts                      -> admin.discounts.index
Content (group label)
  Pages                          -> admin.pages.index
  Navigation                     -> admin.navigation.index
  Themes                         -> admin.themes.index
Analytics                        -> admin.analytics.index
Settings                         -> admin.settings.index
Apps                             -> admin.apps.index
Developers                       -> admin.developers.index
```

Each nav item displays an icon (via Flux icon component) and a text label. The active item receives a highlighted background and bold text. All navigation links use Livewire SPA-style page transitions. Groups are visually separated with Flux separator components.

#### Flux Component Mapping

| UI Element | Flux Component |
|-----------|----------------|
| Platform logo and name | `brand` |
| Navigation icons | `icon` |
| Group dividers | `separator` |

#### Responsive Behavior

- **Desktop (lg and above):** Sidebar is always visible, fixed left, 256px wide. Main content has a left margin of 256px.
- **Mobile (below lg):** Sidebar is hidden off-screen. A hamburger button (icon: bars-3) appears in the top bar. When user clicks it, the sidebar slides in as an overlay with a semi-transparent backdrop. Clicking the backdrop or a nav item closes it.

### 1.3 Top Bar

**Livewire component:** `App\Livewire\Admin\Layout\TopBar`

**Blade view:** `resources/views/livewire/admin/layout/top-bar.blade.php`

#### Component Requirements

| Property | Type | Purpose |
|----------|------|---------|
| `currentStoreName` | `string` | Name of the active store |
| `stores` | `Collection` | List of stores the user has access to |
| `unreadNotificationCount` | `int` | Count of unread notifications |

| Action | Behavior |
|--------|----------|
| `switchStore(string $storeId)` | Switches active store in session, redirects to dashboard |

#### Wireframe

```
+--------------------------------------------------------------------+
| [hamburger*]  [Store Name v]                  [bell(3)]  [avatar v] |
+--------------------------------------------------------------------+
  * hamburger visible on mobile only

  Store Name dropdown:
    - Store A
    - Store B
    - Store C

  Avatar dropdown:
    - Settings (links to /admin/settings)
    - [separator]
    - Log out (POST /logout with CSRF)
```

#### Interaction Flows

- When user clicks the store name, a dropdown appears listing all stores. Clicking a store calls `switchStore()` which updates the session and redirects to the dashboard.
- Notification bell shows an unread count badge (positioned absolutely over the icon) when `unreadNotificationCount > 0`.
- Profile dropdown is triggered by a Flux profile component showing the user's avatar and name.

#### Flux Component Mapping

| UI Element | Flux Component |
|-----------|----------------|
| Store selector | `dropdown` |
| Hamburger (mobile) | `button` (icon: bars-3) |
| Notification bell | `button` (icon: bell, variant: ghost) |
| Notification count | `badge` |
| User profile trigger | `profile` |
| Profile menu | `dropdown` |
| Menu divider | `separator` |

### 1.4 Main Content Area

The main content area sits to the right of the sidebar and below the top bar. Every page renders within this area.

**Breadcrumbs:** Every page includes a Flux breadcrumbs component at the top of the content area. The first item is always "Home" linking to `/admin`. Subsequent items reflect the page hierarchy. Example for product edit: Home > Products > "Blue T-Shirt".

**Layout wrapper:** The content area has consistent padding (larger on desktop, smaller on mobile) and a max-width container centered horizontally.

### 1.5 Toast Notifications

A global toast notification system lives in the layout shell. It uses Alpine.js to listen for Livewire dispatched events.

| Property | Value |
|----------|-------|
| Event name | `toast` |
| Payload | `{ type: 'success' / 'error' / 'info', message: string }` |
| Position | Top-right corner |
| Auto-dismiss | 5 seconds |
| Stacking | Supports multiple toasts |
| Styling | Rounded card, shadow, colored left border (green=success, red=error, blue=info) |

Components dispatch toasts after successful actions (e.g., "Product saved successfully.") or on errors ("Something went wrong. Please try again.").

### 1.6 Dark Mode

The admin supports dark mode via Tailwind's `dark:` variant. The mode follows the user's system preference by default. A toggle can be added in the profile dropdown later. All components and custom markup must include dark mode variants.

Dark mode preference is persisted in `localStorage` under the key `theme` with values `light` or `dark`. On page load, the stored preference is applied before first paint to avoid flash of incorrect theme. If no preference is stored, the system preference (`prefers-color-scheme`) is used as default.

---


content:
     1→# 04 - Storefront UI/UX Specification
     2→
     3→> E-commerce platform - Customer-facing storefront UI/UX requirements
     4→
     5→---
     6→
     7→## Conventions
     8→
     9→| Convention | Detail |
    10→|---|---|
    11→| CSS Framework | Tailwind CSS v4, mobile-first design, dark mode support |
    12→| Interactivity | Livewire v4 for all dynamic UI (cart, filters, checkout) |
    13→| Templating | Blade templates with reusable storefront-prefixed components |
    14→| Accessibility | WCAG 2.2 AA compliance throughout |
    15→| Responsive | Mobile-first with progressive enhancement at small, medium, large, and extra-large breakpoints |
    16→| Currency | All prices stored as integers (cents), formatted with locale-aware helpers |
    17→| Images | Lazy-loaded, with proper alt text and maintained aspect ratios |
    18→
    19→---
    20→
    21→## Currency Formatting
    22→
    23→All monetary amounts are stored as integers in minor units (cents) and must be formatted for display according to these rules:
    24→
    25→| Setting | Value |
    26→|---------|-------|
    27→| Decimal separator | `.` (period) |
    28→| Thousands separator | `,` (comma) |
    29→| Currency symbol | After amount with space (e.g., `24.99 EUR`) |
    30→| Decimal places | Always 2 |
    31→| Free amounts | Display as `0.00 EUR` |
    32→| Negative amounts (refunds) | Prefix with `-` (e.g., `-12.50 EUR`) |
    33→
    34→**Helper:** Use a shared Blade component or helper (e.g., `<x-price :amount="$amount" :currency="$currency" />`) to ensure consistent formatting across all storefront pages.
    35→
    36→**Examples:**
    37→- 2499 cents -> `24.99 EUR`
    38→- 0 cents -> `0.00 EUR`
    39→- 149900 cents -> `1,499.00 EUR`
    40→
    41→---
    42→
    43→## 1. Theme System Architecture
    44→
    45→### 1.1 View Organization
    46→
    47→The storefront views are organized into the following categories:
    48→
    49→| Category | Purpose | Views |
    50→|---|---|---|
    51→| Layouts | Master page shell | app (master layout) |
    52→| Pages | Core storefront pages | home, collection, product, cart, search, page (static CMS) |
    53→| Checkout | Purchase flow | checkout index, order confirmation |
    54→| Account | Customer area | login, register, dashboard, orders (list + detail), addresses |
    55→| Errors | Error states | 404, 503 |
    56→| Components | Reusable UI elements | product-card, price, badge, quantity-selector, address-form, order-summary, breadcrumbs, pagination |
    57→
    58→### 1.2 Layout Hierarchy
    59→
    60→All storefront pages extend a single master layout, which provides the full page shell including header, footer, cart drawer, and global Livewire listeners. Individual page templates fill the main content section.
    61→
    62→### 1.3 Theme Settings
    63→
    64→Theme settings are stored in the database as JSON (keyed by theme ID). A singleton service loads and caches the active theme settings for the current store. Settings control:
    65→
    66→- Announcement bar visibility, text, link, and background color
    67→- Sticky header toggle
    68→- Home page section order and visibility
    69→- Color palette overrides (primary, secondary, accent)
    70→- Typography scale
    71→- Footer content and social links
    72→- Dark mode behavior (system preference, toggle, or forced)
    73→
    74→### 1.4 Section-Based Architecture
    75→
    76→Each page is composed of configurable sections. Sections are Blade includes or Livewire components receiving configuration from theme settings.
    77→
    78→| Section | Description | Used On |
    79→|---|---|---|
    80→| Hero | Full-width banner with heading, subheading, call-to-action | Home |
    81→| Featured Collections | Grid of collection cards | Home |
    82→| Featured Products | Grid of product cards (lazy-loaded) | Home |
    83→| Newsletter | Email signup form | Home, Footer |
    84→| Rich Text | Customizable content block | Home, Pages |
    85→
    86→### 1.5 Blade Components
    87→
    88→All reusable UI elements are implemented as Blade components with a storefront prefix. See Section 16 for the full component library reference.
    89→
    90→---
    91→
    92→## 2. Base Layout
    93→
    94→### 2.1 Document Head
    95→
    96→The document head includes:
    97→
    98→| Element | Description |
    99→|---|---|
   100→| Character encoding | UTF-8 |
   101→| Viewport | Width = device width, initial scale 1 |
   102→| Meta description | Page-specific, yielded from child templates |
   103→| Title | Page-specific title appended with the app name |
   104→| Stylesheets | Compiled CSS via Vite |
   105→| Scripts | Compiled JS via Vite |
   106→| Livewire styles | Injected by Livewire |
   107→| Language attribute | Set on the HTML element from application locale |
   108→| Scroll behavior | Smooth scrolling enabled globally |
   109→
   110→### 2.2 Skip Link
   111→
   112→A visually hidden "Skip to main content" link is the first element in the body. It becomes visible on keyboard focus, positioned fixed at top-left with a clear visual treatment (solid background, shadow, focus ring). Supports dark mode.
   113→
   114→**Accessibility:** Allows keyboard users to bypass the header navigation and jump directly to the main content area.
   115→
   116→### 2.3 Announcement Bar
   117→
   118→Optional, controlled by theme settings. Renders a full-width bar above the header.
   119→
   120→| Attribute | Description |
   121→|---|---|
   122→| Position | Full-width, above the header |
   123→| Background | Configurable color, defaults to dark background (light text) / inverted in dark mode |
   124→| Text | Centered, small size, configurable content |
   125→| Link | Optional, underlined, opens in same tab |
   126→| Dismiss | Close button (X icon) on the right side; dismissal persisted via localStorage |
   127→| Height | Auto height with vertical padding |
   128→
   129→### 2.4 Header
   130→
   131→The header serves as the primary navigation banner.
   132→
   133→**Desktop Layout (large breakpoint and above):**
   134→
   135→```
   136→+--------------------------------------------------------------+
   137→| [Logo]      [Nav Item] [Nav Item] [Nav Item]     [S] [C] [A] |
   138→+--------------------------------------------------------------+
   139→```
   140→
   141→| Element | Position | Description |
   142→|---|---|---|
   143→| Logo | Left | Linked to home page. Shows image if logo URL is set in theme settings, otherwise store name as text. Max height approximately 40px on desktop, 32px on smaller screens |
   144→| Main Navigation | Center | Horizontal nav with links sourced from the "main-menu" navigation menu. Supports one level of dropdown submenus (revealed on hover/focus) |
   145→| Search Icon | Right group | Button that opens the search modal |
   146→| Cart Icon | Right group | Button that opens the cart drawer. Displays item count badge (small circular indicator, primary color background, white text) |
   147→| Account Icon | Right group | Link to account dashboard (logged in) or login page (guest) |
   148→
   149→**Mobile Layout (below large breakpoint):**
   150→
   151→```
   152→+--------------------------------------------------------------+
   153→| [Hamburger]           [Logo]                            [Cart]|
   154→+--------------------------------------------------------------+
   155→```
   156→
   157→| Element | Position | Description |
   158→|---|---|---|
   159→| Hamburger menu | Left | Toggle button for mobile navigation drawer |
   160→| Logo | Center | Same as desktop but horizontally centered |
   161→| Cart icon | Right | Same as desktop |
   162→
   163→**Sticky Header (optional):**
   164→
   165→When enabled in theme settings, the header sticks to the top of the viewport with a semi-transparent background and backdrop blur. A subtle bottom border appears on scroll.
   166→
   167→**Mobile Navigation Drawer:**
   168→
   169→| Attribute | Description |
   170→|---|---|
   171→| Animation | Slides in from the left side |
   172→| Height | Full viewport height |
   173→| Backdrop | Semi-transparent dark overlay behind the drawer |
   174→| Close button | X icon at the top-right of the drawer |
   175→| Navigation items | Vertical stack with comfortable tap targets |
   176→| Submenus | Collapsible accordion pattern with chevron icons |
   177→| Account link | Positioned at the bottom of the navigation list |
   178→| Focus behavior | Focus is trapped within the drawer while open |
   179→| ARIA | Labeled as "Mobile navigation" |
   180→
   181→### 2.5 Main Content Area
   182→
   183→The main content area is a landmark element with a unique ID for skip-link targeting. It occupies at least the full viewport height.
   184→
   185→### 2.6 Footer
   186→
   187→The footer is a content-info landmark.
   188→
   189→**Layout:**
   190→
   191→```
   192→+--------------------------------------------------------------+
   193→| [Column 1: Links]  [Column 2: Links]  [Column 3: Store Info] |
   194→|                                                               |
   195→| [Social Icons]                                                |
   196→| [Copyright] [Payment Icons]                                   |
   197→+--------------------------------------------------------------+
   198→```
   199→
   200→| Element | Description |

content:
# 04 - Storefront UI/UX Specification

> E-commerce platform - Customer-facing storefront UI/UX requirements

---

## Conventions

| Convention | Detail |
|---|---|
| CSS Framework | Tailwind CSS v4, mobile-first design, dark mode support |
| Interactivity | Livewire v4 for all dynamic UI (cart, filters, checkout) |
| Templating | Blade templates with reusable storefront-prefixed components |
| Accessibility | WCAG 2.2 AA compliance throughout |
| Responsive | Mobile-first with progressive enhancement at small, medium, large, and extra-large breakpoints |
| Currency | All prices stored as integers (cents), formatted with locale-aware helpers |
| Images | Lazy-loaded, with proper alt text and maintained aspect ratios |

---

## Currency Formatting

All monetary amounts are stored as integers in minor units (cents) and must be formatted for display according to these rules:

| Setting | Value |
|---------|-------|
| Decimal separator | `.` (period) |
| Thousands separator | `,` (comma) |
| Currency symbol | After amount with space (e.g., `24.99 EUR`) |
| Decimal places | Always 2 |
| Free amounts | Display as `0.00 EUR` |
| Negative amounts (refunds) | Prefix with `-` (e.g., `-12.50 EUR`) |

**Helper:** Use a shared Blade component or helper (e.g., `<x-price :amount="$amount" :currency="$currency" />`) to ensure consistent formatting across all storefront pages.

**Examples:**
- 2499 cents -> `24.99 EUR`
- 0 cents -> `0.00 EUR`
- 149900 cents -> `1,499.00 EUR`

---

## 1. Theme System Architecture

### 1.1 View Organization

The storefront views are organized into the following categories:

| Category | Purpose | Views |
|---|---|---|
| Layouts | Master page shell | app (master layout) |
| Pages | Core storefront pages | home, collection, product, cart, search, page (static CMS) |
| Checkout | Purchase flow | checkout index, order confirmation |
| Account | Customer area | login, register, dashboard, orders (list + detail), addresses |
| Errors | Error states | 404, 503 |
| Components | Reusable UI elements | product-card, price, badge, quantity-selector, address-form, order-summary, breadcrumbs, pagination |

### 1.2 Layout Hierarchy

All storefront pages extend a single master layout, which provides the full page shell including header, footer, cart drawer, and global Livewire listeners. Individual page templates fill the main content section.

### 1.3 Theme Settings

Theme settings are stored in the database as JSON (keyed by theme ID). A singleton service loads and caches the active theme settings for the current store. Settings control:

- Announcement bar visibility, text, link, and background color
- Sticky header toggle
- Home page section order and visibility
- Color palette overrides (primary, secondary, accent)
- Typography scale
- Footer content and social links
- Dark mode behavior (system preference, toggle, or forced)

### 1.4 Section-Based Architecture

Each page is composed of configurable sections. Sections are Blade includes or Livewire components receiving configuration from theme settings.

| Section | Description | Used On |
|---|---|---|
| Hero | Full-width banner with heading, subheading, call-to-action | Home |
| Featured Collections | Grid of collection cards | Home |
| Featured Products | Grid of product cards (lazy-loaded) | Home |
| Newsletter | Email signup form | Home, Footer |
| Rich Text | Customizable content block | Home, Pages |

### 1.5 Blade Components

All reusable UI elements are implemented as Blade components with a storefront prefix. See Section 16 for the full component library reference.

---

## 2. Base Layout

### 2.1 Document Head

The document head includes:

| Element | Description |
|---|---|
| Character encoding | UTF-8 |
| Viewport | Width = device width, initial scale 1 |
| Meta description | Page-specific, yielded from child templates |
| Title | Page-specific title appended with the app name |
| Stylesheets | Compiled CSS via Vite |
| Scripts | Compiled JS via Vite |
| Livewire styles | Injected by Livewire |
| Language attribute | Set on the HTML element from application locale |
| Scroll behavior | Smooth scrolling enabled globally |

### 2.2 Skip Link

A visually hidden "Skip to main content" link is the first element in the body. It becomes visible on keyboard focus, positioned fixed at top-left with a clear visual treatment (solid background, shadow, focus ring). Supports dark mode.

**Accessibility:** Allows keyboard users to bypass the header navigation and jump directly to the main content area.

### 2.3 Announcement Bar

Optional, controlled by theme settings. Renders a full-width bar above the header.

| Attribute | Description |
|---|---|
| Position | Full-width, above the header |
| Background | Configurable color, defaults to dark background (light text) / inverted in dark mode |
| Text | Centered, small size, configurable content |
| Link | Optional, underlined, opens in same tab |
| Dismiss | Close button (X icon) on the right side; dismissal persisted via localStorage |
| Height | Auto height with vertical padding |

### 2.4 Header

The header serves as the primary navigation banner.

**Desktop Layout (large breakpoint and above):**

```
+--------------------------------------------------------------+
| [Logo]      [Nav Item] [Nav Item] [Nav Item]     [S] [C] [A] |
+--------------------------------------------------------------+
```

| Element | Position | Description |
|---|---|---|
| Logo | Left | Linked to home page. Shows image if logo URL is set in theme settings, otherwise store name as text. Max height approximately 40px on desktop, 32px on smaller screens |
| Main Navigation | Center | Horizontal nav with links sourced from the "main-menu" navigation menu. Supports one level of dropdown submenus (revealed on hover/focus) |
| Search Icon | Right group | Button that opens the search modal |
| Cart Icon | Right group | Button that opens the cart drawer. Displays item count badge (small circular indicator, primary color background, white text) |
| Account Icon | Right group | Link to account dashboard (logged in) or login page (guest) |

**Mobile Layout (below large breakpoint):**

```
+--------------------------------------------------------------+
| [Hamburger]           [Logo]                            [Cart]|
+--------------------------------------------------------------+
```

| Element | Position | Description |
|---|---|---|
| Hamburger menu | Left | Toggle button for mobile navigation drawer |
| Logo | Center | Same as desktop but horizontally centered |
| Cart icon | Right | Same as desktop |

**Sticky Header (optional):**

When enabled in theme settings, the header sticks to the top of the viewport with a semi-transparent background and backdrop blur. A subtle bottom border appears on scroll.

**Mobile Navigation Drawer:**

| Attribute | Description |
|---|---|
| Animation | Slides in from the left side |
| Height | Full viewport height |
| Backdrop | Semi-transparent dark overlay behind the drawer |
| Close button | X icon at the top-right of the drawer |
| Navigation items | Vertical stack with comfortable tap targets |
| Submenus | Collapsible accordion pattern with chevron icons |
| Account link | Positioned at the bottom of the navigation list |
| Focus behavior | Focus is trapped within the drawer while open |
| ARIA | Labeled as "Mobile navigation" |

### 2.5 Main Content Area

The main content area is a landmark element with a unique ID for skip-link targeting. It occupies at least the full viewport height.

### 2.6 Footer

The footer is a content-info landmark.

**Layout:**

```
+--------------------------------------------------------------+
| [Column 1: Links]  [Column 2: Links]  [Column 3: Store Info] |
|                                                               |
| [Social Icons]                                                |
| [Copyright] [Payment Icons]                                   |
+--------------------------------------------------------------+
```

| Element | Description |


type: thinking

thinking: The Phase 1 agent is still running. I have a good understanding of all the specs now. Let me check on its progress.


text: Let me check on the Phase 1 agent's progress.


Bash

command: tail -100 /private/tmp/claude-501/-Users-fabianwesner-Herd-shop/tasks/a50a3ae.output 2>/dev/null | head -80

description: Check Phase 1 agent progress