/* ---------------------------------------------------------------------------
   Domèra storefront.

   Built from domeradesignspec.md. The rules that hold everywhere:

     - **Zero border-radius.** Cards, buttons, inputs, images, panels. The two
       exceptions are stated in the spec and are marked where they occur: the
       round discount badge, and the round USP / back-to-top circles.
     - **No shadows.** Depth is hairline borders and background tints. There is
       one 8px hover panel under the nav dropdown, which the spec permits.
     - **Bands.** Sections alternate white and cream so the page reads as
       stacked strata rather than one continuous scroll.

   Order: reset · primitives · header · hero · home sections · listing ·
   product · pages · footer · responsive.
   --------------------------------------------------------------------------- */

/* === reset ================================================================ */

*,
*::before,
*::after { box-sizing: border-box; }

/* The `hidden` attribute must win. The UA rule is one attribute selector, so
   any class that sets `display` — and several below do — silently overrides
   it, leaving an element the markup declared hidden on screen. See the same
   note in admin.css. */
[hidden] { display: none !important; }

html {
    -webkit-text-size-adjust: 100%;
    scroll-behavior: smooth;
    /* An overlaying header hides whatever an in-page link scrolls to. This
       stops the target landing underneath the condensed bar; it applies to
       both the smooth-scroll module and a plain #fragment load. */
    scroll-padding-top: calc(var(--header-main-h) + 16px);
}

body {
    margin: 0;
    /* The fixed header's stand-in. Constant by design — see .site-header. */
    padding-top: var(--header-h);
    background: var(--surface-page);
    color: var(--color-body);
    font-family: var(--font-body);
    font-size: var(--text-base);
    line-height: var(--lh-body);
    letter-spacing: var(--ls-body);
}

/*
   The charte's hierarchy (§04), which is not one family but two: Cormorant
   carries H1 and H2 as display type, and H3 drops to Montserrat in spaced
   capitals — it is a label, not a title, and setting it in the display serif
   at 18px would make it look like a small H2 rather than a different rank.
*/
h1, h2, h4 {
    margin: 0;
    color: var(--color-ink);
    font-family: var(--font-display);
    font-weight: 400;
    line-height: var(--lh-heading);
    letter-spacing: var(--ls-heading);
}

/* H1: Cormorant Light. The charte tightens it by 1 % — a 56px serif at 0
   tracking reads gappy. */
h1 {
    font-weight: 300;
    letter-spacing: var(--ls-display);
}

h3 {
    margin: 0;
    color: var(--color-ink);
    font-family: var(--font-ui);
    font-size: var(--text-lg);
    font-weight: 500;
    line-height: var(--lh-heading);
    letter-spacing: var(--ls-caps);
    text-transform: uppercase;
}

p { margin: 0 0 16px; }
p:last-child { margin-bottom: 0; }

ul, ol { margin: 0; padding: 0; list-style: none; }

img { max-width: 100%; height: auto; display: block; border-radius: var(--radius); }

a { color: inherit; text-decoration: none; transition: color var(--t-colour); }
/* Cyprès, not Olivier: a link is text, and Olivier on cream is 2.9:1. */
a:hover { color: var(--color-primary-deep); }

button, input, select, textarea {
    font: inherit;
    letter-spacing: inherit;
    color: inherit;
    border-radius: var(--radius);
}

button { cursor: pointer; }

/* One focus treatment for the whole site. Keyboard-only by design: a mouse
   user clicking a card should not be given a brown outline, but a keyboard
   user must never lose their place. */
:focus-visible {
    outline: 2px solid var(--color-primary-deep);
    outline-offset: 2px;
}

.visually-hidden {
    position: absolute;
    width: 1px; height: 1px;
    margin: -1px; padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

.skip-link {
    position: absolute;
    left: -9999px;
    z-index: 100;
    padding: 12px 20px;
    background: var(--color-primary-deep);
    color: var(--surface-page);
    font-weight: 500;
}
.skip-link:focus {
    left: 12px;
    top: 12px;
    color: var(--surface-page);
}

/* === primitives =========================================================== */

/*
   Between a fixed container and a fluid one: the page grows with the viewport
   up to --container (1600px) and then stops and centres. On a laptop it is
   effectively full width; on a 2560px monitor it stays a readable block rather
   than a line of text a metre wide.

   The gutter still scales rather than stepping — clamp gives 15px on a phone
   and 48px once there is room, so the content is never against the edge on the
   way up to the cap.
*/
.container {
    width: 100%;
    max-width: var(--container);
    margin-inline: auto;
    padding-inline: clamp(var(--gutter), 3vw, 48px);
}

/*
   The one exception, and it is not a layout decision — it is a reading one.
   The editorial and legal pages are prose, and prose has a comfortable measure
   of roughly 65–75 characters; a full-width paragraph on a wide monitor is a
   400-character line that the eye loses its place in halfway across. Everything
   that is a grid, a band or a photograph goes edge to edge; only running text
   is held back.
*/
.container--narrow { max-width: 780px; }

.band {
    padding: var(--section-pt) 0 var(--section-pb);
    background: var(--surface-page);
}
.band--cream { background: var(--surface-cream); }
.band--tight { padding-top: 50px; }
.band__more { margin: 40px 0 0; text-align: center; }

/* The signature section heading: centred 40/700 with a 70×2 brown rule. */
.section-head { margin: 0 0 46px; text-align: center; }

.section-head__title {
    font-size: var(--text-3xl);
    font-weight: 700;
    line-height: 48px;
    text-transform: capitalize;
}

.section-head__rule {
    display: block;
    width: 70px;
    height: 2px;
    margin: 18px auto 0;
    background: var(--color-primary);
}

.section-head__intro {
    max-width: 620px;
    margin: 18px auto 0;
    color: var(--color-body);
}

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    height: 48px;
    padding: 0 28px;
    border: 1px solid transparent;
    border-radius: var(--radius);
    /* Montserrat 500 in spaced capitals — the charte's "capitales espacées".
       The label is UI, not prose, so it does not inherit Lora. */
    font-family: var(--font-ui);
    font-size: 14px;
    font-weight: 500;
    letter-spacing: var(--ls-button);
    text-transform: uppercase;
    transition: background var(--t-colour), color var(--t-colour), border-color var(--t-colour);
}

/* Cyprès ground with cream type: 6.44:1. Olivier here would be 2.9:1, which
   is why the primary button is not the primary colour. */
.btn--primary { background: var(--color-primary-deep); color: var(--surface-page); }
.btn--primary:hover { background: var(--color-ink); color: var(--surface-page); }

.btn--light { background: var(--surface-page); color: var(--color-primary-deep); }
.btn--light:hover { background: var(--color-primary-deep); color: var(--surface-page); }

.btn--ghost { border-color: var(--color-primary-deep); color: var(--color-primary-deep); background: transparent; }
.btn--ghost:hover { background: var(--color-primary-deep); color: var(--surface-page); }

.btn--wide { width: 100%; }

.prose { color: var(--color-body); }
.prose h2 {
    margin: 34px 0 14px;
    font-size: var(--text-2xl);
    font-weight: 700;
    line-height: 36px;
}
.prose h2:first-child { margin-top: 0; }
.prose h3 { margin: 26px 0 10px; font-size: 20px; font-weight: 700; }
.prose ul { margin: 0 0 16px; padding-left: 20px; list-style: disc; }
.prose li { margin-bottom: 8px; }
.prose a { color: var(--color-primary-deep); text-decoration: underline; text-underline-offset: 3px; }
.prose img { margin: 20px 0; }

/* Marks a legal placeholder the shop still has to fill in. Deliberately
   conspicuous: an unfinished mentions-légales that looks finished never gets
   finished. */
.prose .todo {
    padding: 14px 18px;
    border-left: 3px solid var(--color-sale);
    background: var(--surface-cream);
    color: var(--color-ink-soft);
    font-size: var(--text-sm);
}

.muted { color: var(--color-muted); }
.empty { color: var(--color-body); text-align: center; }
.empty-state { padding: 40px 0; text-align: center; }
.empty-hint { margin-top: 6px; color: var(--color-muted); font-size: var(--text-sm); }
.page-cta { margin-top: 36px; display: flex; gap: 14px; flex-wrap: wrap; }

.flash-stack { margin: 24px auto 0; }
.flash {
    margin: 0 0 10px;
    padding: 14px 18px;
    border-left: 3px solid var(--color-primary);
    background: var(--surface-cream);
    color: var(--color-ink-soft);
    font-size: var(--text-sm);
}
.flash--error { border-left-color: var(--color-sale); }
.flash--success { border-left-color: var(--color-stock); }

.ico { display: block; flex: none; }

/* === header =============================================================== */

/*
   Fixed, not sticky, and the reason is the collapse.

   A sticky header stays in normal flow, so its rendered height *is* its flow
   height. Hiding the 93px family bar therefore shortened the document by 93px
   in the middle of the animation, every frame of it. The browser's scroll
   anchoring then pulled scrollY down to compensate — often far enough to fall
   back under the threshold, which re-expanded the bar, which pushed scrollY
   back up. Measured on the home page that oscillation flipped the header 32
   times during a single slow scroll: the shudder, and the blinking.

   Fixed positioning takes the header out of flow entirely. The space it used
   to occupy is reserved once by body padding below and never changes again, so
   condensing the header cannot move the page, cannot move scrollY, and cannot
   feed back into the threshold that triggered it.
*/
.site-header {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 40;
    background: var(--surface-page);
    color: var(--color-primary-deep);
    border-bottom: 1px solid var(--color-line);
}

/*
   Commentaires client §3 : « Insérer le vrai logo de Domèra (Fond beige). Le
   fond de la barre de menu sera alors en beige et vert. »

   La barre devient réellement beige et verte : le logo garde son fond beige,
   tandis que la zone de navigation et d'outils porte le Vert Cyprès. Le texte
   de menu passe en Crème Lin dans cette zone afin de rester lisible.
*/
.header-bar--main {
    background: var(--color-primary-deep);
    color: var(--surface-page);
}

/*
   The bar heights are custom properties because two things have to agree on
   them and must not drift: the bars themselves, and the body padding standing
   in for the header. The families bar is display:none under 992px, where the
   override sets its share to zero.
*/
:root {
    /*
       Liée à --logo-h, pas saisie à la main.

       .brand entoure le logo de sa zone de protection — la moitié de la
       hauteur de l'artwork de chaque côté — donc la marque occupe
       logo + logo/2 + logo/2, soit exactement le double de --logo-h. La barre
       ne peut pas être plus courte que ce qu'elle contient : lui donner un
       nombre indépendant, c'est laisser les deux diverger, et c'est ce qui
       s'est produit en agrandissant le logo (§3) — la barre mesurait
       réellement 144px alors qu'elle en déclarait 116, et le haut de chaque
       page passait 28px sous l'en-tête fixe, dont la hauteur sert de
       padding-top au corps.
    */
    --header-main-h: calc(var(--logo-h) * 2);
    /*
       §4 : « Diminuer la largeur de la barre secondaire pour qu'elle soit plus
       mince que la barre principale. » Elle passe de 93px à 46px, soit un peu
       moins de la moitié de la principale — la hiérarchie se voit d'un coup
       d'œil au lieu de se deviner.
    */
    --header-families-h: 46px;
    /* §3 : « Agrandir la taille du Logo. » 48 → 72px de haut, soit 180px de
       large, bien au-dessus du minimum de 120px de la charte. */
    --logo-h: 72px;
    /* The header row's own spacing: 20px at 1600, 8px by 1150. Named because
       the brand's protection zone is measured against it — see .brand. */
    --header-row-gap: clamp(8px, calc(2.67vw - 22.7px), 20px);
    --header-h: calc(var(--header-main-h) + var(--header-families-h));
}

.header-bar--main { min-height: var(--header-main-h); display: flex; align-items: center; }

.header-bar__inner {
    display: flex;
    align-items: center;
    min-height: var(--header-main-h);
    background: var(--color-primary-deep);
}

/*
   The protection zone, charte §03: clearance equal to the height of the "O" on
   all four sides, expressed as padding on the container rather than as margin
   baked into the artwork — the file has almost none of its own (2–8px on a
   250×100 canvas).

   The two constraints the charte places on this mark resolve to a single
   number. It must render at least 120px wide, and the artwork is 2.5:1, so the
   floor is 48px tall. The "O" is half the artwork's height, so the zone is
   24px, and 48 + 24 + 24 = 96px — which is exactly what the 97px header bar
   has to give. One size satisfies both, at every breakpoint, so the logo no
   longer steps down as the viewport narrows: it cannot, without breaking the
   minimum.
*/
.brand {
    display: flex;
    align-items: center;
    flex: none;
    /*
       Vertical: the full zone, always — nothing competes for it there.

       Horizontal: the full zone down to about 1400, then easing to 10px. The
       charte's zone is clearance from other elements, and on the side facing
       the nav the row gap already contributes 14–28px of it, so the two
       together never fall below the 24px the mark is owed. This is the last
       thing to give and it gives only once the row is genuinely short.
    */
    padding: calc(var(--logo-h) / 2) clamp(6px, calc(2.22vw - 19.6px), 16px);
    margin-right: var(--header-row-gap);
    background: var(--surface-cream);
    color: var(--color-primary-deep);
}
.brand:hover { color: var(--color-primary-deep); }

/*
   Plus de recolorisation. Le logo était retourné en crème pour tenir sur le
   fond vert ; la barre est maintenant beige (§3), donc le fichier s'affiche
   avec ses propres couleurs — ce qui est précisément ce que « insérer le vrai
   logo (fond beige) » demande.
*/
.brand__logo {
    display: block;
    width: auto;
    height: var(--logo-h);
}

/* min-width:0 lets the nav shrink below its content's intrinsic width instead
   of forcing the header wider than the viewport. A flex item defaults to
   min-width:auto, and this row holds seven nowrap links between a fixed brand
   and a fixed tool cluster — which is exactly the arrangement that pushes a
   page into horizontal scroll at in-between widths. */
.primary-nav { flex: 1 1 auto; min-width: 0; }
/* Stays on one line. The row now carries Accueil plus the catalogue families,
   so the surrounding spacing and search field give first; below 992px the
   drawer takes over. */
/*
   The row spacing is fluid, not stepped.

   Between the burger breakpoint and full width this row has to hold six
   entries plus whatever the catalogue adds, between a fixed logo and a search
   field. Stepping it at two breakpoints meant the nav was comfortable at
   1600px, wrapped to two lines at 1366, and stayed wrapped all the way down —
   the step landed in the wrong place and there was nothing between the steps.

   Interpolating instead keeps one line continuously: the gap, the type size
   and the tracking all give a little at every pixel rather than nothing for
   200px and then a lot. clamp's floor is the point at which giving more would
   cost legibility instead of buying room; below that the nav becomes the
   drawer anyway.
*/
.primary-nav__list {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: nowrap;
    /* 14px at 1600, 6px by 1150. */
    gap: 0 clamp(6px, calc(1.78vw - 14.4px), 14px);
}

.primary-nav__list > li { position: relative; }

.primary-nav__list > li > a {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 12px 0;
    color: var(--surface-page);
    /* Montserrat: the nav is UI, not prose. */
    font-family: var(--font-ui);
    /*
       13px — the charte's caption size — down to 11px as the row tightens.
       Written as a calc rather than a vw alone so the two ends are the
       anchors: 13px at 1600, 11px at 1150, and a straight line between.

       11px is the floor because below it uppercase Montserrat stops being
       comfortable, and because the width it would buy is not enough to change
       the outcome: the row is held on one line by the search field and the
       brand's clearance giving way, not by the type shrinking further.
    */
    /*
       §3 : « Agrandir la police de caractère pour que la barre principale soit
       plus visible que la barre secondaire. » 15px au large, 12px au plus
       serré — contre 12px/11px de la barre secondaire, qui reste donc
       toujours en dessous.
    */
    font-size: clamp(12px, calc(0.44vw + 7.94px), 15px);
    font-weight: 600;
    /*
       Tracking is what actually eats the width here — 0.1em across six
       uppercase labels is most of a word. It is the first thing to give and
       the last thing to notice, so it eases from the charte's +100 down to
       roughly a third of it. In px, not em: em would compound with the font
       size above and fall away twice as fast.
    */
    letter-spacing: clamp(0px, calc(0.18vw - 1.78px), 0.8px);
    text-transform: uppercase;
    white-space: nowrap;
    border-bottom: 2px solid transparent;
}
.primary-nav__list > li > a:hover { color: var(--color-primary-soft); }

/* The current section is marked with a Terre Nabeul rule rather than a colour
   change: the accent is the charte's 10 %, and it carries meaning here without
   having to pass a text-contrast threshold as type. */
.primary-nav__list > li > a[aria-current] {
    color: var(--surface-page);
    border-bottom-color: var(--color-sale);
}

/* Dropdowns open on hover and on keyboard focus. :focus-within is what makes
   the second one work without a line of JavaScript. */
.dropdown {
    position: absolute;
    top: 100%;
    left: 0;
    min-width: 220px;
    padding: 8px 0;
    background: var(--surface-page);
    border: var(--border-hairline);
    border-radius: var(--radius);
    /* No shadow: the charte allows a hairline and a flat tint as the only
       depth devices. The border is what lifts this off the page. */
    opacity: 0;
    visibility: hidden;
    transition: opacity var(--t-colour), visibility var(--t-colour);
}
.has-children:hover .dropdown,
.has-children:focus-within .dropdown { opacity: 1; visibility: visible; }

.dropdown a {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    padding: 9px 20px;
    color: var(--color-ink);
    font-size: 14px;
}
.dropdown a:hover { color: var(--color-primary-deep); background: var(--surface-cream); }
.dropdown__count { color: var(--color-muted); font-size: var(--text-sm); }

/* Shrinkable, but never below the icons: the search field gives up width
   first, and the account and basket controls keep theirs. */
.header-tools {
    display: flex;
    align-items: center;
    gap: clamp(4px, 0.65vw, 10px);
    flex: 0 1 auto;
    min-width: 0;
    padding-right: clamp(8px, 1vw, 18px);
}

/* The one fully-rounded element in the header, per the spec. */
.search { position: relative; display: flex; min-width: 0; }
/*
   The field gives width to the nav as the row tightens, but never below what
   shows a placeholder: a search box narrower than its own prompt reads as
   broken rather than as compact.
*/
.search input {
    width: 100%;
    max-width: clamp(110px, calc(32vw - 294px), 220px);
    min-width: 110px;
    height: 42px;
    padding: 0 44px 0 18px;
    border: 1px solid var(--color-line);
    border-radius: 999px;
    background: #fff;
    font-family: var(--font-ui);
    font-size: 14px;
    letter-spacing: var(--ls-body);
}
/* Gris Olivier, not #999 — 4.6:1 on white, and in the palette. */
.search input::placeholder { color: var(--color-muted); }
.search button {
    position: absolute;
    right: 4px;
    top: 4px;
    width: 34px;
    height: 34px;
    display: grid;
    place-items: center;
    border: 0;
    border-radius: 999px;
    background: transparent;
    color: var(--color-primary-deep);
}
.search button:hover { color: var(--color-ink); }

.header-tools__link { display: grid; place-items: center; width: 40px; height: 40px; color: var(--surface-page); }
.header-tools__link:hover { color: var(--color-primary-soft); }

/* The count bubble sits on the basket's top-right. One of the header's two
   rounded elements, alongside the search pill — everything else stays square. */
.header-tools__cart { position: relative; }

.header-tools__count {
    position: absolute;
    top: 1px;
    right: 0;
    min-width: 17px;
    height: 17px;
    padding: 0 4px;
    display: grid;
    place-items: center;
    border-radius: 999px;
    background: var(--surface-page);
    color: var(--color-primary-deep);
    font-family: var(--font-ui);
    font-size: 10px;
    font-weight: 500;
    line-height: 1;
    /* A cream ring separating the bubble from the basket glyph beneath it.
       Written as an outline rather than a zero-blur box-shadow so that the
       stylesheet contains no box-shadow at all — the charte's "aucune ombre"
       is easier to hold to when it can be checked with a grep. An outline also
       does not affect layout, which is what a ring wants. */
    outline: 2px solid var(--color-primary-deep);
}

.header-tools__burger {
    display: none;
    place-items: center;
    width: 40px; height: 40px;
    border: 0;
    background: transparent;
    color: var(--surface-page);
}

/* Second bar: the family shortcuts. Hidden once the page is scrolled. */
.header-bar--families {
    border-top: 1px solid var(--color-line);
    overflow: hidden;
    max-height: var(--header-families-h);
    transition: max-height var(--t-colour), opacity var(--t-colour);
}

.site-header.is-condensed .header-bar--families {
    max-height: 0;
    opacity: 0;
    border-top-color: transparent;
}

.family-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    min-height: var(--header-families-h);
    overflow-x: auto;
    scrollbar-width: none;
}
.family-bar::-webkit-scrollbar { display: none; }

/*
   §4, quatre demandes sur le même élément :

     — même police que la barre principale : c'est --font-ui des deux côtés,
       et le §2 l'a désormais généralisée à tout le site ;
     — plus petite que la principale, pour que la hiérarchie tienne ;
     — un trait sous la rubrique survolée ;
     — et un cadre coloré qui la met en relief. Le choix de la couleur nous
       était laissé : l'Olivier clair de la charte, qui se pose sur le Sable
       Doux sans le percer.

   Le cadre est une bordure transparente au repos, pas une bordure ajoutée au
   survol : une bordure qui apparaît décale tout ce qui suit, et une barre de
   navigation dont les entrées sautent au passage de la souris est exactement
   ce que le §4 cherche à éviter.
*/
.family-bar a {
    display: flex;
    align-items: center;
    padding: 6px 12px;
    color: var(--color-primary-deep);
    font-family: var(--font-ui);
    font-size: var(--text-xs);
    font-weight: 500;
    letter-spacing: var(--ls-caption);
    white-space: nowrap;
    border: 1px solid transparent;
    border-bottom-width: 2px;
    transition: background var(--t-colour), border-color var(--t-colour);
}

.family-bar a:hover,
.family-bar a:focus-visible,
.family-bar a[aria-current] {
    background: var(--color-primary-soft);
    border-color: var(--color-primary);
    /* Le trait demandé, plus marqué que le reste du cadre. */
    border-bottom-color: var(--color-primary-deep);
}

/* An uploaded icon standing in for the built-in glyph. `contain`, not `cover`:
   a mark cropped to fill a square is a mark with its edges cut off, and these
   are uploaded at whatever proportions the shop had to hand. The box matches
   the 30px the SVG occupies so the row does not change height when one
   category has an icon and its neighbour does not. */
.family-bar__icon {
    flex: none;
    width: 30px;
    height: 30px;
    object-fit: contain;
}

/* === mobile drawer ======================================================== */

.drawer { position: fixed; inset: 0; z-index: 60; }
.drawer[hidden] { display: none; }

/* Cyprès at half opacity rather than neutral black. This is a modal veil,
   not the depth shadow the charte forbids, but it has no reason to be a colour
   that is not in the palette. */
.drawer__scrim { position: absolute; inset: 0; background: rgba(79, 90, 69, 0.55); }

.drawer__panel {
    position: absolute;
    inset: 0 auto 0 0;
    z-index: 1;
    width: min(340px, 86vw);
    padding: 24px;
    overflow-y: auto;
    background: var(--surface-page);
}

.drawer__close {
    margin: 0 0 20px auto;
    display: grid;
    place-items: center;
    width: 40px; height: 40px;
    border: var(--border-hairline);
    background: var(--surface-page);
    color: var(--color-ink);
}

.drawer__list > li > a {
    display: block;
    padding: 12px 0;
    border-bottom: var(--border-hairline);
    color: var(--color-ink);
    font-size: var(--text-lg);
    font-weight: 500;
}
.drawer__list ul { padding: 4px 0 10px 14px; }
.drawer__list ul a { display: block; padding: 7px 0; font-size: 14px; }

/* === hero ================================================================= */

.hero { position: relative; overflow: hidden; }

/*
   The slides are stacked in one grid cell rather than absolutely positioned.
   That keeps every slide in normal flow, so the hero is as tall as its tallest
   slide on its own — no magic height to keep in step with the copy, and a
   slide whose headline wraps to three lines on a narrow screen cannot spill
   out of a fixed box.
*/
.hero__slides { display: grid; }

.hero__slide {
    grid-area: 1 / 1;
    position: relative;
    display: flex;
    align-items: center;
    min-height: 700px;
    opacity: 0;
    /* visibility, not just opacity: a transparent slide still has a link and a
       button in it, and without this they stay focusable and clickable. It is
       held until the fade finishes so the outgoing slide does not vanish. */
    visibility: hidden;
    transition: opacity var(--t-fade), visibility 0s linear var(--t-fade);
}

.hero__slide.is-active {
    opacity: 1;
    visibility: visible;
    transition: opacity var(--t-fade), visibility 0s;
}

/* The photograph. Both banners put their subject on the left and leave the
   right side as pale wall, which is the space the copy sits in. */
.hero__image {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: center;
}

.hero__inner { position: relative; z-index: 2; }

/* Spec 5.2: the text block starts ~48% across and is left-aligned inside it,
   leaving the left half of the frame to the photograph's subject. */
.hero__text { max-width: 560px; margin-left: 48%; }

.hero__eyebrow {
    margin: 0 0 22px;
    padding-left: 20px;
    border-left: 3px solid var(--color-ink);
    color: var(--color-ink);
    font-size: 18px;
    line-height: 30px;
    letter-spacing: 0;
}

/* h1 on the first slide, p on the rest — see the template. Everything the
   heading reset would have given the tag is restated here, colour included, so
   the two render identically: without it the p slides inherit body grey and
   only the first slide's headline is black. */
.hero__title {
    margin: 0 0 34px;
    color: var(--color-ink);
    font-size: var(--text-hero);
    font-weight: 500;
    line-height: 80px;
    letter-spacing: 0;
}

/* --- controls ---
   Spec 5.2: thin chevrons, no circles. The chevron is the control; what sits
   under it is a square white plate, the same device the category tiles use.
   A bare chevron was the first attempt and it disappeared into the foliage on
   the left of the first banner — a control has to survive whatever photograph
   the shop puts behind it, and this one cannot know that in advance. */
.hero__arrow {
    position: absolute;
    top: 50%;
    z-index: 3;
    transform: translateY(-50%);
    display: flex;
    align-items: center;
    justify-content: center;
    width: 48px;
    height: 48px;
    padding: 0;
    border: 0;
    background: rgba(255, 255, 255, 0.8);
    color: var(--color-ink);
    cursor: pointer;
    transition: background-color var(--t-colour), color var(--t-colour);
}

.hero__arrow:hover,
.hero__arrow:focus-visible { background: #fff; color: var(--color-primary-deep); }

.hero__arrow--prev { left: clamp(4px, 1.5vw, 28px); }
.hero__arrow--next { right: clamp(4px, 1.5vw, 28px); }

/* The category banner: the same photograph the home-page tile uses, across the
   full container above the heading. Capped by height rather than left to its
   own proportions — these are uploaded at whatever shape the shop had, and a
   square one would fill the screen before the first product. */
.listing-banner {
    margin-top: var(--section-pt, 40px);
    overflow: hidden;
}

.listing-banner img {
    display: block;
    width: 100%;
    height: clamp(180px, 26vw, 380px);
    object-fit: cover;
}

/* === category tiles ======================================================= */

/*
   auto-fit, not repeat(4, 1fr): the number of tiles is data, not design. It is
   however many root categories are published — three here, because one is
   archived — and a fixed four-column grid leaves the missing ones as a hole at
   the end of the row. That hole was invisible at 1270px and is a quarter of the
   screen once the container is fluid. auto-fit lets the row fill whatever the
   catalogue actually has.
*/
.tiles {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: var(--grid-gap);
}

/* The positioned ancestor for the plate — deliberately the tile and not the
   link, so `bottom` is measured against the tile's full height (image plus the
   40px reserved below it) rather than against the image alone. That difference
   is what makes the plate hang past the photograph instead of sitting inside
   its lower edge. No overflow here: clipping is the frame's job. */
.tile { position: relative; padding-bottom: 40px; }
.tile a { display: block; }

.tile__frame { display: block; height: 350px; overflow: hidden; }

.tile__art {
    display: block;
    height: 100%;
    background: var(--tone-1);
    transition: transform var(--t-image);
    transform-origin: center;
}
.tile:nth-child(2) .tile__art { background: var(--tone-2); }
.tile:nth-child(3) .tile__art { background: var(--tone-4); }
.tile:nth-child(4) .tile__art { background: var(--tone-6); }

/* The uploaded photograph, where one exists. It takes the same box and the
   same hover as the aplat it replaces, so the tile behaves identically
   whether or not the shop has got round to adding a picture. */
.tile__photo {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    transition: transform var(--t-image);
    transform-origin: center;
}

.tile a:hover .tile__art,
.tile a:hover .tile__photo { transform: scale(1.05); }

/* The plate — white, square, inset 30px from the left, hanging 20px below the
   image, i.e. 20px above the tile's own bottom edge. */
/*
   §7 : « Afficher le nom de la rubrique au dessous de l'image en supprimant le
   cadre ainsi que la mention du nombre de pièces disponibles. »

   La plaque blanche qui débordait du coin inférieur gauche de la photographie
   est supprimée, et avec elle le compteur. Reste le nom, centré sous l'image,
   dans la même police et le même vert que le reste du site.
*/
.tile__name {
    display: block;
    margin-top: 14px;
    color: var(--color-primary-deep);
    font-family: var(--font-ui);
    font-size: var(--text-lg);
    font-weight: 500;
    letter-spacing: var(--ls-caption);
    text-align: center;
    text-transform: uppercase;
}

/* === tabs ================================================================= */

.tabs__list {
    display: flex;
    justify-content: center;
    flex-wrap: wrap;
    gap: 24px;
    margin: 0 0 40px;
}

.tab {
    height: 46px;
    padding: 0 28px;
    border: 0;
    border-radius: var(--radius);
    background: #fff;
    color: var(--color-ink);
    font-size: 14px;
    font-weight: 500;
    letter-spacing: var(--ls-button);
    text-transform: uppercase;
    transition: background var(--t-colour), color var(--t-colour);
}
.tab:hover { background: var(--color-primary-hover); color: #fff; }
.tab.is-active { background: var(--color-primary-deep); color: var(--surface-page); }

/* Without JavaScript every panel stays visible — three stacked grids is a
   worse layout than one, but it is not a broken page. shop.js marks the
   container `is-enhanced` and only then do the inactive panels hide. */
.tabs.is-enhanced .tabs__panel { display: none; }
.tabs.is-enhanced .tabs__panel.is-active { display: block; }

/* === product grid and card ================================================ */

.grid { display: grid; gap: var(--grid-gap); }
.grid--4 { grid-template-columns: repeat(4, 1fr); }
.grid--3 { grid-template-columns: repeat(3, 1fr); }

.card {
    position: relative;
    height: 100%;
    display: flex;
    flex-direction: column;
    padding: 15px;
    border: 1px solid transparent;
    border-radius: var(--radius);
    background: #fff;
    transition: border-color var(--t-colour);
    overflow: hidden;
}
.card:hover { border-color: var(--color-line); }

/* Clips both the image scale and the CTA bar. The bar's resting position is
   translateY(100%) — flush outside this box's bottom edge — which is only
   correct because the box ends where the image ends. */
.card__frame { position: relative; overflow: hidden; }

/*
   §9 : « Agrandir l'image dans le cadre (Laisser plus de place à l'image du
   produit) ». Le cadre passe d'un rapport 4:5 à un carré et l'image le
   remplit : à largeur de colonne égale, la photographie gagne toute la marge
   que le format portrait lui prenait en hauteur.
*/
.card__media {
    position: relative;
    display: block;
    aspect-ratio: 1 / 1;
    overflow: hidden;
    background: var(--surface-cream);
}
.card__media img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    transition: transform var(--t-image);
}
.card:hover .card__media img { transform: scale(1.05); }

.card__placeholder {
    display: grid;
    place-items: center;
    width: 100%;
    height: 100%;
    color: rgba(79, 90, 69, 0.3);
    transition: transform var(--t-image);
}
.card:hover .card__placeholder { transform: scale(1.05); }

.card__placeholder--1 { background: var(--tone-1); }
.card__placeholder--2 { background: var(--tone-2); }
.card__placeholder--3 { background: var(--tone-3); }
.card__placeholder--4 { background: var(--tone-4); }
.card__placeholder--5 { background: var(--tone-5); }
.card__placeholder--6 { background: var(--tone-6); }

/* The round discount badge — the spec's one circle in the content area. */
.badge {
    position: absolute;
    top: 15px;
    left: 15px;
    z-index: 2;
    display: grid;
    place-items: center;
    padding: 0 6px;
    font-size: 13px;
    font-weight: 500;
}
.badge--sale {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    background: var(--color-sale);
    color: var(--color-sale-text);
}
.badge--soldout,
.badge--order {
    left: auto;
    right: 15px;
    height: 22px;
    border-radius: var(--radius);
    background: var(--color-ink);
    color: #fff;
    font-size: var(--text-xs);
    letter-spacing: 0.5px;
    text-transform: uppercase;
}
.badge--order { background: var(--color-new); }

/* The action rail slides in from the image's right edge on hover. It is
   pointer-only decoration: the same link is on the title and the CTA, both of
   which are in the tab order. */
.card__rail {
    position: absolute;
    top: 62px;
    right: 15px;
    z-index: 2;
    display: grid;
    gap: 1px;
    transform: translateX(calc(100% + 15px));
    opacity: 0;
    transition: transform var(--t-colour), opacity var(--t-colour);
}
.card:hover .card__rail { transform: translateX(0); opacity: 1; }

.card__rail a {
    display: grid;
    place-items: center;
    width: 36px; height: 36px;
    background: #fff;
    color: var(--color-ink);
}
.card__rail a:hover { background: var(--color-primary-deep); color: var(--surface-page); }

.card__body { padding: 16px 0 0; flex: 1 1 auto; }

.card__title {
    margin: 0 0 8px;
    font-size: var(--text-md);
    font-weight: 400;
    line-height: 24px;
    text-transform: capitalize;
    /* Two lines then ellipsis, so a long name cannot make one card taller
       than the three beside it. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}
.card__title a { color: var(--color-ink); }
.card__title a:hover { color: var(--color-primary-deep); }

.card__price { margin: 0 0 6px; display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.card__price s { color: var(--color-muted); font-size: var(--text-lg); font-weight: 400; }
.card__price strong { color: var(--color-primary-deep); font-size: var(--text-lg); font-weight: 700; }

.card__stock { margin: 0; color: var(--color-muted); font-size: var(--text-sm); }
.card--sold-out .card__media { opacity: 0.65; }

/* The add-to-cart bar of the spec, retargeted at the only action that exists
   today. Slides up over the image bottom on hover. */
.card__cta {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 2;
    padding: 12px 0;
    /* Parked immediately below the frame, where overflow:hidden hides it. */
    transform: translateY(100%);
    background: var(--color-primary-deep);
    color: var(--surface-page);
    font-size: 14px;
    font-weight: 500;
    letter-spacing: var(--ls-button);
    text-transform: uppercase;
    text-align: center;
    transition: transform var(--t-colour);
}
.card:hover .card__cta { transform: translateY(0); }
/*
   §9 : « Préserver le fond vert pour ce texte (parfois il apparaît sur fond
   noir) ». Le fond noir venait d'ici : le survol du bandeau basculait sur
   --color-ink, qui était l'Encre #1F1E1A. Il passe au Vert Cyprès foncé, donc
   le bandeau est vert au repos et vert au survol, en deux nuances.
*/
.card__cta:hover { background: var(--color-primary-hover); color: var(--surface-page); }

/* --- countdown overlay ---------------------------------------------------- */

.countdown {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 2;
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    background: #fff;
}

.countdown__cell {
    display: block;
    padding: 8px 2px;
    text-align: center;
    border-left: var(--border-hairline);
}
.countdown__cell:first-child { border-left: 0; }

.countdown__cell b { display: block; color: var(--color-ink); font-size: 15px; font-weight: 700; }
.countdown__cell i {
    display: block;
    color: var(--color-muted);
    font-size: 10px;
    font-style: normal;
    text-transform: uppercase;
}
.countdown__cell--seconds b { color: var(--color-sale); }

/* === USP strip ============================================================ */

/* §10 : trois valeurs au lieu de quatre garanties. */
.usp { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--grid-gap); }
.usp--values { grid-template-columns: repeat(3, 1fr); }

/*
   §10 : « Le logo de Domèra Mobilier en mentionnant au dessous une marque
   française (avec éventuellement un macaron tricolore discret à côté) ».
   Sous les trois valeurs, centré, le macaron à la taille du texte.
*/
.values-brand {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
    margin: 50px 0 0;
}

.values-brand__logo { display: block; width: auto; height: 56px; }

.values-brand__line {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    color: var(--color-primary-deep);
    font-family: var(--font-ui);
    font-size: var(--text-sm);
    letter-spacing: var(--ls-caption);
    text-transform: uppercase;
}

/* Discret : la hauteur d'une capitale, avec un filet pour que la bande
   blanche du milieu ne se perde pas sur le fond crème. */
.values-brand__flag { display: block; outline: 1px solid var(--color-line); }
.usp__item { text-align: center; }

/* Circles are permitted here by the spec. */
.usp__icon {
    display: grid;
    place-items: center;
    width: 70px; height: 70px;
    margin: 0 auto 12px;
    border: 1px solid var(--color-primary);
    border-radius: 50%;
    color: var(--color-primary-deep);
}

.usp__item h3 { margin: 0 0 6px; font-size: var(--text-lg); font-weight: 700; }
.usp__item p { margin: 0; font-size: 14px; }

/* === promo banners ======================================================== */

.banners { padding: 0 0 var(--section-pb); }
.banners__grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--grid-gap); }

.banner {
    position: relative;
    min-height: 320px;
    display: grid;
    place-items: center;
    padding: 40px;
    text-align: center;
    overflow: hidden;
}
/* A scrim over the tonal ground. The banner heading is white, and white on the
   bare tan measures about 2.5:1 — below the 3:1 that even large text needs.
   Darkening the ground takes it past 4.5:1 and is what a real lifestyle
   photograph would need here too, so the scrim stays when the photo lands. */
/*
   A Cyprès veil over the aplat, so the cream type on top clears AA. The old
   veil was a warm near-black that belonged to the brown scheme.

   The colour is `background-color`, not the second layer of `background-image`.
   It used to be the latter, which was valid while --tone-* held gradients and
   became invalid the moment they became flat colours — `background-image` does
   not accept a colour, so the whole declaration was dropped and both banners
   rendered as empty cream boxes with invisible headings.
*/
.banner--one,
.banner--two {
    background-image: linear-gradient(rgba(47, 54, 41, 0.62), rgba(47, 54, 41, 0.62));
}
.banner--one { background-color: var(--tone-4); }
.banner--two { background-color: var(--tone-2); }

/* The floating 1px white rectangle, inset 20px — a signature detail. */
.banner__inset {
    position: absolute;
    inset: 20px;
    border: 1px solid rgba(255, 255, 255, 0.85);
    pointer-events: none;
}

.banner__body { position: relative; }
.banner__body h2 {
    margin: 0 0 24px;
    color: var(--surface-page);
    font-size: var(--text-3xl);
    font-weight: 500;
    line-height: 52px;
    letter-spacing: 0;
    text-transform: capitalize;
}

/* === workshop / quote cards =============================================== */

.quotes { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--grid-gap); }

.quote {
    padding: 40px;
    background: var(--surface-cream);
    border-radius: var(--radius);
    text-align: center;
}

.quote__mark { display: block; margin-bottom: 16px; color: var(--color-primary-deep); font-size: 34px; line-height: 1; }
.quote__text { margin: 0 0 18px; font-size: 14px; line-height: 24px; }
.quote__by { margin: 0; }
/*
   The one place Italianno appears. §2.2 of the brief calls it "réservé aux
   signatures d'édition" and names this block as the natural candidate — so it
   sets the name underneath the quote, which is literally a signature, and not
   the quote itself, which is prose and stays in Lora.

   Sized up to 26px because a script face at 14px is a smudge: Italianno's
   x-height is a fraction of its em, so it needs roughly double the point size
   of the sans beside it to read at the same visual weight.
*/
.quote__by strong {
    display: block;
    color: var(--color-primary-deep);
    font-family: var(--font-script);
    font-size: 26px;
    font-weight: 400;
    line-height: 1.2;
}
.quote__by span { color: var(--color-body); font-size: 14px; }

/* === breadcrumb and listing =============================================== */

/*
   §5 : « Est-il possible de supprimer cette 3ième barre ? Si non, la mettre
   avec la même police et même couleur (mais plus petit). »

   Conservée : c'est le seul repère de position sur une fiche produit atteinte
   depuis une recherche ou un lien externe, et sa suppression coûterait aussi
   le balisage de fil d'Ariane que les moteurs lisent. Elle prend donc la
   seconde branche de la question — même police (une seule sur le site depuis
   le §2), même vert, et plus petite : 12px au lieu de 13, sur une hauteur
   réduite de moitié.
*/
.breadcrumb {
    padding: 8px 0;
    border-bottom: var(--border-hairline);
    font-family: var(--font-ui);
    font-size: var(--text-xs);
    color: var(--color-primary-deep);
}
.breadcrumb ol { display: flex; flex-wrap: wrap; gap: 8px; }
.breadcrumb li + li::before { content: '/'; margin-right: 8px; color: var(--color-line); }
.breadcrumb a { color: var(--color-body); }
.breadcrumb [aria-current] { color: var(--color-ink); }

.chips { display: flex; flex-wrap: wrap; gap: 10px; margin: 0 0 34px; justify-content: center; }
.chip {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 9px 18px;
    border: var(--border-hairline);
    color: var(--color-ink);
    font-size: 14px;
}
.chip:hover { border-color: var(--color-primary-deep); color: var(--color-primary-deep); }
.chip span { color: var(--color-muted); font-size: var(--text-sm); }

.listing-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 20px;
    flex-wrap: wrap;
    margin: 0 0 30px;
    padding-bottom: 18px;
    border-bottom: var(--border-hairline);
}
.listing-bar__count { margin: 0; color: var(--color-muted); font-size: var(--text-sm); }

.listing-bar__sort { display: flex; align-items: center; gap: 10px; font-size: var(--text-sm); }
.listing-bar__sort select {
    height: 40px;
    padding: 0 12px;
    border: var(--border-hairline);
    background: #fff;
}
.listing-bar__sort button {
    height: 40px;
    padding: 0 16px;
    border: 1px solid var(--color-ink);
    background: transparent;
    font-size: var(--text-sm);
    text-transform: uppercase;
    letter-spacing: var(--ls-button);
}
.listing-bar__sort button:hover { background: var(--color-ink); color: #fff; }

.pagination {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 20px;
    flex-wrap: wrap;
    margin-top: 50px;
}
.pagination__pages { display: flex; gap: 6px; }
.pagination__pages a,
.pagination__pages span,
.pagination__step {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    min-width: 40px;
    height: 40px;
    padding: 0 12px;
    justify-content: center;
    border: var(--border-hairline);
    color: var(--color-ink);
    font-size: 14px;
}
.pagination__pages a:hover,
.pagination__step:hover { border-color: var(--color-primary-deep); color: var(--color-primary-deep); }
/* The number lives in a nested span, which `.pagination__pages span` above
   also matches — so the colour has to be restated for it, or it keeps the ink
   it inherits from that rule and sits dark on a dark ground. */
.pagination__pages [aria-current],
.pagination__pages [aria-current] span {
    background: var(--color-primary-deep);
    border-color: var(--color-primary-deep);
    color: var(--surface-page);
}
.pagination__pages [aria-current] span { border: 0; }
.pagination__gap { border: 0; color: var(--color-muted); }

/* === product page ========================================================= */

.product { display: grid; grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr); gap: 60px; }

.product__photo {
    aspect-ratio: 1 / 1;
    background: var(--surface-cream);
    overflow: hidden;
}
.product__photo img { width: 100%; height: 100%; object-fit: cover; }

.product__photo--placeholder {
    display: grid;
    place-items: center;
    align-content: center;
    gap: 12px;
    color: rgba(79, 90, 69, 0.3);
}
.product__photo--placeholder p { margin: 0; color: var(--color-muted); font-size: var(--text-sm); }
.product__photo--1 { background: var(--tone-1); }
.product__photo--2 { background: var(--tone-2); }
.product__photo--3 { background: var(--tone-3); }
.product__photo--4 { background: var(--tone-4); }
.product__photo--5 { background: var(--tone-5); }
.product__photo--6 { background: var(--tone-6); }

.product__thumbs { display: flex; gap: 10px; margin-top: 10px; flex-wrap: wrap; }
.product__thumbs button {
    width: 84px; height: 84px;
    padding: 0;
    border: 1px solid transparent;
    background: none;
    overflow: hidden;
}
.product__thumbs button[aria-pressed='true'] { border-color: var(--color-primary-deep); }
.product__thumbs img { width: 100%; height: 100%; object-fit: cover; }

.product__title { margin: 0 0 18px; font-size: var(--text-2xl); font-weight: 700; line-height: 36px; }

.product__price { margin: 0 0 6px; display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
.product__price s { color: var(--color-muted); font-size: 20px; }
.product__price strong { color: var(--color-primary-deep); font-size: 30px; font-weight: 700; }
.product__tva { color: var(--color-muted); font-size: var(--text-sm); }

.product__promo-ends { margin: 0 0 18px; color: var(--color-sale); font-size: var(--text-sm); }

.product__facts {
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: 6px 24px;
    margin: 26px 0;
    padding: 22px 0;
    border-top: var(--border-hairline);
    border-bottom: var(--border-hairline);
    font-size: 14px;
}
.product__facts dt { color: var(--color-muted); }
.product__facts dd { margin: 0; color: var(--color-ink); }

.product__actions { margin: 0 0 30px; }
.product__description h2 { margin: 0 0 12px; font-size: 20px; font-weight: 700; }

/* === espace client ======================================================== */

/* The sign-in, register and message cards. Centred and bounded: a login form
   the width of a 1600px container is a form whose label and field are a screen
   apart. */
.auth-card { max-width: 460px; margin-inline: auto; }
.auth-card--wide { max-width: 720px; }
.auth-card--message { text-align: center; }

.auth-card form { margin-top: 26px; }

.auth-card__links {
    margin: 22px 0 0;
    text-align: center;
    font-size: var(--text-sm);
    color: var(--color-muted);
}
.auth-card__links a { color: var(--color-primary-deep); text-decoration: underline; text-underline-offset: 3px; }

/* Two fields on a row where they belong together — given/family name, the two
   password boxes — collapsing to one column before they get cramped. */
.field-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: 0 var(--grid-gap);
}

.field-group {
    margin: 0 0 20px;
    padding: 22px;
    border: var(--border-hairline);
    background: var(--surface-cream);
}
.field-group legend {
    padding: 0 8px;
    font-family: var(--font-ui);
    font-size: var(--text-sm);
    font-weight: 500;
    letter-spacing: var(--ls-caption);
    text-transform: uppercase;
}

.field-help { display: block; margin-top: 6px; font-size: var(--text-xs); color: var(--color-muted); }
.field__error { display: block; margin-top: 6px; font-size: var(--text-xs); color: var(--color-primary-deep); font-weight: 700; }
.field--error input,
.field--error select { border-color: var(--color-primary-deep); }

.choice { display: block; margin: 0 0 12px; }
.choice label { display: flex; align-items: flex-start; gap: 10px; font-size: 14px; }
.choice input { flex: none; margin-top: 3px; }

/* --- the account area --- */
.account {
    display: grid;
    grid-template-columns: minmax(0, 220px) minmax(0, 1fr);
    gap: 50px;
    margin-top: 40px;
}

.account__nav ul { border-top: var(--border-hairline); }
.account__nav li { border-bottom: var(--border-hairline); }
.account__nav a {
    display: block;
    padding: 14px 0;
    font-family: var(--font-ui);
    font-size: var(--text-sm);
    letter-spacing: var(--ls-caption);
    text-transform: uppercase;
}
.account__nav a:hover { color: var(--color-primary-deep); }
.account__nav a[aria-current] { color: var(--color-primary-deep); font-weight: 500; }

.account__signout { margin-top: 26px; }

.account__section-title {
    margin: 38px 0 16px;
    font-family: var(--font-display);
    font-size: var(--text-xl);
    font-weight: 400;
    letter-spacing: 0;
    text-transform: none;
}

.detail-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
    gap: 20px var(--grid-gap);
    margin: 0;
}
.detail-grid dt { font-size: var(--text-xs); color: var(--color-muted); text-transform: uppercase; letter-spacing: var(--ls-caption); }
.detail-grid dd { margin: 4px 0 0; }

/* The VAT status chip. Terre Nabeul for anything not yet confirmed by VIES,
   Cyprès once it is — the difference decides whether a sale can be zero-rated,
   so it should not look like a detail. */
.vat-status {
    display: inline-block;
    margin-left: 8px;
    padding: 2px 8px;
    background: var(--color-sale);
    color: var(--color-sale-text);
    font-family: var(--font-ui);
    font-size: var(--text-xs);
    letter-spacing: var(--ls-caption);
    text-transform: uppercase;
}
.vat-status--vies_valid { background: var(--color-primary-deep); color: var(--surface-page); }

.table-scroll { overflow-x: auto; }

.account-table { width: 100%; border-collapse: collapse; font-size: 14px; }
.account-table th,
.account-table td { padding: 14px 12px; border-bottom: var(--border-hairline); text-align: left; }
.account-table th {
    font-family: var(--font-ui);
    font-size: var(--text-xs);
    font-weight: 500;
    letter-spacing: var(--ls-caption);
    text-transform: uppercase;
    color: var(--color-muted);
}
.account-table .is-numeric { text-align: right; white-space: nowrap; }

/* === contact ============================================================== */

.contact { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); gap: 60px; }

.field { display: block; margin: 0 0 20px; }
.field label { display: block; margin-bottom: 6px; color: var(--color-ink); font-size: 14px; font-weight: 500; }
.field input,
.field textarea {
    width: 100%;
    padding: 13px 16px;
    border: var(--border-hairline);
    background: #fff;
}
.field input:focus,
.field textarea:focus { border-color: var(--color-primary-deep); }
.field textarea { resize: vertical; min-height: 160px; }
.field--error input,
.field--error textarea { border-color: var(--color-sale); }
.field__error { display: block; margin-top: 6px; color: var(--color-sale); font-size: var(--text-sm); }

/* Honeypot: off-screen rather than display:none, because some bots skip
   hidden inputs and this one is meant to be filled. */
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

.contact__details { color: var(--color-body); }
.contact__details li { color: var(--color-body); }

/* === error page =========================================================== */

.error-page { text-align: center; }
.error-page__status { margin: 0 0 10px; color: var(--color-primary-deep); font-size: 60px; font-weight: 700; line-height: 1; }
.error-page__message { margin: 0 auto; max-width: 520px; }
.error-page .page-cta { justify-content: center; }
.error-page__trace {
    margin-top: 40px;
    padding: 20px;
    overflow-x: auto;
    background: var(--surface-cream);
    color: var(--color-ink-soft);
    font-size: 12px;
    text-align: left;
    white-space: pre-wrap;
    word-break: break-word;
}

/* === footer =============================================================== */

/* Le fond du pied de page est passé au Sable Doux (§12). Tout ce qui s'y
   écrivait en crème repasse donc au Vert Cyprès, sans quoi le texte est clair
   sur clair et disparaît. Ancien commentaire, conservé pour l'historique :
   The footer ground is Vert Cyprès now, not black. Everything on it is drawn
   from the light end of the palette — Crème Lin and Sable Doux — rather than
   from plain white, and the olive accents that worked on black do not work
   here: Vert Olivier on Cyprès is 2.2:1. */
/*
   §12 : « Réduire la largeur du pied de page en préservant la police tout en
   modifiant le fond vert par un fond beige. »

   Le vert plein devient Sable Doux, le texte passe au Vert Cyprès — la même
   couleur d'écriture que partout ailleurs depuis le §2 — et le bloc est borné
   à 1200px au lieu de courir sur toute la largeur de l'écran.
*/
.site-footer {
    background: var(--surface-cream);
    color: var(--color-primary-deep);
    border-top: var(--border-hairline);
}

.site-footer .container { max-width: 1200px; }

.footer-grid {
    display: grid;
    grid-template-columns: 1.3fr 1fr 1fr 1.2fr;
    gap: var(--grid-gap);
    padding-top: 70px;
    padding-bottom: 70px;
}

.footer-col__title {
    margin: 0 0 30px;
    color: var(--color-primary-deep);
    font-family: var(--font-ui);
    font-size: var(--text-lg);
    font-weight: 500;
    letter-spacing: var(--ls-caps);
    text-transform: uppercase;
}

.footer-about { color: var(--color-primary-deep); font-size: 14px; line-height: 28px; }
.footer-made { margin: 16px 0 0; color: var(--color-muted); font-size: var(--text-sm); letter-spacing: 1px; text-transform: uppercase; }

.footer-links li { margin-bottom: 12px; }
.footer-links a { color: var(--color-primary-deep); font-size: 14px; }
.footer-links a:hover { color: var(--color-primary); text-decoration: underline; }

/* Scoped to .site-footer on purpose: the same class dresses the contact
   page's own details list, which sits on the cream page. A light colour set on
   the bare class made that block invisible there. */
.footer-contact li { display: flex; gap: 12px; margin-bottom: 16px; font-size: 14px; line-height: 24px; }
.site-footer .footer-contact li { color: var(--color-primary-deep); }
.footer-contact a { color: inherit; }
.footer-contact a:hover { color: var(--color-primary-deep); }
.site-footer .footer-contact a:hover { color: var(--color-primary); }
.footer-contact .ico { margin-top: 3px; color: var(--color-primary-deep); }
.site-footer .footer-contact .ico { color: var(--color-primary); }

.footer-bottom { border-top: var(--border-hairline); }
.footer-bottom__inner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    flex-wrap: wrap;
    padding-top: 15px;
    padding-bottom: 15px;
}
.footer-bottom p { margin: 0; color: var(--color-muted); font-size: 14px; }
.footer-bottom__links { display: flex; gap: 10px; }
.footer-bottom a { color: var(--color-primary-deep); }
.footer-bottom a:hover { color: var(--color-primary); text-decoration: underline; }

/* The other permitted circle. */
.to-top {
    position: fixed;
    right: 24px;
    bottom: 24px;
    z-index: 50;
    display: grid;
    place-items: center;
    width: 44px; height: 44px;
    border-radius: 50%;
    background: var(--color-primary-deep);
    color: var(--surface-page);
    transition: background var(--t-colour);
}
.to-top[hidden] { display: none; }
.to-top:hover { background: var(--color-ink); color: var(--surface-page); }

/* === responsive ===========================================================
   The product grid steps 4 → 3 → 2 → 2 → 1 across the spec's breakpoints.

   There is no step *up*: the container stops growing at 1600px, so a card
   never exceeds about 350px however wide the monitor is. That cap is what
   removes the need for one — a genuinely fluid container had to keep adding
   columns to stop a 1:1 card image becoming a 590px-tall photograph of a
   chair. */

@media (max-width: 1400px) {
    .hero__title { font-size: 60px; line-height: 70px; }
}

/*
   The header row between the burger breakpoint and full width.

   Almost nothing is stepped here any more. The type size, the tracking, the
   nav's own spacing, the row gap, the search field and the logo's protection
   zone all interpolate — measured, the row then holds a single line from
   1920px down to 1200px, where it used to break at 1366 and stay broken.

   Below 1200 the search field steps out and the nav stays on one line until
   the burger takes over at 992.

   What is left below is genuinely a step: the search field gives its space to
   the menu once the row is genuinely tight.
*/
@media (max-width: 1280px) {
    .primary-nav__list > li > a { padding: 10px 0; }
}

@media (max-width: 1350px) {
    .search input { min-width: 96px; }
}

@media (max-width: 1200px) {
    .search { display: none; }
    .grid--4 { grid-template-columns: repeat(3, 1fr); }
    .tiles { grid-template-columns: repeat(2, 1fr); }
}

@media (max-width: 992px) {
    :root { --section-pt: 60px; --section-pb: 40px; --grid-gap: 20px; }

    /* The account menu moves above the panel rather than shrinking into a
       column too narrow for "Vue d'ensemble". */
    .account { grid-template-columns: 1fr; gap: 30px; }

    /* The families bar is not rendered here, so it reserves no space either.
       One property keeps the header and the body padding in agreement. */
    :root { --header-families-h: 0px; }

    .primary-nav,
    .header-bar--families { display: none; }
    .header-tools__burger { display: grid; }
    .header-bar__inner { justify-content: space-between; }

    .grid--4,
    .grid--3 { grid-template-columns: repeat(2, 1fr); }
    .usp { grid-template-columns: repeat(2, 1fr); }
    .quotes { grid-template-columns: 1fr; }
    .banners__grid { grid-template-columns: 1fr; }

    .product,
    .contact { grid-template-columns: 1fr; gap: 36px; }

    .hero__slide { min-height: 520px; }
    .hero__title { font-size: 46px; line-height: 56px; }

    /* The copy moves back to the left edge here, which puts it over the part of
       the photograph the furniture is in — and a portrait viewport crops both
       banners towards their centre, so there is no pale wall left to sit on.
       A wash restores the contrast the wide layout got from the composition. */
    .hero__text { margin-left: 0; }
    .hero__slide::after {
        content: '';
        position: absolute;
        inset: 0;
        z-index: 1;
        background: linear-gradient(to bottom, rgba(255, 255, 255, 0.82), rgba(255, 255, 255, 0.42));
    }

    /* A 2.16:1 photograph in a portrait frame is mostly crop. Centred, that
       crop lands on the empty wall the wide layout wants and throws away the
       furniture — the banner becomes a white rectangle. Pulling the frame left
       keeps the piece that is actually being sold in it. */
    .hero__image { object-position: 32% center; }

    /*
       The chevrons are the only control now, so they cannot be hidden here the
       way they were when dots covered this width. At the edges they would sit
       on top of the copy, which is left-aligned from this breakpoint down — so
       they pair up in the bottom-right corner instead, which the headline and
       the button both leave empty.
    */
    .hero__arrow {
        top: auto;
        bottom: 18px;
        width: 40px;
        height: 40px;
    }
    .hero__arrow--prev { left: auto; right: 66px; }
    .hero__arrow--next { right: 18px; }

    .footer-grid { grid-template-columns: repeat(2, 1fr); row-gap: 40px; }
}

@media (max-width: 768px) {
    .section-head__title { font-size: 30px; line-height: 38px; }
    .banner__body h2 { font-size: 30px; line-height: 40px; }
    .card__cta { display: none; }
    .card__rail { display: none; }
}

@media (max-width: 576px) {
    .grid--4,
    .grid--3,
    .tiles,
    .usp,
    .footer-grid { grid-template-columns: 1fr; }

    .hero__title { font-size: 36px; line-height: 46px; }
    .hero__eyebrow { font-size: 16px; line-height: 26px; }


    .tabs__list { gap: 10px; }
    .tab { padding: 0 18px; }

    .listing-bar { flex-direction: column; align-items: flex-start; }
}

/* --- empty basket --------------------------------------------------------- */

.cart-empty {
    display: flex;
    align-items: flex-start;
    gap: 28px;
    padding: 32px;
    background: var(--surface-cream);
}

.cart-empty__mark { flex: none; color: var(--color-primary-deep); }

@media (max-width: 576px) {
    .cart-empty { flex-direction: column; gap: 18px; }
}

/* === the basket =========================================================== */

/*
   Everything in this section obeys the charte the same way the rest of the
   stylesheet does: square corners, hairlines and flat tints, no shadow. The
   only rounded things remain the search pill, the count bubble and the sale
   badge.

   No `box-shadow` declaration appears below, and no colour that is not a
   token — the two things easiest to reach for and hardest to take back out
   once a second rule has copied them.
*/

/* --- the sidebar ---------------------------------------------------------- */

.cart-drawer { position: fixed; inset: 0; z-index: 70; }
.cart-drawer[hidden] { display: none; }

/*
   Above the mobile navigation drawer's z-index of 60 rather than beside it.
   They share the left edge and shop.js closes one before opening the other,
   but if that ever fails the basket — the panel someone just asked for — is
   the one that should be on top.
*/
.cart-drawer__scrim {
    position: absolute;
    inset: 0;
    /* Cyprès at half opacity, like the navigation drawer's veil. A modal
       scrim, not the depth shadow the charte forbids, and no reason to be a
       colour outside the palette. */
    background: rgba(79, 90, 69, 0.55);
    animation: cart-scrim-in var(--t-colour) both;
}

.cart-drawer__panel {
    position: absolute;
    /* Left, per the brief. `inset: 0 auto 0 0` pins top, bottom and left and
       lets the width decide the rest. */
    inset: 0 auto 0 0;
    z-index: 1;
    display: flex;
    flex-direction: column;
    width: min(400px, 92vw);
    background: var(--surface-page);
    border-right: var(--border-hairline);
    animation: cart-panel-in var(--t-image) both;
}

@keyframes cart-scrim-in {
    from { opacity: 0; }
    to { opacity: 1; }
}

@keyframes cart-panel-in {
    from { transform: translateX(-100%); }
    to { transform: translateX(0); }
}

.cart-drawer__head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 12px;
    padding: 22px 24px 18px;
    border-bottom: var(--border-hairline);
}

.cart-drawer__heading {
    margin: 0;
    font-family: var(--font-display);
    font-size: var(--text-xl);
    font-weight: 400;
    line-height: 1.2;
}

.cart-drawer__count {
    display: block;
    margin-top: 4px;
    font-family: var(--font-ui);
    font-size: var(--text-xs);
    font-weight: 500;
    letter-spacing: var(--ls-caption);
    text-transform: uppercase;
    color: var(--color-muted);
}

.cart-drawer__close {
    flex: none;
    display: grid;
    place-items: center;
    width: 38px; height: 38px;
    border: var(--border-hairline);
    background: transparent;
    color: var(--color-ink);
}
.cart-drawer__close:hover { background: var(--color-primary-deep); color: var(--surface-page); }

/* The scrolling region. `min-height: 0` is what lets a flex child actually
   scroll instead of growing past its parent and pushing the footer off. */
.cart-drawer__body {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
    overflow-y: auto;
}

.cart-drawer__empty {
    margin: auto;
    padding: 40px 24px;
    text-align: center;
    color: var(--color-muted);
}
.cart-drawer__empty span { display: block; margin-bottom: 14px; color: var(--color-primary-soft); }
.cart-drawer__empty p { margin: 0 0 20px; }

/* A correction the basket made for itself. Terre Nabeul rule, like the
   legal-placeholder block: something the reader has to notice, not an error. */
.cart-drawer__notice {
    margin: 0;
    padding: 12px 24px;
    border-left: 3px solid var(--color-sale);
    background: var(--surface-cream);
    font-size: var(--text-sm);
}

.cart-drawer__list { padding: 0 24px; }

.cart-drawer__foot {
    flex: none;
    display: grid;
    gap: 10px;
    padding: 20px 24px 24px;
    border-top: var(--border-hairline);
    background: var(--surface-cream);
}

.cart-drawer__total {
    margin: 0;
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    font-family: var(--font-ui);
    font-size: var(--text-sm);
    letter-spacing: var(--ls-caption);
    text-transform: uppercase;
}
.cart-drawer__total strong {
    font-size: var(--text-xl);
    font-weight: 700;
    letter-spacing: 0;
    text-transform: none;
    color: var(--color-primary-deep);
}

.cart-drawer__note { margin: 0 0 4px; font-size: var(--text-xs); color: var(--color-muted); }

/* --- one line, in the sidebar --------------------------------------------- */

.cart-line {
    display: grid;
    grid-template-columns: 72px 1fr auto;
    gap: 14px;
    padding: 18px 0;
    border-bottom: var(--border-hairline);
}
.cart-line:last-child { border-bottom: 0; }

.cart-line__media { display: block; width: 72px; height: 84px; overflow: hidden; background: var(--surface-cream); }
.cart-line__media img { width: 100%; height: 100%; object-fit: cover; }

.cart-line__placeholder {
    display: grid;
    place-items: center;
    width: 100%;
    height: 100%;
    color: var(--color-primary-deep);
}

.cart-line__body { min-width: 0; }

.cart-line__title {
    margin: 0 0 4px;
    font-family: var(--font-body);
    font-size: var(--text-sm);
    font-weight: 400;
    line-height: 1.35;
}
.cart-line__title a { color: var(--color-ink); }
.cart-line__title a:hover { color: var(--color-primary-deep); }

.cart-line__price { margin: 0 0 10px; font-size: var(--text-xs); color: var(--color-muted); }

.cart-line__aside { display: flex; flex-direction: column; align-items: flex-end; justify-content: space-between; gap: 10px; }
.cart-line__total { margin: 0; font-weight: 700; color: var(--color-primary-deep); white-space: nowrap; }

.cart-line__remove {
    display: grid;
    place-items: center;
    width: 30px; height: 30px;
    border: 0;
    background: transparent;
    color: var(--color-muted);
}
.cart-line__remove:hover { color: var(--color-primary-deep); }

/* --- the quantity stepper ------------------------------------------------- */

/*
   One control, two homes: the sidebar's `.cart-line__qty` and the page's
   `.qty-stepper`. Same rules, so a change to the target size is one edit.
*/
.cart-line__qty,
.qty-stepper {
    display: inline-grid;
    grid-template-columns: 32px minmax(34px, auto) 32px;
    align-items: stretch;
    border: var(--border-hairline);
    background: #fff;
}

.cart-line__qty button,
.qty-stepper button {
    display: grid;
    place-items: center;
    border: 0;
    background: transparent;
    color: var(--color-ink);
    /* 32px is below the 44px touch target the WCAG AAA rule asks for, and
       deliberately so: three of these plus a bin have to fit in a 400px panel
       beside a photograph. The page's basket — where the same edit is made
       with room to spare — is the accessible route, and every one of these has
       a visually-hidden label so the control is at least unambiguous. */
    min-height: 32px;
}
.cart-line__qty button:hover:not(:disabled),
.qty-stepper button:hover:not(:disabled) { background: var(--color-primary-deep); color: var(--surface-page); }
.cart-line__qty button:disabled,
.qty-stepper button:disabled { color: var(--color-primary-soft); cursor: not-allowed; }

.cart-line__qty output,
.qty-stepper output {
    display: grid;
    place-items: center;
    border-inline: var(--border-hairline);
    font-family: var(--font-ui);
    font-size: 14px;
    font-weight: 500;
}

/* While a change is in flight. Not a spinner: the answer arrives in well under
   the time one would take to be worth showing, and a control that dims is
   enough to say "pressed". */
.is-busy { opacity: 0.55; pointer-events: none; }

/* --- the basket page ------------------------------------------------------ */

.cart-layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 340px;
    gap: 40px;
    align-items: start;
}

.cart-table { width: 100%; border-collapse: collapse; }

.cart-table thead th {
    padding: 0 12px 12px;
    border-bottom: 1px solid var(--color-primary-deep);
    font-family: var(--font-ui);
    font-size: var(--text-xs);
    font-weight: 500;
    letter-spacing: var(--ls-caption);
    text-transform: uppercase;
    text-align: left;
    color: var(--color-muted);
}

.cart-table td,
.cart-table tbody th {
    padding: 18px 12px;
    border-bottom: var(--border-hairline);
    vertical-align: middle;
    text-align: left;
    font-weight: 400;
}

.cart-table__media { width: 90px; padding-left: 0; }
.cart-table__media a { display: block; width: 78px; height: 90px; overflow: hidden; background: var(--surface-cream); }
.cart-table__media img { width: 100%; height: 100%; object-fit: cover; }

.cart-table__name a { color: var(--color-ink); font-size: var(--text-md); }
.cart-table__name a:hover { color: var(--color-primary-deep); }

.cart-table__sku,
.cart-table__lead {
    display: block;
    margin-top: 4px;
    font-family: var(--font-ui);
    font-size: var(--text-xs);
    color: var(--color-muted);
}
.cart-table__lead { color: var(--color-primary-deep); }

.cart-table__unit { white-space: nowrap; color: var(--color-muted); }
.cart-table__total { white-space: nowrap; }
.cart-table__total strong { color: var(--color-primary-deep); font-size: var(--text-lg); }

.cart-table__remove { width: 52px; padding-right: 0; text-align: right; }
.cart-table__remove button {
    display: inline-grid;
    place-items: center;
    width: 36px; height: 36px;
    border: 0;
    background: transparent;
    color: var(--color-muted);
}
.cart-table__remove button:hover { color: var(--color-primary-deep); }

.cart-actions {
    margin-top: 26px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    flex-wrap: wrap;
}

/* "Vider le panier" is destructive, so it is deliberately the quietest control
   on the page — a link-weight button rather than one that competes with
   "Commander". */
.btn--quiet {
    height: 48px;
    padding: 0 4px;
    border: 0;
    background: transparent;
    color: var(--color-muted);
    font-family: var(--font-ui);
    font-size: var(--text-sm);
    text-decoration: underline;
    text-underline-offset: 3px;
}
.btn--quiet:hover { color: var(--color-primary-deep); }

/* A control that is present but cannot be used — a sold-out piece's action.
   Rendered as a <span aria-disabled>, not a disabled <button>: there is no
   form to submit, and a disabled button is skipped by the tab order, so a
   keyboard user would never learn the piece is gone. */
.btn--disabled {
    border-color: var(--color-line);
    background: var(--surface-cream);
    color: var(--color-muted);
    cursor: not-allowed;
}

/* --- the summary panel ---------------------------------------------------- */

.cart-summary {
    padding: 26px;
    border: var(--border-hairline);
    background: var(--surface-cream);
    position: sticky;
    /* Clear of the condensed header, which is 84px tall once the family bar
       has collapsed away. */
    top: 100px;
}

.cart-summary__title {
    margin: 0 0 18px;
    font-family: var(--font-display);
    font-size: var(--text-xl);
    font-weight: 400;
}

.cart-summary__lines {
    display: grid;
    grid-template-columns: 1fr auto;
    gap: 10px 14px;
    margin: 0 0 18px;
    padding-bottom: 18px;
    border-bottom: var(--border-hairline);
    font-size: var(--text-sm);
}
.cart-summary__lines dt { color: var(--color-muted); }
.cart-summary__lines dd { margin: 0; text-align: right; }

.cart-summary__total {
    margin: 0 0 18px;
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
    font-family: var(--font-ui);
    font-size: var(--text-sm);
    letter-spacing: var(--ls-caption);
    text-transform: uppercase;
}
.cart-summary__total strong {
    font-size: var(--text-2xl);
    font-weight: 700;
    letter-spacing: 0;
    text-transform: none;
    color: var(--color-primary-deep);
}

.cart-summary__hint { margin: 0 0 16px; font-size: var(--text-xs); color: var(--color-muted); }

/* --- the header's bubble, when it changes --------------------------------- */

/*
   The pulse the basket icon gives when something lands in it. Kept to a scale
   and nothing else — a colour flash on the bubble would fight the cream ring
   that separates it from the glyph underneath.

   It runs under prefers-reduced-motion too, because the global rule collapses
   the duration to 0.01ms rather than removing it: the class is added and
   removed either way, and someone who has asked for less motion still gets the
   state change, just not the movement.
*/
.header-tools__cart.is-bumped .header-tools__count { animation: cart-bump 0.45s ease; }

@keyframes cart-bump {
    0%   { transform: scale(1); }
    35%  { transform: scale(1.45); }
    100% { transform: scale(1); }
}

/* --- the piece in flight -------------------------------------------------- */

/*
   The clone that travels from the photograph to the basket icon.

   `left`, `top`, `width`, `height` and `--fly-x` / `--fly-y` are set from
   JavaScript through the CSSOM — element.style.setProperty — because the CSP
   has no 'unsafe-inline' for styles and a style attribute written into markup
   would be blocked. Everything that is a *look* rather than a coordinate lives
   here.

   `position: fixed` means the coordinates are viewport coordinates, which is
   exactly what getBoundingClientRect returns: no scroll offset to add, and
   nothing to get wrong when the page scrolls mid-flight.
*/
.cart-flyer {
    position: fixed;
    z-index: 90;
    overflow: hidden;
    pointer-events: none;
    background: var(--surface-cream);
    /* The curve is the point. A linear flight reads as a sprite being dragged;
       easing out with a slight overshoot at the start reads as something
       thrown. */
    transition:
        transform 0.65s cubic-bezier(0.55, -0.35, 0.35, 1),
        opacity 0.65s ease-in,
        width 0.65s ease-in,
        height 0.65s ease-in;
}

.cart-flyer img { width: 100%; height: 100%; object-fit: cover; }

/* No photograph to clone: a flat Sable Doux tile still says "that tile moved",
   which is the whole job. */
.cart-flyer--blank { background: var(--surface-cream); border: var(--border-hairline); }

.cart-flyer.is-flying {
    transform: translate(var(--fly-x, 0), var(--fly-y, 0)) scale(0.12);
    opacity: 0.15;
}

/* --- what the basket says out loud ---------------------------------------- */

/*
   The confirmation line. Bottom-right rather than over the header: the sidebar
   opens on the left and the piece flies to the top-right, so the one corner
   left free is the one this takes.
*/
.cart-toast {
    position: fixed;
    right: clamp(15px, 3vw, 40px);
    bottom: clamp(15px, 3vw, 40px);
    z-index: 80;
    max-width: min(380px, calc(100vw - 30px));
    margin: 0;
    padding: 14px 18px;
    border-left: 3px solid var(--color-primary-deep);
    background: var(--surface-page);
    outline: 1px solid var(--color-line);
    font-size: var(--text-sm);
    opacity: 0;
    transform: translateY(8px);
    transition: opacity var(--t-colour), transform var(--t-colour);
}
.cart-toast[hidden] { display: none; }
.cart-toast.is-visible { opacity: 1; transform: translateY(0); }

/* The to-top circle sits in the same corner. It gets out of the way rather
   than being covered — a control half behind a message is a control that
   swallows the click meant for the message. */
.has-cart-open .to-top { opacity: 0; pointer-events: none; }

/* --- the product page's add-to-cart form ---------------------------------- */

/*
   `.product__actions` was a <p> holding one link and is now a <form> holding a
   finish picker, a quantity and two buttons. The selector is unchanged so the
   30px below it stays where the rest of the column expects it; what is added
   here is only the internal layout.
*/
.product__actions { display: grid; gap: 12px; }

.product__actions .field { margin: 0; }

.product__actions select {
    width: 100%;
    height: 46px;
    padding: 0 12px;
    border: 1px solid var(--color-line);
    background: #fff;
    font-family: var(--font-body);
    font-size: var(--text-base);
    color: var(--color-ink);
}
.product__actions select:focus { border-color: var(--color-primary-deep); }

/* A number input wide enough for two digits and no wider: a full-width
   quantity field beside a full-width button reads as two equal choices. */
.product__actions .field--qty input {
    width: 96px;
    height: 46px;
    padding: 0 10px;
    border: 1px solid var(--color-line);
    background: #fff;
    font-family: var(--font-ui);
    font-size: var(--text-base);
}
.product__actions .field--qty input:focus { border-color: var(--color-primary-deep); }

/* --- the basket control on a product tile --------------------------------- */

/*
   The rail gains a second control. `.card__rail form` rather than a new class:
   the button inside it has to be the same 36px square as the link beside it,
   and saying so twice is how they stop matching.
*/
.card__rail form { display: block; }

.card__rail button {
    display: grid;
    place-items: center;
    width: 36px; height: 36px;
    border: 0;
    background: #fff;
    color: var(--color-ink);
}
.card__rail button:hover { background: var(--color-primary-deep); color: var(--surface-page); }

/*
   The rail is pointer-only decoration for the "voir" link, whose destination is
   also on the title. The basket button is not duplicated anywhere else on the
   tile, so it has to be reachable — the rail comes out on keyboard focus as
   well as on hover.
*/
.card__rail:focus-within { transform: translateX(0); opacity: 1; }

/* --- checkout ------------------------------------------------------------- */

.checkout {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 380px;
    gap: 40px;
    align-items: start;
}

.checkout__block {
    margin: 0 0 30px;
    padding: 24px;
    border: var(--border-hairline);
    background: var(--surface-page);
    /* The pair of columns the address fields sit in. A single column would
       put a two-character postcode field on its own line at 1600px. */
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0 20px;
}

.checkout__block legend {
    grid-column: 1 / -1;
    margin-bottom: 18px;
    padding: 0;
    font-family: var(--font-display);
    font-size: var(--text-xl);
    font-weight: 400;
}

.checkout__block .field { margin-bottom: 18px; }
.field--wide { grid-column: 1 / -1; }

.checkout__block select {
    width: 100%;
    height: 46px;
    padding: 0 12px;
    border: 1px solid var(--color-line);
    background: #fff;
    font-family: var(--font-body);
    font-size: var(--text-base);
    color: var(--color-ink);
}
.checkout__block select:focus { border-color: var(--color-primary-deep); }

/* A checkbox and its label on one line, with the box optically aligned to the
   first line of text rather than to the block's centre — a two-line consent
   label centres its box between the lines otherwise. */
.field--check {
    display: grid;
    grid-template-columns: 20px 1fr;
    align-items: start;
    gap: 10px;
}
.field--check input { width: 18px; height: 18px; margin-top: 2px; }
.field--check label { margin: 0; font-weight: 400; }

.field--qty { max-width: 120px; }

.field__help { display: block; margin-top: 6px; font-size: var(--text-xs); color: var(--color-muted); }

/* Requis. The asterisk is not the whole message — every required field is also
   marked `required` for the browser and validated on the server — but a form
   that marks nothing leaves people guessing. */
.field abbr { border: 0; color: var(--color-sale); text-decoration: none; }

.checkout__aside {
    padding: 26px;
    border: var(--border-hairline);
    background: var(--surface-cream);
    position: sticky;
    top: 100px;
}

.checkout__lines { margin: 0 0 18px; padding: 0 0 18px; border-bottom: var(--border-hairline); }
.checkout__lines li {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 14px;
    padding: 8px 0;
    font-size: var(--text-sm);
}
.checkout__line-name b { color: var(--color-muted); font-weight: 400; }
.checkout__line-total { white-space: nowrap; }

.checkout__payment {
    margin: 0 0 20px;
    padding: 16px;
    border-left: 3px solid var(--color-primary);
    background: var(--surface-page);
    font-size: var(--text-sm);
}
.checkout__payment h3 {
    margin: 0 0 8px;
    font-family: var(--font-ui);
    font-size: var(--text-xs);
    font-weight: 500;
    letter-spacing: var(--ls-caps);
    text-transform: uppercase;
}
.checkout__payment p { margin: 0; }

.checkout__obligation {
    margin: 10px 0 0;
    font-size: var(--text-xs);
    color: var(--color-muted);
    text-align: center;
}

.checkout__back { margin: 16px 0 0; text-align: center; font-size: var(--text-sm); }
.checkout__back a { color: var(--color-primary-deep); text-decoration: underline; text-underline-offset: 3px; }

/* --- the confirmation ----------------------------------------------------- */

.order-done { padding: 40px; border: var(--border-hairline); background: var(--surface-cream); text-align: center; }
.order-done__mark { display: block; margin-bottom: 18px; color: var(--color-primary-deep); }

.order-done__title { margin: 0 0 12px; font-size: var(--text-3xl); font-weight: 400; }
.order-done__lead { margin: 0 0 26px; font-size: var(--text-lg); }

.order-done__facts {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 6px 20px;
    margin: 0 0 26px;
    padding: 20px;
    background: var(--surface-page);
    text-align: left;
}
.order-done__facts dt {
    font-family: var(--font-ui);
    font-size: var(--text-xs);
    letter-spacing: var(--ls-caption);
    text-transform: uppercase;
    color: var(--color-muted);
}
.order-done__facts dd { margin: 0; font-size: var(--text-lg); color: var(--color-primary-deep); }
.order-done .page-cta { justify-content: center; }

/* --- narrow screens ------------------------------------------------------- */

@media (max-width: 1100px) {
    .cart-layout,
    .checkout { grid-template-columns: minmax(0, 1fr); }

    /* Sticky is meaningless once the panel is below the content it summarises:
       it would pin the total to the top of a viewport that has already scrolled
       past everything it refers to. */
    .cart-summary,
    .checkout__aside { position: static; }
}

@media (max-width: 768px) {
    .checkout__block { grid-template-columns: minmax(0, 1fr); }
    .checkout__block .field { grid-column: 1 / -1; }

    /*
       The table becomes a stack of cards. Each cell keeps its column heading
       through the `data-label` the template writes, so a row still reads as
       "Prix unitaire — 1 290,00 €" rather than as four unlabelled numbers.
    */
    .cart-table thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
    .cart-table,
    .cart-table tbody,
    .cart-table tr,
    .cart-table td,
    .cart-table tbody th { display: block; }

    .cart-table tr {
        position: relative;
        display: grid;
        grid-template-columns: 78px minmax(0, 1fr);
        gap: 0 14px;
        padding: 18px 0;
        border-bottom: var(--border-hairline);
    }

    .cart-table td,
    .cart-table tbody th { padding: 0 0 8px; border: 0; }

    .cart-table__media { grid-row: 1 / span 4; }
    .cart-table__unit::before,
    .cart-table__qty::before,
    .cart-table__total::before {
        content: attr(data-label) ' ';
        font-family: var(--font-ui);
        font-size: var(--text-xs);
        letter-spacing: var(--ls-caption);
        text-transform: uppercase;
        color: var(--color-muted);
    }

    .cart-table__remove { position: absolute; top: 14px; right: 0; width: auto; padding: 0; }

    .cart-actions { flex-direction: column; align-items: stretch; }
    .cart-actions .btn { width: 100%; }
}

@media (max-width: 576px) {
    .cart-drawer__panel { width: 100%; }
    .order-done { padding: 26px 20px; }
    .order-done__facts { grid-template-columns: minmax(0, 1fr); }
}

/* --- variant picker -------------------------------------------------------
   « À partir de » is a qualifier on the number beside it, not a heading: it
   sits on the same baseline, quieter than the figure, and disappears the
   moment a combination makes that figure exact. */

.card__price-from,
.product__price-from {
    color: var(--color-muted);
    font-size: var(--text-sm);
    letter-spacing: 0.01em;
}

/* Reserves no space when hidden — .card__price is a flex row, so a hidden span
   with display:none simply leaves the row. [hidden] is honoured by the reset,
   but flex containers override display, so it is restated here. */
.product__price-from[hidden] { display: none; }

/* The line under the selectors: only ever filled in when the chosen
   combination cannot be supplied. Empty and invisible the rest of the time,
   but kept in the DOM — a live region inserted at the moment it has something
   to say is one screen readers do not announce. */
.product__variant-state {
    margin: 0;
    color: var(--color-muted);
    font-size: var(--text-sm);
}

.product__variant-state:empty { display: none; }

/* A combination that does not exist. The browser greys disabled options on its
   own; this only makes sure the selects themselves stay legible at the width
   the product column gives them. */
.product__actions select { width: 100%; min-width: 0; }

/* --- colour swatches ------------------------------------------------------
   Real radio inputs, styled. The input is taken out of the flow rather than
   hidden with display:none — a hidden input is skipped by some screen readers
   and cannot receive focus, which would cost the group its keyboard
   navigation.

   The colour itself is an SVG `fill` in the markup, not a background here:
   the storefront's CSP is `style-src 'self'`, so an inline style attribute is
   dropped by the browser and every square renders empty. See the comment in
   product/show.html.twig. */

.swatches {
    border: 0;
    margin: 0 0 4px;
    padding: 0;
    min-width: 0;   /* fieldsets refuse to shrink in a flex or grid column without this */
}

.swatches legend {
    padding: 0;
    margin-bottom: 8px;
    font-size: var(--text-sm);
    color: var(--color-muted);
}

/* The row. An explicit flex container rather than inline-level labels flowing
   inside the fieldset's anonymous content box, which is not a place to rely on
   inline layout. */
.swatches__list {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: 14px 12px;
}

.swatch {
    position: relative;
    display: flex;
    flex: 0 0 auto;
    flex-direction: column;
    align-items: center;
    gap: 5px;
    margin: 0;
    cursor: pointer;
}

.swatch input {
    position: absolute;
    inset: 0;
    opacity: 0;
    width: 100%;
    height: 100%;
    margin: 0;
    cursor: pointer;
}

/* The white padding is the inner ring: it keeps a pale finish distinguishable
   from the page, and it does not change the square's size when it becomes the
   selected one — the selected ring is drawn outside, by box-shadow. */
.swatch__chip {
    box-sizing: border-box;
    display: block;
    width: 38px;
    height: 38px;
    padding: 2px;
    background: #fff;
    border: 1px solid var(--color-line);
    line-height: 0;
    /* box-shadow alone. Nothing in this component may change its geometry on
       hover or on selection — see the ring rules below for why. */
    transition: box-shadow 120ms ease;
}

.swatch__chip svg {
    display: block;
    width: 100%;
    height: 100%;
}

.swatch__name {
    font-size: var(--text-xs);
    color: var(--color-muted);
    max-width: 9ch;
    text-align: center;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Hover and selection are drawn *outside* the border box, by box-shadow.
   Nothing here changes a size, and that is the point.

   The obvious hover cue is `transform: scale(1.06)`, and it shakes. A 38px
   square scaled by 6% is 40.28px: the border lands on a half-pixel and the SVG
   inside is re-rasterised at a fractional size on every frame of the
   transition, which reads as a wobble. Worse, the chip grows *under the
   pointer*, so a cursor resting near its edge is a hover state that can
   re-enter itself.

   A box-shadow ring has neither problem. It paints outside the layout box, so
   it changes no size, moves no neighbour, and is invisible to hit-testing.

   Order matters: the checked rule comes last so that hovering an already
   chosen colour keeps its own ring rather than dropping to the hover one. */
.swatch input:hover:not(:disabled) + .swatch__chip {
    box-shadow: 0 0 0 2px var(--color-line);
}

.swatch input:focus-visible + .swatch__chip {
    outline: 2px solid var(--color-primary-deep);
    outline-offset: 3px;
}

.swatch input:checked + .swatch__chip {
    box-shadow: 0 0 0 2px var(--color-primary-deep);
}

/* Colour only, never weight. Bold text is wider than regular, so weighting the
   chosen name re-measures the label, which reflows the whole row and shifts
   every other swatch sideways the moment somebody picks one. The ring above is
   already the selection cue; this is the quiet second half of it. */
.swatch input:checked ~ .swatch__name { color: var(--color-primary-deep); }

/* A combination that does not exist alongside what is already chosen. Struck
   through rather than removed: the visitor is entitled to see that the finish
   exists and does not come in the size they picked. */
.swatch input:disabled { cursor: not-allowed; }

.swatch input:disabled + .swatch__chip { opacity: 0.3; box-shadow: none; }

.swatch input:disabled ~ .swatch__name { opacity: 0.5; text-decoration: line-through; }

@media (prefers-reduced-motion: reduce) {
    .swatch__chip { transition: none; }
}
