# Plan: CSS migration — Tailwind + DaisyUI to modern hand-written CSS

Status: Complete. All phases done including admin semantic refactor, dead CSS cleanup, color-mix token extraction, and theme CSS bundle extraction.

## Overview

Replace Tailwind CSS v4 and DaisyUI with a modern, layered CSS system using `@layer`, native nesting, container queries, `oklch()`, `@property`, and other 2024+ CSS features. Zero framework dependencies. Smaller output. Full cascade control.

## Final state (post Phase 7)

| Bundle | Minified | Gzipped | Contents |
|--------|----------|---------|----------|
| `shop.css` | 54.1 KB | 9.8 KB | Hand-written component CSS + three-layer theme |
| `admin.css` | 90.8 KB | 17.8 KB | Utility CSS + themes + heroicon data URIs |
| `app.js` | 139.2 KB | 42.9 KB | LiveView + hooks |

Zero framework dependencies. No Tailwind, no DaisyUI, no PostCSS plugins.

Three esbuild profiles handle all asset builds. Three root layouts isolate CSS:
- `shop_root.html.heex` loads `shop.css`
- `root.html.heex` loads `admin.css` (auth pages)
- `admin_root.html.heex` loads both `admin.css` + `shop.css`

## Pre-migration state (for reference)

| Bundle | Compiled | Contents |
|--------|----------|----------|
| `app-shop.css` | **53 KB** | Tailwind v4 (no DaisyUI) + three-layer theme |
| `app.css` | **212 KB** | Tailwind v4 + DaisyUI + three-layer theme |

281 `style=` attributes across 10 files. All removed in Phases 2-4.

**Three-layer theme system** (1,130 lines) preserved throughout:
- Primitives (94 lines): `:root` tokens
- Attributes (717 lines): `.themed` + `data-*` selectors
- Semantic (319 lines): aliases, accessibility, component styles

**CSSGenerator** (Elixir): generates inline `<style>` from DB settings. Unchanged.

## Architecture: CSS layer order

```css
@layer reset, primitives, tokens, components, layout, utilities, overrides;
```

| Layer | Purpose | Source |
|-------|---------|--------|
| `reset` | Box-sizing, margin reset, img/list defaults | New ~40 lines |
| `primitives` | `:root` design tokens (spacing, radii, fonts, type scale) | Existing `theme-primitives.css` |
| `tokens` | Theme token switching via `data-*` attrs on `.themed` | Existing `theme-layer2-attributes.css` |
| `components` | All component styles (product cards, cart, gallery, etc.) | Existing semantic.css + extracted inline styles |
| `layout` | Layout primitives (stack, cluster, row, auto-grid, etc.) | New ~200 lines |
| `utilities` | Tiny utility set (sr-only, truncate, text-balance) | New ~60 lines |
| `overrides` | Theme editor `.preview-frame` rules | Existing from `app.css` |

`@layer` eliminates all `!important` hacks — later layers beat earlier ones.

## Layout primitives (replace Tailwind flex/grid utilities)

Seven primitives cover ~80% of Tailwind layout usage:

- **`.stack`** — vertical flex with gap (replaces `flex flex-col gap-*`)
- **`.cluster`** — horizontal flex-wrap with gap (replaces `flex flex-wrap gap-*`)
- **`.row`** — horizontal flex, no wrap (replaces `flex items-center gap-*`)
- **`.auto-grid`** — intrinsic responsive grid using `auto-fill` + `minmax()` (replaces `grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4`)
- **`.container-page`** — centered max-width (replaces `max-w-7xl mx-auto px-*`)
- **`.with-sidebar`** — main + aside flex layout (replaces PDP two-column grid)
- **`.center`** — flex center alignment

Modifiers via custom properties: `--stack-gap`, `--grid-gap`, `--auto-grid-min`.

## Responsive strategy

**Intrinsic design over breakpoints:**
1. `auto-fill`/`auto-fit` grids eliminate most responsive classes
2. Container queries (`@container`) for component-level responsiveness
3. Two retained media queries for genuinely viewport-dependent layout:
   - `@media (width >= 40em)` — mobile to tablet
   - `@media (width >= 64em)` — tablet to desktop
4. `@media (hover: hover)` for desktop hover effects (already in use)
5. Fluid typography via `clamp()` (already in use)

## Admin component replacements (DaisyUI)

~300 lines of `admin-components.css` replacing DaisyUI's 15 used components:

| DaisyUI | Replacement |
|---------|-------------|
| `drawer` | CSS grid layout + checkbox toggle |
| `btn` variants | `.admin-btn` + modifiers |
| `modal` | Native `<dialog>` + `::backdrop` |
| `table` | Styled `<table>` with `.admin-table` |
| `input/select/textarea` | `.admin-input` with `:focus-visible` |
| `checkbox/toggle` | `appearance: none` + custom styling |
| `range` | `accent-color` on native `<input type=range>` |
| `alert/toast` | Left-border accent box + fixed-position container |
| `badge` | Inline pill with `border-radius: 9999px` |

## Build pipeline changes

**During migration** (Phases 1-5): Tailwind runs alongside new CSS for shop.
**After shop migration** (Phase 5): Remove `tailwind_shop` watcher.
**After full migration** (Phase 7): Remove `tailwind` dep entirely.

**Production CSS**: esbuild handles `@import` resolution + minification (already available). Lightning CSS added optionally in Phase 8 for vendor prefixing.

**Dev**: No CSS build step needed — Phoenix `live_reload` watches `.css` files directly. Modern CSS features (nesting, `@layer`, `@container`) work in all current browsers natively.

## Visual regression testing

**Tool**: Mix task wrapping Playwright for automated screenshots.

**Breakpoints**: 375px (mobile), 768px (tablet), 1024px (laptop), 1440px (desktop)

**Pages** (15 states):
1. Home (`/`)
2. Collection (`/collections/all`)
3. Collection with active filters
4. PDP single image (`/products/:id`)
5. PDP multi-image gallery
6. Cart with items (`/cart`)
7. Cart empty
8. Cart drawer open
9. Checkout success
10. About (`/about`)
11. Contact (`/contact`)
12. Delivery (`/delivery`)
13. Content page (privacy/terms)
14. Search modal open
15. 404 error page

= 60 screenshots per phase (15 pages x 4 breakpoints)

**Process per phase**:
1. Capture "before" golden screenshots
2. Complete phase work
3. Capture "after" screenshots
4. Visual diff (pixelmatch or manual comparison)
5. Fix any regressions
6. Run `mix test` (757 tests)
7. Commit

---

## Phase 0: Foundation and testing infrastructure

**Estimate**: 1 session (~1.5h) | **Shippable**: yes (no visual changes)

Tasks:
1. Create Playwright screenshot Mix task (`mix screenshots`)
2. Capture golden screenshots for all 15 pages at 4 breakpoints
3. Create `assets/css/shop/` directory structure:
   - `reset.css`, `primitives.css`, `tokens.css`, `components.css`, `layout.css`, `utilities.css`, `overrides.css`
4. Create new entry file `assets/css/shop.css` with `@layer` declaration
5. Wire nothing yet — just file structure

Files created:
- `assets/css/shop.css` (entry, not wired)
- `assets/css/shop/reset.css` (skeleton)
- `assets/css/shop/layout.css` (skeleton)
- `assets/css/shop/components.css` (skeleton)
- `assets/css/shop/utilities.css` (skeleton)
- `assets/css/shop/overrides.css` (skeleton)
- `lib/mix/tasks/screenshots.ex`

Acceptance: golden screenshots captured, `mix test` passes, no visual changes.

---

## Phase 1: Layout primitives and reset

**Estimate**: 1 session (~1.5h) | **Shippable**: yes

Tasks:
1. Write CSS reset in `shop/reset.css` (~40 lines)
2. Write layout primitives in `shop/layout.css` (~200 lines)
3. Copy existing theme CSS into `@layer` wrappers in new structure
4. Add LiveView variants and `[data-phx-session] { display: contents }`
5. Wire new `shop.css` alongside existing `app-shop.css` (both loaded)
6. Visual regression — should be no changes (Tailwind still present)

Files modified:
- `assets/css/shop/*.css` (populate reset + layout)
- `assets/css/app-shop.css` (add import of new shop.css)

Acceptance: layout primitives available, no visual changes, all tests pass.

---

## Phase 2: Inline style extraction — product components

**Estimate**: 2 sessions (~3h) | **Shippable**: after each session

**2a** — Product cards, grid, badges, hero, categories (~1.5h):
- Extract 40+ inline styles from `product.ex` into `components.css`
- Classes: `.product-card-body`, `.product-card-title`, `.product-card-price`, `.hero-section`, `.hero-content`, `.category-card`, `.category-card-overlay`
- Visual regression: home page, collection page

**2b** — PDP, variant selector, gallery, accordion (~1.5h):
- Extract remaining ~40 inline styles from `product.ex`
- Classes: `.pdp-layout`, `.variant-selector`, `.color-swatch`, `.size-button`, `.quantity-selector`, `.add-to-cart-btn`, `.accordion-item`
- Visual regression: PDP page

Files modified:
- `lib/berrypod_web/components/shop_components/product.ex` (83 style= -> 0)
- `assets/css/shop/components.css`

Acceptance: `product.ex` has zero inline styles, visual regression clean.

---

## Phase 3: Inline style extraction — layout + cart

**Estimate**: 2 sessions (~3h) | **Shippable**: after each session

**3a** — Layout components (~1.5h):
- Extract 59 inline styles from `layout.ex`
- Classes: `.announcement-bar`, `.mobile-bottom-nav`, `.mobile-nav-item`, `.search-modal-overlay`, `.search-modal-panel`, `.search-result-item`, `.shop-footer-section`
- Visual regression: all pages (header/footer/nav are global)

**3b** — Cart components (~1.5h):
- Extract 51 inline styles from `cart.ex`
- Classes: `.cart-drawer-header`, `.cart-drawer-body`, `.cart-drawer-footer`, `.cart-item-details`, `.cart-remove-btn`, `.cart-empty-state`, `.checkout-btn`, `.order-summary`
- Visual regression: cart page, cart drawer

Files modified:
- `lib/berrypod_web/components/shop_components/layout.ex` (59 -> 0)
- `lib/berrypod_web/components/shop_components/cart.ex` (51 -> 0)
- `assets/css/shop/components.css`

Acceptance: both files zero inline styles, visual regression clean.

---

## Phase 4: Inline style extraction — content + page templates

**Estimate**: 1.5 sessions (~2.5h) | **Shippable**: yes

**4a** — Content components (~1.5h):
- Extract 57 inline styles from `content.ex`
- Classes: `.content-body`, `.contact-form`, `.info-card`, `.trust-badge`, `.review-card`, `.star-rating`, `.newsletter-card`, `.page-title`

**4b** — Page templates + remaining (~1h):
- Extract 29 inline styles from `.heex` page templates
- `checkout_success.html.heex` (23), `content.html.heex` (3), others (3)
- Also `base.ex` (2)

Files modified:
- `lib/berrypod_web/components/shop_components/content.ex` (57 -> 0)
- `lib/berrypod_web/components/shop_components/base.ex` (2 -> 0)
- `lib/berrypod_web/components/page_templates/*.html.heex` (29 -> 0)
- `assets/css/shop/components.css`

Acceptance: **zero inline styles remain** (0/281), full visual regression clean.

---

## Phase 5: Remove Tailwind from shop pages

**Estimate**: 2 sessions (~3h) | **Shippable**: yes

**5a** — Replace Tailwind utility classes in components (~1.5h):
- Replace layout utilities with `.stack`, `.cluster`, `.row`, `.auto-grid`, etc.
- Replace typography utilities with `.t-caption`, `.t-small`, `.t-heading-*`
- Replace colour utilities with theme CSS variables
- Replace responsive prefixes with container queries / media queries

**5b** — Remove Tailwind shop build (~1.5h):
- Replace remaining Tailwind classes in `.heex` page templates
- Remove `@import "tailwindcss"` from `app-shop.css`
- Remove `berrypod_shop` Tailwind profile from `config/config.exs`
- Remove `tailwind_shop` watcher from `config/dev.exs`
- Update `assets.build` and `assets.deploy` Mix aliases
- Full visual regression

Files modified:
- All shop component `.ex` files and page template `.heex` files
- `assets/css/app-shop.css` -> becomes `assets/css/shop.css` (pure CSS entry)
- `config/config.exs`, `config/dev.exs`, `mix.exs`

Acceptance: no Tailwind classes in shop code, Tailwind shop build removed, admin still on Tailwind + DaisyUI, all tests pass, visual regression clean.

---

## Phase 6: Replace DaisyUI (admin pages)

**Estimate**: 2 sessions (~3h) | **Shippable**: yes

**6a** — Admin layout and navigation (~1.5h):
- Create `assets/css/admin-components.css` (~300 lines)
- Admin drawer (CSS grid), navbar, sidebar menu, button styles
- Update `admin.html.heex`, `admin_root.html.heex`

**6b** — Admin components + auth pages (~1.5h):
- Admin modal (`<dialog>`), alert/toast, badge, divider, toggle, range
- Update `core_components.ex`
- Update admin LiveViews and auth pages
- Remove `@plugin "../vendor/daisyui"` from `app.css`
- Delete `assets/vendor/daisyui.js` and `assets/vendor/daisyui-theme.js`

Files modified:
- `assets/css/admin-components.css` (new)
- `assets/css/app.css` (remove DaisyUI)
- `lib/berrypod_web/components/core_components.ex`
- All admin LiveView files
- Auth LiveView files

Acceptance: zero DaisyUI classes, vendor files deleted, admin looks identical.

---

## Phase 7: Remove Tailwind entirely

**Estimate**: 1 session (~1.5h) | **Shippable**: yes

Tasks:
1. Replace remaining Tailwind utilities in admin/auth templates
2. Remove `@import "tailwindcss"` from `app.css`
3. Replace heroicons Tailwind plugin with plain CSS icon sizing
4. Remove `tailwind` dep from `mix.exs`
5. Remove all Tailwind config from `config/config.exs` and `config/dev.exs`
6. Update `assets.build` and `assets.deploy` aliases
7. Delete `assets/vendor/heroicons.js`
8. Full visual regression across shop + admin
9. All tests pass

Files modified:
- `mix.exs` (remove `:tailwind` dep)
- `config/config.exs` (remove tailwind config)
- `config/dev.exs` (remove tailwind watchers)
- `assets/css/app.css` (pure CSS entry)
- All remaining admin/auth template files with Tailwind classes

Acceptance: `tailwind` gone from deps, zero Tailwind classes anywhere, CSS builds correctly, all tests pass, visual regression clean.

---

## Phase 8: Optimisation and modern CSS enhancements

**Estimate**: 1-2 sessions (~2-3h) | **Shippable**: yes (each item independent)

1. **Lightning CSS** — production minification + vendor prefixing
2. **Remove all `!important`** — `@layer` handles cascade
3. **Container queries** — product grid, PDP layout respond to container width
4. **`content-visibility: auto`** — skip paint for offscreen sections
5. **CSS containment** — `contain: layout style paint` on product cards
6. **oklch colours** — upgrade CSSGenerator from HSL to oklch
   - Better perceptual uniformity across hues
   - `color-mix(in oklch, var(--accent) 80%, black)` replaces HSL lightness math
   - Relative colour syntax: `oklch(from var(--accent) calc(l - 0.1) c h)`
7. **`@property`** — register typed custom properties for animated transitions
8. **Font optimisation** — `font-display: swap`, `size-adjust`, latin subsetting
9. **Critical CSS inlining** — inline above-fold CSS in `shop_root.html.heex`
10. **Performance audit** — measure file sizes, Lighthouse scores

---

## Post-migration efficiencies

### Tree-shaking and dead code

With Tailwind, tree-shaking is essential — it generates thousands of utility classes and purges unused ones. **Hand-written CSS doesn't need this** because you only write what you use. The CSS is "pre-shaken" by definition.

Efficiencies that do apply:
1. **Dead rule elimination**: Lightning CSS strips empty rules and unreachable selectors during minification
2. **`@layer` zero-cost empties**: Unused `@layer` blocks add zero bytes and zero specificity impact
3. **CSSGenerator already optimised**: Only generates CSS for the active theme variant (not all 4 moods, 7 typographies, etc.)
4. **Scope-based pruning**: All shop CSS scoped under `.themed` / `.shop-container` — unmatched selectors have no performance cost

### Actual sizes (post migration)

| Bundle | Before | After (minified) | After (gzipped) | Reduction |
|--------|--------|-------------------|-----------------|-----------|
| Shop CSS | 53 KB | 54.1 KB | 9.8 KB | similar size but zero framework |
| Admin CSS | 212 KB | 90.8 KB | 17.8 KB | ~57% minified |
| **Total** | **265 KB** | **144.9 KB** | **27.6 KB** | **~45% minified** |

Shop CSS grew slightly vs original Tailwind because all component styles are now explicit (Tailwind tree-shook aggressively). Admin CSS dropped substantially because DaisyUI's 212 KB base is gone. The gzipped sizes are what matters for users: 9.8 KB shop, 17.8 KB admin.

### Other gains

- **No build step in dev**: CSS changes are instant (no Tailwind compilation)
- **No framework version chasing**: Pure CSS doesn't break on updates
- **Better cacheability**: Semantic class names change less often than utilities
- **Composable cascade**: `@layer` means styles compose without fighting
- **Animatable tokens**: `@property` enables transitions on custom properties (smooth accent colour changes in theme editor)
- **`content-visibility`**: Browsers skip layout/paint for offscreen product grid items

---

## Risk mitigation

1. **Theme editor preview**: `.preview-frame` rules in `overrides` layer always win. Test theme editor after every phase.
2. **CSSGenerator**: Generates inline `<style>` setting CSS custom properties. Completely unaffected by the migration.
3. **JS hooks**: `SearchModal`, `CartDrawer`, `ProductImageScroll`, `Lightbox`, `CollectionFilters` all reference DOM classes/data-attrs. Verify after each phase that referenced class names match.
4. **Rollback**: Each phase is an atomic commit. Tailwind runs alongside new CSS during Phases 1-4, so old styles remain as a safety net.

---

## Summary

| Phase | What | Sessions | Hours |
|-------|------|----------|-------|
| 0 | Foundation + screenshot tooling | 1 | 1.5 |
| 1 | Layout primitives + reset | 1 | 1.5 |
| 2 | Extract product inline styles | 2 | 3 |
| 3 | Extract layout + cart inline styles | 2 | 3 |
| 4 | Extract content + template inline styles | 1.5 | 2.5 |
| 5 | Remove Tailwind from shop | 2 | 3 |
| 6 | Replace DaisyUI (admin) | 2 | 3 |
| 7 | Remove Tailwind entirely | 1 | 1.5 |
| 8 | Optimisation + modern enhancements | 1.5 | 2.5 |
| **Total** | | **14** | **~22h** |

Shop fully migrated after Phase 5. Admin after Phase 7. Phase 8 is polish. Each phase is independently shippable with visual regression verification.
