
  /* base palette now aliases the Concord brand tokens (brand.css) */
  :root { --blue:#0e0c63; --bg:#f5f6fa; --card:#fff; --warn:#b45309; --danger:#d64545; --ok:#047857; --muted:#6b727c; }
  * { box-sizing:border-box; margin:0; }
  body { font-family:system-ui,-apple-system,"Segoe UI",Roboto,sans-serif; background:var(--bg);
         display:flex; flex-direction:column; height:100dvh; color:#1f2937; }
  /* border-bottom carries the product colour on a fiche (see setFicheChrome): an accent
     rather than a fill — only 52% of products have a colour, and 41 of those 50 fail
     contrast against white text, so filling the bar would be both patchy and unreadable. */
  /* padding-top clears the status bar when installed to the home screen: standalone iOS
     draws the clock over the page. env() is 0 in a browser tab, so nothing moves there. */
  header { background:var(--blue); color:#fff; display:flex; align-items:center; gap:.6rem; flex:none;
           padding:calc(.65rem + env(safe-area-inset-top)) 1rem .65rem;
           transition:background-color .15s ease; }
  /* Light product colours: flip the foreground, or white text drops under AA — and the
     translucent-white pills vanish entirely. #111 clears 4.5:1 on every colour on file. */
  /* Which stack am I looking at? — the chat's share of #358. Test = the same
     amber as the admin bar (#b45309, AA on white — rationale in brand.css);
     prod keeps the brand indigo, so the client-facing surface is untouched.
     A fiche's product colour (inline style, #56) still wins over this rule —
     the persistent chrome is the env signal, the fiche bar is a feature. */
  :root[data-env="test"] header { background:#b45309; }
  header.light { color:#000; box-shadow:inset 0 -1px 0 #00000022; }
  header.light #back, header.light #langChip, header.light #env { background:#0000001f; color:#000; }
  /* The 1.9rem hit area is 9px taller than anything else in the bar, so left alone it
     drove the header height and the bar visibly grew when you opened a product. The
     negative margins keep the button its full tappable size while letting it sit inside
     the header's existing vertical padding, so the bar height never changes. */
  #back { background:#fff3; border:none; color:#fff; font-size:1rem; line-height:1; cursor:pointer;
          border-radius:99px; width:1.9rem; height:1.9rem; flex:none; padding:0;
          margin-top:-.3rem; margin-bottom:-.3rem; }
  #back[hidden] { display:none; }
  /* Brand mark on every view (Aurèle 28.07), on a white plate rather than knocked out to
     white: a fiche header takes the PRODUCT's colour, so no single ink is legible on all
     of them, and the plate keeps the wordmark's grey and the droplet's green/blue exact.
     The asset is opaque white anyway (RGB, no alpha) — the plate is what the image already
     is, made deliberate; overflow:hidden is what rounds its square corners. A transparent
     SVG would open the knockout option, if one is ever exported from the Canva sources.
     Detail views narrow it to the droplet alone (the asset's rightmost 26%, right-anchored
     crop — no second file): that header already carries a back button, an uppercase
     product name and the language pulldown, and the full wordmark does not fit at 390px. */
  #brand { flex:none; background:#fff; border-radius:.4rem; padding:.22rem .3rem;
           display:flex; align-items:center; overflow:hidden; }
  #brand img { height:1rem; display:block; }
  #brand.mark img { width:.87rem; object-fit:cover; object-position:100% 50%; }
  /* Measured at 320px: a fiche bar carrying back + chip + env pill leaves the product name
     62px, and the mark takes 33 of them — 3 characters. Decoration loses to information
     there. Top-level views keep it at every width: no back button, and a page name is short. */
  @media (max-width:359px) { #brand.mark { display:none; } }
  /* Another Marque on a product page (#1870, `paintBrand`): its own logo or, while its
     slot is empty, its name in text — never Concord's lockup under another name. The
     droplet crop above is a crop of THAT lockup, so it does not apply to someone
     else's artwork; the slot keeps its 1rem height either way. */
  #brand img[hidden], #brand .brand-name[hidden] { display:none!important; }
  #brand.other-brand.mark img { width:auto; max-width:4.5rem; object-fit:contain; }
  #brand .brand-name { display:block; height:1rem; line-height:1rem; font-size:.8rem;
    font-weight:700; color:#1f2937; white-space:nowrap; letter-spacing:.02em; }
  /* « Ouvrir une référence » (#1870): the catalogue's search narrows what is listed,
     and this opens the one fiche whose exact reference was typed */
  .open-ref { margin-bottom:.5rem; }
  header h1 { font-size:1rem; font-weight:600; flex:1; display:-webkit-box;
              -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
  /* commercial names render UPPERCASE (brand.css: .fiche .name, .prod-row .t — Aurèle
     24.07). The header was the one place a product name escaped that convention. */
  /* One line, ellipsised: this title is revealed mid-scroll, so a name long enough to
     wrap would grow the bar under the user's finger (42px -> 59px). Scoped to .prod so
     other views keep the 2-line clamp. */
  header h1.prod { text-transform:uppercase; -webkit-line-clamp:1; }
  #env { font-size:.62rem; background:#fff3; border-radius:99px; padding:.15rem .5rem; }
  main { flex:1; overflow-y:auto; -webkit-overflow-scrolling:touch; }
  nav { flex:none; display:flex; background:#fff; border-top:1px solid #e5e7eb; padding-bottom:env(safe-area-inset-bottom); }
  nav a { flex:1; text-align:center; padding:.5rem 0 .4rem; text-decoration:none; color:var(--muted); font-size:.7rem; }
  nav a.on { color:var(--blue); font-weight:700; }
  nav a span { display:block; font-size:1.3rem; }
  .page { padding:1rem; display:flex; flex-direction:column; gap:.8rem; }
  h2 { font-size:.8rem; text-transform:uppercase; letter-spacing:.04em; color:var(--muted); }
  .btn { background:var(--blue); color:#fff; border:none; border-radius:.8rem; padding:.8rem 1rem;
         font:inherit; font-weight:600; cursor:pointer; width:100%; }
  .btn.sec { background:#fff; color:var(--blue); border:1.5px solid var(--blue); }
  .btn.warn { background:var(--warn); } .btn.danger { background:var(--danger); } .btn.ok { background:var(--ok); }
  /* a real button, deliberately quieter — for an escape hatch that must be findable
     without competing with the primary action next to it */
  .btn.quiet { background:#fff; color:var(--muted); border:1.5px solid #d1d5db; }
  .item { background:var(--card); border:1px solid #e5e7eb; border-radius:.9rem; padding:.75rem .9rem; cursor:pointer; }
  .item .t { font-weight:600; } .item .s { font-size:.78rem; color:var(--muted); margin-top:.15rem; }
  .badge { display:inline-block; font-size:.65rem; padding:.12rem .5rem; border-radius:99px; margin:.1rem .2rem .4rem 0; }
  .badge.draft { background:#fef3c7; color:#92400e; border:1px solid #f59e0b; }
  .badge.escalate { background:#fee2e2; color:#991b1b; border:1px solid #ef4444; }
  /* #1111 — « Concentré ». Indigo, where `draft` is amber and `escalate` red:
     the other two badges of this row warn about the SHEET, this one is a claim
     about the product and must not borrow their alarm. */
  .badge.concentrated { background:#eef2ff; color:var(--blue); border:1px solid #c7d2fe; }
  .card { background:var(--card); border:1px solid #d1d5db; border-radius:1rem; padding:.9rem; font-size:.93rem; }
  .card h3 { color:var(--blue); font-size:1.05rem; margin-bottom:.15rem; }
  .card .ident { color:#4b5563; font-size:.85rem; margin-bottom:.5rem; }
  .card ul, .card ol { padding-left:1.2rem; display:grid; gap:.3rem; margin:.3rem 0 .6rem; }
  .card .sec-t { font-weight:600; font-size:.78rem; text-transform:uppercase; letter-spacing:.03em; margin-top:.5rem; }
  .warn-t { color:var(--warn); } .never-t { color:var(--danger); }
  .chip { background:#eef2ff; color:var(--blue); border-radius:.4rem; padding:0 .35rem; font-weight:600; text-decoration:none; white-space:nowrap; }
  a.chip:active { background:#dbeafe; }
  /* #510 — the product name rides inside the pill (`CL807 · Détachant…`). Capped
     and ellipsised so a long name cannot wrap a step line or push a phone into
     horizontal scroll; the whole name stays in the chip's title=. */
  .chip .chip-n { display:inline-block; max-width:12rem; overflow:hidden;
                  text-overflow:ellipsis; vertical-align:bottom; font-weight:500; }
  .fallback { background:#f9fafb; border-left:3px solid var(--blue); padding:.4rem .6rem; font-size:.85rem; }
  .disclaimer { color:#9ca3af; font-size:.68rem; margin-top:.6rem; }
  /* carte conseil produit (#175) — same plate as the protocol card, one product
     per row so the ref chip is always the first thing the thumb finds */
  .advice { display:grid; gap:.55rem; }
  .advice .p { display:grid; gap:.15rem; padding:.5rem .6rem; background:#f9fafb;
               border-radius:.6rem; border-left:3px solid var(--blue); }
  .advice .p .top { display:flex; align-items:baseline; gap:.4rem; flex-wrap:wrap; }
  .advice .p .why { color:#4b5563; font-size:.87rem; }
  .advice .p .meta { display:flex; flex-wrap:wrap; gap:.5rem; color:var(--muted); font-size:.78rem; }
  .advice .cmp { background:#eef2ff; border-radius:.6rem; padding:.45rem .6rem; font-size:.85rem; }
  .advice .hint { color:var(--muted); font-size:.78rem; }
  /* chat */
  .chatlog { display:flex; flex-direction:column; gap:.7rem; }
  .msg { max-width:88%; padding:.6rem .8rem; border-radius:.9rem; line-height:1.45; font-size:.95rem; white-space:pre-wrap; word-break:break-word; }
  .user { align-self:flex-end; background:var(--blue); color:#fff; border-bottom-right-radius:.2rem; }
  .bot { align-self:flex-start; background:var(--card); border:1px solid #e5e7eb; border-bottom-left-radius:.2rem; }
  .status { align-self:flex-start; color:var(--muted); font-size:.8rem; font-style:italic; }
  /* a turn's live progress line: the steps done, the one running, the wait so far */
  .activity { display:flex; flex-direction:column; gap:.2rem; font-style:normal;
              --activity-ink:var(--muted); --activity-glint:#d6dde7; }
  .activity[hidden] { display:none; }
  .activity .steps { list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:.1rem; }
  .activity .steps li::before { content:'✓'; display:inline-block; width:1.2em; color:var(--blue); }
  .activity .now { display:flex; align-items:baseline; }
  /* the running step's marker sits in the ticks' column: ✓ done, ◦ under way */
  .activity .now::before { content:''; flex:none; box-sizing:border-box; width:.6em; height:.6em;
                           margin-inline:.1em .5em; border:1.5px solid var(--blue); border-radius:50%;
                           align-self:center; }
  .activity .label { color:transparent; -webkit-background-clip:text; background-clip:text;
                     background-image:linear-gradient(90deg, var(--activity-ink) 0 40%,
                       var(--activity-glint) 50%, var(--activity-ink) 60% 100%);
                     background-size:250% 100%; animation:activity-glint 2s linear infinite; }
  .activity .elapsed { margin-inline-start:.6rem; font-variant-numeric:tabular-nums; opacity:.8; }
  @keyframes activity-glint { from { background-position:100% 0; } to { background-position:0 0; } }
  @media (prefers-reduced-motion:reduce) {
    .activity .label { animation:none; background:none; color:var(--activity-ink); } }
  .error { align-self:center; color:var(--danger); font-size:.85rem; }
  .msg img, .item img { max-width:160px; border-radius:.5rem; display:block; margin-top:.3rem; }
  .card-head { display:flex; gap:.7rem; align-items:flex-start; }
  .card-head .ph img { width:76px; height:76px; object-fit:cover; border-radius:.7rem;
                       border:1px solid #d1d5db; display:block; }
  .card-head > div:last-child { flex:1; min-width:0; }
  .card-photos { display:flex; gap:.6rem; margin-top:.7rem; }
  .card-photos a { text-align:center; font-size:.62rem; color:var(--muted); text-decoration:none; }
  .card-photos img { width:52px; height:52px; object-fit:cover; border-radius:.5rem;
                     border:1px solid #d1d5db; display:block; margin:0 auto .1rem; }
  /* #600 — the packshot takes exactly the slot the stylised bidon has been
     holding since #423: the same box as `.fiche-head .bidon` (brand.css), so
     nothing else on the fiche moves the day the photo exists. `contain`, not
     `cover`: a packshot is a whole container, and cropping it would cut the
     bidon. */
  .fiche-head .packshot { width:76px; height:80px; object-fit:contain; display:block; }
  #lightbox { position:fixed; inset:0; background:#000d; display:none; align-items:center;
              justify-content:center; z-index:60; }
  #lightbox.open { display:flex; }
  #lightbox img { max-width:96vw; max-height:92dvh; border-radius:.6rem; }
  #login { position:fixed; inset:0; background:var(--bg); display:none; align-items:center;
           justify-content:center; z-index:70; padding:1.5rem; }
  #login.open { display:flex; }
  #login form { background:var(--card); border:1px solid #e5e7eb; border-radius:1.2rem;
                padding:1.6rem; width:100%; max-width:22rem; display:grid; gap:.8rem; }
  #login h3 { color:var(--blue); } #login p { font-size:.85rem; color:var(--muted); }
  /* minimal markdown (KB card bodies) */
  .md p { margin:.35rem 0; } .md ul, .md ol { padding-left:1.2rem; margin:.35rem 0; display:grid; gap:.2rem; }
  .md .md-h1, .md .md-h2 { font-weight:700; color:var(--blue); margin:.7rem 0 .2rem; }
  .md .md-h3, .md .md-h4 { font-weight:600; margin:.6rem 0 .2rem; }
  .md code { background:#eef2ff; border-radius:.3rem; padding:0 .3rem; }
  .composer { display:flex; gap:.5rem; align-items:flex-end; padding:.6rem; background:#fff; border-top:1px solid #e5e7eb; flex:none; }
  .composer textarea { flex:1; resize:none; border:1px solid #d1d5db; border-radius:.8rem; padding:.55rem .8rem; font:inherit; max-height:7rem; }
  /* attached-photo draft chip (#508): part of the composer, never of the log —
     the two read as one block, so the border between them is dropped */
  .draft { display:flex; align-items:center; gap:.6rem; padding:.5rem .6rem .1rem;
           background:#fff; border-top:1px solid #e5e7eb; flex:none; }
  .draft[hidden] { display:none; }
  .draft:not([hidden]) + .composer { border-top:none; }
  .draft img { width:2.6rem; height:2.6rem; object-fit:cover; border-radius:.5rem;
               border:1px solid #d1d5db; flex:none; }
  .draft .t { flex:1; font-size:.8rem; color:var(--muted); }
  .draft .x { background:none; border:none; color:var(--muted); font-size:1.3rem;
              line-height:1; padding:.2rem .45rem; cursor:pointer; flex:none; }
  /* voice note (#1415): its own chip, recording bar and refusal line — composer state
     like the photo's, never log content. A photo and a note may ride one turn. */
  .vnote, .rec, .voiceerr { display:flex; align-items:center; gap:.6rem; padding:.5rem .6rem .1rem;
           background:#fff; border-top:1px solid #e5e7eb; flex:none; font-size:.8rem; color:var(--muted); }
  .vnote[hidden], .rec[hidden], .voiceerr[hidden], .mic[hidden] { display:none; }
  .vnote .t, .rec .t { flex:1; }
  .vnote.failed .t { color:#b91c1c; }
  .vnote .play, .vplay .play { background:#fff; border:1px solid #d1d5db; border-radius:50%;
           width:1.9rem; height:1.9rem; cursor:pointer; flex:none; font-size:.75rem; }
  .vnote .x { background:none; border:none; color:var(--muted); font-size:1.3rem;
              line-height:1; padding:.2rem .45rem; cursor:pointer; flex:none; }
  .rec .dot { width:.7rem; height:.7rem; border-radius:50%; background:#dc2626; flex:none;
              animation:recpulse 1s ease-in-out infinite; }
  @keyframes recpulse { 50% { opacity:.25; } }
  .rec button { background:#fff; border:1px solid #d1d5db; border-radius:.6rem; padding:.3rem .7rem;
                font:inherit; cursor:pointer; }
  .rec .stop { background:var(--blue); color:#fff; border:none; }
  .voiceerr { color:#b91c1c; }
  .vplay { display:inline-flex; align-items:center; gap:.4rem; }
  /* chat 2.0's composer is a grid (v2/ui.css); the microphone takes a column only
     when it is shown, so the composer is unchanged wherever voice is off */
  .v2-expert .composer .mic { background:white; color:var(--secondary); border:0; border-radius:4px;
           width:auto; height:44px; padding:0 7px; display:flex; align-items:center; gap:8px; }
  .v2-expert .composer .mic[hidden] { display:none; }
  .v2-expert:not(.v2-new) .mic span { display:none; }
  .v2-expert .composer:has(.mic:not([hidden])) { grid-template-columns:auto auto minmax(0,1fr) 48px; }
  .v2-new .composer:has(.mic:not([hidden])) { grid-template-columns:auto minmax(0,1fr) 48px; }
  .v2-new .composer .mic { grid-column:2; grid-row:2; justify-self:start; font-size:14px; padding:0; }
  .v2-new .composer:has(.mic:not([hidden])) .send { grid-column:3; }
  .v2-expert .vnote, .v2-expert .rec, .v2-expert .voiceerr { margin:0 0 8px; border:0; border-radius:6px;
           background:var(--action-soft); padding:10px; color:var(--secondary); }
  .v2-expert .voiceerr { color:#b91c1c; }
  /* drop target (#28): the pane says it accepts an image only while one is over it —
     a permanent « déposez ici » would be chrome on every phone that cannot drag */
  .dropping { outline:2px dashed var(--blue); outline-offset:-4px; background:#eff6ff; }
  .icon { background:#fff; border:1px solid #d1d5db; border-radius:.8rem; width:2.6rem; height:2.6rem; font-size:1.2rem; cursor:pointer; flex:none; }
  .icon.send { background:var(--blue); color:#fff; border:none; }
  /* overlays */
  #overlay { position:fixed; inset:0; background:#0007; display:none; align-items:flex-end; z-index:40; }
  #overlay.open { display:flex; }
  #sheet { background:var(--bg); width:100%; max-height:88dvh; border-radius:1.2rem 1.2rem 0 0; display:flex; flex-direction:column; }
  #sheet .head { padding:.7rem 1rem; display:flex; align-items:center; border-bottom:1px solid #e5e7eb; background:#fff; border-radius:1.2rem 1.2rem 0 0; }
  #sheet .head b { flex:1; font-size:.9rem; }
  #sheet .body { overflow-y:auto; padding:1rem; }
  /* In-app document viewer (#228). In an installed PWA a PDF opened by a plain link
     REPLACES the only standalone webview — no chrome, no back, the user must kill the
     app to escape. This overlay keeps the app mounted: the file loads in an <iframe>
     (every document is served same-origin by /api/products/…/document — it frames cleanly)
     behind a bar that always offers ✕ and an explicit escape to the system browser,
     the fallback for iOS builds that refuse to paint a PDF inside an iframe. z-index
     70 clears #overlay/#lightbox; docs open from a fiche, never from within them. */
  #docview { position:fixed; inset:0; z-index:70; background:var(--bg); display:none; flex-direction:column; }
  #docview.open { display:flex; }
  #docview .bar { flex:none; display:flex; align-items:center; gap:.5rem; color:#fff; background:var(--blue);
                  padding:calc(.55rem + env(safe-area-inset-top)) .8rem .55rem; }
  #docview .bar b { flex:1; font-size:.9rem; font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  #docview .bar a, #docview .bar button { flex:none; background:#fff3; border:none; color:#fff; font:inherit;
    font-size:.8rem; border-radius:99px; padding:.32rem .7rem; cursor:pointer; text-decoration:none;
    display:inline-flex; align-items:center; gap:.3rem; }
  #docview .bar .x { width:1.9rem; height:1.9rem; padding:0; font-size:1rem; justify-content:center; }
  #docview .bar b { min-width:0; }
  #docview .bar button, #docview .bar a { min-height:44px; justify-content:center; }
  #docview .bar .x { width:44px; height:44px; }
  .docview-nav, .docview-zoom { display:flex; align-items:center; gap:4px; flex:none; }
  #docviewZoom { min-width:4.5em; text-align:center; font-size:.85rem; }
  #docviewPages { flex:1; min-height:0; overflow:auto; padding:8px; touch-action:none;
    position:relative; overscroll-behavior:contain; overflow-anchor:none; }
  #docviewPages.zoomed { cursor:grab; }
  #docviewPages.dragging { cursor:grabbing; user-select:none; }
  .docview-spread { display:flex; gap:8px; margin:0 auto 8px; align-items:flex-start; }
  .docview-page { position:relative; container-type:inline-size; flex:none; background:#fff; }
  .docview-page img { display:block; width:100%; height:100%; position:absolute; inset:0; }
  .docview-text { position:absolute; inset:0; color:transparent; }
  .docview-text span { position:absolute; white-space:pre; line-height:1; }
  .docview-text ::selection { background:#2680eb66; }
  .docview-cover { display:block; max-width:100%; margin:auto; }
  #docviewStatus, #docviewCount { flex:none; text-align:center; font-size:.85rem; }
  #docviewStatus:empty { display:none; }
  #docviewCount { min-width:5.5em; white-space:nowrap; }
  @media (max-width:767px) and (orientation:portrait) {
    #docview .bar { display:grid; grid-template-columns:minmax(0,1fr) auto auto; gap:4px; padding-left:8px; padding-right:8px; }
    #docview .bar b { grid-area:1 / 1; }
    #docview .bar .x { grid-area:1 / 3; justify-self:end; }
    #docview .docview-nav { grid-area:2 / 1; }
    #docview .docview-zoom { grid-area:2 / 2 / 3 / 4; justify-self:end; }
    #docviewOpen { grid-area:1 / 2; justify-self:end; }
    #docview .bar a, #docview .bar button { padding:0 8px; }
    #docviewCount { min-width:4.5em; }
  }
  /* photo source chooser — z-index above #overlay (40) because the chat's 📷 is itself
     inside a sheet; below #lightbox (60), which never coexists with it */
  .pickmenu { position:fixed; inset:0; z-index:55; background:#0007; display:flex; align-items:flex-end; }
  .pickcard { background:var(--bg); width:100%; padding:1rem; display:flex; flex-direction:column;
    gap:.6rem; border-radius:1.2rem 1.2rem 0 0; }
  input[type=text], input[type=password], input[type=search], select { border:1px solid #d1d5db; border-radius:.6rem; padding:.55rem .7rem; font:inherit; width:100%; }
  .kv { display:grid; grid-template-columns:auto 1fr; gap:.2rem .8rem; font-size:.88rem; }
  .kv dt { color:var(--muted); } .kv dd { margin:0; }
  .fab { position:fixed; right:1rem; bottom:4.4rem; width:3.4rem; height:3.4rem; border-radius:50%; border:none;
         background:var(--blue); color:#fff; font-size:1.5rem; box-shadow:0 4px 14px #0004; cursor:pointer; z-index:10; }
  .empty { text-align:center; color:var(--muted); font-size:.85rem; padding:2rem 1rem; }
  #law { text-align:center; color:#9ca3af; font-size:.6rem; padding:.3rem .8rem; flex:none; }
  /* feedback (#236) — the listening channel. Discreet everywhere on purpose: it
     has to be findable from every view and compete with nothing on any of them. */
  #fbLink { font:inherit; font-size:.6rem; color:#9ca3af; background:none; border:none;
    padding:.1rem .3rem; cursor:pointer; text-decoration:underline; }
  #fbLink:hover { color:var(--blue); }
  .fbrow { display:flex; align-items:center; gap:.3rem; flex-wrap:wrap;
    margin:-.25rem 0 .55rem .15rem; }
  .fbrow .lbl { font-size:.68rem; color:var(--muted); }
  .fbrow button.th { font:inherit; font-size:.95rem; line-height:1; background:none;
    border:1px solid transparent; border-radius:.4rem; padding:.15rem .3rem;
    cursor:pointer; opacity:.4; }
  .fbrow button.th:hover { opacity:1; background:#f3f4f6; }
  .fbrow button.th.on { opacity:1; border-color:var(--blue); background:#eef2ff; }
  .fbrow .why { display:flex; gap:.3rem; width:100%; margin-top:.15rem; }
  .fbrow .why input { flex:1; min-width:0; font:inherit; font-size:.75rem;
    padding:.25rem .45rem; border:1px solid #e5e7eb; border-radius:.4rem; }
  .fbrow .why button { font:inherit; font-size:.72rem; padding:.25rem .55rem;
    border:1px solid #e5e7eb; border-radius:.4rem; background:#fff; cursor:pointer; }
  /* persona funnel (Nikola 27.07): landing « catalogue virtuel » */
  #welcome { position:fixed; inset:0; background:var(--bg); display:none; z-index:65;
             overflow-y:auto; padding:1.2rem; }
  #welcome.open { display:block; }
  /* the asset is opaque white (RGB, no alpha), so rounding its own corners is what makes
     it read as a plate like the cards below instead of a hard rectangle on the grey */
  #welcomeLogo { display:block; width:min(14rem, 62%); margin:.4rem auto 1rem; border-radius:.6rem; }
  #welcome .inner { max-width:26rem; margin:0 auto; display:grid; gap:.7rem; padding-bottom:2rem; }
  #welcome h2 { color:var(--blue); font-size:1.15rem; text-transform:none; letter-spacing:0; }
  #welcome p { color:var(--muted); font-size:.88rem; }
  .persona-btn { display:flex; align-items:center; gap:.8rem; background:var(--card);
                 border:1.5px solid #d1d5db; border-radius:1rem; padding:.85rem 1rem; font:inherit;
                 cursor:pointer; text-align:left; width:100%; }
  .persona-btn b { display:block; } .persona-btn small { color:var(--muted); }
  .persona-btn .ic { font-size:1.5rem; }
  /* The language pulldown (#388) wears the pill the persona chip wore. Native
     select kept native (its own chevron, its own picker) — only the closed
     control is dressed; option text renders in the platform's menu, where the
     endonyms need the platform's fonts anyway (ქართული, 中文, العربية). */
  #langChip { background:#fff3; border:none; border-radius:99px; color:#fff; font-size:.68rem;
              padding:.18rem .6rem; cursor:pointer; white-space:nowrap; max-width:7.5rem; }
  #langChip option { color:#000; }
  .tier { color:var(--ok); font-weight:700; font-size:.72rem; margin-left:.35rem; letter-spacing:.05em; }
  .ecobadge { font-size:.62rem; background:#d1fae5; color:#065f46; border-radius:99px;
              padding:.08rem .45rem; margin-left:.3rem; vertical-align:middle; }
  .gammes button, .pfilters button { border:1px solid #d1d5db; background:#fff; border-radius:99px;
    padding:.3rem .7rem; font-size:.78rem; margin:0 .3rem .3rem 0; cursor:pointer; }
  .gammes button.on, .pfilters button.on { background:var(--blue); color:#fff; border-color:var(--blue); }
  /* Certification chips share the row with the gamme chips and must not read as one
     more family (#469): green rather than blue, because « Ecocert » is a fact about
     a product that HAS a family, and the two filters compose (Ecocert × Détergents). */
  .gammes button.cert { border-color:#6ee7b7; color:#065f46; }
  .gammes button.cert.on { background:var(--ok, #059669); border-color:var(--ok, #059669); color:#fff; }
  /* the sort control sits in the filter row rather than on a row of its own, next to the
     sector menu that shares this class (#405). width:auto overrides the global
     `select { width:100% }`, which would otherwise take the whole line. */
  .pfilters .sortsel { width:auto; font-size:.78rem; padding:.3rem 1.4rem .3rem .6rem;
    border-radius:99px; border:1px solid #d1d5db; background:#fff; margin:0 .3rem .3rem 0;
    cursor:pointer; appearance:none;
    background-image:linear-gradient(45deg, transparent 50%, var(--muted) 50%),
                     linear-gradient(135deg, var(--muted) 50%, transparent 50%);
    background-position:calc(100% - .62rem) 55%, calc(100% - .42rem) 55%;
    background-size:.2rem .2rem, .2rem .2rem; background-repeat:no-repeat; }
  .callback { text-align:center; padding:.2rem .6rem .5rem; background:#fff; flex:none; }
  .callback a { color:var(--blue); font-size:.78rem; text-decoration:none; }
  /* comparateur (27.07 soir): la matrice de décision que le catalogue papier ne peut pas faire */
  .prod-row { position:relative; padding-right:3.2rem; }
  .cmp-toggle { position:absolute; right:.6rem; top:50%; transform:translateY(-50%);
    width:2rem; height:2rem; border-radius:50%; border:1.5px solid #d1d5db; background:#fff;
    cursor:pointer; font-size:1.05rem; line-height:1; padding:0;
    display:flex; align-items:center; justify-content:center; }
  .cmp-toggle.on { background:var(--blue); color:#fff; border-color:var(--blue); }
  /* at 4/4 the unpickable rows say so by looking unpickable — the modal that used
     to say it blocked the screen without offering the way out (Aurèle 28.07) */
  .cmp-toggle.muted { opacity:.35; }
  /* the dock: « Comparer (n) » plus its own clear ✕. The ✕ is a separate tap target
     with a gap — always reachable even when the selected rows are filtered off-screen,
     which is exactly the case where untapping them one by one is impossible. */
  .cmpdock { position:fixed; left:50%; transform:translateX(-50%); bottom:4.8rem; z-index:20;
    display:flex; align-items:center; gap:.5rem; }
  .cmpbar { background:var(--blue); color:#fff; border:none; border-radius:99px; padding:.6rem 1.2rem;
    font:inherit; font-weight:600; box-shadow:0 4px 14px #0004; cursor:pointer; white-space:nowrap; }
  .cmpbar.warn { background:var(--warn); }
  .cmp-clear { width:2.5rem; height:2.5rem; flex:none; border-radius:50%; border:none; background:#fff;
    color:var(--muted); box-shadow:0 4px 14px #0004; cursor:pointer; font-size:1rem; line-height:1;
    padding:0; display:flex; align-items:center; justify-content:center; }
  .cmp-wrap { overflow-x:auto; -webkit-overflow-scrolling:touch; background:var(--card);
    border:1px solid #e5e7eb; border-radius:1rem; }
  table.cmp { border-collapse:collapse; font-size:.82rem; width:100%; }
  table.cmp th, table.cmp td { border-bottom:1px solid #e5e7eb; padding:.5rem .6rem;
    text-align:left; vertical-align:top; min-width:8.5rem; }
  table.cmp tr:last-child th, table.cmp tr:last-child td { border-bottom:none; }
  table.cmp td.axis { position:sticky; left:0; background:var(--card); min-width:6.5rem;
    max-width:8rem; font-weight:600; color:var(--muted); font-size:.7rem; text-transform:uppercase;
    letter-spacing:.03em; }
  table.cmp th { background:var(--card); }
  table.cmp th a { color:var(--blue); text-decoration:none; }
  /* drop one column and re-compare: the usual intent is « this one loses », not « start
     over ». Under its own title, never in the cell corner — at the corner it sits against
     the NEXT column's name and you remove the wrong product (28.07, phone width). */
  .cmp-rm { display:block; margin-top:.4rem; padding:.15rem 0; border:none; background:none;
    color:var(--muted); font-size:.68rem; font-weight:600; text-transform:uppercase;
    letter-spacing:.03em; cursor:pointer; }
  .cmp-rm:hover { color:var(--danger); }
  .cmp-actions { display:flex; flex-direction:column; gap:.5rem; }
  table.cmp th .ref { display:block; }
  table.cmp .warn-cell { color:var(--warn); font-weight:600; }
  /* the cells that carry a difference (#109). A tint rather than a border: the matrix is
     already ruled, and one more line per cell reads as a table, not as a signal. */
  table.cmp td.diff { background:#eef2ff; font-weight:600; }
  table.cmp tr.same td.axis, table.cmp tr.same td { color:var(--muted); }
  table.cmp tr.cmp-more td { padding:0; }
  table.cmp tr.cmp-more button { width:100%; background:none; border:none; font:inherit;
    font-size:.78rem; color:var(--blue); padding:.5rem .6rem; text-align:left; cursor:pointer; }
  .cmp-summary { background:#eef2ff; border-left:3px solid var(--blue); border-radius:.5rem;
    padding:.6rem .7rem; font-size:.88rem; }
  /* the served dosage matrix (#1991, /static/dose-render.js): the band and its °f
     on the left, the reference row marked; a simple dose is one line */
  table.dose-matrix td:first-child { text-align:left; }
  table.dose-matrix .dose-band { display:block; font-size:.68rem; color:var(--muted); }
  table.dose-matrix tr.dose-ref td:first-child { font-weight:700; }
  .dose-line { margin:.2rem 0 .5rem; font-weight:600; }
