/** Shopify CDN: Minification failed

Line 3786:0 Unexpected "14c"
Line 3788:69 Unterminated string token
Line 3804:60 Unterminated string token

**/
/*
  aico-components-miniso.css - MINISO shared component layer (CSS layer 2)

  WHAT THIS IS
  Layer 2 of the three CSS layers defined in .claude/skills/_shared/AICO-PAGE-CONTRACT.md §10:
    layer 1  assets/aico-brand-miniso.css   tokens          (loaded globally)
    layer 2  THIS FILE                      components      (loaded globally)
    layer 3  assets/aico-neo-miniso.css     composition     (loaded with the Neo renderer only)

  Loaded GLOBALLY in layout/theme.liquid, immediately after the brand token layer, because the
  PDP, PLP, cart and account consume these components too - templates/product.json is a plain
  Horizon template with no Neo mount, so a page-builder-only stylesheet would never reach it.

  WAVE 1 SCOPE - three components:
    .miniso-btn     docs/reference/design/03-buttons-cta.md   (3 types x 3 states x 4 icon pos x 2 sizes = 72)
    .miniso-input   docs/reference/design/04-input-fields.md  (6 states x 4 icon layouts = 24)
    .miniso-badge   docs/reference/design/05-badge-and-doc-conventions.md (4 states)

  WAVE 2 SCOPE - eight components, harvested from the PDP frames 96:12873 (desktop),
  96:13819 (pdp-mobile) and 96:14218 (pdp-mobile-scroll). Spec:
  docs/reference/design/templates/pdp/COMPONENTS-WAVE-2.md, with per-region values in 01..07.
    .miniso-product-card  .miniso-section-head  .miniso-rating   .miniso-stepper
    .miniso-accordion     .miniso-review-card   .miniso-meter    .miniso-sticky-bar
  Wave 2 also adds three modifiers to .miniso-btn (--block, --label-lg, --muted), inside the
  button section where they belong. Tile and decorative band are still unharvested - do not add
  them here from memory.

  WAVE 3 SCOPE - homepage, section by section (theme sections first, aico page builder later if
  Razvan says so). Harvested from the homepage frames, node ids at each block.
    .miniso-carousel-dots   hero 128:15745 (re-skins Horizon's slideshow-controls dots)
    .miniso-band            IP Worlds 36:3720/36:3722 cloud band over the hero (mask asset)
    .miniso-tile            IP Worlds 36:2691.. photo tile with hover overlay (name + CTA)
    .miniso-section-head--lg  homepage titles 36:3002 etc. at Display/H1
    .miniso-btn--lg         homepage "See all" 36:3009, 212x64 (ASSUMED third size)
    .miniso-product-card--feature  featured card 128:15885 harvested (was = --lg)
    .miniso-carousel-dots--feature 128:15893 dark 20x12 dots, gap 7, inactive 5%
    .miniso-hotspot         featured banner 96:8172/96:8175 shoppable-image dots
    .miniso-band inputs     Categories 128:15897/128:15898: --miniso-band-color, --miniso-band-overlap
    .miniso-tile--category  Categories 128:15905.. pastel tile, name bottom-left, icon button, cut-out
    .miniso-section-head--center  Categories 128:15900 centred title
    .miniso-usp-card        USP row 154:2304.. (homepage 154:2303, PLP 149:20754) icon + title + text
    .miniso-row             PDP card rows 96:13025 / 96:13061 (desktop), 96:13923 / 96:13948 (mobile):
                            title + overflowing track of .miniso-product-card - shared by
                            "Für dich empfohlen" and "Zuletzt angesehen" (04.09)

  RULES THIS FILE OBEYS
  - Variants are modifiers on ONE class, never new classes.
  - Every colour, font family and type size comes from a --miniso-* token in layer 1.
  - Type tokens are already re-declared at 750px and 990px in layer 1, so var(--miniso-sub3) is
    responsive for free. There is NO type media query in this file and never will be.
    WAVE 2 CHANGE, deliberate and narrow: some wave-2 components are drawn with different
    GEOMETRY (radius, padding, gap, icon box, aspect ratio) at the two breakpoints - values layer
    1 carries no token for, so no token can make them responsive. Each such component carries ONE
    `@media (min-width: 750px)` block holding geometry only, at layer 1's own breakpoint (the
    ASSUMPTION block in aico-brand-miniso.css maps Figma's mobile sheet C below 750 and sheet B/A
    above). Not one type size is redeclared in any of them.
  - Where a value has no token, the raw value from the styleguide doc is used and marked
    "NO TOKEN" right at the line. Those are findings for Iulia (design task #42154), not licence.
  - Anything Figma does not define is marked ASSUMPTION with the reason. Nothing is invented silently.

  VALUES WITH NO TOKEN IN LAYER 1 (findings for Iulia, design task #42154)
  1. #FFFFFF - white. The 24-token palette (01-color-system.md) has no white; the Neutral group
     holds only Off-white #F5F5F5. Button labels on Primary, Secondary's fill and border on hover,
     the Badge Default fill and the Badge Hover/Active/Out-of-Stock text are all literal white.
     05-badge-and-doc-conventions.md §Gaps 5 records that Figma itself does not tokenise it.
     Layer 1 does carry --miniso-bg: #FFFFFF, but that sits inside a block marked ASSUMPTION and
     means "page background", not "brand white", so it is not used for component ink here.
  2. #C0C0C0 - Neutral/Silver, the Ghost Disabled label colour. Bound as a variable inside the
     button component set (03-buttons-cta.md) but absent from the 24-token colour sheet, so it has
     no layer-1 token.
  3. Spacing, sizes, heights, border widths, icon sizes. Layer 1 carries no spacing or size scale
     at all (it has colour, families, two radii and the type scale). Every px length below is the
     literal value from the styleguide doc, sourced in a comment.

  NO MOTION. 03/04/05 all record "transition / animation: not specified in Figma". No transition,
  duration or easing is declared here. That is faithful, not an oversight. The wave-2 product card
  swaps its image on hover with no transition for the same reason (Figma records no motion on the
  PDP frames either - 07-sticky-bar.md finding 8).

  SHADOW - ONE EXCEPTION, FLAGGED. Layer 1 says: "The file carries no shadows at all. Do not add
  any." That is true of every styleguide sheet and of both wave-1 and every wave-2 component
  EXCEPT .miniso-sticky-bar, where Figma node 96:14543 carries a measured
  drop-shadow(0 -4px 6px rgba(0,0,0,0.03)) (07-sticky-bar.md finding 2). Built as drawn, at the
  single line where it occurs, marked there. No shadow token was invented and layer 1 was not
  edited: either that comment is now out of date or the bar is the one exception, and only Iulia
  can say which. Every other component in this file still has no shadow.
*/

/* ==========================================================================================
   1. BUTTON / CTA - .miniso-btn
   Source: docs/reference/design/03-buttons-cta.md (component set 14:1267, 72 variants).
   Where the doc sheet 40:1243 and the component bindings disagree, the COMPONENT BINDINGS win
   (README.md cross-cutting finding 2). Conflicts are listed in the report, not resolved here.

   MODIFIER VOCABULARY (compose freely - this is the 72 variants, not 72 classes)
     type   .miniso-btn--primary | --secondary | --ghost        (required, no default type)
     size   (none) = Default 44px | .miniso-btn--sm = Small 36px
     icon   (none) = Icon position None
            .miniso-btn--icon-left   | --icon-right | --icon-only   (--icon-only = Figma "Center")
     state  live:   :hover, :focus-visible, :disabled / [disabled] / [aria-disabled="true"]
            forced: .miniso-btn--hover, .miniso-btn--disabled   (for docs, galleries, and a
                    section that must render a state statically)
   ELEMENTS
     .miniso-btn__icon    20x20 (16x16 on ghost+sm). Hidden unless an icon modifier is present.
     .miniso-btn__label   the text run.
   ========================================================================================== */

.miniso-btn {
  /* Internal plumbing only. These hold nothing but references to layer-1 tokens (plus the two
     untokenised raws listed in the header). They are not design tokens and are not for authors. */
  --_miniso-btn-bg: transparent;
  --_miniso-btn-fg: currentcolor;
  --_miniso-btn-bd: transparent;

  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  /* Content-frame gap, Primary + Secondary. 03 §Geometry. NO TOKEN. */

  /* Type: Heading/Subtitle 3, Fredoka Medium 16/20 desktop. 03 §Typography. */
  font-family: var(--miniso-font);
  font-weight: 500;
  font-size: var(--miniso-sub3);
  line-height: var(--miniso-sub3-lh);
  font-variation-settings: 'wdth' 100;
  letter-spacing: 0;
  text-align: center;
  text-decoration: none;
  white-space: nowrap;
  /* every variant is whitespace-nowrap. 03 §Typography. */

  /* Geometry shared by all 72 variants */
  border-radius: var(--miniso-radius-pill);
  border-style: solid;
  border-width: 0;
  border-color: var(--_miniso-btn-bd);
  background-color: var(--_miniso-btn-bg);
  color: var(--_miniso-btn-fg);

  margin: 0;
  appearance: none;
  -webkit-appearance: none;
  cursor: pointer;
}

.miniso-btn__icon {
  display: none;
  /* shown by an icon-position modifier */
  flex: 0 0 auto;
  width: 20px;
  /* 03 §Geometry, all types except ghost+sm. NO TOKEN. */
  height: 20px;
}

.miniso-btn__icon>svg,
.miniso-btn__icon>img {
  display: block;
  width: 100%;
  height: 100%;
}

.miniso-btn__label {
  display: block;
}

/* ---- Icon position ------------------------------------------------------------------------
   Figma: None | Left | Right | Center. Center is the icon-only form: the label is dropped and
   the frame becomes square. 03 §Variant axes. */

.miniso-btn--icon-left .miniso-btn__icon,
.miniso-btn--icon-right .miniso-btn__icon,
.miniso-btn--icon-only .miniso-btn__icon {
  display: block;
}

.miniso-btn--icon-left .miniso-btn__icon {
  order: 0;
}

.miniso-btn--icon-left .miniso-btn__label {
  order: 1;
}

.miniso-btn--icon-right .miniso-btn__icon {
  order: 1;
}

.miniso-btn--icon-right .miniso-btn__label {
  order: 0;
}

.miniso-btn--icon-only .miniso-btn__label {
  display: none;
}

/* ---- Primary + Secondary box model ---------------------------------------------------------
   03 §Geometry. Default: min-height 44, padding 10/24. Small: min-height 36, padding 8/16.
   min-height, not height: the type token shrinks at mobile, and Figma's declared 44/36 must hold
   at every breakpoint. Sums check out at desktop (Small: 8 + 20 + 8 = 36). */

.miniso-btn--primary,
.miniso-btn--secondary {
  min-height: 44px;
  /* 03: "Height 44px (fixed)". NO TOKEN. */
  padding: 10px 24px;
  /* Figma spacing tokens 2-5 / 6. NO TOKEN in layer 1. */
}

.miniso-btn--primary.miniso-btn--sm,
.miniso-btn--secondary.miniso-btn--sm {
  min-height: 36px;
  /* 03: "Height 36px". NO TOKEN. */
  padding: 8px 16px;
  /* Figma spacing tokens 2 / 4. NO TOKEN in layer 1. */
}

/* Icon-only frames are square with uniform padding, NOT the text form's horizontal padding.
   03 §Geometry note "Icon-only padding differs from the text form". */
.miniso-btn--primary.miniso-btn--icon-only,
.miniso-btn--secondary.miniso-btn--icon-only {
  width: 44px;
  height: 44px;
  min-height: 44px;
  padding: 10px;
}

.miniso-btn--primary.miniso-btn--sm.miniso-btn--icon-only,
.miniso-btn--secondary.miniso-btn--sm.miniso-btn--icon-only {
  width: 36px;
  height: 36px;
  min-height: 36px;
  padding: 8px;
}

.miniso-btn--secondary {
  border-width: 1px;
  /* 1px solid in all three states. 03 §Secondary. */
}

/* ---- Ghost box model -----------------------------------------------------------------------
   03 §Ghost: no fill, no border, no padding. Height is the text line box (20px desktop), which
   var(--miniso-sub3-lh) already gives. Inner gap is 6px, not 8px. */

.miniso-btn--ghost {
  gap: 6px;
  /* 03 §Ghost, Content-frame gap. NO TOKEN. */
  padding: 0;
  min-height: var(--miniso-sub3-lh);
}

.miniso-btn--ghost.miniso-btn--icon-only {
  width: 20px;
  height: 20px;
  min-height: 20px;
}

/* Ghost Small: the ONLY thing the Size prop changes on Ghost is the icon, 20 -> 16.
   Text-only Ghost is identical at both sizes (109 x 20). 03 §Ghost.
   FIGMA DEFECT, NOT REPLICATED: Ghost/Small/Left|Right draw 20px artwork inside a 16px frame at
   inset-[0_-25%_-25%_0], and Ghost/Small/Center is a 16px frame around a 20px content frame
   (03 §Ghost peculiarities, README open question 12). The correct 16x16 artwork is drawn here. */
.miniso-btn--ghost.miniso-btn--sm .miniso-btn__icon {
  width: 16px;
  height: 16px;
}

.miniso-btn--ghost.miniso-btn--sm.miniso-btn--icon-only {
  width: 16px;
  height: 16px;
  min-height: 16px;
}

/* ---- Colours: Type x State -----------------------------------------------------------------
   03 §Colours. Order matters: default, then hover, then disabled last so disabled always wins. */

/* Primary */
.miniso-btn--primary {
  --_miniso-btn-bg: var(--miniso-primary);
  --_miniso-btn-fg: #ffffff;
  /* NO TOKEN: white. Figma Semantic/Button/Primary/Text. Finding 1. */
}

.miniso-btn--primary:hover,
.miniso-btn--primary.miniso-btn--hover {
  /* Hover is a hue swap to black, not a darkened red. 03 §Primary. */
  --_miniso-btn-bg: var(--miniso-secondary);
  --_miniso-btn-fg: #ffffff;
  /* NO TOKEN: white. Finding 1. */
}

/* Secondary */
.miniso-btn--secondary {
  --_miniso-btn-bg: #ffffff;
  /* NO TOKEN: white. Figma Primitive/Brand/Core/White. Finding 1. */
  --_miniso-btn-fg: var(--miniso-primary);
  --_miniso-btn-bd: var(--miniso-primary);
}

.miniso-btn--secondary:hover,
.miniso-btn--secondary.miniso-btn--hover {
  --_miniso-btn-bg: var(--miniso-secondary);
  --_miniso-btn-fg: #ffffff;
  /* NO TOKEN: white. Finding 1. */
  --_miniso-btn-bd: #ffffff;
  /* NO TOKEN: white. Invisible on a white page - see report. */
}

/* Ghost */
.miniso-btn--ghost {
  /* Figma has no fill NODE at all on Ghost, so this is an absent fill, not a transparent colour
     value. `transparent` is the closest CSS expression. 03 §Colours. */
  --_miniso-btn-bg: transparent;
  --_miniso-btn-fg: var(--miniso-secondary);
  --_miniso-btn-bd: transparent;
}

.miniso-btn--ghost:hover,
.miniso-btn--ghost.miniso-btn--hover {
  --_miniso-btn-fg: var(--miniso-primary);
}

/* Disabled - last, so it beats hover. Expressed purely as a colour change: Figma sets no opacity
   on any variant (03 §Geometry notes). */
.miniso-btn--primary:disabled,
.miniso-btn--primary[disabled],
.miniso-btn--primary[aria-disabled='true'],
.miniso-btn--primary.miniso-btn--disabled {
  --_miniso-btn-bg: var(--miniso-primary-40);
  --_miniso-btn-fg: #ffffff;
  /* NO TOKEN: white. Finding 1. */
}

.miniso-btn--secondary:disabled,
.miniso-btn--secondary[disabled],
.miniso-btn--secondary[aria-disabled='true'],
.miniso-btn--secondary.miniso-btn--disabled {
  --_miniso-btn-bg: #ffffff;
  /* NO TOKEN: white. Finding 1. */
  --_miniso-btn-fg: var(--miniso-primary-40);
  --_miniso-btn-bd: var(--miniso-secondary-20);
}

.miniso-btn--ghost:disabled,
.miniso-btn--ghost[disabled],
.miniso-btn--ghost[aria-disabled='true'],
.miniso-btn--ghost.miniso-btn--disabled {
  --_miniso-btn-fg: #c0c0c0;
  /* NO TOKEN: Figma Neutral/Silver, absent from the 24-token palette. Finding 2. */
}

/* Disabled affordance. Figma cannot express cursor or pointer-events and annotates neither,
   so both lines below are ASSUMPTION (for Iulia). Colour alone is the only specified difference. */
.miniso-btn:disabled,
.miniso-btn[disabled],
.miniso-btn[aria-disabled='true'],
.miniso-btn--disabled {
  cursor: not-allowed;
}

.miniso-btn[aria-disabled='true'],
a.miniso-btn--disabled {
  pointer-events: none;
}

/* ---- WAVE 2 ADDITIONS to the button vocabulary ----------------------------------------------
   Three modifiers the 72-variant set does not contain but the PDP needs three times each. They
   live HERE, inside the button component, because the alternative is a section or a wave-2
   component restyling a button locally, which contract §10 forbids.
   All three are ⚠️ ASSUMED (questions for Iulia, design task #42154, listed in
   docs/reference/design/templates/pdp/COMPONENTS-WAVE-2.md §9). */

/* ⚠️ ASSUMED - .miniso-btn--block. The 72 variants have no width axis at all, yet the PDP draws
   a full-width / stretched CTA three times: 440px desktop (96:12993), 100% mobile (96:13900),
   flex:1 inside a 253px slot (96:14556). Width, not a new type or size, so it composes with every
   existing modifier. COMPONENTS-WAVE-2 §9 row 1. */
.miniso-btn--block {
  display: flex;
  width: 100%;
}

/* ⚠️ ASSUMED - .miniso-btn--label-lg. .miniso-btn hardcodes Heading/Subtitle 3. Two buttons on
   this PDP are drawn one step up: "Add a review" desktop is Heading/Subtitle 2 20/24 (96:13103)
   and the sticky-bar CTA label is 16/20 at MOBILE (I96:14556;14:1495) where Subtitle 3 is 14/18.
   var(--miniso-sub2) is 16/20 mobile and 20/24 desktop, i.e. it serves both drawings exactly with
   one modifier. COMPONENTS-WAVE-2 §9 row 4. */
.miniso-btn--label-lg {
  font-size: var(--miniso-sub2);
  line-height: var(--miniso-sub2-lh);
}

/* ⚠️ ASSUMED - .miniso-btn--muted. A Ghost tone the styleguide does not have. "Read more"
   (96:13012 / 96:13916) is a Ghost whose label is Secondary/60 #7A7C7D, and the quantity
   stepper's +/- glyphs are stroked Neutral/Grey #706F6F - a colour with NO layer-1 token, whose
   nearest token is Secondary/60 (#7A7C7D, one hue step away). Styleguide Ghost Default is
   Secondary #000000. Without this modifier .miniso-stepper would have to restyle a button.
   COMPONENTS-WAVE-2 §9 rows 2-3. Hover still goes to Primary, like every other Ghost. */
.miniso-btn--muted {
  --_miniso-btn-fg: var(--miniso-secondary-60);
}

/* ---- Focus ---------------------------------------------------------------------------------
   ASSUMPTION (for Iulia, README open question 1). FOCUS IS NOT SPECIFIED IN FIGMA for any of the
   72 variants; the doc sheet header advertises a "Focus" state that has no corresponding variant
   (03 §Missing states). Shipping without a visible keyboard focus indicator is not an option, so
   this ring is ours: 2px solid Secondary with a 2px offset, chosen because the ONE focus-ish
   thing anywhere in the file is the input's Focused state, a 2px Secondary (#000000) border
   (04 §Focused). Change it here, in one place, once Iulia answers.
   :focus-visible only, so it never fires on mouse click. */
.miniso-btn:focus-visible {
  outline: 2px solid var(--miniso-secondary);
  outline-offset: 2px;
}

/* ==========================================================================================
   2. INPUT FIELD - .miniso-input
   Source: docs/reference/design/04-input-fields.md (component set 37:624, 24 variants).
   The hand-written token table on 39:291 disagrees with the bindings in 8 places; per that doc,
   "the component bindings are the implementable truth". Every value below is a BINDING value.
   The 8 conflicts are listed in the report.

   MODIFIER VOCABULARY
     icon   (none) = Icon None
            .miniso-input--icon-leading | --icon-trailing | --icon-both
     state  (none) = Default
            live:   :hover, :focus-within, :has(control:disabled)
            forced: .miniso-input--hover | --focused | --filled | --disabled | --error
            (--filled and --error are structural, not just colour: use the class, not a pseudo)
   MARKUP
     .miniso-input                                wrapper (carries the error message)
       .miniso-input__box                         the 48px pill
         .miniso-input__icon--leading             16x16
         .miniso-input__field                     flex-1 text column
           .miniso-input__label                   floating label, Filled state only
           .miniso-input__control                 the <input>
         .miniso-input__icon--trailing            16x16
       .miniso-input__message                     error line, Error state only
   ========================================================================================== */

.miniso-input {
  --_miniso-input-bd: var(--miniso-aqua-20);
  --_miniso-input-bw: 1px;
  --_miniso-input-bg: #ffffff;
  /* NO TOKEN: white. Finding 1. */
  --_miniso-input-fg: var(--miniso-secondary-40);
  --_miniso-input-icon: var(--miniso-secondary-40);
  --_miniso-input-icon-trailing: var(--miniso-secondary-40);
  --_miniso-input-pad-block: 12px;
  /* 04 §Shared box model. NO TOKEN. */

  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  /* Figma items-start on a 320px frame; stretch so the field fills its column */
  gap: 0;
  /* Error raises this to 6px */
  width: 100%;
  /* Figma component is a fixed 320 but its inner box is w-full, i.e. stretchable.
                  No min or max width is specified in Figma (04 §Missing sizes). */
  font-family: var(--miniso-font);
}

.miniso-input__box {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  gap: 12px;
  /* icon <-> text. 04 §Shared box model. NO TOKEN. Collapses when no icon is shown. */
  width: 100%;
  height: 48px;
  /* fixed, all states. 04 §Shared box model. NO TOKEN. */
  padding: var(--_miniso-input-pad-block) 16px;
  border: var(--_miniso-input-bw) solid var(--_miniso-input-bd);
  border-radius: var(--miniso-radius-input);
  background-color: var(--_miniso-input-bg);
}

.miniso-input__icon {
  display: none;
  /* shown by an icon-layout modifier */
  flex: 0 0 auto;
  width: 16px;
  /* 04 §Shared box model. NO TOKEN. */
  height: 16px;
  color: var(--_miniso-input-icon);
}

.miniso-input__icon--trailing {
  color: var(--_miniso-input-icon-trailing);
}

.miniso-input__icon>svg,
.miniso-input__icon>img {
  display: block;
  width: 100%;
  height: 100%;
}

.miniso-input--icon-leading .miniso-input__icon--leading,
.miniso-input--icon-both .miniso-input__icon--leading,
.miniso-input--icon-trailing .miniso-input__icon--trailing,
.miniso-input--icon-both .miniso-input__icon--trailing {
  display: block;
}

.miniso-input__field {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  justify-content: center;
  min-width: 0;
  overflow: hidden;
}

.miniso-input__label {
  display: none;
  /* Filled state only - the only label Figma defines (04 §Filled). */
  overflow: hidden;
  color: var(--miniso-secondary-40);
  text-overflow: ellipsis;
  white-space: nowrap;

  /* Body/Functional/Regular - 14/18 desktop */
  font-size: var(--miniso-body-fn);
  font-weight: 400;
  line-height: var(--miniso-body-fn-lh);
}

.miniso-input__control {
  display: block;
  width: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  appearance: none;
  -webkit-appearance: none;
  overflow: hidden;
  color: var(--miniso-secondary);
  text-overflow: ellipsis;
  white-space: nowrap;

  /* Body/Smaller/Regular - 16/22 desktop. The doc sheet's "all input text is 14/18" is wrong;
     only the Filled label + value are 14/18 (04 conflict #6). */
  font-family: inherit;
  font-size: var(--miniso-body-sm);
  font-weight: 400;
  line-height: var(--miniso-body-sm-lh);
  font-variation-settings: 'wdth' 100;
  letter-spacing: 0;
}

.miniso-input__control:focus {
  outline: none;
  /* focus is expressed on the box - see the focus block below */
}

.miniso-input__control::placeholder {
  color: var(--_miniso-input-fg);
  opacity: 1;
}

.miniso-input__message {
  display: none;
  /* Error state only */
  margin: 0;
  color: var(--miniso-error);

  /* Body/Functional/Regular - 14/18 desktop (04 conflict #8: the sheet says 12px). */
  font-size: var(--miniso-body-fn);
  font-weight: 400;
  line-height: var(--miniso-body-fn-lh);
  white-space: nowrap;
  /* Figma's message is nowrap and the 72px total assumes exactly one line. */
}

/* ---- States --------------------------------------------------------------------------------
   04 §Per-state color table, read off the component bindings. */

/* Hover - border colour only, still 1px, so the box does not shift. */
.miniso-input:hover,
.miniso-input--hover {
  --_miniso-input-bd: var(--miniso-aqua);
}

/* Focused - 2px Secondary border. box-sizing: border-box keeps the box at 48px, so there is no
   2px layout jump between Default and Focused (04 §Focused). */
.miniso-input:focus-within,
.miniso-input--focused {
  --_miniso-input-bd: var(--miniso-secondary);
  --_miniso-input-bw: 2px;
}

/* Filled - the only state with a different vertical padding, to fit the two stacked lines. */
.miniso-input--filled {
  --_miniso-input-pad-block: 6px;
  /* 04 §Filled. NO TOKEN. */
}

.miniso-input--filled .miniso-input__label {
  display: block;
}

.miniso-input--filled .miniso-input__control {
  font-size: var(--miniso-body-fn);
  line-height: var(--miniso-body-fn-lh);
}

/* Disabled - the only state whose fill is not white. */
.miniso-input--disabled,
.miniso-input:has(.miniso-input__control:disabled) {
  --_miniso-input-bd: var(--miniso-secondary-20);
  --_miniso-input-bg: var(--miniso-offwhite);
  --_miniso-input-fg: var(--miniso-secondary-20);
  /* VERIFIED 01.09 against Figma 37:617 / icon node 37:543: stroke="#A6A7A9" = Secondary/40.
     The disabled icon does NOT dim to Secondary/20 - it keeps the same grey as every other state,
     so there is deliberately no icon override here. Was assumed Secondary/20 before the node was
     readable; corrected once the quota allowed the read. */
}

/* A disabled field must not pick up hover/focus colours. */
.miniso-input--disabled:hover,
.miniso-input--disabled:focus-within,
.miniso-input:has(.miniso-input__control:disabled):hover {
  --_miniso-input-bd: var(--miniso-secondary-20);
  --_miniso-input-bw: 1px;
}

/* Error - 1px Primary border (NOT 2px), placeholder stays grey, message below at 6px. */
.miniso-input--error {
  --_miniso-input-bd: var(--miniso-primary);
  --_miniso-input-bw: 1px;
  /* VERIFIED 01.09 against Figma 37:623 / icon node 37:596: stroke="#D93832" = Primary. Confirmed.
     Note the LEADING icon in the error state stays #A6A7A9 (node 37:591) - the error colour touches
     the border, the message and the trailing icon only, never the leading icon. */
  --_miniso-input-icon-trailing: var(--miniso-primary);

  gap: 6px;
  /* outer wrapper gap. 48 + 6 + 18 = 72. 04 §Error. NO TOKEN. */
}

.miniso-input--error .miniso-input__message {
  display: block;
}

/* ---- Focus ---------------------------------------------------------------------------------
   NO extra ring on the input, deliberately. A text field ALWAYS matches :focus-visible while
   focused - the selector matches any element that accepts keyboard input, however it was focused -
   so an outline here stacks on top of the 2px black border from :focus-within above and renders a
   double ring on an ordinary mouse click. Observed on the gallery, 01.09.

   Figma's Focused state (the 2px Secondary border) IS therefore already a sufficient keyboard
   indicator on this component, and unlike the button and badge it does not need one inventing.
   Do not re-add an outline here without checking the click case. */

/* ==========================================================================================
   3. BADGE - .miniso-badge
   Source: docs/reference/design/05-badge-and-doc-conventions.md (component set 43:575, 4 states).

   MODIFIER VOCABULARY
     state  (none) = Default
            .miniso-badge--hover | --active | --out-of-stock
            live: a.miniso-badge:hover / button.miniso-badge:hover only. Figma never says whether
            the badge is interactive or a static marker (05 §Gaps 15), so a plain <span> badge
            deliberately does NOT react to the pointer.

   REPRODUCED FAITHFULLY, FLAGGED: Default and Out of Stock are 51px tall, Hover and Active 48px,
   because the 1.5px stroke sits outside the 48px content box and only those two states have one
   (05 §1.2). That is a 3px layout jump on hover. Built as specified; finding for Iulia.
   ========================================================================================== */

.miniso-badge {
  --_miniso-badge-bg: #ffffff;
  /* NO TOKEN: raw #FFFFFF in Figma too - 05 §1.4 "raw hex, no variable bound". */
  --_miniso-badge-fg: var(--miniso-secondary);
  --_miniso-badge-bd: var(--miniso-secondary);
  --_miniso-badge-bw: 1.5px;
  /* 05 §1.2. NO TOKEN. */
  --_miniso-badge-bs: solid;

  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 56px;
  /* the only width constraint Figma states. 05 §1.2. NO TOKEN. */
  padding: 12px 24px;
  /* 05 §1.2. NO TOKEN. */
  overflow: clip;
  border: var(--_miniso-badge-bw) var(--_miniso-badge-bs) var(--_miniso-badge-bd);
  border-radius: var(--miniso-radius-pill);
  background-color: var(--_miniso-badge-bg);
  color: var(--_miniso-badge-fg);

  /* Body/Medium/Regular - Fredoka Regular 18/24 desktop. 05 §1.3. */
  font-family: var(--miniso-font);
  font-size: var(--miniso-body-md);
  font-weight: 400;
  line-height: var(--miniso-body-md-lh);
  font-variation-settings: 'wdth' 100;
  letter-spacing: 0;
  text-align: center;
  text-decoration: none;
}

/* Hover and Active are pixel-identical in Figma: same Primary fill, white text, no stroke.
   Recorded as read, not "fixed" (05 §Gaps 1, README open question 7). */
.miniso-badge--hover,
.miniso-badge--active,
a.miniso-badge:hover,
button.miniso-badge:hover {
  --_miniso-badge-bg: var(--miniso-primary);
  --_miniso-badge-fg: #ffffff;
  /* NO TOKEN: raw #FFFFFF in Figma too. Finding 1. */
  --_miniso-badge-bd: transparent;
  --_miniso-badge-bw: 0;
}

/* ACCESSIBILITY FLAG, NOT FIXED: white on Primary/40 #F0A3A6 is ~2.0:1 and FAILS WCAG AA for
   normal text at 18px/400 (05 §Gaps 16, README open question 6). Built exactly as the designer
   specified. Do not silently darken it - it needs Iulia's decision. */
.miniso-badge--out-of-stock {
  --_miniso-badge-bg: var(--miniso-primary-40);
  --_miniso-badge-fg: #ffffff;
  /* NO TOKEN: raw #FFFFFF in Figma too. Finding 1. */
  --_miniso-badge-bd: #ffffff;
  /* NO TOKEN: raw #FFFFFF in Figma too. Finding 1. */
  --_miniso-badge-bw: 1.5px;
  --_miniso-badge-bs: dashed;
}

/* Out of Stock must not turn red on hover. */
a.miniso-badge--out-of-stock:hover,
button.miniso-badge--out-of-stock:hover {
  --_miniso-badge-bg: var(--miniso-primary-40);
  --_miniso-badge-bd: #ffffff;
  /* NO TOKEN: white. Finding 1. */
  --_miniso-badge-bw: 1.5px;
  --_miniso-badge-bs: dashed;
}

/* ASSUMPTION (for Iulia, README open question 1): no focus state exists on the badge either
   (05 §Gaps 2). Same ring as button and input, so a focusable badge (filter chip) is reachable. */
.miniso-badge:focus-visible {
  outline: 2px solid var(--miniso-secondary);
  outline-offset: 2px;
}

/* ############################################################################################
   WAVE 2 - PDP harvest (frames 96:12873 desktop, 96:13819 pdp-mobile, 96:14218 pdp-mobile-scroll)
   Spec: docs/reference/design/templates/pdp/COMPONENTS-WAVE-2.md (+ 01..07 per region).

   VALUES WITH NO TOKEN IN LAYER 1 that wave 2 has to use raw. All are findings for Iulia
   (design task #42154), recorded here once and again at each line that uses them:
   4. #fafafa   product-card / media-tile background. Raw hex, no Figma variable, absent from the
                24-token palette. Off-white #F5F5F5 is one step away. (05-card-rows finding 1.)
   5. #EDEDED   Figma "Neutral/Ligh Grey" / "Products/Ligh Grey" - a real bound variable that is
                ABSENT from the colour sheet. Stepper border, accordion dividers. (§10.)
   6. #eee      sticky-bar top border. Raw hex. A THIRD grey next to #EDEDED and #F5F5F5. (§10.)
   7. #757575   sticky-bar eyebrow. Raw hex, no variable bound. FOURTH grey. (§10.)
   8. #1a1a1a   sticky-bar title. Raw hex, no variable bound. (§10.)
   9. rgba(0,0,0,0.03) in the sticky bar's shadow. No shadow exists anywhere else. (§8.)
   10. Radii. Layer 1 has exactly two (pill 999, input 24). The PDP draws SEVEN: 8, 12, 16, 20,
       30, 32 and 100(=pill). Every one below is raw px with the node it came from. (§11.)
   11. Every px length: padding, gap, width, height, icon box. Layer 1 carries no size or spacing
       scale at all (colour, families, two radii, the type scale - nothing else).

   FOREIGN FONTS, NOT LOADED (§12). Three appear in the frames; the design system is Fredoka-only
   and none of the three is licensed on this project. Each is built in Fredoka with the token
   whose size/weight matches, and flagged at its line:
     - Info Display Pro Book 18/24 -> quantity value  -> Fredoka 400 var(--miniso-body-lg)
     - Inter Semi Bold 12px        -> mobile card eyebrow -> Fredoka 400 var(--miniso-body-fn)
     - Inter Semi Bold 14/20       -> delivery-box emphasis (NOT a wave-2 component; recorded only)

   PUBLIC COMPONENT INPUTS. Two components take a value from the author as a custom property set
   inline on the element. These are component inputs, NOT brand tokens - they are named without
   the leading underscore only because an author writes them:
     --miniso-rating-fill   on .miniso-rating__star   0%..100%, how much of that star is filled
     --miniso-meter-fill    on .miniso-meter__fill    0%..100%, count / max(count)
   ############################################################################################ */

/* ==========================================================================================
   4. PRODUCT CARD - .miniso-product-card
   Source: docs/reference/design/templates/pdp/05-card-rows.md, nodes 96:13029 / 96:13037 /
   96:13045 / 96:13053 (desktop) and 96:13927 (mobile), read to the leaves.

   ONE CARD, NOT THREE. The same card is drawn NINE times: PDP Recommended and Recently viewed at
   both breakpoints (4), plus the homepage New drop 335.6 (24:1879), Bestseller 334.75 (36:3788)
   and featured 438x670 (36:3697) named in contract §10 (5). A second implementation is a defect
   (00-CONTEXT.md, Razvan 01.09) - the homepage build consumes THIS class.

   MODIFIER VOCABULARY (what a section author writes)
     (base)  responsive card. Compact geometry below 750px, desktop geometry from 750px. This is
             the card as drawn at BOTH breakpoints, so a row that renders once and is read at both
             widths - which is every row on the PDP and the homepage - wants the base class alone.
     .miniso-product-card--sm       force compact geometry at every width (a narrow slot on a wide
                                    viewport: 172px mobile-row card, dense carousel)
     .miniso-product-card--lg       force desktop geometry at every width
     .miniso-product-card--feature  homepage featured card: desktop 128:15885 438x670 (harvested
                                    03.09), mobile 136:2395 "Promo Product" 362x~530 (harvested
                                    04.09) - a bordered, padded shell, see the --feature block.
   ELEMENTS
     .miniso-product-card__media     the image frame (aspect ratio + radius + clip)
     .miniso-product-card__img       the image; add --alt for the hover/second image
     .miniso-product-card__action    absolute slot, top-right - holds a .miniso-btn, nothing else
     .miniso-product-card__details   the text column
     .miniso-product-card__eyebrow   product type, Primary
     .miniso-product-card__title     2 lines, clamped
     .miniso-product-card__price
     .miniso-product-card__link      the title link; carries the focus ring for the whole card

   THE BAG BUTTON IS NOT PART OF THIS COMPONENT. 128:17336 / 96:13933 resolve EXACTLY to
   .miniso-btn--primary.miniso-btn--sm.miniso-btn--icon-only (36x36, padding 8, radius 999, 20px
   icon). Put that button inside __action. Never a card-local button.

   DEGRADATION (00-CONTEXT.md BLOCKER, measured 01.09): 496 of 499 sampled products have exactly
   ONE image. Iulia's annotation on 96:13029 asks for a hover swap to the second gallery image.
   Built as drawn: __img--alt sits on top at opacity 0 and appears on hover. With no second image
   the snippet renders no --alt node and the rule matches nothing; if it renders the same src
   twice the swap is invisible. Either way the card is correct and silent. No fallback invented.
   ========================================================================================== */

.miniso-product-card {
  /* Internal plumbing. Set by the size modifiers and by the ONE geometry media query below.
     Not design tokens, not for authors. Compact (mobile, 96:13927) values are the defaults. */
  --_miniso-card-radius: 16px;
  /* 96:13927. NO TOKEN (radius 16 of seven). */
  --_miniso-card-media-radius: 0px;
  /* 96:13928 has no radius. NO TOKEN. */
  --_miniso-card-media-ratio: 1 / 1;
  /* 96:13928 aspect-ratio 1/1. */
  --_miniso-card-pad: 12px;
  /* 96:13929 padding 12. NO TOKEN. */
  /* TWO gaps, because the breakpoints genuinely differ (corrected 01.09 from Razvan's inspect):
     MOBILE 96:13929 is a flat run - eyebrow y12 h15, title y31, price y53 = a uniform 4 everywhere.
     DESKTOP groups eyebrow+title with NO gap (Frame 632: CATEGORY y0 h18, title y18) and puts 12
     only before the price. A single flat gap put 12px of air under CATEGORY that is not drawn. */
  --_miniso-card-gap: 4px;
  /* eyebrow -> title. 96:13929. NO TOKEN. */
  --_miniso-card-price-gap: 4px;
  /* -> price. 96:13929. NO TOKEN. */
  --_miniso-card-action-inset: 12px;
  /* 96:13933 right 12 / top 12. NO TOKEN. */

  box-sizing: border-box;
  position: relative;
  display: flex;
  flex-direction: column;
  overflow: clip;
  /* 96:13927. Desktop is not clipped, but nothing overflows it either. */
  border-radius: var(--_miniso-card-radius);

  /* NO TOKEN: #fafafa. Raw hex on BOTH breakpoints (96:13029, 96:13927), no variable bound,
     absent from the 24-token palette. Finding 4. Same value as the PDP media tile. */
  background-color: #fafafa;
  /* NO TOKEN - finding 4, see the comment above. */
  font-family: var(--miniso-font);
}

.miniso-product-card * {
  box-sizing: border-box;
}

.miniso-product-card__media {
  position: relative;
  /* anchors __img--alt */
  display: block;
  width: 100%;
  aspect-ratio: var(--_miniso-card-media-ratio);
  overflow: clip;
  border-radius: var(--_miniso-card-media-radius);
}

.miniso-product-card__img {
  /* Absolutely positioned so the image can NEVER size the media box - the box's height must come
     from --_miniso-card-media-ratio alone.
     The bug this fixes (01.09): `height: 100%` resolves against a parent whose height comes from
     `aspect-ratio`, which is not a definite height, so it fell back to `auto`; the image then took
     its own intrinsic ratio and the box grew to contain it. Square photos (1050x1050) landed under
     the aspect-derived height so they looked fine, but a portrait one (1050x1400) pushed the media
     from 476px to 596px - 4/3 instead of 800/852 - and that card's title and price sat ~120px
     lower than its neighbours' in the same row. __img--alt was already absolute, so this also
     makes the two images share one positioning model instead of two. */
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  /* ⚠️ ASSUMED, and it is 05-card-rows finding 5's own proposal: object-fit CONTAIN at both
     breakpoints. Figma draws contain on mobile (96:13928) and an inset-and-clipped image on
     desktop (96:13030: h 85.21%, left 9.79%, top 14.88%, w 79.95%) - i.e. two different fits for
     one component, on a catalogue whose photos are not square. Contain never crops a product.
     The homepage mobile frame (04.09) repeats the split: New drop 138:17861 draws object-cover,
     Bestseller 138:18123 object-contain, and the desktop rows inset the photo (80% x 85% of the
     box). Two of the three drawings show the whole product - contain stays. ⚠️ ASSUMED, for
     Iulia: which fit is meant. The --feature card's mobile shell is the one drawn exception. */
  object-fit: contain;

  /* TRANSFORM ONLY, deliberately - the hover zoom below eases, the image swap does not.
     Cross-fading the two images would put both of them on screen at partial opacity for the
     length of the fade, and both carry `mix-blend-mode: multiply` (see below), so every frame of
     the fade would multiply twice and darken. The swap is instantaneous: exactly one multiplied
     layer is ever painted. */
  transition: transform 0.35s ease;

  /* The catalogue's photos carry their own solid #FFFFFF studio background baked into the file:
     measured 01.09 over 90 products sampled across the index, 0 have ANY transparency - 77% are
     JPEG (a format with no alpha channel at all) and the rest are PNGs whose alpha is fully
     opaque. On the card's #fafafa ground that white sits as a visibly lighter rectangle, i.e. a
     second background, and Figma's card (149:20618) has exactly ONE fill - Background #fafafa -
     with no fill of its own on the media rect (149:20619).
     multiply against a 98%-white ground erases the photo's white into the card and leaves every
     other pixel within ~2% of its own colour, so the card reads as the single surface it is drawn
     as. This is a workaround for the asset data, not a design decision: the real fix is exporting
     the catalogue with transparent backgrounds, after which this rule becomes a no-op. */
  mix-blend-mode: multiply;
}

/* FILL THE HOLDER - Iulia's review 07.09, aico 133084 item 1: photos whose own proportions are
   not the media box's are letterboxed by `contain`, and the #fafafa bands at their sides read as
   a margin baked into the picture. `cover` fixes that but crops, and this catalogue cannot take
   an unbounded crop: measured over all 2563 products that have a featured image (Admin API,
   07.09), the aspect ratio runs from 0.30 to 6.79 - p50 1.000 (1050x1050 is 1100 products alone),
   p95 1.80, p99 2.84, with genuine panoramas in there (1670x246, 3978x1006). `cover` on those
   would throw away 76-86% of the picture.
   So the fit is CROP-BUDGETED, not switched wholesale: the renderer measures the image and adds
   --fill only while `cover` would cut LESS THAN 25% off it, and everything wilder keeps `contain`.
   At 25% the whole square cluster fills (a 1:1 photo loses 6.2% of its height in the 800/852 box)
   and so does the 3:4 phone-photo cluster (20.1%, 1050x1400 and 3024x4032 = 103 products); the
   panoramas do not. 75.8% of the catalogue fills, the rest is letterboxed on purpose.
   The default stays `contain` above, so any surface that does not run the measurement (the
   wishlist grid) keeps exactly today's behaviour rather than cropping blind.
   Set by snippets/miniso-product-card.liquid (Liquid, from image.aspect_ratio) and by
   assets/miniso-collection.js (PLP/search, from the hit's own image dimensions).
   ⚠️ ASSUMED: the 25% budget. Figma draws two different fits for this one component and no rule
   for photos that fit neither - for Iulia. */
.miniso-product-card__img--fill {
  object-fit: cover;
}

/* The hover image. Absent from the DOM on a single-image product. */
.miniso-product-card__img--alt {
  opacity: 0;
  /* position/inset now come from __img above. */
}

/* BUG FIXED 07.09 (Iulia, aico 133084 item 2 - "the hover changes colour and looks buggy"):
   the swap raised __img--alt to opacity 1 and LEFT THE PRIMARY IMAGE PAINTING UNDERNEATH IT.
   Both images carry `mix-blend-mode: multiply` (see __img above), so on hover the top one
   multiplied onto the already-multiplied one below and every mid-tone was squared - the photo
   went dark and over-saturated. It reads as a colour glitch rather than a swap, and it is worst
   exactly where it looks most wrong: ~920 products in this catalogue carry their first photo
   TWICE (aico 133084 item 3 - two byte-identical files under different names), so the "second"
   image is the same picture and the only visible effect of hovering was the darkening.
   The primary is now switched OFF in the same instant the alternate comes on, so exactly ONE
   multiplied layer is ever painted. On a duplicate pair that makes the swap correctly invisible,
   which is what the component's own degradation note asks for.
   The :not() is load-bearing: __img--alt IS a __img, and without it this higher-specificity rule
   would hold the alternate at opacity 0 as well and nothing would ever swap. */
.miniso-product-card:hover .miniso-product-card__media:has(.miniso-product-card__img--alt) .miniso-product-card__img:not(.miniso-product-card__img--alt),
.miniso-product-card:focus-within .miniso-product-card__media:has(.miniso-product-card__img--alt) .miniso-product-card__img:not(.miniso-product-card__img--alt) {
  opacity: 0;
}

.miniso-product-card:hover .miniso-product-card__img--alt,
/* ⚠️ ASSUMED: keyboard parity. Iulia annotated hover only; a keyboard user gets the same swap. */
.miniso-product-card:focus-within .miniso-product-card__img--alt {
  opacity: 1;
}

/* One image and no swap to make: a slight zoom instead of nothing at all (Iulia, 07.09 - aico
   133084 item 2). Scoped with :has() rather than a modifier class so it holds on EVERY surface
   the card renders on, including the ones whose markup this file does not control (the wishlist
   grid). __media already clips, so the zoom stays inside the frame.
   ⚠️ ASSUMED: 1.04 over 350ms. No card hover state is drawn anywhere in the file (see the note
   at the end of this block) - for Iulia. */
.miniso-product-card:hover .miniso-product-card__media:not(:has(.miniso-product-card__img--alt)) .miniso-product-card__img,
.miniso-product-card:focus-within .miniso-product-card__media:not(:has(.miniso-product-card__img--alt)) .miniso-product-card__img {
  transform: scale(1.04);
}

@media (prefers-reduced-motion: reduce) {

  .miniso-product-card__img,
  .miniso-product-card:hover .miniso-product-card__media:not(:has(.miniso-product-card__img--alt)) .miniso-product-card__img,
  .miniso-product-card:focus-within .miniso-product-card__media:not(:has(.miniso-product-card__img--alt)) .miniso-product-card__img {
    transform: none;
    transition: none;
  }
}

.miniso-product-card__action {
  position: absolute;
  top: var(--_miniso-card-action-inset);
  right: var(--_miniso-card-action-inset);
  display: flex;
  /* Must out-rank the stretched title link below, which covers the whole card. Both are
     positioned, and the action comes FIRST in the DOM, so without this the link's overlay paints
     on top of the add-to-cart button and swallows its clicks. */
  z-index: 2;
}

.miniso-product-card__details {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  gap: var(--_miniso-card-gap);
  padding: var(--_miniso-card-pad);
}

/* The flex gap already applies before the price, so add only the difference. Mobile 4+0=4,
   desktop 0+12=12 - each exactly as drawn, with no extra wrapper element in the markup. */
.miniso-product-card__price {
  margin: calc(var(--_miniso-card-price-gap) - var(--_miniso-card-gap)) 0 0;
}

/* BUG FIXED 03.09 (found measuring the featured card): the second .miniso-product-card__price
   block further down declared `margin: 0`, same specificity, later in the file, so this rule
   never applied and NO desktop card ever had its 12px before the price - PDP rows included.
   That block now sets no margin. */

.miniso-product-card__eyebrow {
  margin: 0;
  color: var(--miniso-primary);
  /* 96:13033 and 96:13930 both Primary. */
  text-transform: uppercase;
  /* "CATEGORY" / "PLUSHIES" are drawn caps; product.type is not. */

  /* Desktop 96:13033 is Fredoka Regular Functional 14/18 -> exactly this token.
     ⚠️ FOREIGN FONT, NOT USED: mobile 96:13930 is Inter Semi Bold 12px/normal. Inter is in no
     styleguide sheet and 12px is below the smallest step (Functional is 13px mobile). Built in
     Fredoka at the Functional token, which is 13/16 mobile and 14/18 desktop - i.e. desktop is
     exact and mobile is 1px larger than the (unbuildable) drawing. 05-card-rows finding 2. */
  font-size: var(--miniso-body-fn);
  font-weight: 400;
  line-height: var(--miniso-body-fn-lh);
}

.miniso-product-card__title {
  display: -webkit-box;
  overflow: hidden;
  margin: 0;
  color: var(--miniso-text);
  /* 96:13034 black / 96:13931 Secondary. */

  /* Subtitle 3, Fredoka Medium: 16/20 desktop, 14/18 mobile - the token does both. */
  font-size: var(--miniso-sub3);
  font-weight: 500;
  line-height: var(--miniso-sub3-lh);

  /* ⚠️ ASSUMED, 05-card-rows finding 3's own proposal: TWO lines at both breakpoints. Figma
     clamps desktop to 2 lines with no ellipsis (96:13034) and mobile to 1 line WITH an ellipsis
     (96:13931) - two truncation rules for one component. Titles on this catalogue average well
     over one line, so 1-line mobile would ellipsis nearly every card.
     The homepage mobile frame (04.09, 138:17864 / 138:18127) draws the same one-liner at a
     16px LITERAL on the mobile Subtitle 3 line-height (18) - a size the mobile scale does not
     have (Subtitle 3 is 14/18). Kept: two lines at the token (14/18), so the drawn 254-tall card
     becomes 258 with a one-line title and 276 with two (the card hugs its title, as the desktop
     frame itself does: 473 / 487 / 493). Spec question 9 for Iulia. */
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  line-clamp: 2;
}

.miniso-product-card__link {
  color: inherit;
  text-decoration: none;
}

/* The whole card is clickable, not just the title text. Before 01.09 only the title was a link,
   so clicking the photo - which is what everyone actually clicks - did nothing. This is the
   stretched-link pattern: one anchor, no duplicate <a> around the image, so the accessibility
   tree and the crawler still see a single product link per card. The card is already
   `position: relative`, so the overlay resolves against it. */
.miniso-product-card__link::after {
  position: absolute;
  z-index: 1;
  inset: 0;
  content: '';
}

.miniso-product-card__price {
  /* margin: see the gap rule above - do not reset it here. */
  color: var(--miniso-text);
  /* 96:13035 black / 96:13932 Secondary. */

  /* paragraph smaller, Fredoka Regular: 16/22 desktop, 14/20 mobile - the token does both. */
  font-size: var(--miniso-body-sm);
  font-weight: 400;
  line-height: var(--miniso-body-sm-lh);
}

/* ⚠️ ASSUMED: the PDP's compare-at look, no card sale state in Figma */
.miniso-product-card__compare {
  margin-inline-start: 8px;
  color: var(--miniso-secondary-60);
  white-space: nowrap;
}

/* GEOMETRY ONLY - no type is redeclared here. See the header note on the wave-2 breakpoint rule. */
@media (min-width: 750px) {
  .miniso-product-card {
    --_miniso-card-radius: 32px;
    --_miniso-card-media-radius: 20px;
    --_miniso-card-media-ratio: 800 / 852;
    --_miniso-card-pad: 24px 17px 20px;
    /* Figma inspect: padding 24px 17px 20px 17px. */
    --_miniso-card-gap: 0px;
    /* eyebrow -> title: Frame 632 stacks them flush. */
    --_miniso-card-price-gap: 12px;
    /* -> price: the only 12 in the details column. */
    --_miniso-card-action-inset: 18px;
  }
}

/* Forced compact / forced desktop. The geometry query above moves only the BASE class and sits
   before these two, so --sm and --lg hold at every width - that is the whole point of having them. */
.miniso-product-card--sm {
  --_miniso-card-radius: 16px;
  --_miniso-card-media-radius: 0px;
  --_miniso-card-media-ratio: 1 / 1;
  --_miniso-card-pad: 12px;
  --_miniso-card-gap: 4px;
  --_miniso-card-action-inset: 12px;
}

/* BUG FIXED 07.09 (found by its first consumer, sections/miniso-blind-box.liquid): --lg claims
   to "force desktop geometry at every width" but its two gap values were the pre-01.09 flat
   pair, so it did NOT equal the desktop block below/above it. It set eyebrow -> title = 12
   (desktop draws them flush) and left --_miniso-card-price-gap at the compact 4, which makes
   the price rule's `calc(price-gap - gap)` resolve to -8px and pull the price UP into the
   title. --lg is declared after the min-width:750 query at the same specificity, so on desktop
   it was overriding the correct values with the wrong ones. Both are now the desktop pair, i.e.
   identical to the query and to --feature. Nothing consumed --lg before today, so this changes
   no existing surface. */
.miniso-product-card--lg {
  --_miniso-card-radius: 32px;
  /* 96:13029. NO TOKEN (radius 32 of seven). */
  --_miniso-card-media-radius: 20px;
  /* 96:13030. NO TOKEN (radius 20 of seven). */
  --_miniso-card-media-ratio: 800 / 852;
  /* 96:13030. */
  --_miniso-card-pad: 24px 17px 20px;
  /* 96:13031. NO TOKEN. */
  --_miniso-card-gap: 0px;
  /* 96:13031 Frame 632 stacks eyebrow + title flush. */
  --_miniso-card-price-gap: 12px;
  /* the only 12 in the details column. NO TOKEN. */
  --_miniso-card-action-inset: 18px;
  /* 128:17336 top 18; left 281 in a 335 card = right 18.
                                            Cards 2-4 differ by 0.25-0.75px - flex rounding, not
                                            intent (05-card-rows). NO TOKEN. */
}


/* --feature: the homepage featured card 128:15885 (= 36:3697), 438x670, HARVESTED 03.09 (it was
   "= --lg" until then). Same shell as desktop: radius 32, media 800/852 radius 20, details
   padding 24/17/20 with eyebrow+title flush and 12 before the price. What differs:
   - title is Heading/Subtitle 2 Medium 20/24 (128:15890), two lines = 48;
   - price is Body/Medium 18/24 (128:15891);
   - NO bag icon top-right. Instead a full-width Primary "In den Warenkorb" with the bag icon
     left (128:15892: 402x44 at left 18 / top 604, i.e. 12 under the price and 22 above the
     bottom edge - built in flow inside the details column, so the card height follows its
     content: 466.5 + 24 + 18 + 48 + 12 + 24 + 12 + 44 + 20 = 668.5 against the drawn 670).
   The snippet renders __cta for size feature and __action otherwise; both rules below guard
   the other case anyway. Geometry holds at every width, like --lg. */
.miniso-product-card--feature {
  --_miniso-card-radius: 32px;
  --_miniso-card-media-radius: 20px;
  --_miniso-card-media-ratio: 800 / 852;
  --_miniso-card-pad: 24px 17px 20px;
  --_miniso-card-gap: 0px;
  /* 128:15888: eyebrow and title flush, as on desktop */
  --_miniso-card-price-gap: 12px;
  /* 128:15887 gap */
  --_miniso-card-action-inset: 18px;
}

.miniso-product-card--feature .miniso-product-card__title {
  font-size: var(--miniso-sub2);
  line-height: var(--miniso-sub2-lh);
}

.miniso-product-card--feature .miniso-product-card__price {
  font-size: var(--miniso-body-md);
  line-height: var(--miniso-body-md-lh);
}

.miniso-product-card__cta {
  display: none;
  /* --feature only */
  position: relative;
  z-index: 2;
  /* above the stretched title link, like __action */
  margin-top: 12px;
  /* 128:15892 top 604 - price bottom 592.5. NO TOKEN. */
}

.miniso-product-card--feature .miniso-product-card__cta {
  display: block;
}

.miniso-product-card--feature .miniso-product-card__action {
  display: none;
}

.miniso-product-card__cta form {
  display: block;
}

/* --feature, MOBILE: the promo card 136:2395 "Promo Product" (394-wide frame 136:2383, harvested
   04.09) is a DIFFERENT shell from the desktop feature card: white-ish #FCFCFF with a 1px Off-white
   stroke, radius 24, padding 12 all round, column gap 12; inside it the photo 136:2396 338 x 344
   radius 16 object-cover, then details 136:2397 (gap 8: eyebrow Functional 13/16 Primary, title
   Subtitle 2 16/20 two lines, price Body/Medium 16/22) and the Primary Default 44 CTA 136:2402
   full width, icon left, 12 under the price. Card height = 12 + 344 + 12 + 94 + 12 + 44 + 12 = 530.
   Geometry only: the type is the same tokens the desktop rules above already bind.
   NO TOKEN: #FCFCFF, radius 24 / 16, padding 12, gap 12 / 8 (GAPS §12 items 3 and 7). */
@media (max-width: 749px) {
  .miniso-product-card--feature {
    --_miniso-card-radius: 24px;
    /* 136:2395 */
    --_miniso-card-media-radius: 16px;
    /* 136:2396 */
    --_miniso-card-media-ratio: 338 / 344;
    /* 136:2396 */
    --_miniso-card-pad: 0px;
    /* 136:2397 has no padding of its own; the card's 12 does it */
    --_miniso-card-gap: 8px;
    /* 136:2397 gap 8, eyebrow -> title */
    --_miniso-card-price-gap: 8px;
    /* 136:2397 gap 8, title -> price */

    gap: 12px;
    /* 136:2395 column gap: photo -> details */
    padding: 12px;
    /* 136:2395 */
    border: 1px solid var(--miniso-offwhite);
    /* 136:2395 stroke Off-white #F5F5F5 */
    background-color: #fcfcff;
    /* 136:2395. NO TOKEN (neither Lavender/20 #FCFBFF nor white). */
  }

  /* 136:2396 object-cover on a 338 x 344 box - the one drawn exception to the card's contain
     (see __img above); the catalogue's square studio photos are not cropped by it. */
  .miniso-product-card--feature .miniso-product-card__img {
    object-fit: cover;
  }

  /* 136:2402 sits 12 under the price; the details column's 8px flex gap already applies before
     the CTA, so only the difference is added (same arithmetic as the price rule above). */
  .miniso-product-card--feature .miniso-product-card__cta {
    margin-top: 4px;
  }
}

/* ⚠️ ASSUMED (Iulia, README open question 1). No card state other than default is drawn anywhere -
   no hover fill, no focus ring, no sold-out or sale treatment (05-card-rows finding 8). The card
   is reached by keyboard through its title link, so the ring goes there, and it is wave 1's ring
   so there is exactly one focus look on this site. */
.miniso-product-card__link:focus-visible {
  outline: 2px solid var(--miniso-secondary);
  outline-offset: 2px;
}

/* ==========================================================================================
   5. SECTION HEAD - .miniso-section-head
   Source: COMPONENTS-WAVE-2 §2. PDP nodes 96:13026 / 96:13062 (desktop rows), 96:13924 /
   96:13949 (mobile rows), 96:13098 + 96:13100 + 96:13103 (reviews header, desktop),
   96:13974 + 96:13976 + 96:13979 (reviews header, mobile). Contract §10 already names the class
   off three homepage instances (36:3676, 36:3784, 96:8191).

   MODIFIER VOCABULARY
     (base)                        title only. THE PDP ROWS HAVE NO "see all" - 96:13026 is
                                   justify-between with a single child.
     .miniso-section-head--with-sub     second line under the title (Secondary/60)
     .miniso-section-head--with-action  trailing slot; it holds a .miniso-btn and nothing else
   ELEMENTS  __group  __title  __sub  __action

   ⚠️ ASSUMED - one heading size. Figma draws the reviews heading at h1 64/72 desktop but h3 24/28
   mobile (three steps apart, and neither breakpoint's h1 matches the other's), while the two card
   rows are h3 at both. Built as h3 everywhere - which is 06-customer-reviews' own proposal and
   the only reading in which the page's headings agree with each other.
   ========================================================================================== */

.miniso-section-head {
  --_miniso-head-gap: 6px;
  /* 96:13976 mobile title<->sub gap. NO TOKEN. */

  box-sizing: border-box;
  display: flex;
  gap: 16px;
  /* 96:13975 mobile shell gap; desktop 96:13098 is 32px. NO TOKEN. */
  align-items: center;
  justify-content: space-between;
  width: 100%;
  font-family: var(--miniso-font);
}

.miniso-section-head__group {
  display: flex;
  flex-direction: column;
  gap: var(--_miniso-head-gap);
  min-width: 0;
}

.miniso-section-head__title {
  margin: 0;
  color: var(--miniso-text);
  /* Secondary on every instance. */

  /* Display/H3, Fredoka SemiBold: 36/44 desktop, 24/28 mobile - one token, both. */
  font-size: var(--miniso-h3);
  font-weight: 600;
  line-height: var(--miniso-h3-lh);
}

.miniso-section-head__sub {
  display: none;
  /* --with-sub only */
  margin: 0;
  color: var(--miniso-secondary-60);
  /* 96:13102 Secondary/60. */

  /* paragraph regular 18/24 at both breakpoints. */
  font-size: var(--miniso-body-lg);
  font-weight: 400;
  line-height: var(--miniso-body-lg-lh);
}

.miniso-section-head--with-sub .miniso-section-head__sub {
  display: block;
}

.miniso-section-head__action {
  display: none;
  /* --with-action only */
  flex: 0 0 auto;
}

.miniso-section-head--with-action .miniso-section-head__action {
  display: flex;
}

@media (min-width: 750px) {
  .miniso-section-head {
    --_miniso-head-gap: 4px;
    /* 96:13100 desktop. */

    gap: 32px;
    /* 96:13098 desktop shell gap. */
  }
}

/* ==========================================================================================
   6. RATING - .miniso-rating
   Source: COMPONENTS-WAVE-2 §3, 03-buy-column §3, 06-customer-reviews §2 and §4.
   Nodes: buy column 96:12972..96:12984 (desktop) / 96:13879..96:13892 (mobile);
   summary 96:13108..96:13116 (desktop) / 96:13985..96:13993 (mobile);
   review card 96:13157..96:13165 (desktop) / 96:14035..96:14043 (mobile).

   MODIFIER VOCABULARY
     (base) = --md   buy-column rating. 15.75 x 14 stars, NO gap (96:12970).
     .miniso-rating--lg   summary. 20x20 gap 4 desktop, 18x18 gap 2 mobile.
     .miniso-rating--sm   review card. 14x14 gap 2 desktop, 12x12 gap 2 mobile.
   ELEMENTS  __stars  __star  __value
   AUTHOR INPUT  --miniso-rating-fill on each __star: 0% | 50% | 100% (or any %). Default 0%.
     For a 4.9: four stars at 100% and one at 90%. Compute it, never hardcode a picture of it.

   ⚠️ RESOLVED DESIGN DEFECT - the summary draws "4.9" beside FIVE EMPTY OUTLINE stars
   (96:13108, confirmed in img/06-reviews-summary-desktop.png), while the buy column draws five
   SOLID stars for the same 4.9 and no empty or partial star exists anywhere in the file
   (03-buy-column §3). A rating display that ignores its own score is not shippable, so this
   component always fills to the score. Both of Figma's artworks are kept and given the meaning
   they can only have: the OUTLINE star is the empty remainder, the SOLID star is the filled part,
   and a partial star is the solid one clipped. Question for Iulia - two artworks, five sizes.

   ⚠️ ONE GLYPH, NOT THREE. Figma ships three star artworks: solid 15.75x14 (assets/
   miniso-pdp-star-solid.svg), outline 20x20 stroke 2 (miniso-pdp-star-outline-20.svg) and
   outline 14x14 with a white fill (miniso-pdp-star-outline-14.svg). They are different shapes at
   different aspect ratios, so layering them would not register. Both masks below are the SAME
   20x20 path - filled for the score, stroked for the remainder - so the filled part always sits
   exactly inside the outline. The masks carry geometry only; every colour is a token.
   ========================================================================================== */

.miniso-rating {
  /* NO TOKEN: star box and gap. Sizes are Figma's, three of the five it draws - the other two
     (18 and 12) are the mobile halves of --lg / --sm and live in the geometry query below. */
  --_miniso-rating-size: 14px;
  /* 96:12970: 15.75 x 14 per star. Height is the box; the glyph
                                    keeps its own ratio inside it. NO TOKEN. */
  --_miniso-rating-gap: 0px;
  /* 96:12970 strip has NO gap. NO TOKEN. */

  --_miniso-star-solid: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath fill='%23000' d='M9.76788 1.73243C9.69793 1.77586 9.64152 1.83796 9.605 1.91174L7.68083 5.81091C7.55392 6.0678 7.36648 6.29001 7.13465 6.4584C6.90282 6.62679 6.63355 6.73633 6.35 6.77758L2.04583 7.40675C1.96392 7.41833 1.88691 7.45268 1.82356 7.5059C1.76022 7.55911 1.7131 7.62905 1.68757 7.70774C1.66203 7.78643 1.65911 7.87071 1.67914 7.95097C1.69916 8.03124 1.74133 8.10427 1.80083 8.16175L4.91417 11.1926C5.11967 11.3927 5.27341 11.6398 5.36211 11.9126C5.45082 12.1854 5.47183 12.4757 5.42333 12.7584L4.68917 17.0409C4.67488 17.1223 4.68373 17.2061 4.71471 17.2828C4.74569 17.3594 4.79756 17.4258 4.86441 17.4744C4.93126 17.523 5.01042 17.5519 5.09288 17.5578C5.17533 17.5637 5.25778 17.5463 5.33083 17.5076L9.17833 15.4842C9.4319 15.3511 9.71402 15.2815 10.0004 15.2815C10.2868 15.2815 10.5689 15.3511 10.8225 15.4842L14.6708 17.5076C14.7439 17.5465 14.8265 17.5641 14.9091 17.5583C14.9916 17.5526 15.071 17.5237 15.138 17.4751C15.2049 17.4264 15.2569 17.3599 15.2879 17.2831C15.3189 17.2064 15.3277 17.1224 15.3133 17.0409L14.5783 12.7576C14.5301 12.475 14.5512 12.1849 14.6399 11.9123C14.7286 11.6396 14.8822 11.3927 15.0875 11.1926L18.2008 8.16091C18.2598 8.10337 18.3016 8.03047 18.3213 7.95044C18.3411 7.87042 18.338 7.78647 18.3125 7.70808C18.287 7.6297 18.2401 7.56002 18.1771 7.50691C18.114 7.45381 18.0374 7.4194 17.9558 7.40758L13.6508 6.77758C13.3676 6.73601 13.0987 6.62633 12.8672 6.45796C12.6357 6.28959 12.4485 6.06755 12.3217 5.81091L10.3967 1.91174C10.3601 1.83796 10.3037 1.77586 10.2338 1.73243C10.1638 1.68901 10.0832 1.666 10.0008 1.666C9.91851 1.666 9.83782 1.68901 9.76788 1.73243Z'/%3E%3C/svg%3E");
  --_miniso-star-outline: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' d='M9.76788 1.73243C9.69793 1.77586 9.64152 1.83796 9.605 1.91174L7.68083 5.81091C7.55392 6.0678 7.36648 6.29001 7.13465 6.4584C6.90282 6.62679 6.63355 6.73633 6.35 6.77758L2.04583 7.40675C1.96392 7.41833 1.88691 7.45268 1.82356 7.5059C1.76022 7.55911 1.7131 7.62905 1.68757 7.70774C1.66203 7.78643 1.65911 7.87071 1.67914 7.95097C1.69916 8.03124 1.74133 8.10427 1.80083 8.16175L4.91417 11.1926C5.11967 11.3927 5.27341 11.6398 5.36211 11.9126C5.45082 12.1854 5.47183 12.4757 5.42333 12.7584L4.68917 17.0409C4.67488 17.1223 4.68373 17.2061 4.71471 17.2828C4.74569 17.3594 4.79756 17.4258 4.86441 17.4744C4.93126 17.523 5.01042 17.5519 5.09288 17.5578C5.17533 17.5637 5.25778 17.5463 5.33083 17.5076L9.17833 15.4842C9.4319 15.3511 9.71402 15.2815 10.0004 15.2815C10.2868 15.2815 10.5689 15.3511 10.8225 15.4842L14.6708 17.5076C14.7439 17.5465 14.8265 17.5641 14.9091 17.5583C14.9916 17.5526 15.071 17.5237 15.138 17.4751C15.2049 17.4264 15.2569 17.3599 15.2879 17.2831C15.3189 17.2064 15.3277 17.1224 15.3133 17.0409L14.5783 12.7576C14.5301 12.475 14.5512 12.1849 14.6399 11.9123C14.7286 11.6396 14.8822 11.3927 15.0875 11.1926L18.2008 8.16091C18.2598 8.10337 18.3016 8.03047 18.3213 7.95044C18.3411 7.87042 18.338 7.78647 18.3125 7.70808C18.287 7.6297 18.2401 7.56002 18.1771 7.50691C18.114 7.45381 18.0374 7.4194 17.9558 7.40758L13.6508 6.77758C13.3676 6.73601 13.0987 6.62633 12.8672 6.45796C12.6357 6.28959 12.4485 6.06755 12.3217 5.81091L10.3967 1.91174C10.3601 1.83796 10.3037 1.77586 10.2338 1.73243C10.1638 1.68901 10.0832 1.666 10.0008 1.666C9.91851 1.666 9.83782 1.68901 9.76788 1.73243Z'/%3E%3C/svg%3E");

  box-sizing: border-box;
  display: inline-flex;
  gap: 8px;
  /* 96:12969 stars <-> text. Figma spacing token 2. NO TOKEN in layer 1. */
  align-items: center;
  font-family: var(--miniso-font);
}

.miniso-rating__stars {
  display: inline-flex;
  flex: 0 0 auto;
  gap: var(--_miniso-rating-gap);
  align-items: center;
}

.miniso-rating__star {
  position: relative;
  display: block;
  flex: 0 0 auto;
  width: var(--_miniso-rating-size);
  height: var(--_miniso-rating-size);
}

/* The empty remainder: Figma's outline star, Primary, stroke 2. */
.miniso-rating__star::before {
  content: '';
  position: absolute;
  inset: 0;
  background-color: var(--miniso-primary);
  -webkit-mask: var(--_miniso-star-outline) center / contain no-repeat;
  mask: var(--_miniso-star-outline) center / contain no-repeat;
}

/* The score: Figma's solid star, Primary, clipped left-to-right by the author's fill value. */
.miniso-rating__star::after {
  content: '';
  position: absolute;
  inset: 0;
  background-color: var(--miniso-primary);
  -webkit-mask: var(--_miniso-star-solid) center / contain no-repeat;
  mask: var(--_miniso-star-solid) center / contain no-repeat;
  clip-path: inset(0 calc(100% - var(--miniso-rating-fill, 0%)) 0 0);
}

.miniso-rating__value {
  color: var(--miniso-text);
  /* 96:12987: BOTH runs black, no weight or colour difference. */

  /* Functional, Fredoka Regular: 14/18 desktop, 13/16 mobile. */
  font-size: var(--miniso-body-fn);
  font-weight: 400;
  line-height: var(--miniso-body-fn-lh);
  white-space: nowrap;
}

.miniso-rating--lg {
  --_miniso-rating-size: 18px;
  /* 96:13985 mobile. NO TOKEN. */
  --_miniso-rating-gap: 2px;
  /* 96:13984 mobile. NO TOKEN. */
}

.miniso-rating--sm {
  --_miniso-rating-size: 12px;
  /* 96:14035 mobile. NO TOKEN. */
  --_miniso-rating-gap: 2px;
  /* 96:14034 mobile. NO TOKEN. */
}

/* GEOMETRY ONLY. Figma draws five star sizes on one page; these are the desktop halves. */
@media (min-width: 750px) {
  .miniso-rating--lg {
    --_miniso-rating-size: 20px;
    /* 96:13108. */
    --_miniso-rating-gap: 4px;
    /* 96:13107. */
  }

  .miniso-rating--sm {
    --_miniso-rating-size: 14px;
    /* 96:13157. */
    --_miniso-rating-gap: 2px;
    /* 96:13156. */
  }
}

/* ==========================================================================================
   7. QUANTITY STEPPER - .miniso-stepper
   Source: COMPONENTS-WAVE-2 §4, 03-buy-column §5. Nodes 96:12992 (desktop 106x44),
   96:13899 (mobile, full width, space-between), 96:14554 (sticky bar, 106x36).

   THIS IS NOT .miniso-input, and that is a measured statement, not a preference: 44/36px vs the
   input's fixed 48px, radius 100 vs --miniso-radius-input 24, border #EDEDED vs Aqua/20. Same
   tokens, different component.

   MODIFIER VOCABULARY
     (base)                     106 x 44 pill
     .miniso-stepper--sm        height 36 (sticky bar, 96:14554)
     .miniso-stepper--block     width 100% + space-between (mobile buy column, 96:13899).
                                A layout modifier, not a size one - they compose.
   ELEMENTS  __minus  __value  __plus
   THE +/- BUTTONS ARE .miniso-btn:
     .miniso-btn.miniso-btn--ghost.miniso-btn--muted.miniso-btn--sm.miniso-btn--icon-only
   16x16 frame, 16x16 artwork, Secondary/60 ink. Never a stepper-local button.

   ⚠️ THREE DEFECTS IN THE FIGMA COMPONENT, none replicated, all questions for Iulia:
   1. the value is set in Info Display Pro Book - a font that is not in 02-typography.md and is
      not licensed on this project. Built in Fredoka at the token that matches its 18/24.
   2. its colour is #171717, in no palette group. Built --miniso-text (#000000), the nearest.
   3. the +/- artwork is 20x20 inside a 16x16 frame - the same overflow defect already recorded
      for Ghost/Small in 03-buttons-cta.md. Draw 16x16 artwork; wave 1 already does.
   ========================================================================================== */

.miniso-stepper {
  --_miniso-stepper-h: 44px;
  /* 96:12992. NO TOKEN. */

  box-sizing: border-box;
  display: inline-flex;
  gap: 0;
  align-items: center;
  min-height: var(--_miniso-stepper-h);
  padding: 1px 13px;
  /* 96:12992. 13 + 16 + 48 + 16 + 13 = 106, the drawn width. NO TOKEN. */
  border: 1px solid #ededed;
  /* NO TOKEN: Figma Neutral/Ligh Grey, a bound variable that is absent
                                from the 24-token colour sheet. Finding 5. */
  border-radius: var(--miniso-radius-pill);
  /* Figma 100px; 999 is visually identical on a 44px box. */
  background-color: #ffffff;
  /* NO TOKEN: white. Finding 1. */
  font-family: var(--miniso-font);
}

.miniso-stepper * {
  box-sizing: border-box;
}

.miniso-stepper__value {
  display: block;
  flex: 0 0 auto;
  width: 48px;
  /* I96:12992;266:7921. NO TOKEN. */
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  appearance: none;
  -webkit-appearance: none;
  color: var(--miniso-text);
  /* defect 2: Figma #171717, no palette entry. Nearest is Secondary. */
  text-align: center;

  /* defect 1: Figma is Info Display Pro Book 18/24. Fredoka Regular at paragraph regular is the
     token that matches that size and weight. ⚠️ At >=990 layer 1 raises paragraph regular to
     20/28 while the desktop frame draws 18/24 - a 2px drift accepted to stay tokenised. */
  font-family: var(--miniso-font);
  font-size: var(--miniso-body-lg);
  font-weight: 400;
  line-height: var(--miniso-body-lg-lh);
  font-variation-settings: 'wdth' 100;
}

.miniso-stepper__value:focus-visible {
  outline: 2px solid var(--miniso-secondary);
  /* ⚠️ ASSUMED, wave-1 ring: the value can be an input. */
  outline-offset: 2px;
}

.miniso-stepper--sm {
  --_miniso-stepper-h: 36px;
  /* 96:14554. NO TOKEN. */
}

.miniso-stepper--block {
  display: flex;
  justify-content: space-between;
  width: 100%;
  /* 96:13899: 362px full width, minus / value / plus pushed apart. */
}

/* --xs: the cart drawer's stepper 178:23594 (harvested 04.09): 96 x 32, padding 1/13, the same
   16px +/- buttons. Drawn 96 wide where 13+16+48+16+13 = 106: the value column is 38 there (Figma
   pulls the buttons in with -5 margins). NO TOKEN. */
.miniso-stepper--xs {
  --_miniso-stepper-h: 32px;
}

.miniso-stepper--xs .miniso-stepper__value,
.miniso-stepper--xs input[type='number'] {
  width: 38px;
}

/* Horizon's quantity-selector DOM wearing .miniso-stepper (PDP blocks/quantity.liquid, cart page
   and cart drawer snippets/cart-products.liquid). The component paints the pill; these rules make
   Horizon's own buttons and input take the component's slots. One mapping for the three surfaces
   (sections/product-information.liquid carries an older .miniso-pdp-scoped copy of it). */
.quantity-selector.miniso-stepper {
  display: inline-flex;
  height: var(--_miniso-stepper-h);
  min-height: var(--_miniso-stepper-h);
  width: auto;
}

/* ROOT CAUSE of "the quantity button is not fully rounded" (Iulia 133080.3, 07.09):
   snippets/quantity-selector-styles.liquid:54 sets
     `.product-details .quantity-selector, .quick-add-modal .quantity-selector
        { border-radius: var(--style-border-radius-buttons-primary) }`
   - specificity (0,2,0). `.miniso-stepper`'s own `border-radius: var(--miniso-radius-pill)` is
   only (0,1,0), and `.quantity-selector.miniso-stepper` above never set the property, so Horizon's
   theme-setting radius won on every PDP and quick-add stepper. The drawn value is 100 (96:12992).
   Re-declared here at the same shape as Horizon's rule plus the component class, so it wins in
   both surfaces without touching Horizon's file. The +/- buttons get the same corners because
   Horizon rounds their outer edges from the same variable (lines 44-51 there).

   Same cascade accident, same element, one line further up in Horizon's file: `.quantity-selector`
   (line 11 there) sets `border: var(--style-border-width-inputs) solid var(--color-input-border)`
   at (0,1,0) - the SAME specificity as `.miniso-stepper`'s `1px solid #ededed`, and it is emitted
   later, so the stepper wore Horizon's #DFDFDF input border (measured 07.09) instead of the drawn
   Neutral/Ligh Grey. Restated here with the component class so the drawn border holds too. */
.product-details .quantity-selector.miniso-stepper,
.quick-add-modal .quantity-selector.miniso-stepper,
.quantity-selector.miniso-stepper {
  border: 1px solid #ededed;
  /* 96:12992. NO TOKEN (finding 5). */
  border-radius: var(--miniso-radius-pill);
}

.product-details .quantity-selector.miniso-stepper .quantity-minus,
.product-details .quantity-selector.miniso-stepper .quantity-plus,
.quick-add-modal .quantity-selector.miniso-stepper .quantity-minus,
.quick-add-modal .quantity-selector.miniso-stepper .quantity-plus,
.quantity-selector.miniso-stepper .quantity-minus,
.quantity-selector.miniso-stepper .quantity-plus {
  border-radius: var(--miniso-radius-pill);
}

.quantity-selector.miniso-stepper input[type='number'] {
  width: 48px;
  /* I96:12992;266:7921. NO TOKEN. */
  height: auto;
  min-height: 0;
  max-width: none;
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  box-shadow: none;
  color: var(--miniso-text);
  text-align: center;
  font-family: var(--miniso-font);
  font-size: var(--miniso-body-lg);
  font-weight: 400;
  line-height: var(--miniso-body-lg-lh);
  -moz-appearance: textfield;
}

.quantity-selector.miniso-stepper input[type='number']::-webkit-outer-spin-button,
.quantity-selector.miniso-stepper input[type='number']::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

.quantity-selector.miniso-stepper .quantity-minus,
.quantity-selector.miniso-stepper .quantity-plus {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  min-width: 0;
  min-height: 0;
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: var(--miniso-radius-pill);
  background: none;
  box-shadow: none;
  color: var(--miniso-secondary-60);
  /* the +/- ink (wave 1 stepper). */
}

.quantity-selector.miniso-stepper .quantity-minus:hover,
.quantity-selector.miniso-stepper .quantity-plus:hover {
  background: none;
  color: var(--miniso-primary);
  /* Ghost hover. */
}

.quantity-selector.miniso-stepper .quantity-minus:disabled,
.quantity-selector.miniso-stepper .quantity-plus:disabled {
  color: var(--miniso-secondary-20);
  cursor: default;
}

.quantity-selector.miniso-stepper .svg-wrapper,
.quantity-selector.miniso-stepper .quantity-minus svg,
.quantity-selector.miniso-stepper .quantity-plus svg {
  width: 16px;
  height: 16px;
}

.quantity-selector.miniso-stepper .quantity-minus:focus-visible,
.quantity-selector.miniso-stepper .quantity-plus:focus-visible {
  outline: 2px solid var(--miniso-secondary);
  outline-offset: 2px;
}

/* In the cart (page and drawer) Horizon's snippets/cart-products.liquid pins the selector to
   105 x --button-size-md with 0,2,0 / 0,3,0 selectors and its own square border; the component
   has to out-rank them, so the same slots are restated under .cart-items. */
.cart-items .quantity-selector.miniso-stepper {
  width: auto;
  height: var(--_miniso-stepper-h);
  min-height: var(--_miniso-stepper-h);
  padding: 1px 13px;
  border: 1px solid #ededed;
  /* NO TOKEN - finding 5, same as the base rule. */
  border-radius: var(--miniso-radius-pill);
  background-color: #ffffff;
  /* NO TOKEN - finding 1. */
  box-shadow: none;
}

.cart-items .quantity-selector.miniso-stepper .quantity-minus,
.cart-items .quantity-selector.miniso-stepper .quantity-plus {
  width: 16px;
  height: 16px;
}

.cart-items .quantity-selector.miniso-stepper input[type='number'] {
  max-width: none;
  width: 48px;
  border: 0;
  background: none;
  box-shadow: none;
}

.cart-items .quantity-selector.miniso-stepper--xs input[type='number'] {
  width: 38px;
}

/* ==========================================================================================
   8. ACCORDION - .miniso-accordion
   Source: COMPONENTS-WAVE-2 §5, 04-description-and-accordions. Nodes 96:13015 (Specifications)
   and 96:13020 (Returns) desktop; 96:13917 / 96:13920 mobile.

   Pairs with blocks/accordion.liquid + blocks/_accordion-row.liquid, already in the theme and
   already on _product-details' allowed child list (icon: plus, dividers on). Restyle that, do not
   rebuild it. The markup below also works on a plain <details>/<summary>.

   MODIFIER VOCABULARY
     (base)                       collapsed row
     .miniso-accordion--open      forced open, for docs and for a server-rendered open row.
                                  Live state is <details open>, which is handled too.
   ELEMENTS  __item  __summary  __label  __icon  __panel

   ⚠️ ASSUMED - EVERY ROW GETS ITS TOP BORDER. Desktop draws the 1px #EDEDED rule on Returns only
   (96:13020) and nothing above Description or Specifications; mobile draws it on all three
   (96:13910 / 96:13917 / 96:13920). A rule that appears between rows 2 and 3 but not between 1
   and 2 is a slip, not a pattern - the mobile reading is built. 04 finding 2.

   ⚠️ ASSUMED - THE OPEN STATE. HARVEST GAP: no expanded state, no minus icon and no panel exist
   in any of the three frames (04 finding 4). The + is rotated 45 degrees when open, which needs
   no second artwork and cannot contradict a drawing that does not exist. The panel type is
   paragraph smaller in Neutral-grey, i.e. the same treatment as the description body (96:13010).
   ========================================================================================== */

.miniso-accordion {
  --_miniso-accordion-icon: 24px;
  /* 96:13919 mobile is 24x24. NO TOKEN. */

  box-sizing: border-box;
  display: block;
  width: 100%;
  font-family: var(--miniso-font);
}

.miniso-accordion * {
  box-sizing: border-box;
}

.miniso-accordion__item {
  display: block;
  border-top: 1px solid #ededed;
  /* NO TOKEN: Neutral/Ligh Grey. Finding 5. 96:13020. */
}

.miniso-accordion__summary {
  display: flex;
  gap: 16px;
  align-items: center;
  justify-content: space-between;
  /* 96:13015. */
  width: 100%;
  margin: 0;
  padding: 16px 0;
  /* 96:13015 / 96:13917. NO TOKEN. */
  border: 0;
  background: none;
  appearance: none;
  -webkit-appearance: none;
  cursor: pointer;
  list-style: none;
  /* kills the native <summary> triangle */
  text-align: left;
}

.miniso-accordion__summary::-webkit-details-marker {
  display: none;
}

.miniso-accordion__label {
  color: var(--miniso-primary);
  /* 96:13018 / 96:13023: the label is PRIMARY, not black. */

  /* Heading/Subtitle 2, Fredoka Medium: 20/24 desktop, 16/20 mobile - one token, both. */
  font-size: var(--miniso-sub2);
  font-weight: 500;
  line-height: var(--miniso-sub2-lh);
}

.miniso-accordion__icon {
  display: block;
  flex: 0 0 auto;
  width: var(--_miniso-accordion-icon);
  height: var(--_miniso-accordion-icon);
  color: var(--miniso-primary);
  /* plus glyph stroke #D93832 = Primary. */
}

.miniso-accordion__icon>svg {
  display: block;
  width: 100%;
  height: 100%;
}

.miniso-accordion__panel {
  display: none;
  padding: 0 0 16px;
  /* ⚠️ ASSUMED: no panel is drawn. Matches the row's own 16px rhythm. */
  color: var(--miniso-secondary-60);
  /* ⚠️ the description body is Neutral/Grey #706F6F, which has
                                        NO layer-1 token; Secondary/60 #7A7C7D is the nearest and
                                        the one Figma itself binds on the sibling promise line. */
  font-size: var(--miniso-body-sm);
  font-weight: 400;
  line-height: var(--miniso-body-sm-lh);
}

.miniso-accordion__item[open] .miniso-accordion__panel,
.miniso-accordion--open .miniso-accordion__panel,
.miniso-accordion__item--open .miniso-accordion__panel {
  display: block;
}

/* ⚠️ ASSUMED open affordance - see the block comment. No motion: Figma records none. */
.miniso-accordion__item[open] .miniso-accordion__icon,
.miniso-accordion--open .miniso-accordion__icon,
.miniso-accordion__item--open .miniso-accordion__icon {
  transform: rotate(45deg);
}

/* ⚠️ ASSUMED (Iulia, README open question 1): no focus state is drawn on the accordion either. */
.miniso-accordion__summary:focus-visible {
  outline: 2px solid var(--miniso-secondary);
  outline-offset: 2px;
}

/* GEOMETRY ONLY: the + is 24x24 mobile (96:13919) and 20x20 desktop (96:13019) - the affordance
   gets SMALLER on the bigger screen. Built as drawn; flagged in the report. */
@media (min-width: 750px) {
  .miniso-accordion {
    --_miniso-accordion-icon: 20px;
  }
}

/* ==========================================================================================
   9. REVIEW CARD - .miniso-review-card
   Source: COMPONENTS-WAVE-2 §6, 06-customer-reviews §4. Nodes 96:13152 (desktop card 1, read to
   the leaves), 96:14030 (mobile card 1, read to the leaves); cards 2-4 at both breakpoints read
   at metadata level and identical apart from body height.

   MODIFIER VOCABULARY
     (base)                     responsive: mobile set below 750px, desktop set from 750px
     .miniso-review-card--sm    force the compact set at every width (a narrow column on desktop)
   ELEMENTS  __head  __who  __name  __date  __title  __body  __helpful  __actions  __vote  __count
   The star row is .miniso-rating--sm. Nothing else in layer 2 applies.

   ⚠️ ASSUMED - THE TITLE IS MEDIUM WEIGHT AT BOTH BREAKPOINTS. Desktop draws the title in Fredoka
   Regular at paragraph smaller (96:13168), i.e. pixel-identical to the body underneath it, so
   there is no title. Mobile draws Fredoka Medium at Subtitle 3 (96:14046). Desktop is the slip;
   06's own proposal (Medium) is built, at Subtitle 2 desktop / Subtitle 3 mobile.

   ⚠️ HARVEST GAP: the thumbs have no pressed / voted / counts-changed state anywhere. Nothing is
   invented here - a voted state needs Iulia. The vote control is still a real button with a focus
   ring, because it is keyboard-reachable whether or not its pressed look is designed.
   ========================================================================================== */

.miniso-review-card {
  --_miniso-review-pad: 16px;
  /* 96:14030 mobile. NO TOKEN. */
  --_miniso-review-gap: 12px;
  /* 96:14030 mobile. NO TOKEN. */
  --_miniso-review-name: var(--miniso-sub3);
  /* 96:14033 mobile Subtitle 3. */
  --_miniso-review-name-lh: var(--miniso-sub3-lh);
  --_miniso-review-title: var(--miniso-sub3);
  /* 96:14046 mobile Subtitle 3. */
  --_miniso-review-title-lh: var(--miniso-sub3-lh);
  --_miniso-review-helpful-gap: 12px;
  /* 96:14048 mobile. NO TOKEN. */

  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  gap: var(--_miniso-review-gap);
  padding: var(--_miniso-review-pad);
  border-radius: 16px;
  /* 96:13152 / 96:14030. NO TOKEN (radius 16 of seven). */
  background-color: var(--miniso-lavender-20);
  /* Lavender/20 #FCFBFF - a real token. */
  font-family: var(--miniso-font);
}

.miniso-review-card * {
  box-sizing: border-box;
}

.miniso-review-card__head {
  display: flex;
  gap: 12px;
  align-items: center;
  justify-content: space-between;
  /* 96:13153. */
}

.miniso-review-card__who {
  display: flex;
  gap: 12px;
  /* 96:13154 desktop; 8px mobile (96:14032). One value; 4px, flagged. NO TOKEN. */
  align-items: center;
  min-width: 0;
}

.miniso-review-card__name {
  margin: 0;
  color: var(--miniso-text);
  font-size: var(--_miniso-review-name);
  font-weight: 500;
  /* Fredoka Medium, both breakpoints. */
  line-height: var(--_miniso-review-name-lh);
}

.miniso-review-card__date {
  flex: 0 0 auto;
  color: var(--miniso-secondary-40);
  /* 96:13167 Secondary/40 - a real token. */
  font-size: var(--miniso-body-fn);
  font-weight: 400;
  line-height: var(--miniso-body-fn-lh);
  white-space: nowrap;
}

.miniso-review-card__title {
  margin: 0;
  color: var(--miniso-text);
  font-size: var(--_miniso-review-title);
  font-weight: 500;
  /* ⚠️ ASSUMED Medium - see the block comment. */
  line-height: var(--_miniso-review-title-lh);
}

.miniso-review-card__body {
  margin: 0;
  color: var(--miniso-text);
  /* 96:13169 Secondary. */
  font-size: var(--miniso-body-sm);
  /* paragraph smaller: 16/22 desktop, 14/20 mobile. */
  font-weight: 400;
  line-height: var(--miniso-body-sm-lh);
}

.miniso-review-card__helpful {
  display: flex;
  gap: var(--_miniso-review-helpful-gap);
  align-items: center;
  color: var(--miniso-secondary-40);
  /* "Was this helpful?" Secondary/40. */
  font-size: var(--miniso-body-fn);
  font-weight: 400;
  line-height: var(--miniso-body-fn-lh);
}

.miniso-review-card__actions {
  display: flex;
  gap: 12px;
  /* 96:13172, both breakpoints. NO TOKEN. */
  align-items: center;
}

.miniso-review-card__vote {
  display: inline-flex;
  gap: 4px;
  /* 96:13173 vote gap. NO TOKEN. */
  align-items: center;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  appearance: none;
  -webkit-appearance: none;
  color: var(--miniso-secondary-40);
  /* thumbs stroke #A6A7A9 = Secondary/40, and the counts too. */
  cursor: pointer;
  font: inherit;
}

.miniso-review-card__vote>svg {
  display: block;
  flex: 0 0 auto;
  width: 16px;
  /* 96:13174 / 96:13178. NO TOKEN. */
  height: 16px;
}

/* ⚠️ ASSUMED (Iulia, README open question 1): no focus state drawn. Wave-1 ring. */
.miniso-review-card__vote:focus-visible {
  outline: 2px solid var(--miniso-secondary);
  outline-offset: 2px;
}

/* GEOMETRY + the two type ROLES that change with it. No token is redefined; the two custom
   properties above simply point at a different existing token, which is the whole reason they
   exist. 96:13152: padding 24, gap 16, name Subtitle 2, helpful gap 16. */
@media (min-width: 750px) {
  .miniso-review-card {
    --_miniso-review-pad: 24px;
    --_miniso-review-gap: 16px;
    --_miniso-review-name: var(--miniso-sub2);
    --_miniso-review-name-lh: var(--miniso-sub2-lh);
    --_miniso-review-title: var(--miniso-sub2);
    --_miniso-review-title-lh: var(--miniso-sub2-lh);
    --_miniso-review-helpful-gap: 16px;
  }
}

.miniso-review-card--sm {
  --_miniso-review-pad: 16px;
  --_miniso-review-gap: 12px;
  --_miniso-review-name: var(--miniso-sub3);
  --_miniso-review-name-lh: var(--miniso-sub3-lh);
  --_miniso-review-title: var(--miniso-sub3);
  --_miniso-review-title-lh: var(--miniso-sub3-lh);
  --_miniso-review-helpful-gap: 12px;
}


/* ==========================================================================================
   10. METER - .miniso-meter
   Source: COMPONENTS-WAVE-2 §7, 06-customer-reviews §2 "Distribution list".
   Ten drawings (5 rows x 2 breakpoints): desktop 96:13118..96:13121, mobile 96:13996.

   MODIFIER VOCABULARY
     (base)                 responsive: label 56 / count 36 below 750px, 64 / 48 from 750px
     .miniso-meter--sm      force the narrow label/count columns at every width
   ELEMENTS  __label  __track  __fill  __count
   AUTHOR INPUT  --miniso-meter-fill on __fill: count / max(count) as a percentage. Default 0%.

   ⚠️ THE DRAWN BAR WIDTHS ARE MOCK AND DISAGREE WITH THEMSELVES. Desktop's 4-star bar is 8% of
   its track, mobile's is 15%, and the printed counts (210/25/8/3/1 of 247) are 85 / 10.1 / 3.2 /
   1.2 / 0.4%. None of the three agree, so no width is hardcoded here: the author computes
   count / max(count). 06 §Distribution list.

   ⚠️ Desktop row 5's track is #ffffff while rows 4-1 are Off-white (96:13121); mobile is
   Off-white on all five. Desktop is the slip - Off-white is built for every row. 06 finding 3.
   ========================================================================================== */

.miniso-meter {
  --_miniso-meter-label-w: 56px;
  /* 96:13996 mobile. NO TOKEN. */
  --_miniso-meter-count-w: 36px;
  /* 96:13996 mobile. NO TOKEN. */

  box-sizing: border-box;
  display: flex;
  gap: 12px;
  /* 96:13119 row gap, both breakpoints. NO TOKEN. */
  align-items: center;
  width: 100%;
  font-family: var(--miniso-font);
}

.miniso-meter * {
  box-sizing: border-box;
}

.miniso-meter__label {
  flex: 0 0 auto;
  width: var(--_miniso-meter-label-w);
  color: var(--miniso-text);
  font-size: var(--miniso-body-sm);
  /* paragraph smaller: 16/22 desktop, 14/20 mobile. */
  font-weight: 400;
  line-height: var(--miniso-body-sm-lh);
}

.miniso-meter__track {
  flex: 1 1 0;
  min-width: 0;
  height: 8px;
  /* 96:13120. NO TOKEN. */
  overflow: clip;
  border-radius: var(--miniso-radius-pill);
  /* Figma 9999px. */
  background-color: var(--miniso-offwhite);
  /* Off-white #F5F5F5 - a real token. */
}

.miniso-meter__fill {
  display: block;
  width: var(--miniso-meter-fill, 0%);
  height: 100%;
  border-radius: var(--miniso-radius-pill);
  background-color: var(--miniso-primary);
}

.miniso-meter__count {
  flex: 0 0 auto;
  width: var(--_miniso-meter-count-w);
  color: var(--miniso-text);
  font-size: var(--miniso-body-sm);
  font-weight: 400;
  line-height: var(--miniso-body-sm-lh);
  text-align: right;
}

/* GEOMETRY ONLY. 96:13118 desktop: label 64, count 48. */
@media (min-width: 750px) {
  .miniso-meter {
    --_miniso-meter-label-w: 64px;
    --_miniso-meter-count-w: 48px;
  }
}

.miniso-meter--sm {
  --_miniso-meter-label-w: 56px;
  --_miniso-meter-count-w: 36px;
}

/* --xs: the cart drawer's free-shipping bar 178:23674 (harvested 04.09): a bare 6px track in
   Neutral/Ligh Grey with a Primary fill and a 14px white knob (1px Secondary border) riding the
   fill's end (238:24140). No label / count columns - the author renders __track + __fill + __knob
   only. NO TOKEN: #EDEDED (finding 5), the 6px height, the 14px knob. */
.miniso-meter--xs {
  gap: 0;
  padding-inline: 7px;
  /* room for the knob at 0% and 100%; it is 14 wide, centred on the end. */
}

.miniso-meter--xs .miniso-meter__track {
  height: 6px;
  overflow: visible;
  background-color: #ededed;
  /* NO TOKEN - finding 5. */
}

.miniso-meter--xs .miniso-meter__fill {
  position: relative;
  overflow: visible;
}

.miniso-meter__knob {
  display: none;
}

.miniso-meter--xs .miniso-meter__knob {
  display: block;
  position: absolute;
  top: 50%;
  right: 0;
  width: 14px;
  height: 14px;
  border: 1px solid var(--miniso-secondary);
  border-radius: var(--miniso-radius-pill);
  background-color: #ffffff;
  /* NO TOKEN: white. Finding 1. */
  transform: translate(50%, -50%);
}


/* ==========================================================================================
   11. STICKY BAR - .miniso-sticky-bar
   Source: COMPONENTS-WAVE-2 §8, 07-sticky-bar.md. ONE node: 96:14543 sticky-banner-mobile, in
   frame 96:14218 pdp-mobile-scroll, read to the leaves. Screenshot img/07-sticky-bar-mobile.png.

   ⚠️ CORRECTED 01.09 - THE DESKTOP BAR DOES EXIST: `96:13805 sticky-banner-desktop`, 1512 x 104,
   full page width. An earlier note here said it did not, because get_metadata on 96:12873 returns
   only three children. That was wrong: the node lives on a DIFFERENT Figma page, and the MCP page
   listing returns only "Styleguide" for this file even while the UI shows four pages - so absence
   from a listing is NOT evidence of absence. Razvan supplied the node id. Both breakpoints are now
   built from measured nodes.

   MODIFIER VOCABULARY
     (base)                        position: fixed at the bottom of the viewport
     .miniso-sticky-bar--static    position: static - for the gallery, docs, and any place the bar
                                   must sit in the flow. Not a design variant.
   ELEMENTS  __info  __group  __thumb  __text  __title  __eyebrow  __price  __actions  __cta
   THE CTA IS .miniso-btn:
     .miniso-btn.miniso-btn--primary.miniso-btn--sm.miniso-btn--icon-left.miniso-btn--label-lg
     .miniso-btn--block   (96:14556 is an exact match for primary/sm/icon-left; --label-lg carries
     the 16/20 label the node draws where Subtitle 3 is 14/18; --block fills the 253px slot.)
   The stepper is .miniso-stepper.miniso-stepper--sm (96:14554).

   VISIBILITY IS NOT CSS'S JOB HERE. Iulia's annotation: the bar appears once the user scrolls
   past the MAIN Add to cart. That is an IntersectionObserver on that button, not a pixel offset -
   the buy column's height varies with title length, promises and whether a description exists
   (94% of the catalogue had none at the 27.08 audit). The component ships with no hidden state
   and no transition because none is drawn (07 finding 8); the section toggles [hidden].
   ========================================================================================== */

.miniso-sticky-bar {
  box-sizing: border-box;
  position: fixed;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 10;
  /* ⚠️ ASSUMED: Figma has no z-index. 10 = the header drawer's layer in
                  aico-header-miniso.css, i.e. above the page and below the sticky header (100). */
  display: flex;
  flex-direction: column;
  gap: 12px;
  /* 96:14543. NO TOKEN. */

  /* 96:14543 padding 12px 12px 32px. The 32px reserves the iOS home indicator (the 34px
     iOS Indicator Bar frame 96:14146 sits under it). ⚠️ ASSUMED: max() so a device that reports a
     real inset gets it and everything else keeps the drawn 32px. Figma cannot express env(). */
  padding: 12px 12px max(32px, env(safe-area-inset-bottom));
  border-top: 1px solid #eee;
  /* NO TOKEN: raw hex, and a THIRD grey next to #EDEDED and #F5F5F5.
                                 Finding 6. */
  background-color: #ffffff;
  /* NO TOKEN: white. Finding 1. */

  /* ⚠️ THE ONLY SHADOW IN THE DESIGN SYSTEM. Layer 1 states "The file carries no shadows at all.
     Do not add any." - and this node carries drop-shadow(0 -4px 6px rgba(0,0,0,0.03)). Built as
     measured, as box-shadow (identical on a rectangle, and it does not rasterise children).
     NO TOKEN, and no token invented: layer 1 is not edited on this build's authority.
     Either that comment is stale or the bar is the one exception. Iulia decides. Finding 9. */
  box-shadow: 0 -4px 6px rgb(0 0 0 / 3%);
  font-family: var(--miniso-font);
}

.miniso-sticky-bar * {
  box-sizing: border-box;
}

.miniso-sticky-bar--static {
  position: static;
  z-index: auto;
}

.miniso-sticky-bar__info {
  display: flex;
  gap: 12px;
  align-items: flex-start;
  /* 96:14544. */
  justify-content: space-between;
  width: 100%;
}

/* 96:14545: the thumbnail and the text column are ONE group, 4px apart, and the price is the
   other side of the space-between. Without this group the free space lands between the thumb and
   the title too. */
.miniso-sticky-bar__group {
  display: flex;
  gap: 4px;
  /* 96:14545. NO TOKEN. */
  align-items: center;
  min-width: 0;
}

.miniso-sticky-bar__thumb {
  display: block;
  flex: 0 0 auto;
  width: 48px;
  /* 96:14546. NO TOKEN. */
  height: 48px;
  border-radius: 8px;
  /* 96:14546. NO TOKEN (radius 8 of seven). */
  object-fit: cover;
}

.miniso-sticky-bar__text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  /* 96:14547. NO TOKEN. */
  min-width: 0;
}

.miniso-sticky-bar__title {
  overflow: hidden;
  margin: 0;

  /* NO TOKEN: #1a1a1a, raw hex with no variable bound - unlike every other text node on this page.
     Finding 8. Built as measured. */
  color: #1a1a1a;
  /* NO TOKEN - finding 8, see the comment above. */

  /* 96:14548: Fredoka Medium, raw 16px on line-height var(Subtitle 2) = 20px. At mobile that IS
     Subtitle 2, so the token is exact here; at desktop it grows with the scale. */
  font-size: var(--miniso-sub2);
  font-weight: 500;
  line-height: var(--miniso-sub2-lh);
  text-overflow: ellipsis;
  white-space: nowrap;
  /* 96:14548 truncates at 214px. */
}

.miniso-sticky-bar__eyebrow {
  margin: 0;
  color: #757575;
  /* NO TOKEN: raw hex, a FOURTH grey on this page. Finding 7. Built as measured. */
  font-size: var(--miniso-body-fn);
  /* Functional 13/16 mobile - exact. */
  font-weight: 400;
  line-height: var(--miniso-body-fn-lh);
}

/* The eyebrow is now sometimes a link (category, 18.09). base.css:774 underlines every <a>
   inside a <p> and dims it 0.7 on hover - matched in specificity (element.class element) and
   this file loads after base.css (layout/theme.liquid), so this wins per lesson 1. */
p.miniso-sticky-bar__eyebrow a {
  color: inherit;
  text-decoration: none;
}

p.miniso-sticky-bar__eyebrow a:hover {
  color: inherit;
}

.miniso-sticky-bar__price {
  flex: 0 0 auto;
  margin: 0;
  color: var(--miniso-text);
  /* 96:14551 Secondary - the one text node here that is tokenised. */
  font-size: var(--miniso-sub2);
  /* 96:14551 raw 16/22, Fredoka SemiBold. */
  font-weight: 600;
  line-height: var(--miniso-sub2-lh);
  white-space: nowrap;
}

.miniso-sticky-bar__actions {
  display: flex;
  gap: 12px;
  align-items: center;
  justify-content: space-between;
  /* 96:14552. */
  width: 100%;
}

.miniso-sticky-bar__cta {
  flex: 1 1 auto;
  /* 96:14555 is a 253px wrapper with the button at flex:1 0 0. */
  min-width: 0;
}

/* ---- Desktop, 96:13805 sticky-banner-desktop (1512 x 104) -----------------------------------
   One row: info left, actions right, space-between. Read with get_design_context 01.09. */
@media (min-width: 750px) {
  .miniso-sticky-bar {
    flex-direction: row;
    align-items: center;
    justify-content: flex-start;
    gap: 32px;
    /* 96:13811: price -> stepper/CTA pair. NO TOKEN. */
    padding: 24px 40px;
    /* 96:13805. NO TOKEN. */
    /* Desktop blur is 8px where mobile draws 6px - both measured, not normalised. */
    box-shadow: 0 -4px 8px rgb(0 0 0 / 3%);
  }

  /* On DESKTOP the price is NOT part of the info cluster - 96:13811 actions-group holds
     price-display + Frame 976 (stepper + CTA) with gap 32, while 96:13806 product-info-group is
     only thumbnail + text-labels. Mobile 96:14543 groups the price WITH the info, which is how the
     markup is ordered. `display: contents` lets one DOM order serve both: the wrapper box
     disappears here, so __group / __price / __actions become direct flex children of the bar and
     the price can travel right with the buttons. Corrected 01.09 from Razvan's side-by-side. */
  .miniso-sticky-bar__info {
    display: contents;
  }

  .miniso-sticky-bar__group {
    gap: 16px;
    /* 96:13806. NO TOKEN. */
    /* 96:13806 is 486 wide, but its title node is 628 and overflows the box - the frame renders the
       title in full. A fixed 486/340 here truncates far earlier than the design shows, so the
       cluster flexes and the title ellipsises only when it genuinely runs out of room. */
    flex: 1 1 auto;
    min-width: 0;
  }

  .miniso-sticky-bar__price {
    margin-left: auto;
    /* pushes price + actions to the right edge as one cluster */
    flex: 0 0 auto;
  }

  .miniso-sticky-bar__thumb {
    width: 48px;
    height: 48px;
    border-radius: 8px;
    /* 96:13807. NO TOKEN (radius 8 of seven). */
  }

  .miniso-sticky-bar__text {
    gap: 2px;
    /* 96:13808. NO TOKEN. */
    flex: 1 1 auto;
    /* see __group - the drawn 340 clips the title the frame renders in full. */
    min-width: 0;
  }

  .miniso-sticky-bar__title {
    /* 96:13809 Heading/Subtitle 1, Fredoka Medium 28/36 at this width, #1a1a1a, single line. */
    font-size: var(--miniso-sub1);
    line-height: var(--miniso-sub1-lh);
    font-weight: 500;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .miniso-sticky-bar__eyebrow {
    /* 96:13810 Body/Functional/Regular 14/18, #757575. */
    font-size: var(--miniso-body-fn);
    line-height: var(--miniso-body-fn-lh);
  }

  .miniso-sticky-bar__actions {
    /* 12, not 32: Frame 976 (96:13814) sets gap 12 between the stepper and the CTA. The 32 belongs
       to 96:13811, i.e. between the PRICE and that pair - carried by the bar's own gap below. */
    gap: 12px;
    justify-content: flex-end;
    width: auto;
    flex: 0 0 auto;
  }

  .miniso-sticky-bar__price {
    /* 96:13813 Fredoka SemiBold 22px, colour Secondary. 22 sits between sub2 (20) and sub1 (28) -
       NO TOKEN, raw value, flagged. */
    font-size: 22px;
    font-weight: 600;
    color: var(--miniso-secondary);
  }

  /* 96:13814 gap 12 between stepper (106x44) and CTA (185x44). The CTA is a fixed 185 here, not
     block - it does not stretch on desktop. */
  .miniso-sticky-bar__cta {
    /* 96:13817 draws 185, but that was measured on the English "Add to cart". The German
       "In den Warenkorb legen" overflows it and clips. min-width keeps the drawn size as the floor
       and lets the button grow for longer locales. */
    min-width: 185px;
    flex: 0 1 auto;
  }
}


/* ############################################################################################
   WAVE 3 - homepage harvest (hero 128:15740, read to the leaves 02.09.2026).

   VALUES WITH NO TOKEN IN LAYER 1 that wave 3 has to use raw. Findings for Iulia (design task
   #42154), recorded here once and again at each line that uses them:
   12. opacity 0.3 for the inactive carousel dot (128:15748-15750). Layer 1 has no opacity scale.
   13. rgba(33, 36, 39, 0.3) hero overlay (128:15740). #212427 is not one of the 24 palette tokens.
   14. 32px copy gutter inside the hero (128:15741) against the 64px page gutter every other band
       uses - a second desktop gutter. Used in sections/miniso-hero.liquid, listed here so the
       register stays complete.
   15. radius 48 on every homepage tile (36:2691 and siblings). Layer 1 has pill 999 and input 24
       only; 48 is a third radius.
   16. rgba(255, 255, 255, 0.6) tile hover fill (88:6988). Neither the white nor the 60% has a token.
   17. the IP Worlds frame 36:2707 sits at x 35 in the 1509 group while every other band draws its
       content at x 64. Built at the 64px page gutter (ASSUMED); used in sections/miniso-ip-worlds.liquid.
   18. #F52F69 - the featured banner copy colour (128:15879 / 128:15880). Not Rose (#FFAFC4), not
       Primary; absent from the 24-token palette. Used raw in sections/miniso-featured.liquid.
   19. rgba(255, 255, 255, 0.5) hotspot ring (96:8176). Neither the white nor the 50% has a token.
   ############################################################################################ */

/* ==========================================================================================
   12. CAROUSEL DOTS - .miniso-carousel-dots
   Source: homepage hero 128:15745 (row, full width, centred on the slide) > 128:15746 (gap 4) >
   128:15747-15750 (four 20x10 pills, radius 100, white; inactive at 30% opacity).
   Second instance: Featured product 83:6921 ("3 carousel dots") - NOT harvested yet, so no colour
   modifier is invented: the pills paint in currentColor and the host section sets `color`.

   HOST: Horizon's snippets/slideshow-controls.liquid rendered with style: 'dots' and
   class: 'miniso-carousel-dots' (the class lands on <slideshow-controls>). This block re-skins
   its <ol class="slideshow-controls__dots"> > li > <button ref="dots[]"> children. The active
   state is [aria-selected="true"], written by assets/slideshow.js (set current). The inline
   `animation-timeline: --slide-N` on each button is inert: the only .slideshow-control animation
   in base.css is scoped to slideshow-arrows. No Horizon file is edited.

   CASCADE: the snippet's own stylesheet is emitted through content_for_header
   (layout/theme.liquid:198), i.e. AFTER this file (theme.liquid:138). Equal specificity loses,
   so every rule below sits one specificity step above the Horizon rule it replaces. Do not
   "simplify" a selector here without re-checking snippets/slideshow-controls.liquid.

   POSITIONING over the media is the host section's job (composition), not this block's.
   No hover state is drawn, no motion: Horizon's hover only moves --color, which is now inert.
   ========================================================================================== */

/* Element qualifier + ancestor are load-bearing: Horizon's
   `slideshow-controls:has(.slideshow-controls__dots)` and
   `slideshow-component[autoplay] slideshow-controls` are (0,1,1) and set
   mix-blend-mode: difference, which would invert white pills against the photo. */
slideshow-component slideshow-controls.miniso-carousel-dots {
  --_miniso-dot-w: 20px;
  /* 128:15747. NO TOKEN (finding 11). */
  --_miniso-dot-h: 10px;
  /* 128:15747. NO TOKEN (finding 11). */
  --_miniso-dot-gap: 4px;
  /* 128:15746: pills at x 0/24/48/72. NO TOKEN (finding 11). */
  --_miniso-dot-hit: 24px;
  /* ASSUMED: invisible hit height; a 10px target is below any minimum. */

  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 0;
  /* Horizon: min-height var(--minimum-touch-target) = 44px */
  mix-blend-mode: normal;
  /* Horizon: difference */
  color: inherit;
}

.miniso-carousel-dots .slideshow-controls__dots {
  display: flex;
  gap: var(--_miniso-dot-gap);
  height: var(--_miniso-dot-h);
  margin: 0;
  margin-inline: auto;
  /* centring must be ours: Horizon forces justify-content: space-between
                            on the host at (0,4,1) for centred non-media controls, and its
                            :only-child margin would otherwise win the tie. */
  padding: 0;
  /* Horizon: var(--padding-sm) var(--padding-lg) */
  border-radius: 0;
  /* Horizon: 3rem */
  overflow: visible;
  /* Horizon: hidden; the 24px hit area overhangs the 10px row */
  list-style: none;
}

.miniso-carousel-dots .slideshow-controls__dots li {
  display: flex;
}

.miniso-carousel-dots .slideshow-controls__dots button {
  --color: currentcolor;
  /* neutralise Horizon's rgb()/opacity colour plumbing */

  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--_miniso-dot-w);
  height: var(--_miniso-dot-hit);
  /* Horizon: 44x44 */
  margin: 0;
  /* Horizon: calc(var(--size) / -2) */
  padding: 0;
  font-size: 0;
  color: inherit;
  background: transparent;
  border: 0;
  border-radius: var(--miniso-radius-pill);
  cursor: pointer;
}

.miniso-carousel-dots .slideshow-controls__dots button::after {
  content: '';
  display: block;
  width: var(--_miniso-dot-w);
  height: var(--_miniso-dot-h);
  border-radius: var(--miniso-radius-pill);
  /* Figma 100px; identical result on a 10px pill */
  background-color: currentcolor;
  opacity: 0.3;
  /* inactive, 128:15748-15750. NO TOKEN - finding 12. */
  box-shadow: none;
  /* Horizon draws inset 0 0 0 1.5px. No shadows on MINISO. */
}

.miniso-carousel-dots .slideshow-controls__dots button[aria-selected='true']::after {
  opacity: 1;
  /* active, 128:15747 */
  box-shadow: none;
  /* repeat is required: Horizon's active rule is (0,2,2) and loads later */
}

.miniso-carousel-dots .slideshow-controls__dots button:focus-visible {
  outline: 2px solid currentcolor;
  /* ASSUMED: the styleguide defines no focus state (open q.1) */
  outline-offset: 2px;
}

/* Mobile hero 138:18374 > 138:18375-18378 (harvested 04.09): pills 16 x 6, gap 4, same 30%.
   Before the --feature rule below so its 12px height still wins where it renders (the featured
   section hides its dots on mobile anyway). */
@media (max-width: 749px) {
  slideshow-component slideshow-controls.miniso-carousel-dots {
    --_miniso-dot-w: 16px;
    /* 138:18375. NO TOKEN (finding 11). */
    --_miniso-dot-h: 6px;
    /* 138:18375. NO TOKEN (finding 11). */
  }
}

/* Second instance, harvested 03.09: featured product 128:15893 > 128:15894-15896 - three 20x12
   pills, gap 7, Secondary, inactive at 5% opacity, on the white page under the card. Same
   component, a size/tone modifier; the host sets `color: var(--miniso-secondary)`. */
/* Same (0,1,2) shape as the base rule above, or the base plumbing wins. */
slideshow-component slideshow-controls.miniso-carousel-dots--feature {
  --_miniso-dot-h: 12px;
  /* 128:15894. NO TOKEN (finding 11). */
  --_miniso-dot-gap: 7px;
  /* 128:15893. NO TOKEN (finding 11). */
}

.miniso-carousel-dots--feature .slideshow-controls__dots button::after {
  opacity: 0.05;
  /* inactive, 128:15895-15896. NO TOKEN - finding 12 (a second opacity step). */
}


/* ==========================================================================================
   13. BAND - .miniso-band
   Source: homepage Group 36:3720 > Frame 648 36:3723 (1509x1481 at y 835 of a 1009-tall hero)
   > Vector 36:3722 (= 128:15805; 3214x1481 at x -700, single white path). The white cloud body
   rises 174px over the hero photo and carries the "Your IP Worlds" band; its scalloped top edge
   spans y 0..137 of the visible window, everything under it is solid. Second instance: Categories
   128:15897 > Vector 128:15898 (3214x1514 at x -700, fill Pistachio/40): the same artwork 2.2%
   taller (1514 vs 1481, scallops end at 140 instead of 137) - NOT re-cut, the one asset serves
   both at a 3px difference. In the dev page that frame starts exactly at the bottom of the
   1049-tall featured frame 128:15876, so its overlap is 0 (the older 83:6922 drew 165 over a
   taller frame); the host decides through --miniso-band-overlap.

   ELEMENTS  __body (the solid part, paints the band colour; the host puts its padding here)
   AUTHOR INPUT  --miniso-band-mask: url(<asset>) on the host, because {% stylesheet %} cannot
   run asset_url. The asset is assets/miniso-band-clouds.svg, cropped to viewBox 700 0 1512 200.
   AUTHOR INPUT  --miniso-band-color   the band's fill; default --miniso-bg (IP Worlds: white
                 over the hero photo; Categories: --miniso-pistachio-40 over the white page).
   AUTHOR INPUT  --miniso-band-overlap how far the cloud rises over the previous section, as a
                 percentage of the width; default 11.508% (IP Worlds); Categories 0%.

   HOW IT PAINTS. Every length is a percentage of the band's own width, so the shape scales with
   the 1512 drawing instead of being pinned in px: margin-top -174/1512 pulls the band over the
   previous section, ::before is the mask (width 100%, aspect 1512/200) filled with the page
   background token, __body starts under it 1px early so no fractional-DPR seam shows.
   The colour is --miniso-bg, not "white" (finding 1): this IS the page background, shaped.
   ========================================================================================== */

.miniso-band {
  --_miniso-band-overlap: var(--miniso-band-overlap, 11.508%);
  /* 174 / 1512: Group 36:3720 y 835 under the 1009 hero. */
  --_miniso-band-cap: 13.228%;
  /* 200 / 1512: mask height; the scallops end at y 137. */

  position: relative;
  z-index: 1;
  /* over the hero photo and its media overlay */
  margin-top: calc(-1 * var(--_miniso-band-overlap));
  padding-top: var(--_miniso-band-cap);
}

.miniso-band::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  aspect-ratio: 1512 / 200;
  background-color: var(--miniso-band-color, var(--miniso-bg));
  -webkit-mask: var(--miniso-band-mask) center top / 100% 100% no-repeat;
  mask: var(--miniso-band-mask) center top / 100% 100% no-repeat;
  pointer-events: none;
}

.miniso-band__body {
  position: relative;
  margin-top: -1px;
  /* overlaps the mask by 1px: a hairline seam otherwise shows at DPR 1.5/2 */
  background-color: var(--miniso-band-color, var(--miniso-bg));
}

/* ==========================================================================================
   14. TILE - .miniso-tile
   Source: homepage "Your IP Worlds" 36:2707 > tiles 36:2691 (hover state drawn), 36:2692,
   36:2694, 36:2698, 36:2701, 36:2703, 36:2704, 36:2705 (default state: photo only). Radius 48,
   overflow clip, photo object-fit cover. Hover (36:2691 = 128:15810): 1px Secondary stroke on
   the tile, full-card fill white at 60% (88:6988 / 128:15811, 469x480 centred - wider than the
   tile, i.e. covers it), name Display/H3 SemiBold centred (88:6990 / 128:15812, text box = tile
   width, word-break), then the CTA Button/CTA Secondary sm "Explore" (88:6992 / 128:15813: 36
   high, 8/16 padding, Subtitle 3) at top 259, which is 10px under the name's 44px line box. The
   name's centre sits at 50% - 13px and the whole group's centre therefore 10px BELOW the tile
   centre; built as drawn (padding-top 20).
   MOBILE (homepage 136:2161, harvested 04.09, docs/reference/design/pages/homepage/02-ip-worlds.md):
   tile 136:2242 271x359, radius 32, NO stroke, overlay 136:2243 white at 53% inset 0, stack
   136:2244 gap 12 centred at 50% + 0.5 (no offset), name Display/H3 (24/28 mobile token),
   button 136:2246 Secondary Small. Iulia's annotation on 136:2242, verbatim: "No hover on
   mobile - instead, the overlay with the white semi-transparent background, IP World name, and
   CTA button is shown permanently when the card scrolls into the viewport (on-enter-view
   trigger). The overlay, name, and CTA are always visible once the card is in view - no user
   interaction required." That is the .is-in-view state: assets/miniso-in-view.js sets it from
   an IntersectionObserver, this file paints it below 750 only. Without JS the mobile tile is
   the photo alone (progressive enhancement); hover / focus-visible stay the desktop states.
   The Categories tile (128:15905 etc., 424x278, name bottom-left, icon button top-left) is a
   different arrangement: modifier .miniso-tile--category, block 14b below.

   SIZE IS THE HOST'S JOB: the tile fills whatever box the section's grid gives it. No aspect
   ratio, no width here.
   ELEMENTS  __img  __overlay  __name  (+ a .miniso-btn inside the overlay)
   STATES    :hover / :focus-visible / .miniso-tile--hover (gallery) / .is-in-view (mobile, JS)
   HOVER ZOOM (Iulia 07.09): the photo scales on hover - see the block under .miniso-tile__img.
   AUTHOR INPUT  --miniso-tile-gap  name-to-button gap: 10 as drawn on IP Worlds 128:15813,
                 12 on Characters 128:15975 (the host sets it; mobile is 12 on both frames).
   ⚠️ ASSUMED - keyboard parity: the overlay also opens on :focus-visible of the tile link.
   ⚠️ ASSUMED - the 1px Secondary stroke is part of the HOVER state (it is drawn on the hovered
   tile only, and the mobile in-view tile 136:2242 has none). Painted as an inset shadow so the
   photo does not shift by 1px.
   ⚠️ ASSUMED - the in-view state is keyed on the 750 breakpoint (the mobile frame), not on
   (hover: none): the drawing is per breakpoint and the spec resolves values per breakpoint.
   ⚠️ ASSUMED - overlay 60% desktop / 53% mobile: both values are drawn (128:15811 / 136:2243),
   whether the difference is intentional is GAPS.md question 2 for Iulia. Built as drawn.
   ⚠️ ASSUMED - the in-view overlay fades in over 200ms (nothing is specified; a hard cut on a
   scrolling row reads as a glitch); no fade under prefers-reduced-motion. NO TOKEN.
   NO TOKEN: radius 48 / 32 (finding 15), overlay white at 60% / 53% (finding 16), gap 10-12 /
   padding 20 (3).
   ========================================================================================== */

.miniso-tile {
  --_miniso-tile-gap: var(--miniso-tile-gap, 10px);
  /* 128:15813: 10 under the name's line box. NO TOKEN. */

  position: relative;
  display: block;
  box-sizing: border-box;
  overflow: hidden;
  border-radius: 48px;
  /* 36:2691 and every other tile. NO TOKEN - finding 15. */
  background-color: var(--miniso-surface);
  /* ground while the photo loads. ASSUMPTION. */
  font-family: var(--miniso-font);
  color: var(--miniso-text);
  text-decoration: none;
  cursor: pointer;
}

.miniso-tile__img {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  /* Figma crops each photo by hand; the merchant crops in Shopify. */
}

/* Hover zoom (Iulia, 07.09, subtask 133087 item 2: "add a zoom-in on hover on the images in
   Ihre IP-Welten and Nach Charakter shoppen"). It lives on the SHARED tile image, not on the two
   sections, so every .miniso-tile host inherits it - IP Worlds, Characters and the Marken page's
   photo tiles. The tile already clips (overflow: hidden), so the scaled photo stays in the
   rounded box; only the photo moves, the overlay does not.
   Excluded: .miniso-tile--category, whose __img is the cut-out and already owns a transform
   (the hover lift below) - two transform rules on one element would cancel each other.
   Hover-capable devices only, so a tap on mobile never leaves a tile zoomed; nothing under
   prefers-reduced-motion.
   ⚠️ ASSUMED - factor 1.06 and 400ms ease-out: nothing about this zoom is drawn or specified.
   The duration is deliberately slower than the category cut-out's 150ms lift, which is a nudge;
   this is a photo pan. NO TOKEN. */
@media (hover: hover) and (pointer: fine) {
  .miniso-tile:not(.miniso-tile--category) .miniso-tile__img {
    transition: transform 400ms ease-out;
    /* ⚠️ ASSUMED duration and easing. */
    will-change: transform;
  }

  .miniso-tile:not(.miniso-tile--category):hover .miniso-tile__img,
  .miniso-tile:not(.miniso-tile--category):focus-visible .miniso-tile__img,
  .miniso-tile--hover:not(.miniso-tile--category) .miniso-tile__img {
    transform: scale(1.06);
    /* ⚠️ ASSUMED zoom factor. */
  }
}

@media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: reduce) {
  .miniso-tile:not(.miniso-tile--category) .miniso-tile__img {
    transition: none;
  }

  .miniso-tile:not(.miniso-tile--category):hover .miniso-tile__img,
  .miniso-tile:not(.miniso-tile--category):focus-visible .miniso-tile__img,
  .miniso-tile--hover:not(.miniso-tile--category) .miniso-tile__img {
    transform: none;
  }
}

.miniso-tile__overlay {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--_miniso-tile-gap);
  padding-top: 20px;
  /* moves the group centre 10px down, as drawn (name centre at 50% - 13). */
  background-color: rgba(255, 255, 255, 0.6);
  /* 88:6988 / 128:15811. NO TOKEN - finding 16 (white: 1). */
  border-radius: inherit;
  /* so the inset stroke below follows the 48px corners */
  box-shadow: inset 0 0 0 1px var(--miniso-secondary);
  /* 36:2691 stroke. ASSUMED hover-only. */
  opacity: 0;
}

.miniso-tile:hover .miniso-tile__overlay,
.miniso-tile:focus-visible .miniso-tile__overlay,
.miniso-tile--hover .miniso-tile__overlay {
  opacity: 1;
}

.miniso-tile__name {
  margin: 0;
  width: 100%;
  /* 88:6990: text box = tile width, no inset. */
  color: var(--miniso-text);
  /* Secondary */
  text-align: center;

  /* Display/H3, Fredoka SemiBold: 36/44 desktop, 24/28 mobile - one token, both. */
  font-size: var(--miniso-h3);
  font-weight: 600;
  line-height: var(--miniso-h3-lh);
  overflow-wrap: anywhere;
  /* Figma: word-break */
}

/* Same ring as .miniso-btn:focus-visible - one focus decision, made once (see the button). */
.miniso-tile:focus-visible {
  outline: 2px solid var(--miniso-secondary);
  outline-offset: 2px;
}

@media (max-width: 749px) {

  /* The mobile tile as drawn on 136:2242 (IP Worlds) = 138:18090 (Characters): radius 32, the
     overlay at 53% with no stroke, the name/button stack gap 12 centred on the tile. Shared by
     every .miniso-tile host below 750 - the Categories tile 136:2414 draws the same radius 32. */
  .miniso-tile {
    --_miniso-tile-gap: 12px;
    /* 136:2244 stack gap. NO TOKEN. */

    border-radius: 32px;
    /* 136:2242. NO TOKEN - finding 15 (mobile step). */
  }

  .miniso-tile__overlay {
    padding-top: 0;
    /* 136:2244 centred at 50% + 0.5: no offset (the .5 is Figma rounding). */
    background-color: rgba(255, 255, 255, 0.53);
    /* 136:2243. NO TOKEN - finding 16. ⚠️ ASSUMED per-breakpoint. */
    box-shadow: none;
    /* 136:2242: no stroke on the in-view tile. */
    transition: opacity 200ms ease-out;
    /* ⚠️ ASSUMED - see the header. NO TOKEN. */
  }

  /* On-enter-view (annotation 136:2242): set by assets/miniso-in-view.js once the tile is in
     the viewport, kept - "always visible once the card is in view". GAPS.md question 1. */
  .miniso-tile.is-in-view .miniso-tile__overlay {
    opacity: 1;
  }

  @media (prefers-reduced-motion: reduce) {
    .miniso-tile__overlay {
      transition: none;
    }
  }
}

/* ==========================================================================================
   14b. TILE, category arrangement - .miniso-tile--category
   Source: homepage Categories 128:15897 > tiles 128:15905 / 128:15910 / 128:15915 / 128:15921 /
   128:15926 (424x278, radius 48): a pastel fill (Aqua, Pistachio, Lavender, Pistachio, Aqua),
   the name Heading/Subtitle 1 Medium 28/36 bottom-left inside the 32/24 padding (text box = the
   inner width, word-break), a 40x40 white icon button with a 1px Primary border and the
   arrow-up-right glyph top-left at 24/20 (178:21530), and the product CUT-OUT (a PNG with
   alpha, 128:15907) placed by hand at ~274x292, top -38 - poking 38 above the tile, its box
   running ~40 past the right edge while the toy itself ends at the tile edge - with the
   "toy shadow" effect on it. Layer order as drawn: name, cut-out over it, button on top.
   No hover state is drawn. Composes with the base (same class, radius, focus ring); the host
   simply does not render the IP Worlds overlay.
   ELEMENTS  __name (in flow, bottom-left)  __img (the cut-out, absolute)  __action (the button:
             a .miniso-btn --secondary --icon-only carrying this element class)
   TONES     .miniso-tile--aqua | --pistachio | --lavender  (the three drawn fills)
   ⚠️ ASSUMED - one placement rule for the cut-out, read off the five hand-placed drawings: box
   top -13.67% / right 0 / width 64.6% / height 105% of the tile, object-fit contain anchored
   right, so a tight PNG ends at the tile edge like the penguin and a tall one pokes above.
   ⚠️ ASSUMED - hover: the icon button takes the Secondary button's own hover (black fill, white
   glyph), also on keyboard focus of the tile; nothing else moves.
   ⚠️ ASSUMED - the name box is capped at 50% of the tile so it wraps before the cut-out the way
   the drawn names are hand-broken ("Plüsch & \nKuscheltiere" 128:15906); a full-width box ran
   the names under the toys at 1280, where the tiles are 368 (no 1280 frame).
   MOBILE (frame 136:2414, 202x242, harvested 04.09 - 05-categories.md): the anatomy MIRRORS:
   radius 32, padding 16, overflow CLIP, the button 138:18156 40x40 top-RIGHT at right 13 /
   top 16, the cut-out 136:2416 162x162 top-LEFT at left -27 / top 16 (radius 16, toy shadow;
   the other four: 150 @ -11/23, 170 @ 3/14, 191x203 @ -42/12, 147x157 @ -11/16), the name
   Subtitle 1 (20/26 on the mobile root) bottom-left, box 157 of the 170 inner width.
   ⚠️ ASSUMED - one cut-out rule read off the five hand-placed drawings, as on desktop: box
   left -13.4% / top 6.6% / 80.2% x 67% of the tile (= 136:2416), object-fit contain anchored
   left top, and the tile clips whatever pokes out at the left edge as drawn.
   Question for Iulia (GAPS 13.6): the mirror between breakpoints - intended?
   NO TOKEN: the button is 40px - neither the 44 default nor the 36 small of 03 §Geometry
   (finding 20); the "toy shadow" effect, five stacked drop shadows in #9A9EB1 (finding 21);
   padding 32/24, offsets 24/20 (3); radius 48 (15).
   ========================================================================================== */

.miniso-tile--category {
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  align-items: flex-start;
  overflow: visible;
  /* the cut-out pokes above the tile (128:15907 top -38) */
  padding: 32px 24px;
  /* 128:15905. NO TOKEN. */
  background-color: var(--miniso-aqua);
  /* 128:15905; the tones below override */
}

.miniso-tile--aqua {
  background-color: var(--miniso-aqua);
  /* 128:15905, 128:15926 */
}

.miniso-tile--pistachio {
  background-color: var(--miniso-pistachio);
  /* 128:15910, 128:15921 */
}

.miniso-tile--lavender {
  background-color: var(--miniso-lavender);
  /* 128:15915 */
}

.miniso-tile--category .miniso-tile__name {
  position: relative;
  width: 100%;
  /* 128:15906: text box = inner width 376 */
  text-align: left;

  max-width: 50%;
  /* wraps before the cut-out, like the hand-broken names 128:15906. ⚠️ ASSUMED. */

  /* Heading/Subtitle 1, Fredoka Medium: 28/36 desktop, 24/30 tablet, 20/26 mobile - one token. */
  font-size: var(--miniso-sub1);
  font-weight: 500;
  line-height: var(--miniso-sub1-lh);
}

.miniso-tile--category .miniso-tile__img {
  inset: auto;
  top: -13.669%;
  /* -38 / 278 (128:15907) */
  right: 0;
  width: 64.623%;
  /* 274 / 424 */
  height: 105.036%;
  /* 292 / 278 */
  object-fit: contain;
  object-position: right center;
  pointer-events: none;

  /* "toy shadow" 128:15907: Figma blur r -> CSS drop-shadow blur r/2; the two outermost layers
     (alpha .01 and 0) are dropped as invisible. NO TOKEN - finding 21. */
  filter: drop-shadow(3px 2px 3.5px rgb(154 158 177 / 10%)) drop-shadow(12px 6px 7px rgb(154 158 177 / 9%)) drop-shadow(27px 14px 9px rgb(154 158 177 / 5%));
}

/* Flat photo (JPEG, no alpha, e.g. Blindboxen on 03.09): no drop shadow, it would outline the
   rectangle; and multiply so the baked-in white ground erases into the pastel tile, the same cure
   as the product card. Cut-out PNGs must NOT get the blend, it tints their light pixels with the
   tile colour (the penguin sank into the aqua). The section sets the modifier from the file
   extension; swap the asset for a cut-out PNG and both revert on their own. */
.miniso-tile--category .miniso-tile__img--flat {
  filter: none;
  mix-blend-mode: multiply;
}

.miniso-tile--category .miniso-tile__action {
  position: absolute;
  top: 20px;
  /* 178:21530 */
  left: 24px;
  width: 40px;
  /* 178:21530: 10 + 20 + 10 with the 1px stroke inside. NO TOKEN - finding 20. */
  height: 40px;
  min-height: 40px;
  padding: 9px;
}

.miniso-tile--category:hover .miniso-tile__action,
.miniso-tile--category:focus-visible .miniso-tile__action,
.miniso-tile--category.miniso-tile--hover .miniso-tile__action {
  /* = .miniso-btn--secondary:hover, driven by the tile because the button is decorative. */
  --_miniso-btn-bg: var(--miniso-secondary);
  --_miniso-btn-fg: #ffffff;
  /* NO TOKEN: white. Finding 1. */
  --_miniso-btn-bd: #ffffff;
  /* NO TOKEN: white. Finding 1. */
}

/* Hover lift (Iulia via Razvan, 04.09, decided): the cut-out lifts a few px when the tile is
   hovered. The first motion in this file: the tile overlay itself has no transition (NO MOTION,
   file header), so there is no duration to reuse. ⚠️ ASSUMED: 6px, 150ms ease-out. Hover-capable
   devices only, nothing under prefers-reduced-motion; also on keyboard focus of the tile and on
   the gallery's .miniso-tile--hover, like the button's hover above. */
@media (hover: hover) {
  .miniso-tile--category .miniso-tile__img {
    transition: transform 150ms ease-out;
    /* ⚠️ ASSUMED duration and easing. */
  }

  .miniso-tile--category:hover .miniso-tile__img,
  .miniso-tile--category:focus-visible .miniso-tile__img,
  .miniso-tile--category.miniso-tile--hover .miniso-tile__img {
    transform: translateY(-6px);
    /* ⚠️ ASSUMED "a few px" = 6. */
  }
}

@media (hover: hover) and (prefers-reduced-motion: reduce) {
  .miniso-tile--category .miniso-tile__img {
    transition: none;
  }

  .miniso-tile--category:hover .miniso-tile__img,
  .miniso-tile--category:focus-visible .miniso-tile__img,
  .miniso-tile--category.miniso-tile--hover .miniso-tile__img {
    transform: none;
  }
}

@media (max-width: 749px) {

  /* Mobile frame 136:2414 (202x242), see the header. Category-specific overrides only; the
     shared mobile tile geometry (radius 32) sits on .miniso-tile, restated here because the
     frame draws it on this tile (NO TOKEN - finding 15). */
  .miniso-tile--category {
    overflow: hidden;
    /* 136:2414 clips: the cut-out pokes out at the LEFT and is cut. */
    padding: 16px;
    /* 136:2414. NO TOKEN. */
    border-radius: 32px;
    /* 136:2414. NO TOKEN - finding 15. */
  }

  .miniso-tile--category .miniso-tile__name {
    max-width: 100%;
    /* 136:2417: box 157 of the 170 inner width, under the cut-out. */
  }

  .miniso-tile--category .miniso-tile__action {
    top: 16px;
    /* 138:18156 */
    right: 13px;
    left: auto;
  }

  .miniso-tile--category .miniso-tile__img {
    top: 6.612%;
    /* 16 / 242 (136:2416) */
    right: auto;
    left: -6.436%;
    /* -13 / 202 (136:2416, re-read from Figma 08.09). The 04.09 harvest recorded
                      left -27 and the cut-out sat half under the tile's left edge - Razvan, with
                      the frame open: "exista spatiu intre pinguin si marginea stanga". */
    width: 80.198%;
    /* 162 / 202 */
    height: 66.942%;
    /* 162 / 242 */
    object-position: left top;
  }
}

/* ==========================================================================================
   15. SECTION HEAD, homepage size - .miniso-section-head--lg
   Source: the five homepage section titles are all Display/H1 SemiBold 64/72 in Secondary:
   36:3002 "Your IP Worlds" (941 wide), 36:3152 / 128:15830 "New drop", 36:3785 "Bestseller",
   83:6923 "Categories", 96:8192 "Shop by character". The PDP rows (component 5) are H3.
   One modifier; --with-sub / --with-action compose with it (New drop 128:15828 uses both:
   ghost "Alle ansehen" on the title row, Rose H3 subline under it).
   var(--miniso-h1) is 32/36 below 750, 40/48 to 989, 64/72 from 990 - the mobile steps are
   the layer-1 type scale, since no homepage mobile frame exists (⚠️ ASSUMED mobile, for Iulia).
   ========================================================================================== */

.miniso-section-head--lg .miniso-section-head__title {
  font-size: var(--miniso-h1);
  line-height: var(--miniso-h1-lh);
}

/* Homepage geometry that the PDP rows never needed:
   - the action centres on the TITLE's line box (128:15831 at y 24 of the 72px row), not on the
     whole group, so a subline does not push it down;
   - the subline in this size is the campaign line 128:15833: Display/H3 SemiBold in Rose, 8px
     under the title (128:15828 gap 8). Rose is the only colour drawn for it; the Secondary/60
     paragraph subline of the PDP reviews head is untouched at the base size. ⚠️ ASSUMED that
     the colour is fixed and not per instance. */
.miniso-section-head--lg {
  align-items: flex-start;
}

.miniso-section-head--lg .miniso-section-head__group {
  --_miniso-head-gap: 8px;
  /* 128:15828. NO TOKEN. */
}

.miniso-section-head--lg .miniso-section-head__action {
  align-items: center;
  min-height: var(--miniso-h1-lh);
}

.miniso-section-head--lg .miniso-section-head__sub {
  color: var(--miniso-rose);
  /* 128:15833 */
  font-size: var(--miniso-h3);
  font-weight: 600;
  line-height: var(--miniso-h3-lh);
}

/* Centred title: Categories 128:15900 (text-align center over the whole content width, no
   action). Composes with --lg; ⚠️ ASSUMED it is only ever used there, every centred title
   drawn is a homepage H1. */
/* --sm: the compact head for overlays - first use the header search modal (04.09), which has no
   Figma frame. ASSUMED: title at Heading/Subtitle 2 Medium (20/24 desktop, 16/20 mobile - the
   feature card's title step); the action is still a .miniso-btn (Ghost "Löschen" there); shell
   gap 16 at both breakpoints. Sits after the desktop query above so it holds at every width. */
.miniso-section-head--sm {
  gap: 16px;
}

.miniso-section-head--sm .miniso-section-head__title {
  font-size: var(--miniso-sub2);
  font-weight: 500;
  line-height: var(--miniso-sub2-lh);
}

.miniso-section-head--center {
  justify-content: center;
  text-align: center;
}

.miniso-section-head--center .miniso-section-head__group {
  align-items: center;
}

/* ==========================================================================================
   16. BUTTON, homepage size - .miniso-btn--lg
   ⚠️ ASSUMED. The styleguide has two heights, 44 and 36 (03 §Geometry). The homepage draws a
   third: the centred "See all" under IP Worlds (36:3009) and under Categories (83:6959) is a
   Primary at 212x64, padding 10/24, label Heading/Subtitle 2 20/24 (I36:3009;14:1518). Built
   as a size modifier so it composes with --primary like --sm does; the label size is the same
   one --label-lg already carries. Lives in the button section's vocabulary, registered here
   because it is a wave-3 harvest. Question for Iulia: is this a real third size?
   NO TOKEN: 64 / 212 / 10 / 24 (finding 3).
   ========================================================================================== */

.miniso-btn--primary.miniso-btn--lg,
.miniso-btn--secondary.miniso-btn--lg {
  min-height: 64px;
  min-width: 212px;
  padding: 10px 24px;
  font-size: var(--miniso-sub2);
  line-height: var(--miniso-sub2-lh);
}

/* Below 750 the homepage draws the SAME "Alle ansehen" as the styleguide's Primary Default 44:
   IP Worlds 136:2295 (130x44, px 24 / py 10, label Subtitle 3 = the mobile 14/18 token),
   Characters 138:18101, Categories 136:2467 - all left-aligned under their row. So --lg is a
   desktop-only size and reverts to the base .miniso-btn--primary/--secondary box and label
   here; the host only changes the alignment. 24 + 82 (the 14px label) + 24 = 130 as drawn.
   The mobile label at 14/18 where the button sheet fixes 16/20 is GAPS.md finding 6 for Iulia. */
@media (max-width: 749px) {

  .miniso-btn--primary.miniso-btn--lg,
  .miniso-btn--secondary.miniso-btn--lg {
    min-height: 44px;
    min-width: 0;
    padding: 10px 24px;
    font-size: var(--miniso-sub3);
    line-height: var(--miniso-sub3-lh);
  }
}

/* ==========================================================================================
   17. HOTSPOT - .miniso-hotspot
   Source: featured product banner 128:15877 > "Dot" instances 96:8172 (Active, 43x43) and
   96:8175 (Default, 24x24): a white circle at 50% opacity with a solid white core inset
   20.93% (active) / 12.5% (default). Three of them sit on the photo at fixed positions and
   correspond one to one to the three cards in the carousel beside it, so the active one is
   the product currently shown. Placement (left/top as % of the banner) is the host's job.
   ELEMENTS  none (the button IS the ring; ::after is the core)
   STATES    [aria-current="true"] = Active
   ⚠️ ASSUMED - the drawn 24px target is below any touch minimum: ::before adds an invisible
   44px hit area centred on the ring, the same reasoning as the carousel dot's 24px hit height.
   NO TOKEN: white (1), 50% (finding 19), 24 / 43 / 12.5% / 20.93% (3). NO MOTION.
   ========================================================================================== */

.miniso-hotspot {
  --_miniso-hotspot-size: 24px;
  --_miniso-hotspot-core: 12.5%;

  position: absolute;
  box-sizing: border-box;
  width: var(--_miniso-hotspot-size);
  height: var(--_miniso-hotspot-size);
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: var(--miniso-radius-pill);
  background-color: rgba(255, 255, 255, 0.5);
  /* 96:8176. NO TOKEN - finding 19 (white: 1). */
  translate: -50% -50%;
  /* the host's left/top name the CENTRE */
  cursor: pointer;
}

.miniso-hotspot::before {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 44px;
  /* ASSUMED hit area */
  height: 44px;
  translate: -50% -50%;
}

.miniso-hotspot::after {
  content: '';
  position: absolute;
  inset: var(--_miniso-hotspot-core);
  border-radius: var(--miniso-radius-pill);
  background-color: #ffffff;
  /* 96:8177. NO TOKEN - finding 1. */
}

.miniso-hotspot[aria-current='true'] {
  --_miniso-hotspot-size: 43px;
  /* 96:8172 */
  --_miniso-hotspot-core: 20.93%;
  /* 96:8174 */
}

.miniso-hotspot:focus-visible {
  outline: 2px solid #ffffff;
  /* ASSUMED: wave-1 ring, in the only colour visible on a photo */
  outline-offset: 2px;
}

/* ==========================================================================================
   PAGE GUTTER - Horizon override
   ------------------------------------------------------------------------------------------
   Horizon ships `--page-margin: 16px` / `40px` from 750 up (assets/base.css). Every section
   MINISO has built uses 64px on desktop - the header, the footer, the recently-viewed row, the
   customer-reviews row and the collection page - because that is the gutter Figma draws (the
   1512 frames put all content at x=64). The two never met, so on a product page the Horizon
   sections ("Recommended for you", the media gallery, cart, search, 404) sat at 40 while the
   MINISO sections sat at 64: measured 01.09 on the PDP as 40 / 64 / 65 on one screen.

   base.css is loaded at layout/theme.liquid:124 and this file at :138, so the same selectors
   later in the cascade win. Mobile is unchanged - Horizon's 16px is already MINISO's 16px.
   ========================================================================================== */

.page-width-wide,
.page-width-normal,
.page-width-narrow,
.page-width-content {
  --page-margin: 16px;
}

@media screen and (min-width: 750px) {

  .page-width-wide,
  .page-width-normal,
  .page-width-narrow,
  .page-width-content {
    --page-margin: 64px;
    /* was Horizon's 40px. NO TOKEN - the gutter is a raw 64 in Figma. */
  }
}

/* ==========================================================================================
   18. USP CARD - .miniso-usp-card
   Source: USP row 154:2303 (homepage, under the store banner) = 149:20754 / 149:21156 (PLP):
   USP-Item 154:2304 "Schneller Versand", 154:2310 "Click & Collect", 154:2316 "Einfache
   Retouren", 154:2322 "Sicher zahlen". Lavender/40 fill, radius 30, padding 32, row gap 16,
   items-start: Icon-Wrapper 36x36 (154:2305) holding a 28x28 Primary glyph (truck 154:2332,
   map-pin-house 154:2335, arrow-left-right 154:2329, basil:folder-lock-outline 149:18938 at
   32x33) | Text-Block (154:2307, column gap 12, 218 wide): Title Heading/Subtitle 1 Medium
   28/36 Secondary (whitespace-nowrap), Description Body/Smaller Regular 16/22 Secondary.
   Drawn 334x156 (348 where the title is longer): the card hugs its copy.
   SIZE IS THE HOST'S JOB: no width here; the section's grid decides.
   ELEMENTS  __icon (36 box, 28 glyph via snippets/miniso-icon)  __body  __title  __text
   ⚠️ ASSUMED - Figma draws the title nowrap and 225 wide inside the 218 block, i.e. it runs
   7px into the right padding. Built the same way from 1440 up (where a four-up row gives the
   card its drawn 334); below that the title wraps.
   MOBILE (frame 158:2321, harvested 04.09 - 09-usp.md): padding 16, radius 24, row gap 16,
   items-CENTER, icon box 158:2322 40x40 (glyph 28; folder-lock 158:2344 32x32), text block
   158:2325 column gap 4, title Subtitle 1 (20/26 on the mobile root), text 14/20. Drawn card
   heights 102 (two-line text) / 82 (one line) follow from these.
   ⚠️ ASSUMED (tablet 750-989, no frame): the desktop geometry.
   NO TOKEN: radius 30 (the featured banner's radius, not the card's 32 or the tile's 48 -
   finding 22) / 24 on mobile, padding 32 / 16, gaps 16 / 12 / 4, icon box 36 / 40, glyph 28 (3).
   ========================================================================================== */

.miniso-usp-card {
  box-sizing: border-box;
  display: flex;
  align-items: flex-start;
  gap: 16px;
  /* 154:2304. NO TOKEN. */
  margin: 0;
  padding: 32px;
  /* 154:2304. NO TOKEN. */
  border-radius: 30px;
  /* 154:2304. NO TOKEN - finding 22. */
  background-color: var(--miniso-lavender-40);
  /* 154:2304 */
  font-family: var(--miniso-font);
  color: var(--miniso-text);
  /* Secondary */
}

.miniso-usp-card__icon {
  display: flex;
  flex: 0 0 36px;
  align-items: center;
  justify-content: center;
  width: 36px;
  /* 154:2305. NO TOKEN. */
  height: 36px;
  color: var(--miniso-primary);
  /* every drawn glyph is Primary */
}

.miniso-usp-card__icon>svg {
  display: block;
  width: 28px;
  /* 154:2332. NO TOKEN. */
  height: 28px;
}

.miniso-usp-card__icon>.miniso-icon--32 {
  width: 32px;
  /* 149:18938 basil:folder-lock-outline is drawn 32x33 in the same 36 box. */
  height: 33px;
}

.miniso-usp-card__body {
  display: flex;
  flex-direction: column;
  gap: 12px;
  /* 154:2307. NO TOKEN. */
  min-width: 0;
}

.miniso-usp-card__title {
  margin: 0;

  /* Heading/Subtitle 1, Fredoka Medium: 28/36 desktop, 24/30 tablet, 20/26 mobile - one token. */
  font-size: var(--miniso-sub1);
  font-weight: 500;
  line-height: var(--miniso-sub1-lh);
}

.miniso-usp-card__text {
  margin: 0;

  /* Body/Smaller Regular: 16/22 desktop, 14/20 below 990 - one token. */
  font-size: var(--miniso-body-sm);
  font-weight: 400;
  line-height: var(--miniso-body-sm-lh);
}

@media (min-width: 1440px) {
  .miniso-usp-card__title {
    white-space: nowrap;
    /* 154:2308: 225 wide in the 218 block, overflowing into the padding. */
  }
}

@media (max-width: 749px) {

  /* Mobile frame 158:2321, see the header. */
  .miniso-usp-card {
    align-items: center;
    /* 158:2321 items-center */
    padding: 16px;
    /* 158:2321. NO TOKEN. */
    border-radius: 24px;
    /* 158:2321. NO TOKEN. */
  }

  .miniso-usp-card__icon {
    flex-basis: 40px;
    width: 40px;
    /* 158:2322. NO TOKEN. */
    height: 40px;
  }

  .miniso-usp-card__icon>.miniso-icon--32 {
    height: 32px;
    /* 158:2344: 32x32 in the 40 box */
  }

  .miniso-usp-card__body {
    gap: 4px;
    /* 158:2325. NO TOKEN. */
  }
}

/* ------------------------------------------------------------------------------------------
   PAGE CONTAINERS - full-bleed above 1512 (Razvan, 01.09)
   Horizon caps its page containers and centres them: `section--page-width` resolves to
   --narrow-page-width (90rem = 1440px), so above 1440 + margins the content drifts inward while
   the MINISO-built bands do not. Measured on the PDP: "Recommended for you" at 64 / 240 / 560 for
   viewports 1512 / 1920 / 2560, against a flat 64 for the header, footer, recently-viewed and
   customer-reviews rows. Three conventions on one page.
   Decision: the whole site is full-bleed at the 64px gutter, matching the already-approved chrome.
   Raising these three to an unreachable value makes Horizon's own
   `min(--page-width - margin*2, 100% - margin*2)` always pick the second branch - the grid keeps
   working exactly as designed, it simply never hits the cap.
   --normal-content-width is deliberately NOT touched: that is the 42rem TEXT measure behind
   `.page-width-content`, and prose still needs a readable line length. */
:root {
  --narrow-page-width: 1000rem;
  --normal-page-width: 1000rem;
  --wide-page-width: 1000rem;
}

/* ==========================================================================================
   WAVE 3 - CONTENT PAGES (03.09.2026). NO FIGMA FRAME EXISTS for the generic page, FAQ,
   Läden, Kontakt, Versand & Retouren or Über uns (design README: "no content pages"). Razvan's
   go of 03.09: build from the styleguide + this component layer, Iulia reviews afterwards.
   Everything in this block is therefore ⚠️ ASSUMED for Iulia unless it cites a component
   above. Three shared pieces, each used by more than one page:
     .miniso-prose   rich text from the Shopify page body (page-content block, FAQ answers,
                     store card text). Type scale = the styleguide's, mapped: h1 = page title
                     (H1), h2 = H3 (the PDP's section-head size), h3 = Subtitle 1, h4+ =
                     Subtitle 2, body = Paragraph Regular. blockquote = the callout (shipping
                     facts on Versand & Retouren) - it is the one block-level element the admin
                     rich-text editor can produce without HTML editing.
     .miniso-surface   a flat surface (tinted card) for the contact info cards and the store
                     card. No shadow (layer 1: the file carries none). Radius 16 = the review
                     card's radius (wave 2, NO TOKEN finding "radius 16 of seven").
     .miniso-input--multiline   the contact form's textarea on the input component. 04 defines
                     no textarea; built as the same box, taller. ⚠️ ASSUMED.
     .miniso-input__label--above   a visible label above the box (label-above pattern) for the
                     textarea, where a placeholder alone is not enough. 04 defines no resting
                     label. ⚠️ ASSUMED.
     --miniso-measure   the one content-page measure, see the layout rule right below.

   ⚠️ ASSUMED (layout rule from UK site, for Iulia) - 03.09.2026, Razvan's feedback on Kontakt:
   minisoshop.co.uk (Magento) runs every content page in ONE contained column, centred on the
   page: contact = 58/42 two-column row (form left, capped at 700px; channels right), help/FAQ =
   one column with centred headings and a full-width accordion, store locator = one column,
   text pages = one column. Applied here: every content page (generic, FAQ, Läden, Kontakt)
   renders ONE column of --miniso-measure, centred, inside the 64/32/16 page gutter
   (sections/main-page.liquid consumes it). Blocks fill that column; Kontakt splits it 7/5
   (the UK 58/42). Under 990 everything stacks. 1040px: no token, no Figma frame - a value that
   keeps a 7/5 split usable (580/410) and ends the 64-to-64 full bleed with a hole in the middle.
   ========================================================================================== */

:root {
  --miniso-measure: 1040px;
  /* ⚠️ ASSUMED (layout rule from UK site, for Iulia). NO TOKEN. */
}

/* ---- 12. PROSE - .miniso-prose ------------------------------------------------------------ */

.miniso-prose {
  box-sizing: border-box;
  max-width: 65ch;
  /* ⚠️ ASSUMED: reading measure. NO TOKEN. */
  color: var(--miniso-text);
  font-family: var(--miniso-font);
  font-size: var(--miniso-body-md);
  /* Paragraph Regular 18/24 desktop, 16/22 mobile. */
  font-weight: 400;
  line-height: var(--miniso-body-md-lh);
  overflow-wrap: anywhere;
}

.miniso-prose--full {
  max-width: none;
}

.miniso-prose> :first-child {
  margin-top: 0;
}

.miniso-prose> :last-child {
  margin-bottom: 0;
}

.miniso-prose p,
.miniso-prose ul,
.miniso-prose ol,
.miniso-prose table,
.miniso-prose figure {
  margin: 0 0 16px;
  /* ⚠️ ASSUMED paragraph rhythm. NO TOKEN. */
}

.miniso-prose h1,
.miniso-prose h2,
.miniso-prose h3,
.miniso-prose h4,
.miniso-prose h5,
.miniso-prose h6 {
  color: var(--miniso-text);
  font-family: var(--miniso-font);
}

.miniso-prose h1 {
  margin: 48px 0 16px;
  font-size: var(--miniso-h2);
  /* a second H1 inside the body is demoted one step. */
  font-weight: 600;
  line-height: var(--miniso-h2-lh);
}

.miniso-prose h2 {
  margin: 48px 0 16px;
  /* ⚠️ ASSUMED section spacing. NO TOKEN. */
  font-size: var(--miniso-h3);
  /* Display/H3 36/44 desktop - the PDP section-head size. */
  font-weight: 600;
  line-height: var(--miniso-h3-lh);
}

.miniso-prose h3 {
  margin: 32px 0 12px;
  font-size: var(--miniso-sub1);
  /* Heading/Subtitle 1, Medium 28/36 desktop. */
  font-weight: 500;
  line-height: var(--miniso-sub1-lh);
}

.miniso-prose h4,
.miniso-prose h5,
.miniso-prose h6 {
  margin: 24px 0 8px;
  font-size: var(--miniso-sub2);
  /* Heading/Subtitle 2, Medium 20/24 desktop. */
  font-weight: 500;
  line-height: var(--miniso-sub2-lh);
}

.miniso-prose strong,
.miniso-prose b {
  font-weight: 600;
  /* Paragraph Regular / Semi Bold is the only bold the sheets define. */
}

.miniso-prose a {
  color: var(--miniso-primary);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.15em;
}

.miniso-prose a:hover {
  color: var(--miniso-primary-60);
  /* ⚠️ ASSUMED: link hover = the button's hover tint (03). */
}

.miniso-prose a:focus-visible {
  outline: 2px solid var(--miniso-secondary);
  /* ⚠️ ASSUMED (README open question 1). */
  outline-offset: 2px;
  border-radius: 2px;
}

.miniso-prose ul,
.miniso-prose ol {
  padding-inline-start: 1.5em;
}

.miniso-prose li {
  margin: 0 0 6px;
}

.miniso-prose li::marker {
  color: var(--miniso-primary);
}

.miniso-prose hr {
  height: 0;
  margin: 32px 0;
  border: 0;
  border-top: 1px solid var(--miniso-border);
}

.miniso-prose img {
  display: block;
  max-width: 100%;
  height: auto;
  border-radius: 16px;
  /* NO TOKEN (radius 16 of seven). */
}

.miniso-prose table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--miniso-body-sm);
  /* Paragraph Smaller 16/22 desktop. */
  line-height: var(--miniso-body-sm-lh);
}

.miniso-prose th,
.miniso-prose td {
  min-width: 8em;
  /* ⚠️ ASSUMED: a cell never squeezes below one word ("Versandkosten"). */
  padding: 12px 16px;
  /* ⚠️ ASSUMED cell padding. NO TOKEN. */
  border-bottom: 1px solid var(--miniso-border);
  text-align: left;
  vertical-align: top;
  overflow-wrap: normal;
  /* cells break at spaces only; the table scrolls instead (below). */
}

.miniso-prose th {
  background-color: var(--miniso-offwhite);
  font-weight: 600;
}

@media (max-width: 749px) {

  /* ⚠️ ASSUMED (mobile, for Iulia): a wide table scrolls inside its own box, never the page. */
  .miniso-prose table {
    display: block;
    overflow-x: auto;
  }
}

/* The callout. Admin rich text writes <blockquote>; on Versand & Retouren it carries the three
   shipping facts (price, free-from threshold, delivery time) which live in the PAGE BODY, not in
   theme settings - they are data from the Shopify shipping zone. */
.miniso-prose blockquote {
  margin: 24px 0;
  padding: 20px 24px;
  /* ⚠️ ASSUMED. NO TOKEN. */
  border: 0;
  border-radius: 16px;
  /* NO TOKEN (radius 16 of seven). */
  background-color: var(--miniso-aqua-20);
  /* ⚠️ ASSUMED: Aqua/20, the lightest accent tint. */
  color: var(--miniso-text);
  font-size: var(--miniso-body-md);
  line-height: var(--miniso-body-md-lh);
}

.miniso-prose blockquote p {
  margin: 0 0 8px;
}

.miniso-prose blockquote p:last-child {
  margin-bottom: 0;
}

.miniso-prose blockquote strong {
  color: var(--miniso-primary);
  /* ⚠️ ASSUMED: the bold figure in a callout reads in Primary. */
}

/* ---- 13. SURFACE - .miniso-surface ------------------------------------------------------------ */

.miniso-surface {
  box-sizing: border-box;
  display: block;
  padding: 24px;
  /* ⚠️ ASSUMED. NO TOKEN. */
  border-radius: 16px;
  /* NO TOKEN (radius 16 of seven). */
  background-color: var(--miniso-offwhite);
  color: var(--miniso-text);
  font-family: var(--miniso-font);
}

.miniso-surface--aqua {
  background-color: var(--miniso-aqua-20);
}

.miniso-surface--rose {
  background-color: var(--miniso-rose-20);
}

.miniso-surface--lavender {
  background-color: var(--miniso-lavender-40);
  /* /20 is near-white on this surface; /40 reads. */
}

.miniso-surface--pistachio {
  background-color: var(--miniso-pistachio-40);
}

.miniso-surface--outline {
  background-color: var(--miniso-bg);
  border: 1px solid var(--miniso-border);
}

@media (max-width: 749px) {
  .miniso-surface {
    padding: 20px;
    /* ⚠️ ASSUMED (mobile, for Iulia). */
  }
}

/* ---- 14. INPUT, multiline modifier - .miniso-input--multiline -----------------------------
   The single-line box is a fixed 48px pill (04). A textarea inside it centred and overflowed
   the pill on both sides (Razvan, 03.09) - here the box grows with its content: six rows of
   the input's own line-height plus the 12px block padding, radius 24 stays (on a 160px box that
   is a rounded corner, not a pill), the user resizes vertically and the box follows. */

.miniso-input--multiline .miniso-input__box {
  height: auto;
  min-height: calc(6 * var(--miniso-body-sm-lh) + 2 * var(--_miniso-input-pad-block) + 2px);
  /* 6 rows + padding + border. ⚠️ ASSUMED. */
  align-items: stretch;
  border-radius: var(--miniso-radius-input);
  /* 24 on the corners only - never a pill. */
}

.miniso-input--multiline .miniso-input__field {
  justify-content: flex-start;
  overflow: visible;
  /* the resize handle sits at the control's corner; do not clip it */
}

.miniso-input--multiline .miniso-input__control {
  min-height: calc(6 * var(--miniso-body-sm-lh));
  /* rows="6" at Body/Smaller 16/22 = 132px desktop. */
  overflow: auto;
  resize: vertical;
  white-space: pre-wrap;
  text-overflow: clip;
}

/* ---- 15. INPUT, label above - .miniso-input__label--above --------------------------------
   Label-above pattern for the textarea (Razvan, 03.09): the label is visible, sits above the
   box and stays put while typing. Type = Body/Functional 14/18 (the Filled label's size, 04
   §Filled), Medium, in Secondary. Gap to the box 6px = the Error state's wrapper gap. Every
   value ⚠️ ASSUMED for Iulia. */

.miniso-input__label--above {
  display: block;
  margin-bottom: 6px;
  color: var(--miniso-secondary);
  font-weight: 500;
  white-space: normal;
}

14c. TILE,
logo arrangement - .miniso-tile--logo (Marken page, 03.09, no Figma frame) Source: none drawn;
rules from Razvan's review of 03.09. Composes the IP Worlds tile (14)
 with the Categories tones (14b): a pastel ground carrying EITHER a brand logo in a fixed inner box - 60% x 50% of the tile,
centred,
object-fit contain,
so every logo scales the same way and never touches the tile edge (the UK shop scales every logo into one white box the same way) - OR,
when the store has no logo for the vendor,
the brand name in Fredoka SemiBold,
centred both ways,
sized to the tile: about 11% of the tile width,
clamped between Subtitle 3 and Display/H3,
wrapping inside 70% of the width so "Hello Kitty and Friends"
stays inside. The host may put the 14 hover overlay on top;
nothing here depends on it. ELEMENTS __logo (an <img>, transparent PNG/SVG) __word (the typographic fallback) TONES .miniso-tile--aqua | --pistachio | --lavender from 14b,
unchanged. The tile is a size container (inline-size) so the word can use cqw;

browsers without container units keep the Display/H3 fallback declared first. ⚠️ ASSUMED - the box 60/50,
the word's 11cqw and its unitless 1.15 line-height (the type
 tokens carry px line-heights that cannot follow a fluid size). NO TOKEN.==========================================================================================*/ .miniso-tile--logo {
  container-type: inline-size;
}

.miniso-tile--logo .miniso-tile__logo {
  position: absolute;
  top: 25%;
  left: 20%;
  display: block;
  width: 60%;
  /* ⚠️ ASSUMED (Razvan 03.09): one inner box for every logo. NO TOKEN. */
  height: 50%;
  object-fit: contain;
  object-position: center;
}

.miniso-tile--logo .miniso-tile__word {
  position: absolute;
  inset: 0;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 15%;
  /* the word lives in the middle 70% of the tile. ⚠️ ASSUMED. NO TOKEN. */
  color: var(--miniso-text);
  text-align: center;
  font-family: var(--miniso-font);
  font-size: var(--miniso-h3);
  font-size: clamp(var(--miniso-sub3), 11cqw, var(--miniso-h3));
  /* ⚠️ ASSUMED. NO TOKEN. */
  font-weight: 600;
  line-height: 1.15;
  /* ⚠️ ASSUMED. NO TOKEN. */
  overflow-wrap: break-word;
}

/* ==========================================================================================
   17. CARD ROW - .miniso-row
   Source: docs/reference/design/pages/pdp/05-card-rows.md (re-read 04.09, geometry unchanged
   since 01.09). Nodes 96:13025 "Für Sie empfohlen" and 96:13061 "Zuletzt angesehen" (desktop),
   96:13923 / 96:13948 (mobile).

   WHY THIS IS A COMPONENT AND NOT SECTION CSS: the same row is drawn four times on the PDP and
   rendered by TWO different sections (Horizon's product-recommendations, restyled, and
   miniso-recently-viewed). Until 04.09 each carried its own layout - Horizon's carousel sized the
   recommendation cards from the viewport (255 at 1280, 236 at 393) while the other row used the
   drawn card (334 / 172) - and Razvan saw two rows of two different sizes on one page. One
   composition, consumed by both, and by nothing else: the homepage row (miniso-product-row) is
   the homepage's own H1-headed variant with a "see all" and stays where it is.

   GEOMETRY (desktop 96:13025): padding 32 64, head -> track 24, cards 334.75 -> 334 with gap 28,
   track 1423 wide in a 1384 content width = it runs under the right page edge (the fifth card is
   cut by the viewport, no arrows, no dots - 05-card-rows finding 7).
   MOBILE (96:13923): padding 24 16, head -> track 16, cards 172 with gap 4, cards at 0/176/354
   in a 362 frame = the same overflow.
   ELEMENTS  .miniso-section-head (component 5) as the head  .miniso-row__track  the scroller
   ========================================================================================== */

.miniso-row {
  --_miniso-row-gutter: 16px;
  /* 96:13924 x=16. NO TOKEN (page gutter). */
  --_miniso-row-pad: 24px;
  /* 96:13923 title at y 24. NO TOKEN. */
  --_miniso-row-gap: 16px;
  /* 96:13924 h28 at 24 -> grid at 68. NO TOKEN. */
  --_miniso-row-card: 172px;
  /* 96:13927. NO TOKEN. */
  --_miniso-row-card-gap: 4px;
  /* 96:13925: 0 / 176 / 354. NO TOKEN. */

  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  gap: var(--_miniso-row-gap);
  padding: var(--_miniso-row-pad) var(--_miniso-row-gutter);
  font-family: var(--miniso-font);
}

/* The `hidden` ATTRIBUTE is only `[hidden] { display: none }` in the UA stylesheet and ANY author
   `display` beats it - so `display: flex` above would silently cancel it and an empty "Zuletzt
   angesehen" head would render for every visitor with no history (Razvan caught exactly this in
   the theme editor, 01.09). */
.miniso-row[hidden] {
  display: none !important;
}

/* The track starts on the gutter and runs under the right edge of the page, as drawn. Negative
   margins pull it out of the section padding; the padding puts the first card back on the gutter
   and lets the last one scroll to the gutter too. */
.miniso-row__track {
  display: flex;
  gap: var(--_miniso-row-card-gap);
  margin-inline: calc(-1 * var(--_miniso-row-gutter));
  padding-inline: var(--_miniso-row-gutter);
  padding-bottom: 4px;
  /* room for the card's focus ring */
  overflow-x: auto;
  scroll-snap-type: x proximity;
  scroll-padding-inline: var(--_miniso-row-gutter);
  scrollbar-width: none;
}

.miniso-row__track::-webkit-scrollbar {
  display: none;
}

.miniso-row__track>.miniso-product-card {
  flex: 0 0 var(--_miniso-row-card);
  scroll-snap-align: start;
}

/* GEOMETRY ONLY - no type is redeclared here. */
@media (min-width: 750px) {
  .miniso-row {
    --_miniso-row-gutter: 64px;
    /* 96:13026 x=64. */
    --_miniso-row-pad: 32px;
    /* 96:13026 y=32. */
    --_miniso-row-gap: 24px;
    /* 96:13025 gap 24: title 32..76, grid at 100. */
    --_miniso-row-card: 334px;
    /* 96:13029 334.75 - flex rounding in Figma, not intent. */
    --_miniso-row-card-gap: 28px;
    /* 96:13028 gap 28. */
  }
}
/* Popular terms shown where a search found no products - search page empty
   state and the header popup, pills are the Secondary Small .miniso-btn */
.miniso-search-suggest {
  margin-top: 24px;
}

.miniso-search-suggest__title {
  margin: 0 0 12px;
  font-size: var(--miniso-sub3);
  line-height: var(--miniso-sub3-lh);
  color: var(--miniso-secondary-60);
}

.miniso-search-suggest__list {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* ==========================================================================================
   19. REVIEW CARD, aico Reviews extensions + REVIEW FORM - .miniso-review-form
   The aico Reviews module carries three things the Figma card never drew (06-customer-reviews §4
   is explicit that they are absent): a verified-purchase badge, review photos and the shop's
   reply. They are built from the tokens the card already uses - no new colour, no new font size.
   ⚠️ ASSUMED (for Iulia): badge, photo grid and reply block have no Figma node. The badge reuses
   the footnote type and Secondary/60; the photos are 64px squares at the card's own radius minus
   the 8 step; the reply is indented by the card gap with a Lavender rule.

   The write form (shown only when the theme setting aico_reviews_pdp_form is on) is composed
   from existing components: .miniso-input for every text field, .miniso-btn for the actions,
   .miniso-rating__star for the native radio stars. Only the composition lives here.
   ========================================================================================== */

.miniso-review-card__verified {
  flex: 0 0 auto;
  color: var(--miniso-secondary-60);
  font-size: var(--miniso-body-fn);
  line-height: var(--miniso-body-fn-lh);
  font-weight: 400;
}

.miniso-review-card__photos {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.miniso-review-card__photo img {
  display: block;
  width: 64px;
  height: 64px;
  object-fit: cover;
  border-radius: 8px;
  /* NO TOKEN (radius 8, the smallest of the seven the PDP draws). */
}

.miniso-review-card__reply {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding-inline-start: var(--_miniso-review-gap);
  border-inline-start: 2px solid var(--miniso-lavender);
}

.miniso-review-card__reply-title {
  margin: 0;
  color: var(--miniso-secondary-60);
  font-size: var(--miniso-body-fn);
  line-height: var(--miniso-body-fn-lh);
  font-weight: 500;
}

.miniso-review-card__reply-body {
  margin: 0;
  color: var(--miniso-text);
  font-size: var(--miniso-body-sm);
  line-height: var(--miniso-body-sm-lh);
  font-weight: 400;
}

/* ---------------------------------------------------------------------------- the form */

.miniso-review-form[hidden] {
  /* [hidden] loses to the display below without this - the same trap as .miniso-btn[hidden]. */
  display: none;
}

.miniso-review-form {
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding: 24px;
  border-radius: 16px;
  /* NO TOKEN - the review card's radius, the form sits in the same column. */
  background-color: var(--miniso-lavender-20);
  font-family: var(--miniso-font);
}

.miniso-review-form * {
  box-sizing: border-box;
}

.miniso-review-form__group {
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}

.miniso-review-form__row {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
}

.miniso-review-form__row > .miniso-review-form__group {
  flex: 1 1 240px;
}

.miniso-review-form__label {
  padding: 0;
  color: var(--miniso-text);
  font-size: var(--miniso-sub3);
  line-height: var(--miniso-sub3-lh);
  font-weight: 500;
}

.miniso-review-form__hint,
.miniso-review-form__error,
.miniso-review-form__status {
  margin: 0;
  font-size: var(--miniso-body-fn);
  line-height: var(--miniso-body-fn-lh);
  color: var(--miniso-secondary-60);
}

.miniso-review-form__error,
.miniso-review-form__status[data-kind='error'] {
  color: var(--miniso-primary);
}

/* Native radio stars: the input is visually hidden and keeps the label, so the control is a real
   radio group for the keyboard and for a screen reader ("1 Stern" ... "5 Sterne"). */
.miniso-review-form__star-row {
  display: flex;
  align-items: center;
  gap: 4px;
  /* the .miniso-rating component's own gap is 8px between stars and value; this row has neither */
}

.miniso-review-form__star {
  display: inline-flex;
  cursor: pointer;
}

.miniso-review-form__star-row {
  --_miniso-rating-size: 24px;
  /* NO TOKEN - the --lg star (20px) is the largest drawn; a touch target needs 24. */
}

.miniso-review-form__star .miniso-rating__star {
  --miniso-rating-fill: 0%;
}

.miniso-review-form__star-input:focus-visible + .miniso-review-form__star {
  outline: 2px solid var(--miniso-primary);
  outline-offset: 2px;
  border-radius: 4px;
}

.miniso-review-form__star.is-selected .miniso-rating__star,
.miniso-review-form__star.is-preview .miniso-rating__star {
  --miniso-rating-fill: 100%;
}

/* The input component has no multi-line variant; the box grows instead of holding 48px. */
.miniso-input--textarea .miniso-input__box {
  height: auto;
  align-items: stretch;
  border-radius: 16px;
  /* NO TOKEN - the pill radius of the 48px field would bow a 5-row box. */
}

.miniso-input--textarea .miniso-input__control {
  min-height: 96px;
  resize: vertical;
}

.miniso-review-form__file {
  font-size: var(--miniso-body-sm);
  line-height: var(--miniso-body-sm-lh);
  color: var(--miniso-text);
}

.miniso-review-form__check {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  font-size: var(--miniso-body-sm);
  line-height: var(--miniso-body-sm-lh);
  color: var(--miniso-text);
}

.miniso-review-form__check input {
  margin: 0;
  width: 16px;
  height: 16px;
  accent-color: var(--miniso-primary);
}

/* Honeypot (§10.1 of the contract): off-screen, never focusable, never announced. */
.miniso-review-form__pot {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.miniso-review-form__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 16px;
}

.miniso-review-form__notice {
  color: var(--miniso-secondary-60);
  font-size: var(--miniso-body-sm);
  line-height: var(--miniso-body-sm-lh);
  text-decoration: underline;
}

/* Thank-you state: the fields go, the status line stays where the form was. */
.miniso-review-form--sent > *:not(.miniso-review-form__status) {
  display: none;
}

.miniso-review-form--sent .miniso-review-form__status {
  color: var(--miniso-text);
  font-size: var(--miniso-body-lg);
  line-height: var(--miniso-body-lg-lh);
}
