/* ===== styles/base.css ===== */
/* ===========================================================================
   The skeleton, and nothing with a personality in it.
   ===========================================================================

   This file holds what is true of both shops: the reset, the box model, the
   layout primitives, and the NAMES of the tokens. It sets almost no values.

   The two brands are not variations of one another. ithos is built to the
   shape of a Polish wooden-lamp shop the owner chose — white, Montserrat,
   terracotta, a four-column grid of square frames. cathelier is built to the
   shape of a Portuguese laser-cut shop she chose separately — cream, a brush
   serif, round occasion badges. Someone landing on one from the other should
   feel they changed website.

   So every value lives in brands/ithos.css or brands/cathelier.css, and this
   file only promises that the names exist. If a colour or a typeface appears
   below, it is a bug.
   ------------------------------------------------------------------------- */

/* --- the typefaces, served from this domain ------------------------------
   Not from fonts.gstatic.com. A font request to Google is a transfer of the
   visitor's IP address to a third party: it has to be named in the privacy
   policy and, on several readings, consented to. Served from here, the site
   contacts nobody — which is why it needs no cookie banner at all. */

@font-face { font-family: 'Montserrat'; font-style: normal; font-weight: 300 600;
  font-display: swap; src: url('/assets/fonts/montserrat-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; }
@font-face { font-family: 'Montserrat'; font-style: normal; font-weight: 300 600;
  font-display: swap; src: url('/assets/fonts/montserrat-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF; }

@font-face { font-family: 'Cormorant Garamond'; font-style: normal; font-weight: 300 500;
  font-display: swap; src: url('/assets/fonts/cormorant-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; }
@font-face { font-family: 'Cormorant Garamond'; font-style: normal; font-weight: 300 500;
  font-display: swap; src: url('/assets/fonts/cormorant-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF; }
@font-face { font-family: 'Cormorant Garamond'; font-style: italic; font-weight: 300;
  font-display: swap; src: url('/assets/fonts/cormorant-300-italico-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; }

@font-face { font-family: 'Klee One'; font-style: normal; font-weight: 400;
  font-display: swap; src: url('/assets/fonts/kleeone-400-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; }
@font-face { font-family: 'Klee One'; font-style: normal; font-weight: 400;
  font-display: swap; src: url('/assets/fonts/kleeone-400-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF; }
@font-face { font-family: 'Klee One'; font-style: normal; font-weight: 600;
  font-display: swap; src: url('/assets/fonts/kleeone-600-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; }
@font-face { font-family: 'Klee One'; font-style: normal; font-weight: 600;
  font-display: swap; src: url('/assets/fonts/kleeone-600-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF; }

@font-face { font-family: 'Grandstander'; font-style: normal; font-weight: 100 700;
  font-display: swap; src: url('/assets/fonts/grandstander-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; }
@font-face { font-family: 'Grandstander'; font-style: normal; font-weight: 100 700;
  font-display: swap; src: url('/assets/fonts/grandstander-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF; }

/* --- reset ---------------------------------------------------------------- */

*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }

html {
  /* Reserved so that hiding the scrollbar while the menu is open does not
     shift the whole page 15px sideways on a machine with classic scrollbars.
     On macOS overlay scrollbars it costs nothing. */
  scrollbar-gutter: stable;
  -webkit-text-size-adjust: 100%;
  overflow-x: clip;                 /* no page scrolls sideways, ever */
  scroll-behavior: smooth;
  /* The header is sticky, so without this every anchor lands BEHIND it.
     Measured before adding it: /contact/#faq put the heading 85px under the
     bar -- completely hidden, with the page looking as if the link had gone
     nowhere. The value is the RESTING header plus a little air, not the
     shrunk one: a jump from the top of a page happens while the header is
     still at full height, and over-clearing when it is shrunk only leaves a
     gap. Each brand raises it where its header is taller. */
  scroll-padding-block-start: 94px;
}
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  min-height: 100dvh;
  font-family: var(--font-body);
  font-size: var(--size-body);
  line-height: 1.7;
  color: var(--ink);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

img, picture, svg, video { display: block; max-width: 100%; }
img { height: auto; }
input, button, select, textarea { font: inherit; color: inherit; }
button { background: none; border: 0; padding: 0; cursor: pointer; }
a { color: inherit; }
[hidden] { display: none !important; }

h1, h2, h3, h4 { font-weight: inherit; line-height: 1.2; text-wrap: balance; }
p { text-wrap: pretty; }

/* Keyboard focus must stay visible. It is also the one ring that must never
   read as "this item is selected" — on the first build the drawer threw focus
   at the first menu link on open, and the owner read the ring as a selection
   and asked why. Focus goes somewhere neutral; the ring stays. */
:where(a, button, input, select, textarea, summary):focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 2px;
}

.skip {
  position: fixed; inset-block-start: 0; inset-inline-start: 0; z-index: 200;
  padding: .75rem 1.25rem; background: var(--ink); color: var(--bg);
  transform: translateY(-120%);
}
.skip:focus { transform: none; }

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}

/* --- layout primitives ----------------------------------------------------
   The gutter is set once, here, and every page inherits it. A page that wants
   to bleed to the edges pulls back with negative margin rather than removing
   the padding, so nothing can end up flush against the glass by accident. */

:root {
  --gutter: clamp(1rem, 4vw, 2.5rem);
  --shell: 1222px;                  /* the model shop's content width */
}

.shell { width: 100%; max-width: var(--shell); margin-inline: auto; padding-inline: var(--gutter); }
.shell--narrow { max-width: 46rem; }
.bleed { margin-inline: calc(var(--gutter) * -1); }

.stack > * + * { margin-block-start: var(--stack, 1rem); }
.measure { max-width: 62ch; }

/* --- the product grid -----------------------------------------------------
   Four across on a desktop, two on a phone. The owner asked for exactly this
   and named the number: "4 em cada total de ecrã". Three in between, because
   four at tablet width makes a 170px frame that shows nothing. */
.grid-products {
  display: grid;
  gap: var(--grid-gap, 30px);
  grid-template-columns: repeat(2, minmax(0, 1fr));
}
@media (width >= 48rem) { .grid-products { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
/* Four across only once there is room for four to be BIG.
 *
 * The owner asked for the products to carry more weight. On a 1280 laptop four
 * columns gave a 273px frame; three give 390px, which is half again as much
 * photograph for the same screen. Four returns above 90rem, where each one is
 * still 340px — wider than the four-up ever was on a laptop.
 *
 * The phone is untouched: two columns, four lamps on a screen, which is the
 * number she asked for by name. */
@media (width >= 90rem) { .grid-products { grid-template-columns: repeat(4, minmax(0, 1fr)); } }

/* Every product frame is 1:1. This is the single most visible thing the owner
   asked for, and the reason scripts/square.py exists. */
/* 3:4 e não 1:1. Dois terços das fotografias do estúdio são 2:3, e um quadrado
   deitava fora um terço de cada uma -- o foguetão sem bico, a girafa sem pés.
   O número está aqui, em scripts/cards.py (que corta) e em src/lib/photo.mjs (a
   proporção que o HTML declara), e scripts/guards.mjs compara os três com os
   ficheiros no disco. A variável existe para os sítios que precisam de outra
   forma, como a fotografia da oficina, os declararem em vez de a repetirem.
   É um NÚMERO (largura/altura) e não `3 / 4`: `aspect-ratio` aceita os dois,
   mas `calc()` só aceita o número, e a galeria da ficha precisa dele para
   calcular a sua largura máxima. Um nome, não dois. */
.frame { aspect-ratio: var(--frame-ratio, .75); overflow: hidden; background: var(--frame-bg); }

/* <picture> is a wrapper with no size of its own, and an <img> inside it sizes
   against THAT, not against the frame. Without these two lines the square
   frames were the photograph's natural width and the hero stopped short of the
   right edge. */
.frame > picture, .hero__photo > picture { display: block; width: 100%; height: 100%; }
.frame img, .hero__photo img { width: 100%; height: 100%; object-fit: cover; }

/* --- tables that refuse to shrink ---------------------------------------- */
.table-wrap { overflow-x: auto; }


/* ===== styles/brands/ithos.css ===== */
/* ===========================================================================
   ithos — built to the shape of the shop the owner chose as her model.
   ===========================================================================

   Measured from that shop rather than guessed at:

     body            Montserrat 14px, #212121 on white
     product name    Montserrat 500, 14px, #333
     price           Montserrat 600, 14px, #A5523B
     button          Montserrat 600, 13px, uppercase, white on #A5523B
     grid            4 columns, 30px gap, 1:1 frames from 600px sources
     swatches        35px boxes holding 27px images
     content width   1222px

   One substitution in the whole file. Their headline face is Canela, which is
   licensed; Cormorant Garamond at 300 is the nearest free relative — the same
   high contrast, the same long delicate serifs, the same air at large sizes.
   ------------------------------------------------------------------------- */

[data-brand='ithos'] {
  --bg: #FFFFFF;
  --head-bg: rgb(255 255 255 / .92);   /* a barra depois de descer: fosca, não opaca */
  --bg-soft: #FAF7F4;
  --bg-trust: #FBF6F1;
  --ink: #212121;
  --ink-soft: #565656;
  --muted: #6E6E6E;           /* the model shop uses #777, which is 4.48:1 on
                                 white — it fails by two hundredths, and two
                                 hundredths is still a fail */
  --line: #E8E3DD;
  --line-soft: #F1ECE6;
  --accent: #A5523B;          /* the terracotta on prices and buttons */
  --accent-deep: #8C4230;
  --accent-ink: #FFFFFF;
  --frame-bg: #F6F1EC;

  /* The cover panel. 72% and not more so the photograph reads through it, and
     not less because the worst case has to hold: composited over a BLACK
     photograph this cream lands at #B8B1AA, and #4A3226 on that is 5.58:1.
     Over the actual terracotta it is far better -- 5.58 is the floor, not the
     measurement. scripts/battery samples the real pixels and checks. */
  /* O véu da capa: a cor aqui, a opacidade de origem aqui, e a que a dona
     escolhe no painel (covers.json "veil") por cima, no style da capa. O
     guards.mjs confere as duas contra a regra dos 4,5:1 sobre branco. */
  --cover-veil-rgb: 16 10 7;
  --cover-veil-a: .58;
  --cover-ink: #FFFFFF;

  --font-body: 'Montserrat', system-ui, sans-serif;
  --font-display: 'Cormorant Garamond', Georgia, serif;
  --size-body: 14px;

  --radius: 0px;              /* that shop is square-cornered throughout */
}

/* --- type ----------------------------------------------------------------- */

/* The serif is for the HERO LINE AND NOTHING ELSE.
 *
 * That is the correction that came out of reading the model shop properly.
 * Its section headings are Montserrat 22/600 and its product titles are
 * Montserrat 34/500 — the serif appears exactly once per page, over the
 * photograph, and that is what makes it feel like a moment instead of a
 * typeface choice. I had it on every heading, which spends it. */
[data-brand='ithos'] h1,
[data-brand='ithos'] h2 {
  font-family: var(--font-body);
  font-weight: 600;
  letter-spacing: normal;
  color: #242424;
}
[data-brand='ithos'] h1 { font-size: clamp(1.55rem, 1.1rem + 1.6vw, 2.15rem); }
[data-brand='ithos'] h2 { font-size: clamp(1.25rem, 1.05rem + .7vw, 1.375rem); }
[data-brand='ithos'] h3 { font-family: var(--font-body); font-weight: 500; font-size: 1rem; }
/* A faixa da campanha (portes grátis), como a da cathelier, nas cores daqui. */
:where([data-brand='ithos']) .announce {
  background: var(--bg-trust); color: var(--ink-soft);
  font-family: var(--font-body); font-size: 13px; letter-spacing: .04em;
}
[data-brand='ithos'] .display { font-family: var(--font-display); font-weight: 300; }

/* The product title, at their size and weight. */
[data-brand='ithos'] .product__detail h1 { font-size: clamp(1.5rem, 1.1rem + 1.7vw, 2.125rem); font-weight: 500; color: #333; }

[data-brand='ithos'] .eyebrow {
  font-size: 11px; font-weight: 600; letter-spacing: .18em;
  text-transform: uppercase; color: var(--muted);
}
[data-brand='ithos'] .lede { font-size: 15px; color: var(--muted); line-height: 1.8; }
/* On a phone the catalogue's opening paragraph is the only thing standing
   between the fold and the products. The owner asked for four lamps on a
   screen and named the number, so it is measured, not estimated: at 375x812
   the budget is 812 minus two card heights, and this paragraph has to live
   inside what is left. */
@media (width < 30rem) {
  [data-brand='ithos'] .lede { font-size: 13.5px; line-height: 1.65; }
  [data-brand='ithos'] h1 { font-size: 1.65rem; }
}

/* --- header ---------------------------------------------------------------
   Burger left, wordmark dead centre, cart right. The centring is absolute and
   not a grid column: with a grid, the logo shifted left when the cart counter
   went from one digit to two. */

/* The owner asked for this twice: the mark large, and without the
   "handmade in Portugal" line under it. Both are honoured in the asset itself
   — assets/brand/ithos-wordmark.svg has the strapline path removed and the
   viewBox tightened, so nothing here has to crop it. */

/* --- the footer is WARM, and it is the one thing that stops a white shop
   reading as a spreadsheet. Theirs is this exact peach. */
[data-brand='ithos'] .foot { background: #FCE1C5; border-block-start: 0; }
[data-brand='ithos'] .foot__group,
[data-brand='ithos'] .foot__base { border-color: rgb(140 66 48 / .18); }
[data-brand='ithos'] .foot__mark img { block-size: 64px; }
/* Everything that prints on the peach, in one list. The last two times a new
   class appeared in the footer it inherited a grey chosen against white and
   went straight to 4.06:1 — under the minimum, and invisible to anyone reading
   the CSS rather than measuring it. */
[data-brand='ithos'] .foot__group a,
[data-brand='ithos'] .foot__contact a,
[data-brand='ithos'] .foot__end,
[data-brand='ithos'] .foot__end a,
[data-brand='ithos'] .foot__note,
[data-brand='ithos'] .foot__cost { color: #6B4A38; }   /* 6.28:1 on the peach */
[data-brand='ithos'] .foot__group summary,
[data-brand='ithos'] .foot__withdraw a { color: #3A2219; border-color: rgb(140 66 48 / .35); }
[data-brand='ithos'] .foot__social a { border-color: rgb(140 66 48 / .28); color: #6B4A38; }
[data-brand='ithos'] .foot__social a:hover { color: var(--accent-deep); border-color: var(--accent-deep); }
/* The word beside the three lines, in this shop's voice: spaced upper case.
   It sat at 12px, one step above the 11px eyebrow, and read as a caption on a
   control rather than as the control's name. */
[data-brand='ithos'] .open-menu__word { font-weight: 400; color: var(--ink-soft); }

/* Removing the link bar took 28px off the ithos header (112 -> 84, measured at
   1280 and 1440), because its second grid row went with it. Some of that comes
   back as a bigger wordmark rather than as empty air: with nothing else in the
   middle track, the mark IS the header. */
@media (width >= 64rem) {
  /* --head-h lives on the ROOT and not on .head__row, because the cover
     reads it too: it is what the cover is pulled up by so the bar can
     float on it. One declaration, two readers, no way to disagree. */
  [data-brand='ithos'] { --head-h: 96px; scroll-padding-block-start: 106px; }
  [data-brand='ithos'] .head__mark img { height: clamp(64px, 5.5vw, 76px); }
}

/* --- product card ---------------------------------------------------------
   Frame, swatches, name, price — in that order, which is the order that shop
   uses and the order the owner pointed at. The swatches are the "bolinhas" she
   asked for: little squares of the actual finish, so nobody has to read the
   words "itho rato" to know what they are choosing. */

/* --- buttons -------------------------------------------------------------- */

/* --- section headings ----------------------------------------------------- */


/* ===== styles/brands/cathelier.css ===== */
/* ===========================================================================
   cathelier — built to the shape of the second shop the owner chose.
   ===========================================================================

   Measured from that shop rather than guessed at:

     body        Klee One 400, 14px in copy, letter-spacing 0.72px
     navigation  Klee One 400, 21.6px, SENTENCE CASE — not uppercase, not
                 letter-spaced. This is the part the owner picked out by name.
     headings    Grandstander at weight 100. Not 500. The whole character of
                 that shop is a hairline display face over a brush serif, and
                 at 500 it reads as a different shop entirely.
     header      a three-column grid, logo left, nav centre, icons right
     ink         #6B4E3C, with #634A3A under it
     structure   announcement bar, centred crest, full-bleed seasonal banner,
                 a row of ROUND occasion badges, then featured collections

   Both of its typefaces are free, so unlike the ithos side there is no
   substitution here: this is the same lettering.

   And this is deliberately nothing like the ithos world. Someone arriving here
   from the lamps should feel they changed website — same workshop, different
   shop. Where ithos is white, square-cornered and terracotta, this is cream,
   round, and brown. */

[data-brand='cathelier'] {
  --bg: #FFF8F2;
  --head-bg: rgb(255 248 242 / .92);   /* a barra depois de descer: fosca, não opaca */
  --bg-soft: #FFFFFF;
  --bg-trust: #F7EDE3;
  --ink: #6B4E3C;
  /* The model shop's own muted brown is rgb(107 78 60 / .62), and on its cream
     that is 2.96:1 — 54 separate failures on one page. These are the same
     brown at the opacity that actually passes, flattened to a solid value so
     the answer does not change with whichever of the three backgrounds it
     lands on. Measured by painting the colour onto a canvas and reading it
     back: reading it with a regular expression breaks on color-mix(). */
  --ink-soft: #7A5C48;        /* 5.6:1 on cream */
  --muted: #816757;           /* 4.5:1 on the darkest of the three grounds */
  --line: #EADDCF;
  --line-soft: #F2E7DA;
  --accent: #634A3A;

  /* Same 72% panel, but the cover ink is DARKER than the body ink on purpose:
     --ink (#6B4E3C) over the worst-case panel is 3.58:1, which fails. #4A3527
     is 5.44:1 on the same worst case. A brand colour that works on the page
     does not automatically work over a photograph. */
  /* O véu da capa: a cor aqui, a opacidade de origem aqui, e a que a dona
     escolhe no painel (covers.json "veil") por cima, no style da capa. O
     guards.mjs confere as duas contra a regra dos 4,5:1 sobre branco. */
  --cover-veil-rgb: 16 10 7;
  --cover-veil-a: .58;
  --cover-ink: #FFFFFF;
  --accent-deep: #4B372A;
  --accent-ink: #FFF8F2;
  --frame-bg: #F7EFE6;
  --sibling-bg: #FFFFFF;

  /* A LETRA É A DA ITHOS desde 18 out 2026 (a dona: «o tipo de letra do
     cathelier preferia que fosse igual ao do ithos»). Era Klee One no texto e
     Grandstander nos títulos, as da loja que ela tinha escolhido como modelo;
     as notas sobre elas, por cima, ficam como história. */
  --font-body: 'Montserrat', system-ui, sans-serif;
  --font-display: 'Cormorant Garamond', Georgia, serif;
  --size-body: 14px;

  --radius: 14px;             /* that shop is round-cornered throughout */
  --pill: 999px;
  --grid-gap: 22px;
}

[data-brand='cathelier'] body { line-height: 1.75; }

/* --- type ----------------------------------------------------------------- */

[data-brand='cathelier'] h1,
[data-brand='cathelier'] h2,
[data-brand='cathelier'] h3 {
  /* Os títulos como os da ithos: Montserrat a 600 (o serifado fica para os
     momentos, .display). */
  font-family: var(--font-body);
  font-weight: 600;
  letter-spacing: normal;
  /* No colour here, and that is the point. A brand rule with an attribute
     selector outranks `.hero__title`, so setting it painted the hero headline
     brown on brown: 1.08:1, which is invisible. Headings INHERIT, the body
     sets the colour, and a dark panel can therefore still turn its own text
     white. Measured, not looked at — it looked like a shadow. */
}
[data-brand='cathelier'] h1 { font-size: clamp(1.55rem, 1.1rem + 1.6vw, 2.15rem); }
[data-brand='cathelier'] h2 { font-size: clamp(1.25rem, 1.05rem + .7vw, 1.375rem); }
[data-brand='cathelier'] h3 { font-size: 1rem; font-weight: 500; }
[data-brand='cathelier'] .display { font-family: var(--font-display); font-weight: 300; }

[data-brand='cathelier'] .eyebrow {
  font-family: var(--font-body);
  font-size: 13px; font-weight: 400; letter-spacing: .045em;
  text-transform: none; color: var(--muted);
}
[data-brand='cathelier'] .lede { font-size: 16px; color: var(--ink-soft); line-height: 1.85; }

/* --- the announcement bar --------------------------------------------------
   That shop runs one above everything, and it is where a seasonal line lives:
   "Christmas orders close on the 10th" and nothing else. */
/* --- header ----------------------------------------------------------------
   Sentence case, Klee One, and no letter-spacing to speak of. The uppercase
   spaced-out version I had before belongs to the other brand; this one reads
   like handwriting on a label, which is the whole point. */
[data-brand='cathelier'] .head__mark img { height: clamp(40px, 5vw, 54px); }
/* Sentence case and the body face, like everything else in this shop — the
   spaced upper case beside it belongs to the other brand. */
[data-brand='cathelier'] .open-menu__word {
  font-family: var(--font-body); font-size: 16px; letter-spacing: .03em;
  text-transform: none; color: var(--ink-soft);
}

@media (width >= 64rem) {
  /* On the root, not on .head__row: the cover reads it too (see ithos.css). */
  [data-brand='cathelier'] { --head-h: 92px; scroll-padding-block-start: 102px; }
  [data-brand='cathelier'] .head__mark img { height: clamp(50px, 4.2vw, 62px); }
}
[data-brand='cathelier'] .drawer__nav a { font-size: 1.15rem; letter-spacing: .03em; }

/* --- the round occasion badges ----------------------------------------------
   The row of circles under the banner, which is how that shop's whole
   navigation works and exactly how the owner described hers: by date and by
   occasion, not by what the thing is made of. */
:where([data-brand='cathelier']) .occasions {
  /* SEM BARRA DE ROLAGEM À VISTA. A dona fotografou-a: uma barra cinzenta de
     sistema por baixo dos círculos, larga e escura, a competir com eles por
     atenção num sítio onde nada mais tem cromados. O que diz que a fila anda
     é o círculo cortado ao meio na beira, como na tira das miniaturas dos
     cartões. */
  scrollbar-width: none;
  /* NUM TELEMÓVEL É UMA TIRA QUE SE ARRASTA, NÃO UMA GRELHA.
     Medido a 375px: com cinco colunas cada uma dá 66px, e nenhuma destas
     etiquetas cabe lá -- CHRISTMAS em maiúsculas espaçadas mede cerca de 90px.
     O texto saía da célula, encavalitava-se no vizinho e empurrava a PÁGINA
     INTEIRA para o lado. Com quatro colunas cabe menos ainda e dez ocasiões
     caem em 4/4/2, com meia fila vazia.
     Encurtar os nomes não resolve: CHRISTMAS é o nome da ocasião e é longo.
     Por isso abaixo de 48rem isto é uma tira horizontal com rolamento próprio
     -- o padrão que qualquer loja usa para navegação por categoria, todas as
     dez continuam alcançáveis, e o rolamento fica DENTRO da tira em vez de
     ficar no documento. A partir de 48rem volta a ser a fila única da
     referência. */
  display: flex; gap: 1rem;
  overflow-x: auto; overscroll-behavior-inline: contain;
  scroll-snap-type: x proximity;
  padding-block: clamp(1.5rem, 4vw, 2.75rem);
  /* A tira sangra até às margens para se ver que há mais do lado de lá. */
  margin-inline: calc(-1 * var(--gutter)); padding-inline: var(--gutter);
}
:where([data-brand='cathelier']) .occasions > * { flex: 0 0 96px; scroll-snap-align: start; }
:where([data-brand='cathelier']) .occasions::-webkit-scrollbar { display: none; }

@media (width >= 48rem) {
  :where([data-brand='cathelier']) .occasions {
    display: grid; gap: 1.75rem 0;
    grid-template-columns: repeat(auto-fit, minmax(84px, 1fr));
    overflow-x: visible; margin-inline: 0; padding-inline: 0;
  }
  :where([data-brand='cathelier']) .occasions > * { flex: none; }
}
/* `align-content: start`: a grelha da fila estica cada círculo à altura do
   mais alto, e sem isto as duas filas de dentro repartiam a sobra -- um
   rótulo de uma linha ficava a meio caminho de um de quatro. Com ela, todos
   penduram do mesmo sítio, logo abaixo do círculo. */
:where([data-brand='cathelier']) .occasion { display: grid; justify-items: center; align-content: start; gap: .55rem; text-decoration: none; text-align: center; }
:where([data-brand='cathelier']) .occasion__badge {
  width: clamp(56px, 12vw, 74px); aspect-ratio: 1; border-radius: 999px;
  display: grid; place-items: center; overflow: hidden;
  /* Cheio e não pálido, com o desenho a branco e um anel claro à volta -- é a
     referência que a dona mandou, e é medido: branco sobre --accent dá 8,17:1,
     muito acima dos 3:1 de que um desenho precisa. Sem anel à volta: a dona
     pediu-o fora depois de o ver. */
  background: var(--accent); border: 0;
  color: #FFFFFF;
  transition: background-color .2s ease, transform .2s ease;
}
:where([data-brand='cathelier']) .occasion:hover .occasion__badge { background: var(--accent-deep); transform: translateY(-2px); }
@media (prefers-reduced-motion: reduce) { :where([data-brand='cathelier']) .occasion__badge { transition: none; } :where([data-brand='cathelier']) .occasion:hover .occasion__badge { transform: none; } }
:where([data-brand='cathelier']) .occasion__badge img { width: 100%; height: 100%; object-fit: cover; }
/* O DESENHO EM PERCENTAGEM DO CÍRCULO, não num tamanho fixo.
   Estava a 30px dentro de um disco que vai de 56 a 74: ocupava 40% do círculo
   na largura maior e não se percebia o que era. Em percentagem cresce e encolhe
   com o disco, e a proporção desenho/círculo é a mesma em todos os ecrãs. O
   atributo width/height que occasionArt() escreve fica como tamanho intrínseco
   -- serve de reserva antes de a folha de estilos chegar. */
:where([data-brand='cathelier']) .occasion__badge svg { inline-size: 62%; block-size: 62%; }
:where([data-brand='cathelier']) .occasion__name {
  font-family: var(--font-body); font-size: 13px; font-weight: 500;
  letter-spacing: .1em; text-transform: uppercase;
  color: var(--accent); line-height: 1.35;
  max-width: 11ch;
}
:where([data-brand='cathelier']) .occasion:hover .occasion__name { color: var(--accent-deep); }

/* --- a fila de ocasiões na home ---------------------------------------------
   A navegação desta loja é por ocasião -- a dona descreveu-a assim e o site foi
   construído à volta disso. A fila é a primeira coisa por baixo da capa, que é
   onde uma navegação serve para alguma coisa, e cada círculo leva à lista
   completa já filtrada: as dez páginas de ocasião deixaram de existir. */
:where([data-brand='cathelier']) .occasions-lead { padding-block: clamp(.5rem, 2vw, 1rem); background: var(--bg-trust); }

/* --- collection blocks ------------------------------------------------------ */
:where([data-brand='cathelier']) .collection { padding-block: clamp(2rem, 5vw, 3.5rem); }
:where([data-brand='cathelier']) .collection__head { text-align: center; max-width: 46rem; margin-inline: auto; }
:where([data-brand='cathelier']) .collection__head .eyebrow { display: block; margin-block-end: .5rem; }
/* «Ver as 20 peças de Natal», por baixo das quatro irmãs na ficha de uma peça. */
:where([data-brand='cathelier']) .collection__mais { margin: 2rem 0 0; text-align: center; }

/* --- cards, round instead of square ----------------------------------------- */
[data-brand='cathelier'] .frame { border-radius: var(--radius); }
[data-brand='cathelier'] .card__name { font-family: var(--font-body); font-size: 15px; font-weight: 400; color: var(--ink); text-align: center; letter-spacing: .03em; }
[data-brand='cathelier'] .card__price { color: var(--accent); text-align: center; font-size: 13px; }
/* A tira das miniaturas centra-se, como tudo o resto neste cartão -- na ithos
   o nome alinha à esquerda e a tira acompanha-o. `safe` e não `center` seco:
   num contentor que rola, centrar conteúdo que não cabe empurra o princípio
   para fora do alcance, e `safe` volta ao início nesse caso. */
[data-brand='cathelier'] .card__thumbs.ready { justify-content: safe center; }

/* A QUARTA COLUNA CHEGA MAIS CEDO NESTA LOJA.
   A grelha partilhada guarda as quatro colunas para as 90rem porque a dona
   pediu peso às fotografias da ithos -- vinte e seis candeeiros, cada um uma
   coisa diferente, e a fotografia é o produto. Aqui são quarenta e uma peças
   e a leitura é outra: percorre-se a lista à procura da forma certa.
   Medido: às 1280, que é o portátil mais comum, três colunas davam um cartão
   de 366px e 627px de altura -- uma fila por ecrã. Com a quarta às 64rem o
   mesmo ecrã dá 267px e 498px, e às 1024 dá 214px, que continua bem acima dos
   170px que o comentário da grelha partilhada aponta como o mínimo em que uma
   fotografia deixa de se ver.
   Sem `:where()`, de propósito: esta regra TEM de ganhar à de base.css, e
   ganhar por especificidade é mais seguro do que ganhar pela ordem dos
   ficheiros. O telemóvel não se toca: duas colunas, como está. */
@media (width >= 64rem) {
  [data-brand='cathelier'] .grid-products { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
[data-brand='cathelier'] .filter { font-family: var(--font-body); font-size: 14px; letter-spacing: .03em; text-transform: none; }
[data-brand='cathelier'] .btn { border-radius: var(--pill); font-family: var(--font-body); font-size: 15px; font-weight: 400; letter-spacing: .04em; text-transform: none; }
[data-brand='cathelier'] .field input, [data-brand='cathelier'] .field textarea,
[data-brand='cathelier'] .field select { border-radius: 10px; }
[data-brand='cathelier'] .choice { border-radius: 10px; }
[data-brand='cathelier'] .reassure { border-radius: var(--radius); }
[data-brand='cathelier'] .foot__group summary { font-family: var(--font-body); font-size: 15px; font-weight: 400; letter-spacing: .03em; text-transform: none; }

/* --- a piece with no photograph yet ------------------------------------------
   Forty-one pieces are written and not one has been photographed. Rather than
   a broken image or a grey box, the frame shows the piece's name set the way
   it would be engraved. It is honest about being a placeholder, it keeps the
   grid working, and it disappears by itself the moment a photograph lands. */
:where([data-brand='cathelier']) .awaiting {
  display: grid; place-items: center; padding: 1rem; text-align: center;
  background:
    repeating-linear-gradient(103deg, rgb(107 78 60 / .045) 0 2px, transparent 2px 9px),
    var(--frame-bg);
}
:where([data-brand='cathelier']) .awaiting span {
  font-family: var(--font-display); font-size: 13px; font-weight: 500;
  letter-spacing: .06em; color: var(--muted); max-width: 14ch; line-height: 1.4;
}

:where([data-brand='cathelier']) .collection--alt { background: var(--bg-soft); }

/* --- the announcement line ------------------------------------------------- */
:where([data-brand='cathelier']) .announce {
  margin: 0; padding-block: .75rem; text-align: center;
  background: var(--bg-trust); color: var(--ink-soft);
  font-family: var(--font-body); font-size: 15px; letter-spacing: .03em;
}

/* --- questions, on the home page ------------------------------------------
   Two columns of pills. That shop puts them here rather than only on a
   contact page, and it is right to: the questions people have about a
   personalised piece are the reason they do not order one. */
:where([data-brand='cathelier']) .ask { padding-block: clamp(2rem, 5vw, 3.5rem); }
:where([data-brand='cathelier']) .ask__grid { display: grid; gap: .75rem; margin-block-start: 1.75rem; }
@media (width >= 56rem) { :where([data-brand='cathelier']) .ask__grid { grid-template-columns: 1fr 1fr; gap: .75rem 1.25rem; } }
:where([data-brand='cathelier']) .ask__item { background: var(--bg-soft); border: 1px solid var(--line); border-radius: 12px; }
:where([data-brand='cathelier']) .ask__item summary {
  display: flex; align-items: center; gap: .75rem;
  padding: 1rem 1.15rem; cursor: pointer; list-style: none;
  font-family: var(--font-body); font-size: 15px; color: var(--ink);
}
:where([data-brand='cathelier']) .ask__item summary::-webkit-details-marker { display: none; }
:where([data-brand='cathelier']) .ask__item summary::after { content: '⌄'; margin-inline-start: auto; color: var(--muted); }
:where([data-brand='cathelier']) .ask__item[open] summary::after { content: '⌃'; }
:where([data-brand='cathelier']) .ask__item svg { flex: none; color: var(--accent); }
:where([data-brand='cathelier']) .ask__item > div { padding: 0 1.15rem 1.15rem; font-size: 14px; line-height: 1.8; color: var(--ink-soft); }
:where([data-brand='cathelier']) .ask__item > div p + p { margin-block-start: .75rem; }

/* --- the product page ------------------------------------------------------
   A big, light title, the way that shop sets them: Grandstander at 100, half
   again the size everything else on the page uses. At 500 and 24px it read as
   a paragraph heading; the point of a hairline face is that it can be large
   without shouting. */
[data-brand='cathelier'] .product__detail h1 {
  font-size: clamp(1.5rem, 1.1rem + 1.7vw, 2.125rem);
  font-weight: 500; line-height: 1.2;
}
[data-brand='cathelier'] .product__price { font-family: var(--font-body); font-size: 21px; font-weight: 400; }
[data-brand='cathelier'] .field > label { font-size: 12px; letter-spacing: .1em; text-transform: uppercase; font-weight: 400; }
:where([data-brand='cathelier']) .field__req { color: var(--muted); letter-spacing: 0; text-transform: none; font-size: 12px; }
[data-brand='cathelier'] .product__form { gap: .9rem; }

/* An <a> inside an <h2> is inline: it gets the glyphs' box, not the heading's
   line box, so it measured 22px tall — below the 24px minimum, on a link that
   is the only way out of 41 pages. */
[data-brand='cathelier'] .collection__head h2 a {
  color: inherit; text-decoration: none;
  display: inline-block; min-height: 28px;
}
[data-brand='cathelier'] .collection__head h2 a:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 6px; }

/* --- a faixa dos pedidos especiais --------------------------------------------
   Só aparece no separador «Custom orders», por cima das peças por medida. O
   `hidden` que a esconde vence este `display` por causa da regra global
   `[hidden] { display: none !important }` em base.css -- sem ela, declarar
   `display` aqui deixava-a sempre à vista. */
:where([data-brand='cathelier']) .custom-note {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
  gap: .75rem 1.25rem; margin-block-start: 1.5rem; padding: 1.1rem 1.25rem;
  background: var(--bg-soft); border-radius: 14px; text-align: center;
}
:where([data-brand='cathelier']) .custom-note p { margin: 0; max-inline-size: 46ch; }


/* ===== styles/shop.css ===== */
/* ===========================================================================
   The furniture: the parts that behave the same in both shops.
   ===========================================================================
   Everything here reads its colours and typefaces from the brand tokens, so a
   drawer on the ithos side and a drawer on the cathelier side share a skeleton
   and share no appearance. */

/* --- the cover ------------------------------------------------------------
   One picture filling the first screen, with a few words over it and the bar
   floating on top of both.

   TWO SHAPES, AND THEY ARE THE FILM'S SHAPES
   When the cover carries a film, the picture under it is that film's own first
   frame, cut to the same two shapes the film is cut to and switching at the
   same width -- otherwise the cover would jump between two framings at the
   moment the film began. Without a film it is the brand's photograph, and the
   4:5 master is the tallest shape any screen asks for, because `object-fit:
   cover` takes a wide band out of a tall picture and never the other way round.

   THE WORDS SIT ON A FLAT VEIL, NOT ON THE PICTURE
   Text straight on a picture has no contrast that can be measured: the ground
   is whatever the picture is at that pixel, and it changes the day the owner
   swaps the file. A gradient is worse than nothing, because a guard reads the
   declared colour -- the strong end -- and passes while the weak end is
   unreadable. So the whole cover carries ONE flat veil, and the worst ground
   it can ever be is itself over white. See src/lib/cover.mjs for the numbers
   and scripts/guards.mjs for the arithmetic that checks them. */
.cover {
  position: relative;
  isolation: isolate;
  display: grid;
}
/* AS QUATRO CAMADAS, COM A ORDEM DECLARADA E NÃO DEIXADA AO ACASO.
   Sem estes z-index a ordem era EMERGENTE, e mudava sozinha a meio de uma
   transição: um elemento com `opacity` abaixo de 1 cria um contexto de
   empilhamento e é pintado com os posicionados, ACIMA do véu; no instante em
   que a opacidade chega exactamente a 1 deixa de o criar e cai para BAIXO
   dele. O filme aparecia claro durante os 0,9s do desvanecimento e escurecia
   de repente no último quadro -- que foi exactamente o que a dona descreveu.
   Medido: a 0,99 a capa vinha sem véu, a 1,0 vinha com ele.
   Os quatro são itens de grelha, e `z-index` aplica-se a itens de grelha sem
   precisarem de `position` -- é o que torna isto declarável em vez de
   remendado com `position: relative` em cada um. */
.cover__media,
.cover__film,
.cover__veil,
.cover__words { grid-area: 1 / 1; }
.cover__media { z-index: 0; }
.cover__film  { z-index: 1; }
.cover__veil  { z-index: 2; }

.cover__media {
  display: block;
  inline-size: 100%;
  aspect-ratio: 4 / 5;
  /* On a phone the cover should be the screen, not a postcard on it. 150vw is
     the floor; the viewport cap stops a short landscape phone from pushing
     everything else below the fold entirely. Note what the floor does to the
     SHAPE: on a 390x844 phone it wins at 585px, so the frame is really 2:3 and
     not 4:5, which is the shape the tall film is cut to. */
  min-block-size: min(70svh, 150vw);
  max-block-size: calc(100svh - var(--head-h) - var(--anuncio-h, 0px) - 3rem);
  overflow: hidden;
}
.cover__img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  object-position: var(--cover-focus, 50% 50%);
}

/* The film sits between the photograph and the veil. It starts invisible and
   with no source at all: until shop.js turns it on, this is an empty box over
   the picture, and the cover looks exactly as it does with no JavaScript. The
   fade is what stops the first frame arriving as a jump-cut -- and it is
   skipped for anyone who asked for less motion, who never gets this far. */
/* O FILME NÃO DIMENSIONA A CAPA. É a fotografia que lhe dá a forma, e o filme
   é despejado lá dentro.
   Estava `inline-size: 100%; block-size: 100%`, e um `block-size` em
   percentagem numa linha de grelha automática conta como `auto` para efeitos
   de dimensionamento: assim que os metadados do vídeo chegam, o elemento passa
   a ter proporção intrínseca (16:9) e contribui com a altura toda dela. A
   fotografia ao lado está travada pelo `max-block-size: calc(100svh - 3rem)` e
   não acompanha.
   Medido num ecrã de 1905x1005: a capa abria com 957px, tudo a bater certo; no
   instante em que os metadados do filme chegavam, a capa saltava para 1072 e a
   fotografia ficava nos 957 -- 115px em que só havia véu por cima do fundo da
   página. É a barra escura que aparecia em baixo antes de o filme entrar.
   `position: absolute` tira-o da conta: um item posicionado não contribui para
   a altura da linha. O `inset: 0` sobre a área de grelha 1/1 dá-lhe
   exactamente a caixa da capa, seja ela qual for. */
.cover__film {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  opacity: 0;
  pointer-events: none;
}
.cover__film[data-on] { opacity: 1; }
@media (prefers-reduced-motion: no-preference) {
  .cover__film { transition: opacity .9s ease; }
}

/* THE ONE FLAT GROUND ON THE COVER.
   Everything readable on this screen -- the headline, the button's label, and
   the whole header bar floating above it -- is read against THIS and nothing
   else. It is one colour at one opacity over the entire cover, which is what
   makes the worst case arithmetic instead of hope: the brightest thing any
   picture or film can put underneath is white, and scripts/guards.mjs checks
   the composite over white rather than over this particular photograph.
   It must never become a gradient. A gradient's declared colour is its strong
   end, so a guard reads the strong end and passes while the weak end is
   unreadable -- the failure this veil exists to make impossible. */
/* A cor e a opacidade em separado: a opacidade pode vir do style da capa (a
   que a dona escolhe no painel), e uma variável só se resolve onde é usada --
   um --cover-veil já composto na raiz levava a opacidade da raiz. */
.cover__veil { background: rgb(var(--cover-veil-rgb) / var(--cover-veil-a)); }

.cover__words {
  position: relative;
  z-index: 3;
  display: grid;
  place-items: center;
  text-align: center;
  /* A barra já não está por cima da capa, por isso as palavras centram-se na
     caixa toda. */
  padding: clamp(1rem, 4vw, 3rem);
}
.cover__block {
  display: grid;
  justify-items: center;
  gap: clamp(1.1rem, 3vw, 1.75rem);
  /* In rem and NOT in ch. `ch` resolves against the font of the element it is
     written on, and this block inherits the 14px body size: 26ch here measured
     241px, which put every single word of a 64px headline on its own line. */
  inline-size: min(100%, 44rem);
}
/* TWO classes, and that is a correctness requirement rather than tidiness.
   Both brand files carry `[data-brand='…'] h1 { font-size: … }`, which at
   (0,1,1) outranks a lone `.cover__title` at (0,1,0) -- the old cover's
   font-size was dead code for exactly that reason, and the headline quietly
   took the brand's body-copy h1 size. (0,2,0) is what it takes to win. */
.cover .cover__title {
  margin: 0;
  color: var(--cover-ink);
  /* Também pela altura do ecrã (8svh): num portátil baixo a capa encolhe
     para deixar ver o que vem a seguir, e um título de quatro linhas ao
     tamanho de um ecrã alto saía por baixo dela, com o botão. */
  font-size: clamp(2rem, min(6.5vw, 8svh), 4rem);
  line-height: 1.04;
  text-wrap: balance;
}
.cover__btn { margin: 0; }

@media (width >= 48rem) {
  .cover__media {
    aspect-ratio: 4 / 3;
    min-block-size: 0;
    max-block-size: calc(100svh - var(--head-h) - var(--anuncio-h, 0px) - 3rem);
  }
  .cover__img { object-position: var(--cover-focus-wide, 50% 50%); }
}
@media (width >= 64rem) {
  .cover__media { aspect-ratio: 16 / 9; }
}

/* A CAPA JÁ NÃO É O ECRÃ INTEIRO (a dona, 18 out 2026: «gostava que na parte
 * inferior se conseguisse ver "os mais vendidos", para se perceber que há
 * coisas para baixo»). Em setembro ela tinha pedido o contrário -- o filme a
 * ocupar a janela toda --, e a capa passou a ter a altura da janela numa faixa
 * de feitios (10/7 a 11/5) onde o filme largo mantinha 80% de si. Medido agora:
 * num portátil de 1440x800 a capa acabava no fundo da janela e o título das
 * mais vendidas começava 80px abaixo dela, fora do ecrã.
 *
 * Por isso a faixa saiu, e com ela a altura mínima de um ecrã inteiro (e a guarda que
 * a refazia). Fica a regra que já existia por baixo dela: a capa tem a forma do
 * filme largo (16:9, ou 4:3 entre 48 e 64rem) e NUNCA passa da janela menos
 * 9rem -- o espaço para o título da secção seguinte se ver. No telemóvel nada
 * muda: a capa alta já deixava ver o título (medido a 390x664 e a 430x932). */
@media (width >= 48rem) {
  /* Com a barra fora da capa, o que fica para o título seguinte é o resto da
     janela depois da barra e da capa: 5rem, o mesmo que os 9rem de antes
     davam com a barra (4rem) por cima. */
  .cover__media { max-block-size: calc(100svh - var(--head-h) - var(--anuncio-h, 0px) - 5rem); }
  /* NA CATHELIER, OS SEPARADORES À VISTA (a dona, 20 out 2026: «no cathelier
     também não se veem aqueles ícones»). A seguir à capa da página inicial
     vem a fila dos círculos (Natal, Nascimento, Nomes…), e os 5rem só
     deixavam ver o cimo deles: medido a 1440x790, a fila acaba 160px abaixo
     do topo da secção. A capa guarda 11rem para ela; num ecrã muito baixo
     não desce dos 22rem, para o título e o botão continuarem a caber. */
  .cover:has(+ .occasions-lead) .cover__media {
    max-block-size: max(22rem, calc(100svh - var(--head-h) - var(--anuncio-h, 0px) - 11rem));
  }
}

.hero__cta {
  display: inline-flex; align-items: center; min-height: 26px;
  font-size: 12px; font-weight: 600; letter-spacing: .22em; text-transform: uppercase;
  text-decoration: none;
}

/* --- breadcrumbs ---------------------------------------------------------- */
.crumbs { padding-block: .5rem .25rem; font-size: 12px; color: var(--muted); }
.crumbs ol { display: flex; flex-wrap: wrap; gap: .4rem; list-style: none; padding: 0; margin: 0; }
.crumbs li + li::before { content: '/'; margin-inline-end: .4rem; color: var(--line); }
.crumbs a, .crumbs span { text-decoration: none; display: inline-flex; align-items: center; min-height: 26px; }
.crumbs a:hover { color: var(--accent); }


/* --- header, basket counter, cards, buttons: SHAPE ONLY --------------------
   These came out of brands/ithos.css, where they had no brand scope and
   therefore governed cathelier too. That is how `.head__mark { position:
   absolute }` survived: the cathelier header was told to put its mark in grid
   column 1 while a rule in the other brand's file had already taken it out of
   the grid entirely. The navigation then printed straight through the
   wordmark, which is what the owner photographed.

   base.css promises that a brand file holds values and this one holds shape.
   It is a promise worth keeping. */

/* --head-h is declared on the ROOT and not on .head__row, because two
   different things read it: the bar's own height, and the negative top margin
   that pulls the cover up underneath the bar so it can float on it. Written
   once, read twice, and therefore unable to disagree. The brand files raise it
   on wide screens, also on the root. The SHRUNK value stays on the row, which
   is right: shrinking is a state of the bar, and the cover never sees it
   because by the time the bar has shrunk the cover is off the screen. */
html { --head-h: 84px; }
/* A faixa da campanha dentro da barra (shell.mjs, header): uma linha de altura
   fixa, para o espaço que se guarda por baixo da barra ser uma conta e não uma
   medição. Recolhe-se quando a barra encolhe ao descer. */
html:has(.head .announce) { --anuncio-h: 34px; }
.head .announce {
  box-sizing: border-box; height: var(--anuncio-h); margin: 0; padding: 0 1rem; overflow: hidden;
  display: flex; align-items: center; justify-content: center;
  white-space: nowrap; text-overflow: ellipsis; font-size: 13px; line-height: 1;
}
/* Recolher é encolher a barra: só para quem não pediu menos movimento (a
   guarda das barras mede isto); para os outros a faixa fica sempre. */
@media (prefers-reduced-motion: no-preference) {
  .head[data-anim='yes'] .announce { transition: height var(--head-move) var(--head-ease); }
  .head[data-shrunk='yes'] .announce { height: 0; }
}

/* A barra já não ocupa espaço, por isso o espaço dela é devolvido aqui -- e a
   partir de --head-h, que é a altura EM REPOUSO. Ligá-lo à altura encolhida
   faria a página inteira subir 34px a meio do gesto, que é precisamente o
   defeito que sair do fluxo veio evitar.
   Nas duas páginas com capa também: a capa passava por baixo da barra, e a
   barra, com as letras brancas, obrigava o véu a ficar nos 56% para o «Menu»
   se ler sobre a luz de um candeeiro. A dona achou o vídeo muito escuro (18
   out 2026) e pôs o véu no mínimo; com a barra fora da capa, o véu só tem de
   segurar o título grande, e desce até 44%. */
body { padding-block-start: calc(var(--head-h) + var(--anuncio-h, 0px)); }

/* A BARRA DEPOIS DE DESCER: VIDRO FOSCO, NÃO UM BLOCO BRANCO.
   Quatro coisas em vez de uma, e é o conjunto que faz a diferença: fundo a
   92% em vez de opaco, desfoque do que passa por trás, uma sombra baixa em vez
   de só um fio, e tudo no mesmo relógio. A curva já era esta.
   Os 92% não são gosto, são o que a aritmética permite. A pior coisa que passa
   por trás da barra é a parte escura da capa, que em pixels é quase preta, e o
   chão da barra nunca desce abaixo de `alfa × --bg`. A 92% a tinta mais fraca
   do cabeçalho lê 6,13:1 na ithos e 4,84:1 na cathelier; scripts/guards.mjs
   calcula-o e mata a construção se deixar de dar. */
/* FORA DO FLUXO, E É ISSO QUE FAZ O RESTO FUNCIONAR.
   Com `sticky` a barra ocupava espaço, e encolhê-la mudava a altura do
   documento -- por isso o encolher tinha de viver longe, aos 240px e com 90px
   de histerese: para não baloiçar numa página curta, e para o «Skip to
   content», que aterra a y=123, não o disparar. Fora do fluxo esse custo
   desaparece, e é por isso que tudo passa a poder acontecer junto e cedo: o
   fundo, a sombra, a altura da barra e o logótipo mexem no MESMO gesto e no
   mesmo relógio. O espaço que ela deixava é devolvido no `body`. */
.head {
  position: fixed; inset-block-start: 0; inset-inline: 0; z-index: 50;
  background: var(--head-bg, var(--bg));
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
  box-shadow: 0 2px 20px rgb(0 0 0 / .06);
  border-block-end: 1px solid var(--line-soft, var(--line));
}
.head__row { min-height: var(--head-h); }

/* --- the bar has no ground of its own until you move ------------------------
   AT THE TOP THE BAR IS NOT A BAR, IT IS FOUR CONTROLS STANDING ON THE COVER.

   Two states, and the second one is the safe one. `data-scrolled` is written
   on the ROOT by a one-line script in the head -- before the first paint, and
   reading the real scroll position, so a reload part-way down a page does not
   start with a transparent bar over the text. Its absence is what a browser
   with no JavaScript sees, and absence means the bar keeps its background:
   the rule below needs data-scrolled='no' to be there, not merely not 'yes'.
   A bar that loses its ground only when a script says so can never be left
   floating illegibly over the page body by a script that failed to load.

   Nas páginas com capa a barra já não clareia (letras brancas sobre o
   filme): desde 19 out 2026 a capa começa por baixo dela, e o que está por
   trás da barra transparente é o fundo da página, como em todas as outras. */
html[data-scrolled='no'] .head {
  background: transparent;
  /* blur(0px) e não `none`: `none` não interpola, e o desfoque entraria de uma
     vez enquanto o fundo ainda vinha a caminho. */
  -webkit-backdrop-filter: blur(0px);
  backdrop-filter: blur(0px);
  box-shadow: 0 0 0 rgb(0 0 0 / 0);
  border-block-end-color: transparent;
}
.head__sibling-mark { display: inline-flex; align-items: center; }
/* Sized per SHAPE and not per brand-that-owns-it: the door in the ithos shop
   shows the cathelier lockup, which is wide (117x54), and the door in the
   cathelier shop shows the ithos one, which is nearly square (119x120). One
   height for both would make one of them a postage stamp. */
.head__sibling-mark img { inline-size: auto; block-size: 30px; }
[data-brand='ithos'] .head__sibling-mark img { block-size: 20px; }

/* A LÍNGUA. Duas letras por língua, a actual sublinhada e sem ligação (é onde
   se está). Pequeno, porque quase ninguém muda -- mas cada letra é um alvo de
   28px, acima do mínimo de 24, e as de fora têm o contraste do texto normal.
   No menu repete-se em ponto grande, que é onde se procura num telemóvel. */
.linguas { display: inline-flex; align-items: center; font-size: .75rem; font-weight: 600; letter-spacing: .08em; }
.linguas :is(a, span) {
  display: inline-grid; place-items: center; min-inline-size: 28px; min-block-size: 28px; padding-inline: 3px;
  color: var(--ink-soft); text-decoration: none;
}
.linguas a:hover { color: var(--accent); }
.linguas [aria-current] { color: var(--ink); text-decoration: underline; text-decoration-thickness: 1.5px; text-underline-offset: .35em; }
.linguas--gaveta {
  display: flex; gap: .25rem; flex: none; padding: .75rem var(--gutter) 1.25rem;
  border-block-start: 1px solid var(--line-soft, var(--line)); font-size: .9rem;
}
.linguas--gaveta :is(a, span) { min-inline-size: 44px; min-block-size: 44px; }

/* The state changes instantly; only the FADE is motion, so only the fade sits
   behind the gate. A reader who asked for less motion still gets a bar that
   is transparent at the top and solid below it -- they just get it at once. */
@media (prefers-reduced-motion: no-preference) {
  .head[data-anim='yes'],
  .head[data-anim='yes'] :is(.icon-btn, .open-menu__word, .head__sibling) {
    transition: background-color var(--head-move) var(--head-ease),
                border-block-end-color var(--head-move) var(--head-ease),
                box-shadow var(--head-move) var(--head-ease),
                -webkit-backdrop-filter var(--head-move) var(--head-ease),
                backdrop-filter var(--head-move) var(--head-ease),
                color var(--head-move) var(--head-ease),
                opacity var(--head-move) var(--head-ease);
  }
}

/* --- the header shrinks on the way down ------------------------------------
   EXACTLY TWO THINGS MOVE, AND THEY MOVE ON ONE CLOCK.
   Everything else in the bar -- the three lines, the word beside them, the
   door to the other brand, the basket -- is centred by the grid, so it rides
   the row's height and needs no timing of its own. Only the row's min-height
   and the wordmark's height actually change, and until now only the first of
   them was animated: the wordmark went from 61px to 40px IN ONE FRAME while
   the bar glided around it for 200ms. The biggest thing in the header was the
   one thing that jumped, which is what the owner was seeing.

   One duration and one curve for both, and that is a correctness requirement
   rather than a preference. A block's height is max(min-height, content), and
   with both endpoints moving under the SAME easing the gap between them is a
   straight blend of two positive numbers and can never close mid-flight.
   Give the two different curves and the content can take over the height
   part-way down, which is a stutter nobody would be able to explain.

   The transitions hang off data-anim, not off .head, so they can be switched
   off for the two moments when a size change is not a shrink: the first
   decision after load, and a window being dragged (the wordmark's resting
   height is a vw clamp, so every pixel of a drag is a new computed height and
   a transition would have it trailing the rest of the bar for the whole
   gesture). */
/* Duas durações e duas curvas, porque são dois gestos diferentes.
   O encolher é um movimento e quer uma curva que trave no fim (ease-out).
   O fundo a aparecer é uma mudança de TOM, e a mesma curva punha-lhe 80% da
   mudança nos primeiros 80ms: media-se como uma transição e via-se como um
   salto. Mais tempo e uma curva simétrica, e o gesto lê-se.
   Nota honesta sobre o que acontece a meio: enquanto o fundo atravessa, a
   tinta atravessa com ele, e há uma fracção de segundo em que texto claro
   está sobre um fundo a meio caminho. É inerente a cruzar um chão escuro com
   um claro -- não há ordem de fade que o evite -- e os dois ESTADOS, que são
   o que a norma mede, passam os dois com folga. */
/* UM RELÓGIO SÓ, e isso é requisito de correcção e não arrumação.
   Eram dois -- 220ms para o tamanho e 350ms para a cor -- e com os dois gestos
   a acontecerem agora ao mesmo tempo isso via-se: a barra acabava de encolher
   enquanto o fundo ainda vinha a caminho. Uma duração e uma curva para tudo o
   que mexe, e o gesto lê-se como um só. */
.head { --head-move: 350ms; --head-ease: cubic-bezier(.4, 0, .2, 1); }
.head[data-anim='yes'] .head__row { transition: min-height var(--head-move) var(--head-ease); }
/* Height AND opacity in ONE declaration. A second `transition` on the same
   element replaces this one rather than adding to it, and this selector
   carries an element name -- (0,3,1) -- so any list-of-classes rule written
   later would lose and the wordmark would cross-fade in one frame. */
.head[data-anim='yes'] :is(.head__mark, .head__sibling-mark) img {
  transition: height var(--head-move) var(--head-ease),
              filter var(--head-move) var(--head-ease);
}

/* The shrink itself is motion, so a reader who asked for less of it does not
   get a header that changes size at all -- doing it instantly instead of
   smoothly would be worse, not better.
   BOTH rules have to sit inside this gate, and the brand files must not carry
   copies: `[data-brand='ithos'] .head[data-shrunk='yes'] .head__row` outranks
   an ungated rule here, so gating only this file would leave ithos sliding
   96 -> 62 and stopping dead at 76 -- the worst of both. The brand copies are
   deleted; this one rule serves both, because (0,3,0) beats the brands'
   (0,2,0) resting values. */
@media (prefers-reduced-motion: no-preference) {
  .head[data-shrunk='yes'] .head__row { --head-h: 62px; }
  .head[data-shrunk='yes'] .head__mark img { height: 40px; }
}
/* An SVG with a viewBox and no intrinsic size collapses to nothing when it is
   given `width: auto` and no height. Moving these rules between files dropped
   the height and the ithos wordmark went to zero pixels wide — the link was
   still there, the file still resolved, and every guard passed. Only looking
   at the page found it, which is why there is now a battery check that the
   mark has a size. */
.head__mark img { width: auto; height: clamp(46px, 7vw, 64px); }
[data-brand='cathelier'] .head__mark img { height: clamp(40px, 5vw, 54px); }
/* the shrunk height lives with the other shrink rule, inside the
   prefers-reduced-motion gate further up */
.drawer__mark { height: 34px; width: auto; }

.icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 44px; height: 44px; color: var(--ink);
}
.icon-btn:hover { color: var(--accent); }

.cart-count {
  min-width: 20px; height: 20px; padding-inline: 5px;
  display: grid; place-items: center; margin-inline-start: -8px; margin-block-start: -14px;
  background: var(--accent); color: var(--accent-ink);
  border-radius: 999px; font-size: 12px; font-weight: 600; font-variant-numeric: tabular-nums;
}
.cart-count[data-empty='yes'] { display: none; }

.card { display: flex; flex-direction: column; }
.card__media { display: block; text-decoration: none; }
/* The name is now the card's only link, and a bare line of text is a 20px
   target -- the whole card used to be the link, so this was never a question
   before. Block and a little padding puts it over the 24px floor and makes the
   whole name row clickable, which is what a reader aims at anyway. */
.card__link {
  text-decoration: none; color: inherit;
  display: block; padding-block: 3px;
}
.card__frame { position: relative; }
/* O vídeo do cartão: por cima da fotografia, enquanto o rato está no cartão
   (shop.js, videosDosCartoes). Começa invisível; o dos cartões à vista já
   vem carregado, e aparece quase de imediato (a dona queria-o instantâneo). */
.card__video {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; opacity: 0; pointer-events: none;
  transition: opacity .12s ease;
}
.card[data-a-tocar='yes'] .card__video { opacity: 1; }

/* --- the slow drift from one photograph to the next -------------------------
   TWO LAYERS, AND ONLY ONE OF THEM IS EVER THE CARD.

   Layer A is the <picture> the build wrote. It is in normal flow, its opacity
   is never set, and it rests at scale 1. It IS the card. It is never parked at
   opacity 0 and never absolutely positioned -- the resting state has to be the
   correct state, the same lesson the side panel above carries.

   Layer B does not exist in the HTML at all. The script clones A when it first
   needs a second photograph and REMOVES it again when the card comes to rest,
   so a card nobody has hovered is byte-for-byte what the server sent.

   Only B's opacity moves. A stays fully opaque underneath it, which is why the
   dissolve has no dip: fade two layers against each other and the picture sags
   at the halfway mark, because two 50% images do not add up to one. */
.card__frame > picture {
  transition: scale var(--shot-zoom, 1500ms) cubic-bezier(.4, 0, .6, 1);
}
.card__frame > picture.over {
  position: absolute; inset: 0;
  opacity: 0;
  transition: opacity var(--shot-fade, 500ms) linear;
}
.card__frame > picture.over.up { opacity: 1; }
/* Set by the script while a step is being set up: the incoming layer has to be
   placed without animating into place. */
.card__frame > picture.still { transition: none !important; }
.card[data-showing='yes'] .card__frame > picture:not(.over) { scale: var(--shot-scale, 1.05); }
/* Going in is slow on purpose; coming back is not. A pointer that leaves
   should let go of the picture at once, not creep back over a second and a half
   seconds after the reader has moved on. */
.card:not([data-showing]) .card__frame > picture { transition-duration: 400ms; }

@media (prefers-reduced-motion: reduce) {
  .card__frame > picture,
  .card__frame > picture.over { transition: none; }
  .card[data-showing='yes'] .card__frame > picture:not(.over) { scale: 1; }
}

/* --- the other photographs, underneath --------------------------------------
   A PARTIR DE TODAS AS LARGURAS, agora também no telemóvel, a pedido da dona.
   Estiveram só a partir de 48rem por três razões, e vale a pena dizer o que
   aconteceu a cada uma: num telemóvel não há ponteiro para pousar (verdade, e
   continua a ser -- a deriva automática continua presa a um ponteiro fino;
   mas TOCAR numa miniatura sempre funcionou, é um acto deliberado); a tira
   acrescenta uma faixa a cada cartão de uma grelha de duas colunas (verdade, e
   é o preço do que foi pedido); e punha sessenta e sete imagens a mais na
   página (falso desde que os botões passaram a ir vazios -- o script só
   constrói a tira do cartão que está prestes a ser visto).

   ABAIXO DE 48rem A TIRA CORRE DE LADO EM VEZ DE ENCOLHER.
   Medido: a 390px o cartão tem 164px e a 320px tem 129px. Seis miniaturas a
   encolher dentro de 164px dão 24,0px -- exactamente o mínimo de um alvo de
   toque, sem folga nenhuma -- e dentro de 129px nem quatro lá cabem. Encolher
   até caber era passar por baixo do chão, por isso ficam do tamanho que devem
   ter e é a tira que rola. É o mesmo padrão da fila de ocasiões da cathelier.
   A altura não muda com o número de fotografias, que é o que mantém os nomes
   de dois cartões vizinhos alinhados.

   A partir de 48rem a coluna é larga e encolher é melhor do que rolar: mostra
   as seis de uma vez em vez de esconder duas atrás de um gesto.

   A fila vazia num candeeiro de uma só fotografia é de propósito: cinco dos
   vinte e seis têm uma, e sem ela o nome deles ficava mais acima do que o de
   quem está ao lado. */
/* Hidden until the script has filled it: an empty strip is a band of nothing
   under every card, and without a script there is no strip to fill. The card
   then reads exactly as it did before this existed. */
.card__thumbs { display: none; --shot-size: 32px; }
/* A FILA VAZIA TEM DE OCUPAR O LUGAR, E NÃO OCUPAVA.
   O comentário acima sempre disse que a fila num candeeiro de uma só
   fotografia era de propósito, mas `.card__thumbs--none` ficava preso no
   `display: none` da linha de cima e media ZERO: o nome do Caracol estava
   cinquenta pixels acima do nome do Ouriço, ao lado dele, na mesma linha da
   grelha. A intenção estava escrita e não estava a acontecer. Quem a marca
   como pronta é o script, tal como às outras, senão sem JavaScript era o
   contrário -- os de uma fotografia é que ficavam mais altos. */
.card__thumbs.ready {
  display: flex; gap: 4px; margin-block-start: 8px;
  min-height: calc(var(--shot-size) / var(--frame-ratio, .75));
}
.card__thumb {
  /* A mesma forma da fotografia grande: um botão quadrado sobre um ficheiro
     3:4 voltava a cortá-lo, e a tira deixava de parecer a mesma fotografia
     em pequeno. */
  aspect-ratio: var(--frame-ratio, .75);
  padding: 0; border: 1px solid var(--line); background: none;
  border-radius: var(--radius, 0); overflow: hidden; cursor: pointer;
  opacity: .62; transition: opacity .2s ease, border-color .2s ease;
}
/* Um botão ainda sem fotografia guarda o lugar e não desenha nada: um
   contorno vazio a piscar antes da imagem chegar é pior do que espaço. */
.card__thumb:empty { border-color: transparent; }
.card__thumb img { display: block; width: 100%; height: 100%; object-fit: cover; }
.card__thumb:hover, .card__thumb:focus-visible { opacity: 1; }
.card__thumb.is-on { opacity: 1; border-color: var(--accent); }
@media (prefers-reduced-motion: reduce) { .card__thumb { transition: none; } }

@media (width < 48rem) {
  .card__thumbs.ready {
    overflow-x: auto; overflow-y: hidden;
    /* A barra de rolagem roubava a altura à tira e desalinhava os nomes. */
    scrollbar-width: none;
    scroll-snap-type: x proximity;
  }
  .card__thumbs.ready::-webkit-scrollbar { display: none; }
  .card__thumb {
    flex: 0 0 var(--shot-size, 32px);
    scroll-snap-align: start;
    /* A tira corta o que sai dela, e um anel de foco desenhado por fora era
       cortado com o resto. Para dentro, fica visível. */
    outline-offset: -2px;
  }
}

@media (width >= 48rem) {
  .card__thumbs { --shot-size: 48px; }
  /* Encolhem, mas nunca abaixo do alvo de toque; e quando nem assim cabem
     (o Conjunto 12 desejos, 19 out 2026, com nove fotografias), a tira rola,
     como no telemóvel, em vez de as miniaturas saírem do cartão. */
  .card__thumbs.ready { gap: 6px; margin-block-start: 10px; overflow-x: auto; overflow-y: hidden; scrollbar-width: none; }
  .card__thumbs.ready::-webkit-scrollbar { display: none; }
  .card__thumb { flex: 0 1 var(--shot-size); min-width: 26px; outline-offset: -2px; }
}

.card__swatches { display: flex; flex-wrap: wrap; gap: 6px; margin-block-start: 12px; }
/* `display` is the load-bearing word. Width and height do not apply to a
   non-replaced INLINE element, so these were 0x0 boxes with a hairline round
   them -- the rule looked right and drew nothing. */
.swatch {
  display: inline-block; width: 27px; height: 27px;
  border: 1px solid var(--line); border-radius: var(--radius, 0);
  background-size: cover; background-position: center;
}
.swatch[aria-pressed='true'] { outline: 1px solid var(--accent); outline-offset: 2px; }

.card__name { margin-block-start: 10px; line-height: 1.5; }
.card__price { margin-block-start: 4px; font-variant-numeric: tabular-nums; color: var(--accent); }
.card__from { font-weight: 400; color: var(--muted); font-size: 12px; }

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: .6em;
  min-height: 48px; padding-inline: 2rem;
  background: var(--accent); color: var(--accent-ink);
  border: 1px solid var(--accent); border-radius: var(--radius, 0);
  text-decoration: none; transition: background .18s ease, border-color .18s ease;
}
.btn:hover { background: var(--accent-deep); border-color: var(--accent-deep); }
.btn--ghost { background: transparent; color: var(--ink); border-color: var(--line); }
.btn--ghost:hover { background: transparent; border-color: var(--ink); color: var(--ink); }
.btn--wide { width: 100%; }
.btn[aria-disabled='true'] { opacity: .45; cursor: not-allowed; }

.section { padding-block: clamp(2.5rem, 6vw, 5rem); }
.section--soft { background: var(--bg-soft); }
.section__head { text-align: center; margin-block-end: clamp(1.5rem, 4vw, 2.75rem); }

/* --- header and drawer: three tracks, never overlapping -------------------
   The logo lives in a column of its own. Before this it was absolutely centred
   while the navigation sat in normal flow beside it, and at the width where
   the navigation reached the middle the two printed on top of each other.

   The drawer's close button had a subtler version of the same fault:
   `.icon-btn:last-of-type` matches by TAG, not by class, so the only <button>
   in the row was "the last of its type" and collected a `margin-left: auto`
   meant for the cart. Two auto margins share the space between them, which
   parked the ✕ in the middle of the row, right beside the wordmark. */
.head__row {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  grid-template-areas: 'left mark right';
  align-items: center;
  /* Enough air that the navigation reads as a separate thing from the mark
     even when it fills its whole track. At 1rem they were 20px apart, which
     is not overlapping but looks like it is. */
  gap: clamp(1rem, 3vw, 2.75rem);
}
.head__left { grid-area: left; display: flex; align-items: center; gap: 1.5rem; min-width: 0; }
.head__right { grid-area: right; display: flex; align-items: center; justify-content: flex-end; gap: .35rem; }
/* The target is 44px even when the artwork is 40. Shrunk, the ithos wordmark
   is 39.7x40 -- the only control on the site under 44px, sitting between a
   44px burger and a 44px basket, and it is the way back to the top of the
   shop. The art keeps its size; the box around it does not. */
.head__mark {
  grid-area: mark; justify-self: center;
  display: grid; place-items: center;
  min-width: 44px; min-height: 44px;
}

.drawer__top {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 1rem;
}
.drawer__mark { justify-self: center; }
.drawer__cart { justify-self: end; }
.drawer__top .icon-btn { justify-self: start; }
.drawer__cart.icon-btn { justify-self: end; }

/* --- the other brand, in the header --------------------------------------
   It carries the other shop's OWN lockup, small, and an arrow that points out.
   A label does not point out of the page; a door does.

   On a phone it is not here at all. The lockup does FIT beside the burger, the
   wordmark and the basket -- measured, 74px in a 375px bar -- but fitting was
   never the question the owner was answering: on a small screen the door to
   the other shop belongs in the side menu, with the rest of the navigation,
   and a fourth thing in the bar is a fourth thing to read past.

   The arrow is 12px and not smaller. It is aria-hidden, but it is still a
   glyph with a box, and the battery's floor for anything with text in it is
   12px: at 11 it read as a defect, correctly. */
.head__sibling {
  display: none; align-items: center; gap: .35em;
  /* --ink-soft e não --muted. Aqui o tom só pinta a setinha -- o nome da outra
     loja é o lockup ao lado -- e --muted é o tom mais fraco da casa: na
     cathelier já está a 4,63:1 sobre creme OPACO, portanto qualquer
     transparência na barra afundava-o abaixo do mínimo. Um tom acima resolve,
     e a seta fica mais fácil de ler de caminho. */
  text-decoration: none; color: var(--ink-soft); padding: .5rem;
}
@media (width >= 48rem) { .head__sibling { display: inline-flex; } }
.head__sibling:hover { color: var(--accent); }
.head__sibling-arrow { font-size: 12px; opacity: .8; }

/* --- the word beside the three lines --------------------------------------
   From 48rem the button carries the word as well as the drawing. On a phone
   there is no room and the icon is universal; on a wide screen a lone burger
   floating in a 480px track reads as an orphan, and the word is what makes the
   left flank carry ink at all now that the link bar is gone.

   The button keeps aria-label="Open the menu", which CONTAINS the visible word
   — WCAG 2.5.3 wants the accessible name to contain the visible label, and
   "menu" is inside "Open the menu". */
.open-menu__word { display: none; }
@media (width >= 48rem) {
  /* `width: auto` E `flex: none` NO DESENHO, e as duas são obrigatórias.
     O .icon-btn tem `width: 44px` fixo, que é o alvo de toque. A partir daqui
     este botão passa a levar também a palavra, um intervalo e vinte pixels de
     enchimento -- perto de cem pixels de conteúdo dentro de uma caixa de 44 --
     e num contentor flex quem paga é quem encolhe: o SVG, que é o único com
     tamanho próprio, era espremido até **zero** e as três linhas
     desapareciam. Medido: 0x24 com a palavra, 24x24 sem ela, igual a 12px,
     14px e 16px. O botão lia-se "MENU" sem hambúrguer nenhum, e a palavra
     transbordava para fora do alvo declarado. A altura continua nos 44. */
  .open-menu { width: auto; gap: .6rem; padding-inline: .5rem .75rem; }
  .open-menu svg { flex: none; }
  .open-menu__word { display: inline; font-size: 14px; letter-spacing: .18em; text-transform: uppercase; }
}

/* An option that is out of stock. Greyed AND labelled: colour alone is not a
   message, and "why is this one pale?" is a question the reader should not
   have to ask. The radio carries `disabled`, so it is skipped by the keyboard
   within its group, which is the right behaviour for a radio -- unlike a
   button, where disabling the thing you just pressed leaves the focus with
   nowhere to go. */
/* The limit, said out loud. A maxlength the reader only discovers when the
   keyboard stops responding is worse than a number on the page -- and it is
   printed from o.max, so it cannot come to disagree with the field it
   describes, or with the Worker, which enforces the same number. */
.field__limit { display: block; margin-block-start: .35rem; font-size: 12px; color: var(--muted); }

.choice--off { opacity: .55; cursor: not-allowed; }
.choice--off input { cursor: not-allowed; }
.choice__off {
  font-style: normal; font-size: 12px; letter-spacing: .06em;
  text-transform: uppercase; color: var(--muted);
}

/* --- sort by ----------------------------------------------------------------
   Built by the script, so it is never on a page that cannot use it. A native
   <select>: it is one tab stop, it is the control every phone already knows
   how to open full screen, and it needs no keyboard handling written by hand. */
.sortby {
  display: flex; align-items: center; gap: .6rem;
  margin-block-start: 1rem; justify-content: flex-end;
}
.sortby__label { font-size: 12px; letter-spacing: .12em; text-transform: uppercase; color: var(--muted); }
/* --- a seta de um dropdown, a mesma nos dois que existem --------------------
   ANTES NÃO ERA DESENHADA DE TODO: era a nativa do browser, com tamanho, traço
   e cor decididos pelo sistema e diferentes em cada um. Numa pílula
   completamente redonda ficava encostada à curva, com um vão grande entre ela
   e a palavra.

   Dois lados de uma caixa rodada, e não um SVG em data-URI, porque assim herda
   a cor da marca com `currentColor` em vez de obrigar a uma cópia para o
   #212121 da ithos e outra para o #6B4E3C da cathelier. O cinzento e a voz são
   os mesmos dos `+` e `−` do rodapé.

   O invólucro existe porque um <select> não aceita ::before nem ::after. É uma
   classe partilhada: há dois dropdowns no sítio -- a ordenação das listas e o
   país do carrinho -- e dois desenhos diferentes para o mesmo controlo era
   exactamente o que isto veio arrumar. */
.select {
  position: relative; display: inline-flex;
  /* A FOLGA PARA A SETA VIVE AQUI, NUM SÍTIO SÓ, e é herdada por quem lá está
     dentro. Foi escrita em `.select > select` e perdeu: `.field select` traz um
     `padding` ABREVIADO, tem a mesma especificidade e vem depois, por isso
     repunha o enchimento em 13,6px -- e a seta ocupa dos 15,2 aos 22,2. Um
     nome de país comprido passava por baixo dela. Cada controlo declara agora
     o seu próprio enchimento e vai buscar esta medida, que é o que impede os
     dois de discordarem outra vez. */
  --arrow-room: 2.1rem;
}
.select > select { appearance: none; -webkit-appearance: none; }
.select::after {
  content: ''; position: absolute; pointer-events: none;
  inset-inline-end: .95rem; inset-block-start: 50%;
  inline-size: 7px; block-size: 7px;
  border-inline-end: 1.5px solid currentColor;
  border-block-end: 1.5px solid currentColor;
  /* A tinta de um quadrado rodado fica abaixo do centro da caixa, por isso a
     subida é maior do que metade: assim o V assenta na linha do meio. */
  transform: translateY(-68%) rotate(45deg);
  color: var(--muted);
  transition: color .2s ease;
}
.select:hover::after { color: var(--accent); }
@media (prefers-reduced-motion: reduce) { .select::after { transition: none; } }

.sortby__select {
  min-height: 40px;
  padding-block: .35rem; padding-inline: .9rem var(--arrow-room, .9rem);
  border: 1px solid var(--line); border-radius: var(--pill, 999px);
  background: var(--bg); color: var(--ink);
  font: inherit; font-size: 14px; cursor: pointer;
}
.sortby__select:hover { border-color: var(--accent); }
[data-brand='cathelier'] .sortby__label { text-transform: none; letter-spacing: .03em; font-size: 13px; }

/* --- filters --------------------------------------------------------------
   They WRAP. A row that scrolls sideways on a phone hides what is in it: the
   first build hid 154px of chips on one page and 927px on another, and the
   owner photographed the cut-off one and asked why. Two rows, then a "+N" that
   opens the rest, so nothing is ever off the screen without a way to know. */
.filters { display: flex; flex-wrap: wrap; gap: 8px; }
.filter {
  flex: none; min-height: 40px; padding: .45em .85em;
  border: 1px solid var(--line); background: transparent; color: var(--muted);
  border-radius: var(--pill, 999px); font-size: 12px; letter-spacing: .02em;
  transition: color .18s, border-color .18s, background .18s;
}
.filter:hover { border-color: var(--accent); color: var(--accent); }
.filter[aria-pressed='true'] { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }
.filter--more { color: var(--accent); border-style: dashed; }

/* --- product page ---------------------------------------------------------- */
/* The column must be minmax(0, 1fr) at EVERY width, not just the wide one.
   A grid column defaults to `auto`, which sizes to its content's max-content —
   and the gallery track is six slides at 100% each. On a phone that made the
   column 560px inside a 375px screen and the whole page scrolled sideways.
   Only the six-photo product showed it; the four-photo ones did not. */
.product { display: grid; grid-template-columns: minmax(0, 1fr);
  gap: clamp(1.5rem, 4vw, 3.5rem); padding-block: clamp(1rem, 3vw, 2rem) clamp(2.5rem, 6vw, 4.5rem); }
@media (width >= 64rem) { .product { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: start; } }

.gallery { position: relative; overflow: hidden; }
.gallery__track { display: flex; transition: translate .4s cubic-bezier(.22,.61,.36,1); }
@media (prefers-reduced-motion: reduce) { .gallery__track { transition: none; } }
/* A MOLDURA DA FICHA É A DO CARTÃO, E O FICHEIRO CONTINUA INTEIRO.
   Era a forma de cada fotografia (--shot), e por isso a fotografia mudava de
   forma entre a montra e a ficha: 3:4 no cartão, 2:3 aqui, à frente de quem
   tinha acabado de clicar. Agora é a mesma moldura em todo o lado.
   O que está no DOM continua a ser o ficheiro INTEIRO -- é isso que a lupa
   abre, e é o que resolveu o Pai Natal decapitado. Aqui ele é `cover`ido pela
   moldura, e --focus, escrito pela construção, reproduz EXACTAMENTE o corte
   que scripts/cards.py faz ao master: mesma janela, mesmo foco escrito à mão.
   Cortar pelo meio é o que este projecto já pagou para deixar de fazer. */
.gallery__slide {
  flex: 0 0 100%;
  aspect-ratio: var(--frame-ratio, .75);
  background: var(--frame-bg);
  overflow: hidden;
}
/* THE CAP GOES ON THE WIDTH, NOT THE HEIGHT.
   Capping the height while the slide is still 100% wide does not shrink the
   frame, it RESHAPES it: measured at 916px tall, a 2:3 slide 721px wide became
   721x714 -- square again, with the photograph letterboxed inside it. Capping
   the gallery's width instead keeps the ratio and simply makes the whole frame
   smaller on a tall screen. The slides stay 100% of it, so the carousel is
   untouched. */
.gallery { max-inline-size: calc(78svh * var(--frame-ratio, .75)); margin-inline: auto; }
.gallery__slide > picture { display: block; width: 100%; height: 100%; }
.gallery__slide img {
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: var(--focus, 50% 50%);
}
.gallery__track { align-items: flex-start; }
/* UM VÍDEO NA GALERIA ocupa a mesma moldura que as fotografias, cortado da
   mesma maneira; os controlos são os do browser. Num vídeo não há nada para a
   lupa ampliar, e ela esconde-se (shop.js, gallery). */
.gallery__video { display: block; width: 100%; height: 100%; object-fit: cover; background: var(--frame-bg); }
.gallery[data-video] .gallery__open { display: none; }
.gallery__thumb--video { position: relative; display: block; }
.gallery__thumb--video img { display: block; width: 100%; height: auto; aspect-ratio: 3 / 4; object-fit: cover; }
.gallery__thumb--video svg { position: absolute; inset: 0; margin: auto; color: #fff; filter: drop-shadow(0 1px 3px rgb(0 0 0 / .45)); }
/* --- the photograph, full size ----------------------------------------------
   Declared `position: fixed`, like the menu panel, and for the same measured
   reason: a dialog is fixed only while it is MODAL, and opened with show() --
   which is how the battery opens one -- it is `position: absolute` and lands
   at its static position far down the document. */
.lightbox {
  position: fixed; inset: 0;
  width: 100%; max-width: none; height: 100dvh; max-height: 100dvh;
  margin: 0; border: 0; padding: 0;
  background: var(--bg); color: var(--ink);
  display: flex; flex-direction: column;
}
.lightbox:not([open]) { display: none; }
.lightbox::backdrop { background: rgb(33 33 33 / .55); }
[data-brand='cathelier'] .lightbox::backdrop { background: rgb(74 53 39 / .5); }

.lightbox__bar {
  flex: none; display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; padding: .5rem var(--gutter); min-height: 56px;
  border-block-end: 1px solid var(--line-soft, var(--line));
}
/* PASSAR ENTRE AS FOTOGRAFIAS NA LUPA (a dona, 20 out 2026). O contador à
   esquerda da barra, e duas setas redondas por cima da fotografia, a meio da
   altura -- as mesmas da galeria, maiores. */
.lightbox__count { font-size: 14px; color: var(--muted); font-variant-numeric: tabular-nums; }
.lightbox__nav {
  position: absolute; inset-block-start: 50%; translate: 0 -50%;
  width: 48px; height: 48px; display: grid; place-items: center;
  border: 1px solid var(--line); border-radius: 999px;
  background: var(--bg); color: var(--ink); cursor: pointer;
  box-shadow: 0 4px 16px rgb(0 0 0 / .12);
}
.lightbox__nav[hidden] { display: none; }
.lightbox__nav--prev { inset-inline-start: max(12px, var(--gutter)); }
.lightbox__nav--next { inset-inline-end: max(12px, var(--gutter)); }
.lightbox__nav:hover { color: var(--accent); border-color: var(--accent); }
.lightbox__nav:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.lightbox__zoom {
  min-height: 40px; padding: .35rem 1rem;
  border: 1px solid var(--line); border-radius: var(--pill, 999px);
  background: none; color: var(--ink); font: inherit; font-size: 14px; cursor: pointer;
}
.lightbox__zoom[aria-pressed='true'] { border-color: var(--accent); color: var(--accent); }

/* THE STAGE IS A SCROLLER, AND THAT IS THE WHOLE ZOOM.
   At actual size the photograph is given its own pixel width and the stage
   scrolls, so dragging, the trackpad, the scrollbars, a two-finger swipe and
   the arrow keys all pan it with no code. Nothing here sets touch-action, so
   pinch-zoom on a phone is untouched as well. */
.lightbox__stage {
  flex: 1 1 auto; min-height: 0;
  display: flex;
  overflow: auto; overscroll-behavior: contain;
  padding: var(--gutter);
  background: var(--frame-bg);
}
/* `margin: auto` and NOT `place-items: center`.
   Centring by alignment clips the top and the left of anything bigger than the
   box -- the scroll simply cannot reach it. An auto margin centres a small
   photograph and gets out of the way of a big one.
   And the height limit lives on the PICTURE, not the img: in a grid whose row
   is auto-sized, `max-height: 100%` on the child is circular and resolves to
   the content's own height, which is why the fit state was still scrolling --
   measured at 1060px of photograph in a 780px stage. */
/* `display: contents` on the <picture>, so the <img> IS the flex item.
   With the picture as a box in between, the img's `max-height: 100%` resolves
   against a parent whose own height is auto -- a circular percentage, which is
   simply dropped. Measured: 1060px of photograph inside an 854px stage, still
   scrolling in the state called "fit". (This is safe here in a way it is not
   for a card: there is nothing focusable inside a <picture>, which is what
   made display:contents dangerous the last time this project used it.) */
.lightbox__stage > picture { display: contents; }
/* `align-self: center` because a flex item stretches by default: without it
   the <img> BOX was 721x791 for a 2:3 photograph -- the picture inside was
   still contained and undistorted, but the box around it was not the
   photograph, which makes every click and every measurement land somewhere
   the reader is not looking. */
.lightbox__img {
  display: block; margin: auto; align-self: center;
  width: auto; height: auto; max-width: 100%; max-height: 100%;
}

/* The button that opens it, over the photograph. Its ground is the photograph,
   which has no measurable colour, so it is an opaque chip rather than a
   translucent one -- the same reasoning the cover panel carries. */
.gallery__open {
  position: absolute; inset-block-end: 10px; inset-inline-end: 10px;
  width: 44px; height: 44px; display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--line); border-radius: var(--pill, 999px);
  background: var(--bg); color: var(--ink); cursor: pointer;
}
.gallery__open:hover { color: var(--accent); border-color: var(--accent); }

.gallery__arrow {
  position: absolute; inset-block-start: 50%; translate: 0 -50%;
  width: 40px; height: 40px; display: grid; place-items: center;
  background: rgb(255 255 255 / .85); color: var(--ink); border-radius: 999px;
}
.gallery__arrow--prev { inset-inline-start: 10px; }
.gallery__arrow--next { inset-inline-end: 10px; }
.gallery__arrow:hover { background: #fff; }

/* UMA LINHA SÓ, E ENCOLHEM PARA CABER.
 *
 * Isto já foi duas coisas erradas. Primeiro um rolador lateral com a barra de
 * rolamento apagada nos dois motores: no telemóvel as últimas miniaturas
 * ficavam fora do ecrã sem nada a dizê-lo -- seis miniaturas são 544px numa
 * coluna de 288 -- e as duas guardas de transbordo da bateria são cegas a
 * isso por construção (a da página mede o documento, onde um rolador nunca
 * chega; a do elemento isenta de propósito tudo o que esteja dentro de um).
 * Depois passou a envolver, e num telemóvel as quatro fotografias da raposa
 * ficavam três em cima e uma sozinha por baixo, que foi o que a dona
 * fotografou.
 *
 * A terceira versão não precisa de escolher: `flex: 0 1 84px` mantém os 84px
 * enquanto houver espaço -- no computador ficam exactamente como estavam -- e
 * encolhe-os a todos por igual quando não houver. Medido no pior caso do
 * catálogo, o cogumelo com seis fotografias: 41px cada num ecrã de 320. O
 * `min-inline-size: 0` é o que autoriza um item de flex a passar abaixo do seu
 * conteúdo; sem ele, encolher não encolhe nada. */
.gallery__thumbs { display: flex; flex-wrap: nowrap; gap: 8px; margin-block-start: 8px; }
.gallery__thumb { flex: 0 1 84px; min-inline-size: 0; border: 1px solid transparent; padding: 0; background: none; cursor: pointer; }
.gallery__thumb[aria-selected='true'] { border-color: var(--accent); }

.product__price { margin-block-start: .5rem; font-size: 1.35rem; font-weight: 600; color: var(--accent); font-variant-numeric: tabular-nums; }
/* Os preços por quantidade: uma tabela pequena por baixo do preço, número à
   esquerda e preço à direita, alinhados em colunas. */
.product__tiers { margin-block-start: .75rem; padding: .75rem 1rem; border: 1px solid var(--line); border-radius: var(--radius, 0); max-inline-size: 22rem; }
.product__tiers-title { font-size: 13px; font-weight: 600; letter-spacing: .02em; }
.product__tiers dl { display: grid; gap: .2rem; margin: .4rem 0 0; font-size: 14px; font-variant-numeric: tabular-nums; }
.product__tiers dl > div { display: flex; justify-content: space-between; gap: 1rem; }
.product__tiers dd { margin: 0; font-weight: 600; }
.product__tiers-note { margin-block-start: .5rem; font-size: 12px; color: var(--muted); }
/* O total ao pé do botão (shop.js, productForm): o preço do que se vai juntar ao cesto. */
.product__total { margin-block-start: .6rem; font-size: 14px; font-weight: 600; font-variant-numeric: tabular-nums; }
.product__lead { margin-block-start: .5rem; font-size: 13px; color: var(--muted); }
.product__form { margin-block-start: 1.75rem; display: grid; gap: 1.25rem; }
.product__buy { display: flex; gap: .75rem; align-items: stretch; min-width: 0; }
/* The button has to be allowed to shrink. `width: 100%` in a flex row next to
   a fixed-width stepper resolves against the container, not the space left
   over, and 2rem of padding each side made its minimum wider than a 320px
   phone. */
.product__buy .btn { flex: 1 1 auto; min-width: 0; padding-inline: 1rem; }
.product__text { margin-block-start: 2rem; font-size: 14px; line-height: 1.85; color: var(--ink-soft, var(--ink)); }
.product__safety { margin-block-start: 1.5rem; border-block-start: 1px solid var(--line); padding-block-start: 1rem; }
.product__safety summary { cursor: pointer; font-size: 13px; font-weight: 600; letter-spacing: .06em; }
.product__safety ul { margin-block-start: .75rem; padding-inline-start: 1.1rem; font-size: 13px; color: var(--muted); line-height: 1.8; }
/* A ligação para a página de cuidados da cathelier, no fim dos avisos. A altura
   mínima é a do alvo de toque: numa linha de 13 px a ligação ficava com 23. */
.product__safety-mais { margin-block-start: .5rem; font-size: 13px; }
.product__safety-mais a { display: inline-flex; align-items: center; min-height: 24px; color: var(--accent); text-underline-offset: 3px; }

/* --- fields ---------------------------------------------------------------- */
/* `min-width: 0` on a fieldset is not cosmetic. Browsers give <fieldset> a
   default `min-width: min-content`, which is the one element in HTML that
   refuses to shrink below its content — so at 320px the form's rows measured
   321 inside a 288 column and the whole page scrolled sideways. The grid
   children need the same, because a grid item's default `min-width: auto`
   has the same effect. */
.field { border: 0; padding: 0; margin: 0; min-width: 0; }
.product__form > * { min-width: 0; }
.field legend, .field > label { font-size: 12px; font-weight: 600; letter-spacing: .1em; text-transform: uppercase; display: block; margin-block-end: .5rem; }
.field__optional { font-weight: 400; text-transform: none; letter-spacing: 0; color: var(--muted); }
.field__help { font-size: 12px; color: var(--muted); margin-block-end: .6rem; line-height: 1.6; }
/* A LISTA DE TIPOS TEM DE ESTAR COMPLETA, e não estava.
   Faltava `tel`, e por isso o campo do telefone no carrinho -- e o do MB WAY
   na escolha do pagamento -- saíam com o desenho do browser: mais estreitos,
   mais baixos, com outra borda, ao lado de seis campos iguais entre si. Não
   havia erro nenhum a apanhá-lo; havia um campo com outro aspecto e ninguém a
   ligar os dois factos.
   Os tipos que faltavam entram todos de uma vez, e não só o que doeu: um
   `number` ou um `search` escritos daqui a um mês tinham exactamente o mesmo
   destino. `scripts/guards.mjs` compara esta lista com os tipos que as páginas
   construídas usam mesmo, e morre se aparecer um que esta regra não vista. */
.field input[type='text'], .field input[type='email'], .field input[type='tel'],
.field input[type='number'], .field input[type='search'], .field input[type='url'],
.field input[type='password'], .field input[type='date'], .field input:not([type]),
.field select, .field textarea {
  width: 100%; min-height: 48px; padding: .6rem .85rem;
  border: 1px solid var(--line); background: var(--bg); border-radius: var(--radius, 0);
}
.field .select { display: block; }
.field select { padding-inline-end: var(--arrow-room, .85rem); }
.field input:focus, .field select:focus, .field textarea:focus { border-color: var(--accent); outline: none; }

/* --- o formulário de entrega, no carrinho ----------------------------------
   Duas colunas a partir de 48rem e uma no telemóvel. A morada e o nome ocupam
   a linha toda porque são os campos longos; o código postal e a terra andam em
   par, que é como se escrevem. */
.checkout { margin-block-start: clamp(2rem, 5vw, 3rem); }
.checkout__grid { display: grid; gap: 1rem; margin-block-start: 1.25rem; }
@media (width >= 48rem) {
  .checkout__grid { grid-template-columns: 1fr 1fr; }
  /* OS CAMPOS LARGOS DIZEM-SE PELO NOME, E NÃO PELA POSIÇÃO.
     Aqui estava `:nth-child(-n+4)`, a contar os quatro primeiros: nome, email,
     morada e a segunda linha da morada. No dia em que a segunda linha saiu do
     formulário, o quarto lugar passou a ser o código postal -- que ficou com a
     linha toda, e a terra emparelhou com o telefone, e o contribuinte ficou
     sozinho. Nada disto deu erro: deu um formulário com os pares trocados.
     Uma regra que conta posições parte-se sempre que alguém acrescenta ou tira
     um campo, que é a coisa mais normal que há num formulário. */
  .checkout__grid .field--wide { grid-column: 1 / -1; }
}
.checkout .field > span { display: block; margin-block-end: .4rem; }

.choices { display: grid; gap: .5rem; }
.choice { display: flex; align-items: flex-start; gap: .65rem; padding: .7rem .85rem; border: 1px solid var(--line); cursor: pointer; font-size: 13px; }
.choice:has(input:checked) { border-color: var(--accent); }
.choice:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }
.choice input { margin-block-start: .25rem; accent-color: var(--accent); }
.choice em { font-style: normal; color: var(--accent); }
/* O quadradinho de cor de um modelo (o Carro, o Foguetão): o mesmo do cartão
   do catálogo, mais pequeno, ao lado do nome. O nome continua lá -- uma cor
   sozinha não diz qual é a quem não a distingue. */
.choice__cor {
  display: inline-block; flex: none; width: 16px; height: 16px; margin-block-start: .15rem;
  border: 1px solid var(--line); border-radius: var(--radius, 0);
}

.qty { display: flex; align-items: center; border: 1px solid var(--line); flex: none; }
.qty__btn { width: 44px; height: 48px; display: grid; place-items: center; color: var(--muted); }
.qty__btn:hover { color: var(--accent); }
.qty__input { width: 48px; height: 48px; text-align: center; border: 0; background: none; font-variant-numeric: tabular-nums; -moz-appearance: textfield; }
.qty__input::-webkit-outer-spin-button, .qty__input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

/* --- the reassurance box -------------------------------------------------- */
.reassure { margin-block-start: 1.75rem; padding: 1.25rem; background: var(--bg-soft, var(--bg)); display: grid; gap: .8rem; }
.reassure p { display: flex; align-items: center; gap: .75rem; font-size: 13px; color: var(--ink-soft, var(--ink)); margin: 0; }
.reassure svg { flex: none; color: var(--accent); }
/* A ligação para o aviso da garantia legal, e o fabricante (Reg. (UE)
   2023/988, art. 19.º al. a)): à vista, sem caixa que se abra. */
.product__rights { margin-block-start: .75rem; font-size: 13px; }
.product__rights a { color: var(--ink); text-underline-offset: 3px; display: inline-flex; align-items: center; min-height: 28px; }
.product__maker { margin-block-start: 1.25rem; font-size: 12px; line-height: 1.7; color: var(--muted); overflow-wrap: anywhere; }
.product__maker strong { color: var(--ink); font-weight: 600; }
.product__maker a { color: inherit; text-underline-offset: 3px; }
/* Junto do campo da gravação: não é ajuda, é uma condição do contrato
   (DL 24/2014, art. 4.º n.º 1 al. p)), por isso na cor do texto. */
.field__aviso { margin-block-start: .5rem; font-size: 12px; line-height: 1.6; color: var(--ink); }

/* --- drawer ----------------------------------------------------------------

   THE PAGE BEHIND A MODAL DIALOG SCROLLS.
   It is a common belief that showModal() locks it; it does not. Two
   independent measurements in Chrome, with the panel open and a real wheel
   over the backdrop: scrollY 0 -> 478 in one, 0 -> 1000 in the other. The spec
   does not require the lock (whatwg/html#7732), so it cannot be relied on in
   any engine. With a full-screen drawer nobody could see the page move; with a
   panel the shop slides about beside the menu.

   The lock is a CLASS, deliberately, and not `html:has(.drawer[open])`. The
   battery opens the drawer with show() to measure it, which sets the same
   `open` attribute — a :has() lock would engage for the whole battery run,
   take the scrollbar away, change clientWidth and quietly alter every
   geometry measurement it then made. A class set in the click path never fires
   for show().
   `hidden`, not `clip`: clip destroys the scrollport and the page jumps to the
   top, so closing the menu loses the reader's place. */
html.menu-open { overflow: hidden; }


.drawer {
  width: 100%; max-width: none; margin: 0; border: 0; padding: 0;
  background: var(--bg); color: var(--ink);
  overscroll-behavior: contain; display: flex; flex-direction: column;

  /* `position: fixed` is DECLARED and not left to the UA. A dialog is fixed
     only while it is MODAL; opened with show() it is `position: absolute` and
     sits at its static position near the bottom of the document — measured at
     y=2940 on a 3457px page. The battery opens it with show() on purpose, so
     without this line every geometry check would be measuring a box parked
     down by the footer. */
  position: fixed;
  inset-block: 0;
  inset-inline: 0 auto;

  /* KEEP THIS MAX-HEIGHT. The UA sheet carries
     `dialog:modal { max-height: calc(100% - 6px - 2em) }`, which wins over
     `inset-block: 0` with an automatic height and takes about 34px off the
     BOTTOM of the panel — which is exactly where the telephone number and the
     call-cost line are pinned. Simplifying this line away shortens the panel
     from the end nobody looks at. */
  height: 100dvh; max-height: 100dvh;
}

/* The scrim. LITERAL colours, never var().
   ::backdrop only inherits custom properties from its originating element
   since the 2023 spec change. Where it does not, `background: var(--scrim)` is
   invalid at computed-value time, which computes to `unset` — and since
   background-color does not inherit, that is `initial`, i.e. TRANSPARENT. Not
   the UA's 10% black: nothing at all. Behind a 400px panel that is the whole
   design gone, silently.
   It used to be `var(--bg)`, painted OPAQUE, because the drawer covered the
   screen and the backdrop was never seen. A panel that leaves the shop
   visible beside it is the entire point of what was asked for. */
.drawer::backdrop { background: rgb(33 33 33 / .42); }
[data-brand='cathelier'] .drawer::backdrop { background: rgb(74 53 39 / .38); }

.drawer:not([open]) { display: none; }

/* --- the panel, from 48rem -------------------------------------------------
   48rem and not 64rem. 64rem was never a width in its own right: it was the
   width at which the horizontal link bar appeared, and the bar is gone, so the
   number has nothing left to mean. 48rem is where the header already changes
   (.head__sibling), and 768px is a width the battery drives.

   The width is the owner's sentence turned into a number. "Só um bocado do
   ecrã" — only a bit of the screen — is `48vw`: never as much as half, at any
   width. The rem term is what actually applies on a laptop (352px / 400px at
   1440); the vw term only bites on a tablet, where 25rem of 768px would have
   been 52% and wrong. */
@media (width >= 48rem) {
  .drawer {
    width: min(22rem, 48vw);
    /* The gutter is set on :root as clamp(1rem, 4vw, 2.5rem), and 4vw is 4% of
       the VIEWPORT, not of the panel — so the wider the screen the fatter the
       panel's padding, which is precisely backwards. Measured: 40px a side
       inside a 400px panel, leaving 340px for rows designed against a
       full-width drawer. Redefining the token here moves every child at once:
       .drawer__top, __nav, __sibling, __contact all read it. */
    --gutter: 1.25rem;
    border-inline-end: 1px solid var(--line);
  }
  /* The header is visible BESIDE the panel now, and it already carries the
     wordmark and the basket. Showing them again 40px away is the kind of
     duplicate the owner photographs. The ✕ stays, and the grid keeps its
     1fr auto 1fr so it stays where it is. */
  .drawer__mark, .drawer__cart { display: none; }
  /* Same reasoning the other way round: the panel's door to the other brand
     carries a description, the header's is a bare label. While the panel is
     open the richer one speaks. */
  .menu-open .head__sibling { visibility: hidden; }

  /* Square, like the other one. This shop is round-cornered everywhere else
     (--radius: 14px), but the owner looked at the panel and wanted its corners
     straight, so the radius is not inherited here. The shadow stays: it is
     what still tells the two panels apart. */
  [data-brand='cathelier'] .drawer {
    width: min(25rem, 48vw);
    border-inline-end: 0;
    border-radius: 0;
    box-shadow: 0 0 44px rgb(107 78 60 / .18);
  }
}

/* --- the panel comes out of the button ------------------------------------
   A KEYFRAME on [open], not a transition from a resting off-screen position.

   The obvious way to animate a <dialog> is `translate: -100%` at rest,
   `translate: 0` when open, with `display` and `overlay` made transitionable
   by `allow-discrete` and the entry value supplied by @starting-style. It is
   also a trap, and I walked into it: the resting state of the menu becomes
   "entirely off the left edge of the screen", and it is only ever ON screen
   because an animation ran. Anything that stops that animation leaves a shop
   whose menu button appears to do nothing. I measured a panel sitting at
   x=-352 with the attribute set and the rule present -- in that instance
   because the browser pane was hidden and rAF never fired, but a stalled
   frame budget or an engine without @starting-style would read the same way
   to a customer.

   With a keyframe the resting state is the CORRECT state -- in place, flush
   to the edge -- and the animation only decorates the arrival. If it never
   runs, the menu simply appears. The exit is not animated, which is the
   honest price: closing instantly is unremarkable, a menu that never appears
   is not. */
@media (prefers-reduced-motion: no-preference) {
  .drawer[open] { animation: panel-in .26s cubic-bezier(.22, .61, .36, 1) both; }
  .drawer[open]::backdrop { animation: veil-in .26s ease both; }
}
@keyframes panel-in { from { translate: -100% 0; } }
@keyframes veil-in { from { opacity: 0; } }

.drawer__top { min-height: 72px; padding-inline: var(--gutter); border-block-end: 1px solid var(--line-soft, var(--line)); flex: none; }
.drawer__mark { height: 34px; width: auto; }
/* The drawer has three parts, and only the middle one moves: the wordmark
 * stays reachable at the top and the telephone at the bottom however long the
 * list gets.
 *
 * The middle used to hold two accordions — ten occasions for cathelier, four
 * families for ithos — because ten occasions laid out flat were 1100px of
 * content in an 844px screen. The owner asked for both to go, and the pages
 * they led to lost nothing: each occasion is linked from ten to fourteen other
 * pages, and the families were anchors into a catalogue that has the same four
 * as filter chips at the top.
 *
 * The guard that came with them stays, and this is the part that matters: it
 * asks whether the middle needs scrolling at all, which a scrolling list would
 * have made true by construction — which is how the last blind guard got
 * here. */
/* overflow-x is PINNED, not left to compute. When one axis is not `visible`
   the other computes to `auto`, so .drawer__body was a horizontal scroll
   container by accident — and the battery's two overflow guards are both blind
   to it: the page-level one measures documentElement.scrollWidth, which a
   scroll container never reaches, and the element-level one deliberately
   exempts every descendant of a scroller. A long name would have scrolled
   sideways inside the panel with two ✓ printed over it. */
/* Uma COLUNA e não um bloco, e a razão é a linha de baixo: a porta para a
   outra loja é empurrada para o fundo com `margin-block-start: auto`, e uma
   margem automática só empurra dentro de um contentor flex ou grid. Como
   bloco, a margem valia zero e a porta ficava colada à navegação com o vazio
   todo por baixo dela. */
.drawer__body {
  flex: 1 1 auto; min-height: 0;
  display: flex; flex-direction: column;
  overflow-y: auto; overflow-x: hidden; overscroll-behavior: contain;
}
.drawer__nav { display: flex; flex-direction: column; padding-inline: var(--gutter); }

.drawer__nav a { display: flex; align-items: center; gap: 1rem; min-height: 58px; text-decoration: none; border-block-end: 1px solid var(--line-soft, var(--line)); font-size: 1.1rem; }

/* On a short phone the drawer was 578px of content in a 568px screen: the
   call-cost line fell off the bottom. The guard that exists to catch exactly
   this never ran, because the battery opened every page in an iframe 900px
   tall — `578 <= 901` is true for every phone ever made.
   
   The rows tighten rather than the list scrolling. A scrolling list would make
   the guard unfalsifiable, which is how this got here. 52px is still more than
   twice the 24px minimum. */
@media (height < 40rem) {
  .drawer__nav a { min-height: 52px; }
  .drawer__sibling { min-height: 52px; padding-block: .6rem; }
  .drawer__contact { padding-block: .5rem 1rem; }
  .drawer__contact a { min-height: 44px; }
  .drawer__top { min-height: 62px; }
}
.drawer__nav a > span:first-child { flex: 1; }
.drawer__count { font-size: .85rem; color: var(--muted); font-variant-numeric: tabular-nums; }

.drawer__sibling {
  display: grid; grid-template-columns: 1fr auto; align-items: center;
  gap: 0 1rem; padding: .85rem var(--gutter); min-height: 58px; text-decoration: none;
  background: var(--sibling-bg, var(--bg-soft));
  /* Ao fundo do menu, encostada ao bloco de contactos. O espaço livre fica
     ACIMA dela e não por baixo, que é onde estava.
     A margem automática encolhe sozinha até zero quando o conteúdo já enche a
     coluna, por isso num telemóvel baixo -- onde este menu já teve 578px de
     conteúdo para 568px de ecrã -- ela não empurra nada para fora: volta a
     ficar logo a seguir à navegação, e a coluna rola como antes. */
  margin-block-start: auto;
}
/* A porta no menu lateral mostra o lockup da outra loja, não o nome escrito.
   O nome não se perdeu: vive no aria-label da ligação, que é o que um leitor
   de ecrã anuncia, e a linha por baixo continua a dizer o que lá se vende --
   que é informação, não a marca outra vez. O painel tem fundo próprio e claro,
   por isso é o lockup escuro que se usa aqui, sem o par claro do cabeçalho. */
.drawer__sibling-mark { display: block; }
.drawer__sibling-mark img { display: block; inline-size: auto; block-size: 34px; }
[data-brand='ithos'] .drawer__sibling-mark img { block-size: 26px; }
.drawer__sibling-note { grid-column: 1; font-size: .8125rem; color: var(--muted); }
/* The column must be named. A grid item with a definite row and an automatic
   column is placed BEFORE the fully automatic ones (Grid 8.5 step 3), so the
   arrow took column 1 and it read "↗ cathelier" instead of "cathelier ↗". */
.drawer__sibling > span:last-child { grid-column: 2; grid-row: 1 / span 2; color: var(--accent); }

.drawer__contact { flex: none; padding-block: 1rem 1.5rem; border-block-start: 1px solid var(--line-soft, var(--line)); }
.drawer__contact a { display: flex; align-items: center; gap: .85rem; min-height: 48px; padding-inline: var(--gutter); text-decoration: none; font-size: 1rem; }
.drawer__contact a:first-child { color: var(--accent); }
.drawer__contact svg { flex: none; }
.drawer__cost { margin-block: -.35rem .35rem; padding-inline: var(--gutter); font-size: 12px; color: var(--muted); }

/* --- footer ---------------------------------------------------------------- */
/* NO TOP MARGIN. It carried clamp(3rem, 8vw, 6rem), which on a page whose last
   section has a background of its own -- every product page ends with the
   cream "More from the workshop" band -- painted a white stripe of up to 63px
   between two coloured blocks. The owner saw it and read it as a mistake,
   which is what it looked like. The footer's own top padding is what separates
   it from the text above; the margin was separating it from nothing. */
/* The bottom padding clears the round back-to-top button (fixed, 46px at
   1.25rem from the corner): with 2rem the last row ran under it. */
.foot { background: var(--bg-soft, var(--bg)); border-block-start: 1px solid var(--line); padding-block: clamp(2.25rem, 5vw, 3.5rem) 4.75rem; }
/* A MARCA À ESQUERDA E AS PÁGINAS AO LADO (20 out 2026): o logótipo, as três
   maneiras de falar connosco e as redes numa coluna; as três listas de
   páginas noutras três. No telemóvel, a marca por cima e as listas em
   acordeão por baixo. */
.foot__top { display: grid; gap: 1.5rem; }
@media (width >= 60rem) { .foot__top { grid-template-columns: minmax(14rem, 1fr) 3fr; gap: 3rem; } }
.foot__mark { display: inline-flex; }
.foot__mark img { display: block; block-size: 40px; inline-size: auto; }
.foot__contact { list-style: none; margin: 1.25rem 0 0; padding: 0; display: grid; font-size: 13px; }
.foot__grid { display: grid; gap: 0; }
@media (width >= 60rem) { .foot__grid { grid-template-columns: repeat(3, 1fr); gap: 2rem; } }

.foot__group { border-block-end: 1px solid var(--line); }
.foot__group summary { display: flex; align-items: center; justify-content: space-between; min-height: 52px; cursor: pointer; font-size: 12px; font-weight: 600; letter-spacing: .14em; text-transform: uppercase; list-style: none; }
.foot__group summary::-webkit-details-marker { display: none; }
.foot__group summary::after { content: '+'; font-size: 1.1rem; color: var(--muted); }
.foot__group[open] summary::after { content: '\2212'; }
.foot__group ul { list-style: none; padding: 0 0 1rem; margin: 0; display: grid; font-size: 13px; }
/* A link on its own line in a list is a target, not a word inside a sentence:
   the standard's exemption does not cover it. These were 16px tall. */
:is(.foot__group, .foot__contact) a { text-decoration: none; color: var(--muted); display: inline-flex; align-items: center; min-height: 28px; }
:is(.foot__group, .foot__contact) a:hover { color: var(--accent); }
/* O DESENHO NÃO PODE SER ESMAGADO. Estas ligações já eram `inline-flex`, e num
   contentor flex quem paga é quem encolhe: um SVG sem `flex: none` fica a zero
   de largura -- visível, opaco, da cor certa e sem desenho nenhum, que é
   exactamente como o hambúrguer da barra esteve. */
:is(.foot__group, .foot__contact) a { gap: .5rem; }
:is(.foot__group, .foot__contact) a svg { flex: none; }
/* O custo da chamada é irmão da ligação e não filho dela, por isso não herda o
   avanço do desenho: sem isto ficava encostado à margem, desalinhado do número
   que explica. */
.foot__with-icon .foot__note { margin-inline-start: calc(16px + .5rem); }
@media (width >= 60rem) {
  .foot__group { border: 0; }
  .foot__group summary { pointer-events: none; min-height: auto; margin-block-end: 1rem; }
  .foot__group summary::after { display: none; }
}

.foot__note { display: block; font-size: 12px; color: var(--muted); margin-block-start: -.15rem; }
.foot__social { display: flex; gap: .6rem; margin-block-start: 1.25rem; }
.foot__social:empty { display: none; }
.foot__social a { width: 44px; height: 44px; display: grid; place-items: center; border: 1px solid var(--line); border-radius: 999px; color: var(--muted); }
.foot__social a:hover { color: var(--accent); border-color: var(--accent); }
/* A LINHA DE BAIXO: os meios de pagamento e a retratação à direita, o © à
   esquerda; no telemóvel, uns por cima dos outros, com o © no fim. */
.foot__base { margin-block-start: 2rem; padding-block-start: 1.25rem; border-block-start: 1px solid var(--line);
  display: flex; flex-direction: column; gap: 1rem; }
.foot__base-end { display: flex; flex-wrap: wrap; align-items: center; gap: .75rem 1.25rem; }
.foot__end { margin: 0; font-size: 12px; color: var(--muted); }
@media (width >= 60rem) {
  .foot__base { flex-direction: row-reverse; align-items: center; justify-content: space-between; gap: 1rem 2rem; }
}
/* Os meios de pagamento (shell.mjs, marcasDePagamento): todos à mesma altura,
   em cartões brancos sem contorno -- no creme da ithos lêem-se como fichas, no
   branco da cathelier as marcas ficam soltas. Só o Google Pay tem contorno, e
   é dele: «Don't change the color or weight of the mark's outline» (as regras
   da Google). O ficheiro traz margem à volta da pílula (x 170,7–922,7 e y
   170–570 num desenho de 1094×742), que se corta aqui para a marca ficar da
   mesma altura das outras -- a Google proíbe-a mais pequena. A marca não muda:
   só a margem transparente do ficheiro fica de fora. */
.foot__pay { margin: 0; }
.pay-marks { --h: 32px; list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.pay-mark { display: flex; block-size: var(--h); }
.pay-mark img { display: block; block-size: 100%; inline-size: auto; }
.pay-mark--cartao { box-sizing: border-box; padding: calc(var(--h) * .12) calc(var(--h) * .22); background: #fff; border-radius: calc(var(--h) * .18); }
.pay-mark--google { position: relative; overflow: hidden; inline-size: calc(var(--h) * 752 / 400); }
.pay-mark--google img { position: absolute; block-size: calc(var(--h) * 742 / 400); inline-size: auto; max-inline-size: none;
  inset-inline-start: calc(var(--h) * -170.7 / 400); inset-block-start: calc(var(--h) * -170 / 400); }
/* A FUNÇÃO DE RETRATAÇÃO (art. 11.º-A da Diretiva 2011/83, pela Diretiva (UE)
   2023/2673): em todas as páginas, fora do acordeão, bem visível. É a única
   ligação do rodapé que a lei manda destacar, por isso tem forma de botão --
   leve, com o traço das outras linhas e à altura das marcas de pagamento. No
   telemóvel vem primeiro. */
.foot__withdraw { margin: 0; }
.foot__withdraw a {
  display: inline-flex; align-items: center; min-height: 32px; padding: .25rem .95rem;
  border: 1px solid color-mix(in srgb, var(--ink) 30%, transparent); border-radius: var(--pill, var(--radius, 0));
  font-size: 13px; font-weight: 500; color: var(--ink); text-decoration: none; background: transparent;
}
.foot__withdraw a:hover { color: var(--accent); border-color: currentColor; }
@media (width < 60rem) {
  .foot__withdraw { order: -1; flex-basis: 100%; }
  /* O último grupo do acordeão já tem o seu traço por baixo: um segundo, logo
     a seguir, lia-se como uma linha a mais. */
  .foot__base { border-block-start: 0; margin-block-start: 1.5rem; padding-block-start: 0; }
  /* As cinco marcas numa fila num telemóvel de 375 px: um pouco mais baixas e
     mais juntas, até aos 360 px (o MB WAY fica com 44 px de largura, 50 no computador). Todas à mesma altura (a Google não deixa a dela menor). */
  .pay-marks { --h: 28px; gap: 5px; }
  .pay-mark--cartao { padding-inline: calc(var(--h) * .1); }
}

/* --- back to top ----------------------------------------------------------- */
.to-top {
  position: fixed; inset-block-end: 1.25rem; inset-inline-end: 1.25rem; z-index: 40;
  width: 46px; height: 46px; display: grid; place-items: center;
  background: var(--accent); color: var(--accent-ink); border-radius: 999px;
  box-shadow: 0 6px 20px rgb(0 0 0 / .18);
}
.to-top[hidden] { display: none; }

/* --- cookies --------------------------------------------------------------- */
.cookies { position: fixed; inset-block-end: 0; inset-inline: 0; z-index: 60; padding: 1.25rem var(--gutter); background: var(--bg); border-block-start: 1px solid var(--line); font-size: 13px; display: grid; gap: .85rem; }
@media (width >= 48rem) { .cookies { inset-inline-start: auto; max-width: 30rem; margin: 1.25rem; border: 1px solid var(--line); } }
.cookies__buttons { display: flex; gap: .6rem; flex-wrap: wrap; }
.cookies .btn { min-height: 42px; padding-inline: 1.25rem; }

/* --- odds and ends --------------------------------------------------------- */
.small { font-size: 12px; }
.muted { color: var(--muted); }

/* --- prose pages ----------------------------------------------------------- */
.page-prose { font-size: 15px; line-height: 1.85; color: var(--ink-soft, var(--ink)); }
.page-prose h1 { margin-block-end: 1.5rem; }
.page-prose h2 { margin-block: 2.5rem 1rem; }
.page-prose h3 { margin-block: 1.75rem .5rem; }
.page-prose p + p, .page-prose ul + p, .page-prose p + ul, .page-prose ol { margin-block-start: 1rem; }
.page-prose ul, .page-prose ol { padding-inline-start: 1.25rem; display: grid; gap: .5rem; }
.page-prose a { color: var(--accent); text-underline-offset: 3px; }
/* …but not a button. `.page-prose a` outranks `.btn`, so a button inside a
   prose page had its label repainted in the accent — on the accent. Terracotta
   on terracotta, 1:1, a solid block where "Back to the basket" should be. It
   is on the thank-you page and the cancelled-order page, which is to say on
   both ends of a purchase. */
.page-prose a.btn { color: var(--accent-ink); text-decoration: none; }
.page-prose a.btn--ghost { color: var(--ink); }
.page-prose hr { border: 0; border-block-start: 1px solid var(--line); margin-block: 2rem; }
.blank { display: inline-block; min-width: 6rem; max-width: 100%; flex: 1;
  border-block-end: 1px solid var(--line); vertical-align: baseline; }
/* --- o aviso harmonizado da garantia legal ---------------------------------
   O ficheiro oficial, sem mexer nele. Numa coluna larga ocupa a coluna; num
   telemóvel NÃO encolhe abaixo do tamanho a que se lê (a folha A4 a 1 pt = 1
   px), e a caixa desloca-se para o lado -- a página não. */
.aviso-garantia { margin-block: 1.5rem 2rem; }
.aviso-garantia__caixa {
  overflow-x: auto; overscroll-behavior-x: contain;
  border: 1px solid var(--line); background: #FFFFFF; border-radius: var(--radius, 0);
}
.aviso-garantia__caixa:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.aviso-garantia__caixa img { display: block; inline-size: 100%; min-inline-size: 595px; block-size: auto; }
.aviso-garantia figcaption { margin-block-start: .75rem; font-size: 14px; }
.aviso-garantia figcaption p + p { margin-block-start: .35rem; }
.aviso-garantia__deslizar { color: var(--muted); font-size: 13px; }
/* A ligação para o PDF está sozinha numa linha, e não no meio de uma frase:
   precisa de um alvo de toque de gente (a bateria mediu 18 px de altura). */
.aviso-garantia__pdf { display: inline-flex; align-items: center; min-height: 28px; }
/* A frase de deslizar só faz sentido onde a caixa desliza: a coluna de texto
   tem 46rem, e a partir de 40rem de ecrã o aviso já cabe inteiro. */
@media (width >= 40rem) { .aviso-garantia__deslizar { display: none; } }

/* --- a função de retratação -------------------------------------------------- */
.retratar-ja { margin-block: 0 1.5rem; }
.retratar { display: grid; gap: 1.1rem; margin-block: 1.5rem 0; padding: 1.25rem; border: 1px solid var(--line); background: var(--bg-soft, var(--bg)); border-radius: var(--radius, 0); }
.page-prose .retratar h2 { margin-block: 0; }
.retratar textarea { min-height: 6rem; resize: vertical; }
.retratar .btn { justify-self: start; }
.retratar__msg { margin-block: 1rem 0; padding: .8rem 1rem; border-inline-start: 3px solid var(--accent); background: var(--frame-bg); color: var(--ink); }
.retratar__ok { margin-block: 1.5rem 0; padding: 1.25rem; border: 1px solid var(--accent); border-radius: var(--radius, 0); }
.page-prose .retratar__ok h2 { margin-block: 0 .75rem; }
.retratar__ok h2:focus { outline: none; }

.page-prose table { width: 100%; border-collapse: collapse; font-size: 14px; }
.page-prose th, .page-prose td { text-align: start; padding: .7rem .75rem; border-block-end: 1px solid var(--line); }
.page-prose th { font-weight: 600; color: var(--ink); }
/* A link alone in a table cell is a target too. The email address on the
   identification page was 17px tall. */
.page-prose td a { display: inline-flex; align-items: center; min-height: 26px; }
/* Um título que é todo ele uma ligação -- o nome de cada peça, na página de
   cuidados da cathelier, que leva à ficha dela. Não está no meio de uma frase,
   e a linha de 16 px ficava abaixo do alvo de toque de 24. */
.page-prose h3 > a { display: inline-flex; align-items: center; min-height: 26px; }
.page-prose .table-wrap { margin-block-start: 1rem; }

/* --- contact ---------------------------------------------------------------- */
.contact-cards { display: grid; gap: .75rem; margin-block: 1.75rem; }
@media (width >= 44rem) { .contact-cards { grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); } }
.contact-card {
  display: flex; gap: .9rem; align-items: flex-start; padding: 1rem;
  border: 1px solid var(--line); border-radius: var(--radius, 0);
  text-decoration: none; font-size: 13px; line-height: 1.5; color: var(--ink-soft, var(--ink));
}
.contact-card:hover { border-color: var(--accent); }
.contact-card svg { flex: none; color: var(--accent); margin-block-start: .15rem; }
.contact-card strong { color: var(--ink); }

/* --- the map, and the one consent question on the site ---------------------- */
.map { margin-block-start: 1.25rem; border: 1px solid var(--line); border-radius: var(--radius, 0); overflow: hidden; }
.map__ask {
  display: flex; gap: .9rem; align-items: flex-start; text-align: start;
  width: 100%; padding: 1.5rem; background: var(--bg-soft, var(--bg));
  font-size: 13px; line-height: 1.6; color: var(--ink-soft, var(--ink));
}
.map__ask:hover { background: var(--frame-bg); }
.map__ask svg { flex: none; color: var(--accent); margin-block-start: .15rem; }
.map__ask strong { color: var(--ink); }
.map iframe { display: block; width: 100%; height: 360px; border: 0; }

/* --- questions -------------------------------------------------------------- */
.faq { margin-block-start: 1.25rem; }
.faq__item { border-block-end: 1px solid var(--line); }
.faq__item summary {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  min-height: 58px; cursor: pointer; font-size: 15px; color: var(--ink); list-style: none;
}
.faq__item summary::-webkit-details-marker { display: none; }
.faq__item summary::after { content: '+'; font-size: 1.25rem; color: var(--muted); }
.faq__item[open] summary::after { content: '\2212'; }
.faq__item > div { padding-block-end: 1.25rem; font-size: 14px; line-height: 1.8; }
.faq__item > div p + p { margin-block-start: .75rem; }

/* --- basket ------------------------------------------------------------------ */
.basket { display: grid; gap: 2rem; margin-block-start: 1.5rem; }
@media (width >= 60rem) { .basket { grid-template-columns: minmax(0, 1fr) 20rem; align-items: start; } }
.basket__lines { display: grid; gap: 1rem; }
.basket-line {
  display: grid; grid-template-columns: 84px minmax(0, 1fr) auto; gap: 1rem;
  padding-block-end: 1rem; border-block-end: 1px solid var(--line); align-items: start;
}
.basket-line__name { font-size: 14px; color: var(--ink); }
.basket-line__opts { font-size: 12px; color: var(--muted); margin-block-start: .25rem; line-height: 1.6; }
.basket-line__stock { font-size: 12px; color: var(--ink); margin-block-start: .15rem; line-height: 1.6; }
.basket__lead { margin-block: 0 1rem; font-size: 13px; line-height: 1.6; color: var(--ink); }
/* O aviso das peças personalizadas, antes do botão de encomendar: é uma
   condição do contrato e lê-se como tal, na cor do texto e não em cinzento. */
.basket__aviso { margin-block-start: 1.5rem; padding: .8rem 1rem; border-inline-start: 3px solid var(--accent); background: var(--bg-soft, var(--bg)); font-size: 13px; line-height: 1.6; color: var(--ink); }
.basket__direitos { margin-block-start: .5rem; }
.basket__direitos a { color: var(--ink); text-underline-offset: 3px; display: inline-flex; align-items: center; min-height: 28px; }
.basket-line__price { font-size: 14px; font-weight: 600; color: var(--accent); font-variant-numeric: tabular-nums; text-align: end; }
.basket-line__drop {
  font-size: 12px; color: var(--muted); margin-block-start: .35rem; text-decoration: underline;
  display: inline-flex; align-items: center; min-height: 26px;   /* it was 20px tall */
}
.basket__total { padding: 1.25rem; background: var(--bg-soft, var(--bg)); border: 1px solid var(--line); border-radius: var(--radius, 0); }
.basket__sums { display: grid; grid-template-columns: 1fr auto; gap: .6rem 1rem; margin-block: 1.25rem; font-size: 14px; }
.basket__sums dd { margin: 0; text-align: end; font-variant-numeric: tabular-nums; }
.basket__grand { font-weight: 600; color: var(--ink); font-size: 16px; padding-block-start: .6rem; border-block-start: 1px solid var(--line); }

/* --- the maker's block ------------------------------------------------------
   The model shop signs its home page: a short hello on the left and a
   photograph of the two people who make the things on the right, with their
   names under it. It is the most human thing on that page and it was the piece
   I had reduced to a centred paragraph.

   Ours carries a lamp for now. A photograph of the workshop, or of her at the
   bench, would be worth more than anything else that could go in this space. */
.maker { display: grid; gap: clamp(1.5rem, 4vw, 3rem); align-items: center; }
@media (width >= 56rem) { .maker { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); } }
.maker__text { font-size: 17px; line-height: 1.8; color: var(--muted); }
.maker__text h2 { margin-block-end: 1.25rem; }
.maker__text p + p { margin-block-start: 1rem; }
.maker__sign { margin-block-start: 1.5rem; color: var(--ink); }
/* Este bloco não tinha botão na ithos, onde é uma apresentação. Na cathelier
   é um convite e leva um: sem isto encostava ao parágrafo. */
.maker__text .btn { margin-block-start: 1.75rem; }
.maker__photo { --frame-ratio: 1.3333; }
.oficina__foto { max-inline-size: 22rem; margin: 1.5rem auto 2rem; border-radius: var(--radius, 0); }
/* Uma pessoa de pé numa moldura deitada: o corte ao meio levava-lhe a cara.
   A dona escolhe outro no painel («Ajustar»), e chega aqui como --foco. */
.maker__photo--retrato img { object-position: var(--foco, 50% 22%); }
.oficina__foto img { object-position: var(--foco, 50% 50%); }

.product__blurb { margin-block-start: 1rem; font-size: 15px; line-height: 1.8; color: var(--muted); }
.product__lead { display: flex; align-items: center; gap: .5rem; }
.product__lead svg { flex: none; color: var(--accent); }
/* Na cathelier a linha é uma frase com uma ligação («Precisa para uma data
   certa? Fale connosco.»): corrida, e não duas colunas de um flex. */
.product__lead--texto { display: block; }
.product__lead--texto a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
/* THE STOCK LINE WAITS FOR THE ANSWER, INVISIBLE AND IN PLACE.
   It is drawn with the slow case ("Out of stock — we make yours"), which is
   true of every lamp and is what a reader without the script keeps. With the
   script it stays invisible until the Worker answers -- `visibility`, not
   `display`, so nothing below it jumps when it appears -- and shop.js shows
   it after two and a half seconds whatever happens. */
html[data-js='yes'] .product__lead[data-lead]:not([data-lead-ready]) { visibility: hidden; }


/* --- escolher como se paga, e depois pagar ----------------------------------
   Até aqui isto vivia numa página da ifthenpay. Duas regras dão a esta parte
   toda a sua forma:

   1. A REFERÊNCIA É O PRODUTO DA PÁGINA. Quem chega à página do Multibanco vem
      copiar nove dígitos para o telemóvel ou para a aplicação do banco, muitas
      vezes com o teclado do multibanco à frente. Por isso a referência é
      grande, tabular e copiável com um toque -- e o valor está ao lado dela,
      porque pagar um cêntimo a menos não confirma encomenda nenhuma.

   2. O ALVO DE TOQUE NÃO ENCOLHE. Os botões de copiar têm 44px de altura
      mínima em qualquer largura: são a única coisa que se carrega nesta
      página, e são carregados com uma mão só. */
.pay-choice { display: grid; gap: .6rem; margin-block-start: 1rem; }
.pay-choice__opt {
  display: flex; gap: .85rem; align-items: center;
  padding: .9rem 1rem; border: 1px solid var(--line); border-radius: var(--radius, 0);
  background: var(--bg-soft, var(--bg)); cursor: pointer;
}
.pay-choice__opt:hover { border-color: var(--accent); }
/* `:has()` e não uma classe posta por JavaScript: o estado escolhido é o do
   rádio, e qualquer outra cópia dele fica desactualizada à primeira vez que
   alguém navegue com o teclado. */
.pay-choice__opt:has(input:checked) { border-color: var(--accent); background: var(--frame-bg, var(--bg-soft)); }
.pay-choice__opt:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }
.pay-choice__opt input { margin-block-start: .25rem; flex: none; accent-color: var(--accent); }
/* `flex: 1 1 auto` E NÃO O `flex: 0 1 auto` POR OMISSÃO.
   Sem ele o bloco de texto encolhe ao seu conteúdo e a caixa do logótipo fica
   onde a frase acabar -- e as três frases têm comprimentos diferentes, por
   isso as três marcas ficavam a três distâncias diferentes da margem. Foi
   exactamente isto que a caixa de largura fixa existia para evitar, e passou
   despercebido em duas capturas seguidas até as medir: 510, 545 e 375 px.
   `min-width: 0` continua a ser preciso: um item de flex não encolhe abaixo do
   seu conteúdo mínimo sem isto, e uma palavra comprida empurrava o logótipo
   para fora da caixa. */
.pay-choice__body { display: block; flex: 1 1 auto; min-width: 0; }
.pay-choice__name { display: block; font-weight: 600; color: var(--ink); }
.pay-choice__note { display: block; margin-block-start: .2rem; font-size: 13px; line-height: 1.6; color: var(--muted); }

/* O LOGÓTIPO VAI À ESQUERDA, ANTES DO NOME, NUMA CAIXA DE LARGURA FIXA.
   A largura fixa é o que torna a esquerda possível: as três marcas têm
   larguras diferentes -- 49, 32 e 72px -- e postas em fluxo empurravam o nome
   do método para três sítios diferentes, um por linha. Com a caixa, as marcas
   centram-se dentro dela e os três nomes começam no mesmo x.
   `flex: none` porque uma caixa de largura fixa num flex encolhe à primeira
   linha de texto comprida -- e o que encolhe primeiro é sempre o SVG, que fica
   visível, opaco, da cor certa e sem desenho nenhum. */
.pay-choice__marca {
  flex: none; inline-size: 5.25rem;
  display: grid; place-items: center;
}
.pay-choice__marca img { display: block; inline-size: auto; max-inline-size: 100%; }
/* O cartão leva duas marcas lado a lado, a Visa e a Mastercard (pages.mjs). */
.pay-choice__marca--duas { display: flex; align-items: center; justify-content: center; }
.pay-choice__marca--duas img { max-inline-size: none; }
/* NUM ECRÃ ESTREITO O LOGÓTIPO MUDA DE LINHA, EM VEZ DE ENCOLHER.
   A primeira tentativa foi encolher a caixa para 3,75rem: a frase ganhava 25px
   e o Multibanco caía para 30px de altura -- abaixo dos ~38 a que a palavra
   deixa de se ler, que é medição e não gosto. Encolher um logótipo até ele
   deixar de se reconhecer é mostrar um borrão e chamar-lhe logótipo.
   Abaixo dos 22rem a opção passa a duas linhas: o texto ocupa a largura toda e
   a marca vai por baixo, no tamanho a que se lê. */
/* NUM ECRÃ ESTREITO A MARCA MUDA DE LINHA, EM VEZ DE ENCOLHER OU TRANSBORDAR.
   Tentei primeiro encolher a caixa para 3,25rem e deixar as marcas
   transbordarem (`max-inline-size: none`). Medido a 320px: o Payshop ficou 10px
   POR CIMA do texto e o Google Pay 6px -- «Payshop reference to pay in cash»
   com o logótipo em cima da frase.
   E encolher também não serve: a 52px de largura o Google Pay ficaria com 21px
   de altura, mais pequeno do que o MB WAY a 24 -- e tanto a Google como a Apple
   proíbem por escrito mostrar a marca delas menor do que as outras.
   Por isso abaixo dos 24rem a opção passa a duas linhas: rádio e marca em cima,
   no tamanho a que se lêem, e a frase com a largura toda por baixo. */
@media (width < 24rem) {
  .pay-choice__opt {
    display: grid; grid-template-columns: auto 1fr;
    row-gap: .5rem; padding-inline: .75rem; align-items: center;
  }
  .pay-choice__marca { inline-size: auto; justify-items: start; }
  /* A ÁREA VAI INTEIRA, linha E colunas: um item de posição definida é colocado
     ANTES dos automáticos, e sem a linha explícita roubava a célula da marca. */
  .pay-choice__body { grid-area: 2 / 1 / auto / -1; }
}

.pay-amount { font-size: clamp(28px, 7vw, 40px); font-weight: 600; color: var(--accent); font-variant-numeric: tabular-nums; margin-block: 1.25rem; }
.pay-clock { font-size: 15px; color: var(--muted); }
.pay-clock strong { color: var(--ink); font-variant-numeric: tabular-nums; }

.pay-ref { margin-block: 1.5rem; display: grid; gap: .5rem; }
.pay-ref__row {
  display: flex; flex-wrap: wrap; gap: .35rem 1rem; align-items: center; justify-content: space-between;
  padding: .85rem 1rem; border: 1px solid var(--line); border-radius: var(--radius, 0);
  background: var(--bg-soft, var(--bg));
}
.pay-ref dt { font-size: 12px; font-weight: 600; letter-spacing: .1em; text-transform: uppercase; color: var(--muted); }
.pay-ref dd {
  margin: 0; display: flex; align-items: center; gap: .75rem; flex: 1 1 12ch; justify-content: flex-end;
  font-size: clamp(20px, 5vw, 26px); font-weight: 600; color: var(--ink);
  font-variant-numeric: tabular-nums; letter-spacing: .04em;
}
.pay-copy {
  flex: none; min-height: 44px; padding-inline: .9rem;
  font: inherit; font-size: 13px; letter-spacing: .06em; text-transform: uppercase;
  color: var(--accent); background: transparent; border: 1px solid var(--line);
  border-radius: var(--radius, 0); cursor: pointer;
}
.pay-copy:hover { border-color: var(--accent); }
.pay-copy[data-copied] { color: var(--accent-ink); background: var(--accent); border-color: var(--accent); }
/* O campo do telemóvel encosta à opção de cima, e é ela que o explica: um
   traço do lado esquerdo em vez de uma caixa própria, para não parecer uma
   quarta forma de pagar. */
.pay-choice__extra {
  margin-block-start: -.2rem; margin-inline-start: 1.6rem;
  padding-inline-start: 1rem; border-inline-start: 2px solid var(--line);
  max-width: 24rem;
}
.pay-choice__extra > span:first-child { font-size: 12px; font-weight: 600; letter-spacing: .1em; text-transform: uppercase; }
/* O `width: 100%` que aqui estava era um remendo sobre o sintoma: o campo é
   `type="tel"` e a regra do `.field` não vestia esse tipo. Corrigida a lista lá
   em cima, isto deixou de ser preciso -- e um remendo que sobrevive à
   correcção da causa é o que faz a causa parecer resolvida em dois sítios. */

/* UMA REFERÊNCIA NÃO SE PARTE EM DUAS LINHAS.
   No telemóvel a linha ficava «123 456 / 789» -- e este número é lido dígito a
   dígito em frente a um multibanco, muitas vezes por alguém que o está a
   copiar à mão. Partido ao meio deixa de se ler de uma vez.
   Abaixo dos 30rem a linha empilha: o rótulo em cima, o número e o botão
   debaixo, com a largura toda para eles. E `nowrap` no número, que passa a
   caber porque deixou de dividir o espaço com o rótulo. */
.pay-ref dd > span { white-space: nowrap; }
@media (width < 30rem) {
  .pay-ref__row { flex-direction: column; align-items: stretch; gap: .5rem; }
  .pay-ref dd { justify-content: space-between; flex: 1 1 auto; }
}

/* --- resellers -----------------------------------------------------------------
   Everything here appears only after a reseller has signed in; a visitor never
   sees any of it. The reseller's price sits under the retail price and never
   replaces it: the retail price is the recommended one, and the reseller needs
   both side by side to see their margin. */
.rv-bar {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
  gap: .4rem 1.1rem; padding: .55rem var(--gutter);
  background: var(--accent); color: var(--accent-ink);
  font-size: .875rem; text-align: center;
}
.rv-bar a { color: inherit; text-underline-offset: .2em; }
.rv-bar button {
  min-height: 32px; padding: .2rem .9rem; border: 1px solid currentColor; border-radius: 999px;
  background: transparent; color: inherit; font: inherit; cursor: pointer;
}
.rv-price { margin: .15rem 0 0; font-size: .875rem; color: var(--ink); }
.rv-price strong { font-weight: 700; }
.rv-price span { color: var(--muted); margin-inline-start: .25rem; }
.rv-price--big { font-size: 1rem; margin-block: .35rem .6rem; }
.rv-price--none { color: var(--muted); }
.rv-rrp { color: var(--muted); text-decoration: line-through; margin-inline-start: .35rem; }
.rv-check { display: flex; gap: .6rem; align-items: flex-start; margin-block: .75rem; font-size: .9rem; line-height: 1.45; }
.rv-check input { margin-block-start: .25rem; inline-size: 1.1rem; block-size: 1.1rem; flex: none; }

.resellers .rv-form { display: grid; gap: .9rem; margin-block: 1rem 0; max-inline-size: 26rem; }
.resellers .check { display: flex; gap: .6rem; align-items: center; margin-block: .75rem; }
.resellers .rv-msg { margin-block: .9rem 0; padding: .8rem 1rem; border-inline-start: 3px solid var(--accent); background: var(--frame-bg); }
.rv-table-wrap { overflow-x: auto; margin-block: 1rem 2rem; }
.rv-table { inline-size: 100%; border-collapse: collapse; font-size: .9rem; font-variant-numeric: tabular-nums; }
.rv-table th, .rv-table td { padding: .55rem .6rem; border-block-end: 1px solid var(--line); text-align: start; vertical-align: top; }
.rv-table td { white-space: nowrap; }
.rv-table thead th { font-size: .75rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
.rv-table .rv-marca, .rv-table .rv-menos { color: var(--muted); font-size: .8rem; }
.rv-table .rv-stock { font-size: .875rem; font-variant-numeric: tabular-nums; }
/* ON A PHONE EACH ROW BECOMES A SMALL CARD. Four columns of prices and stock
   were 626px wide in a 343px column: the reader scrolled sideways to find
   their own price, which is the one number the page exists for. The name goes
   on top, and the three figures sit under it with their labels -- the labels
   come from `data-rotulo` on each cell, so the header row can step aside. */
@media (width < 40rem) {
  .rv-table thead { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); }
  .rv-table tr { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); column-gap: .75rem;
    padding-block: .75rem; border-block-end: 1px solid var(--line); }
  .rv-table th[scope='row'] { grid-column: 1 / -1; padding: 0 0 .35rem; border: 0; }
  .rv-table td { padding: 0; border: 0; white-space: normal; }
  .rv-table td::before { content: attr(data-rotulo); display: block; font-size: .7rem; letter-spacing: .08em;
    text-transform: uppercase; color: var(--muted); margin-block-end: .15rem; }
}
.rv-aviso { margin: 0; padding: .7rem var(--gutter); background: var(--frame-bg); border-block-end: 1px solid var(--line); text-align: center; font-size: .9rem; color: var(--ink); }

/* --- a janela da campanha (src/lib/shell.mjs, avisoCampanha) ---------------
   A da roomgaga, que a dona mostrou: a fotografia em cima, a frase num painel
   da cor da marca por baixo, e o X por cima da fotografia. As cores saem das
   variáveis de cada marca (o terracota da ithos com branco; o castanho da
   cathelier com creme), e o padding é zero para um clique fora -- no fundo
   escuro -- ser um clique no próprio <dialog>, que o shop.js fecha. */
.aviso-campanha {
  /* margin: auto -- o reset do site põe margin: 0 em tudo, e era isso que
     centra um <dialog> modal: sem ele, a janela ficava no canto de cima. */
  margin: auto; padding: 0; border: 0; width: min(25rem, calc(100vw - 2rem)); max-height: calc(100dvh - 2rem);
  overflow: auto; border-radius: calc(var(--radius) + 6px); background: var(--accent); color: var(--accent-ink);
  box-shadow: 0 24px 60px rgb(0 0 0 / .35);
}
.aviso-campanha::backdrop { background: rgb(16 10 7 / .55); }
.aviso-campanha form { margin: 0; }
.aviso-campanha__fechar {
  position: absolute; top: .6rem; right: .6rem; z-index: 1; width: 44px; height: 44px; display: grid; place-items: center;
  border: 0; border-radius: 999px; background: rgb(255 255 255 / .92); color: #212121; cursor: pointer;
}
.aviso-campanha__foto { aspect-ratio: 16 / 10; overflow: hidden; background: var(--frame-bg); }
.aviso-campanha__foto img { width: 100%; height: 100%; object-fit: cover; display: block; }
.aviso-campanha__texto { display: grid; gap: .75rem; padding: 1.6rem 1.5rem 1.5rem; text-align: center; }
.aviso-campanha__texto h2 { margin: 0; font-family: var(--font-display); font-weight: 500; font-size: clamp(1.6rem, 6vw, 2rem); line-height: 1.15; color: inherit; text-wrap: balance; }
.aviso-campanha__texto p { margin: 0; font-size: 1.05rem; line-height: 1.45; text-wrap: balance; }
.aviso-campanha__ok {
  margin-top: .35rem; min-height: 44px; padding: .65rem 1.4rem; border: 0; border-radius: var(--pill, 999px);
  background: var(--accent-ink); color: var(--accent); font: 600 .95rem/1.2 var(--font-body); cursor: pointer;
}
.aviso-campanha__fechar:focus-visible, .aviso-campanha__ok:focus-visible { outline: 3px solid var(--accent-ink); outline-offset: 3px; }
@media (prefers-reduced-motion: no-preference) {
  .aviso-campanha[open] { animation: aviso-campanha-entra .28s ease-out; }
  .aviso-campanha[open]::backdrop { animation: aviso-campanha-fundo .28s ease-out; }
}
@keyframes aviso-campanha-entra { from { opacity: 0; transform: translateY(12px) scale(.98); } }
@keyframes aviso-campanha-fundo { from { opacity: 0; } }
