# Responsive layout ## Breakpoints — Bootstrap 5, and only these | Name | `min-width` | Typical device | | --- | --- | --- | | (base) | — | phones, 320–575 px | | `sm` | 576px | large phones | | `md` | 768px | tablets | | `lg` | 992px | small laptops | | `xl` | 1200px | desktops | | `xxl` | 1400px | large desktops | Mobile-first, `min-width` only: ```css /* ✅ base = mobile, enhance upwards */ .products-grid { display: grid; gap: var(--ac-space-md); grid-template-columns: 1fr; } @media (min-width: 768px) { .products-grid { grid-template-columns: repeat(2, 1fr); } } @media (min-width: 1200px) { .products-grid { grid-template-columns: repeat(4, 1fr); } } ``` ```css /* ❌ desktop-first bands: overlapping ranges, unclear cascade, private breakpoints */ @media (max-width: 1599px) and (min-width: 1400px) { … } @media (max-width: 720px) { … } ``` ## Prefer intrinsic layout — most grids need no media query at all ```css .products-grid { display: grid; gap: var(--ac-space-md); grid-template-columns: repeat(auto-fill, minmax(min(100%, 16rem), 1fr)); } ``` `min(100%, 16rem)` is what keeps it from overflowing at 320 px. Reach for a media query only when the layout must genuinely *change* (sidebar moves, table becomes cards), not merely resize. ## Fluid type and space ```css :root { --ac-text-sm: clamp(0.875rem, 0.85rem + 0.1vw, 0.9375rem); --ac-text-base: clamp(1rem, 0.96rem + 0.2vw, 1.125rem); --ac-text-lg: clamp(1.25rem, 1.15rem + 0.5vw, 1.5rem); --ac-text-xl: clamp(1.5rem, 1.3rem + 1vw, 2.25rem); --ac-space-md: clamp(1rem, 0.9rem + 0.5vw, 1.5rem); } ``` One continuous scale beats three step-changes at breakpoints. Never redefine a token inside a media query to make it smaller. Body copy: 1rem minimum, line-height ≥ 1.5, measure 45–75 characters (`max-width: 65ch`). ## Touch targets and pointer WCAG 2.2 AA (§2.5.8 Target Size, Minimum) asks for **24×24 px**. The familiar **44×44 px** is §2.5.5, level **AAA** — treat it as the house rule for anything a thumb hits, applied by pointer type rather than by viewport width: a 1024 px tablet needs the big target, a 1024 px laptop doesn't. ```css .qty-control .btn, .tag-filter-badge { min-width: 2.25rem; /* 36px — comfortable with a mouse */ min-height: 2.25rem; } @media (pointer: coarse) { .qty-control .btn, .tag-filter-badge { min-width: 2.75rem; /* 44px */ min-height: 2.75rem; } } @media (hover: hover) and (pointer: fine) { .product-card:hover { transform: translateY(-2px); box-shadow: var(--ac-shadow-md); } } ``` Hover effects belong inside `@media (hover: hover)` — on touch they stick after a tap and look broken. ## Tables → cards, without restructuring ```css @media (max-width: 767.98px) { .checkout-summary-table thead { /* keep it in the a11y tree; hide visually only */ position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); } .checkout-summary-table tr { display: grid; grid-template-columns: 1fr auto; gap: var(--ac-space-xs); padding: var(--ac-space-sm) 0; border-bottom: 1px solid var(--ac-border-light); } } ``` This is the one place a `max-width` query is honest: it's an override of the desktop table. Keep the markup semantic — never emit different HTML per viewport. ## Sticky bars (cart / checkout actions on mobile) ```css .checkout-actions { position: sticky; bottom: 0; z-index: var(--ac-z-sticky); padding: var(--ac-space-sm); padding-bottom: calc(var(--ac-space-sm) + env(safe-area-inset-bottom, 0px)); background: var(--ac-surface, #fff); box-shadow: 0 -2px 8px rgb(0 0 0 / 8%); } ``` `env(safe-area-inset-bottom)` keeps the button above the iOS home indicator. Sticky elements must not eat more than ~20 % of a 568 px-tall viewport. ## Images ```xml ``` - `width`/`height` always — they reserve the box and prevent layout shift (CLS). - `loading="lazy"` for everything below the fold; **never** on the LCP image (first product row, hero) — that delays the largest paint. - CSS: `max-width: 100%; height: auto;` plus `object-fit: cover` with a fixed `aspect-ratio` when cards must align. - Odoo serves resized variants: `/web/image/product.product//image_256` etc. Ask for the size you render, not `image_1920` scaled down in CSS. ## Container and page rhythm ```css .eskaera-page { width: 100%; max-width: 75rem; margin-inline: auto; padding-inline: clamp(1rem, 4vw, 2rem); } ``` Use logical properties (`margin-inline`, `padding-block`, `inset-inline-start`) — Odoo auto-generates an RTL stylesheet, and logical properties survive the flip without a second rule. ## Test matrix | Width | Why | | --- | --- | | 320 px | smallest supported; the one that breaks first | | 360 / 390 px | real Android / iPhone | | 768 px | tablet portrait, first breakpoint | | 1024 px | tablet landscape / small laptop | | 1280 / 1440 px | desktop | | 1280 px @ 200 % zoom | WCAG 1.4.4 reflow — equivalent to 640 px | Run the stack (`docker-compose up -d`, http://localhost:8070) and check, per viewport: no horizontal scroll, no clipped text, touch targets still ≥ 44 px on coarse pointers, sticky bars not covering content, images not stretched. DevTools device toolbar is enough for layout; check touch targets by actually tapping on a phone once before calling it done.