/* ==========================================================================
   U Jabka — Soda design system: CATALOGUE PAGE
   Page-specific layout only. Every reusable recipe (card, pill, seg, badge,
   battery, breadcrumb, empty state) lives in soda/components.css and is used
   here as-is; this file adds the page's rhythm, the filter panel and the few
   catalogue-only signals.

   VISUAL CONTRACT: the approved homepage preview (homepage-preview.html,
   sections "Kategorie" and "Vybrané kusy skladem"). Where this file used to
   deviate from it — squat category tiles, a letterbox card image, a white hover
   edge, a recessed add-to-cart pill — it now follows the preview instead. The
   catalogue must read as the same design system as the homepage, not as a
   denser cousin of it.

   MOOD RULE: the mood (--accent / --halo) only ever appears here as an ACCENT:
   an edge, a badge, a selected state, a small CTA disc, an icon. Never as a
   surface behind text, never as body copy colour. All contrast comes from the
   static ink tokens.

   Scoped under html.theme-soda like the rest of the system, so a page that has
   not opted into the redesign cannot be affected by loading it.
   ========================================================================== */

html.theme-soda .soda-catalog {
    /* The catalogue is a working surface, not a stage: less air above than a
       marketing section, so the first row of stock is visible immediately. */
    padding-block: var(--page-intro-space) var(--section-gap);

    /*
     * THE DARK VESPER GROUND, in both surface themes, exactly as the homepage
     * carries it. The brief's grammar for this page is "a Vesper white content
     * plane on the dark global ground": the head and the category tabs sit
     * directly on the dark, and .soda-catalog__workspace is the plane.
     *
     * Its ink is scoped here rather than read from the theme for the same
     * reason .soda-hero scopes its own: the ground is Vesper's, not the surface
     * theme's, so it must not turn ivory under the light theme.
     */
    background: var(--vesper-void);
    color: #fff;

    --ink: #ffffff;
    --ink-muted: rgba(255, 255, 255, .72);
    --ink-faint: rgba(255, 255, 255, .55);
    --ink-2: rgba(255, 255, 255, .72);
    --ink-3: rgba(255, 255, 255, .55);
    --line: rgba(255, 255, 255, .15);
    --line-hi: rgba(255, 255, 255, .3);
    --glass-border: rgba(255, 255, 255, .15);
    --panel: transparent;
    --panel-hi: rgba(255, 255, 255, .06);
    --hover-wash: rgba(255, 255, 255, .07);
    --focus: #ffffff;

    /*
     * BUTTON CHROME MUST BE SCOPED WITH THE INK, or it silently keeps the
     * SURFACE THEME's values while everything around it flips.
     *
     * Found by driving a real order through in a browser rather than by
     * reading the stylesheet: --btn-bg is rgba(255,255,255,.88) under the light
     * theme, so a plain .soda-btn on this dark ground painted a near-white pill
     * carrying the scope's white label — 1.0:1, invisible. tokens.css records
     * exactly this failure for the hero CTA; the same three tokens close it
     * here.
     *
     * --accent goes white for the same reason it goes ink on the plane: these
     * pages carry no brand colour, so the CTA's square tile is ink-on-white
     * like the hero's rather than an orange chip.
     */
    --btn-bg: rgba(0, 0, 0, .42);
    --btn-bg-hover: rgba(0, 0, 0, .68);
    --btn-border: rgba(255, 255, 255, .18);
    --accent: #ffffff;
    --accent-rgb: 255, 255, 255;
    --accent-ink: #000000;
}

/* The display title is stated, not inherited: base.css declares
   `color: var(--ink)` on every heading to beat the legacy sheet, and an
   explicit declaration wins over the scope above. */
html.theme-soda .soda-catalog > .soda-container > nav,
html.theme-soda .soda-catalog__head .soda-sec-title {
    color: #fff;
}

/* ------------------------------------------------------------------- head */

html.theme-soda .soda-catalog__head {
    max-width: 62ch;
    margin-bottom: clamp(1.8rem, 3.4vw, 2.8rem);
}

html.theme-soda .soda-catalog__head .soda-sec-title {
    font-size: clamp(1.9rem, 3.4vw, 3rem);
}

/* Honest result count. Rendered as text, never as a decorative number. */
html.theme-soda .soda-catalog__count {
    margin: 1rem 0 0;
    font-size: .85rem;
    color: var(--ink-3);
}

html.theme-soda .soda-catalog__count strong {
    font-family: var(--font-display);
    font-size: 1.05rem;
    font-weight: 700;
    color: var(--ink);
    font-variant-numeric: tabular-nums;
    margin-right: .35rem;
}

/* -------------------------------------------------------- category tiles */

html.theme-soda .soda-catalog__cats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
    gap: var(--gap);
    margin-bottom: clamp(1.6rem, 3vw, 2.4rem);
}

/* Geometry deliberately NOT overridden: the tiles use the shared
   `.soda-cat-card` size (min-height 210px, 54px icon, 1.16rem name), which is
   the preview's `.cat-card`. The previous 138px/34px variant made the same
   component read as a different, cheaper one two clicks from the homepage.
   The only thing still needed here is sizing the sprite icon, because the
   catalogue markup nests a generic `.ico` (20px in soda/base.css) inside the
   54px icon slot. */
html.theme-soda .soda-catalog__cats .soda-cat-card__icon .ico {
    width: 100%;
    height: 100%;
}

/* The current category keeps a permanent accent edge, so the active state is
   readable without relying on hover. */
html.theme-soda .soda-cat-card.is-active {
    border-color: rgba(var(--accent-rgb), .5);
    background: var(--panel-hi);
}

html.theme-soda .soda-cat-card.is-active .soda-cat-card__glow {
    opacity: 1;
}

/* Preview parity: hover lifts the edge to the accent, not to plain white. */
html.theme-soda .soda-catalog__cats .soda-cat-card:hover,
html.theme-soda .soda-catalog__cats .soda-cat-card:focus-within {
    border-color: rgba(var(--accent-rgb), .55);
}

/* ------------------------------------------------------------ filter panel */

html.theme-soda .soda-catalog__filters {
    margin-bottom: clamp(1.6rem, 3vw, 2.4rem);
    background: var(--glass-bg);
    border: 1px solid var(--glass-border);
    border-radius: var(--r-card);
    -webkit-backdrop-filter: blur(var(--blur-sm));
    backdrop-filter: blur(var(--blur-sm));
    transition: border-color var(--dur) ease;
}

html.theme-soda .soda-catalog__filters[open] {
    border-color: rgba(var(--accent-rgb), .22);
}

html.theme-soda .soda-catalog__filters-summary {
    display: flex;
    align-items: center;
    gap: .8rem;
    padding: .95rem 1.25rem;
    font-family: var(--font-display);
    font-size: .9rem;
    font-weight: 600;
    letter-spacing: .01em;
    color: var(--ink);
    cursor: pointer;
    list-style: none;
}

html.theme-soda .soda-catalog__filters-summary::-webkit-details-marker {
    display: none;
}

/* The disclosure needs a visible affordance on every viewport, not only on the
   one where JS collapses it. A CSS chevron keeps that CSP-safe and JS-free. */
html.theme-soda .soda-catalog__filters-summary::after {
    content: '';
    flex: none;
    width: 8px;
    height: 8px;
    margin-right: .15rem;
    border-right: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    transform: translateY(-2px) rotate(45deg);
    opacity: .7;
    transition: transform var(--dur) var(--ease), opacity var(--dur) ease;
}

html.theme-soda .soda-catalog__filters[open] .soda-catalog__filters-summary::after {
    transform: translateY(2px) rotate(-135deg);
    opacity: 1;
}

html.theme-soda .soda-catalog__filters-summary:hover {
    color: var(--ink);
}

html.theme-soda .soda-catalog__filters-count {
    display: inline-grid;
    place-items: center;
    min-width: 26px;
    height: 26px;
    margin-left: auto;
    padding: 0 .5rem;
    border-radius: var(--r-mark);
    background: rgba(255, 255, 255, .1);
    font-size: .72rem;
    font-variant-numeric: tabular-nums;
    color: var(--ink-2);
}

html.theme-soda .soda-catalog__filters-body {
    display: flex;
    flex-direction: column;
    gap: .9rem;
    padding: 0 1.25rem 1.15rem;
    border-top: 1px solid var(--line);
    padding-top: 1.05rem;
}

html.theme-soda .soda-catalog__row {
    display: flex;
    align-items: center;
    gap: .9rem;
    min-width: 0;
}

html.theme-soda .soda-catalog__row-label {
    flex: none;
    width: 6.5rem;
    font-family: var(--font-display);
    font-size: .74rem;
    font-weight: 600;
    letter-spacing: .12em;
    text-transform: uppercase;
    color: var(--ink-3);
}

/* A control row must never be wider than the panel: it is the row that scrolls
   internally, not the page. Without the min-width reset a long model list makes
   the flex item grow to max-content and the tail is simply clipped away by the
   page's overflow-x guard, i.e. unreachable. */
html.theme-soda .soda-catalog__row > .soda-pill-row,
html.theme-soda .soda-catalog__row > .soda-catalog__seg {
    min-width: 0;
    max-width: 100%;
}

/* Anchors reuse the segmented recipe: filters must survive with JS disabled,
   so every control is a real link, never a button that needs a handler. */

/* The small count that rides inside a filter control. */
html.theme-soda .soda-catalog__n {
    font-size: .68rem;
    font-variant-numeric: tabular-nums;
    opacity: .62;
}

html.theme-soda .soda-catalog__note {
    margin: .2rem 0 0;
}

html.theme-soda .soda-catalog__reset {
    margin: .3rem 0 0;
}

/* ------------------------------------------------------------------- grid */

html.theme-soda .soda-catalog__grid {
    align-items: stretch;
}

/*
 * THE PRODUCT CARDS, deliberately the same object as the homepage's featured
 * stock (.soda-prod__item in home.css). That has always been the rule here; what
 * changed is what the object IS.
 *
 * It used to be a hairline-SEPARATED cell, because a bordered box with a gutter
 * read as a card stack. That held while the media was an outline drawing. With
 * real product photography on the tiles it stopped holding: four rules around
 * every photograph turns a page of phones into a table of rows with pictures in
 * it. The homepage moved to gallery tiles for that reason and the catalogue
 * follows, so the two lists remain one component rather than drifting into two.
 *
 * Still no shadow, no hover lift, no glow behind the media, no gutter-plus-fill
 * card stack: one hairline, one restrained corner, real gutters.
 */
html.theme-soda .soda-catalog__grid {
    gap: 1.111vw;
}

/* ============================== THE CARD FRAME — 21st product-list.md, ported
 *
 * Source root:
 *   <Card className="w-full max-w-sm overflow-hidden group bg-background
 *                    text-foreground shadow-xl hover:shadow-lg
 *                    transition-all duration-300 rounded-md">
 * plus shadcn <Card>'s own `border`.
 *
 * THE FRAME AND THE SHELL ARE TWO ELEMENTS HERE, and that is the one
 * structural translation this card needs. The source clips its own content
 * with `overflow-hidden`; the owner-approved pointer-following arc is drawn by
 * this element's ::before at inset -1px — ENTIRELY outside the padding box —
 * so an overflow:hidden frame deletes the arc, exactly as it silently did
 * before `.soda-catalog-card` was given overflow:visible. The <article>
 * therefore keeps the source's border, radius and max-width and stays
 * unclipped; `.soda-catalog-card__shell` inside it carries the source's
 * overflow-hidden, its radius and its shadow. Same painted result, arc intact.
 * ---------------------------------------------------------------------- */
html.theme-soda .soda-catalog-card {
    position: relative;
    display: flex;
    flex-direction: column;
    width: 100%;
    max-width: var(--t21-max-sm);           /* max-w-sm  = 24rem            */
    padding: 0;                             /* the source <Card> has none   */
    border: 1px solid rgba(0, 0, 0, .12);   /* shadcn `border`              */
    border-radius: var(--t21-radius-md);    /* rounded-md = .375rem         */
    background: #fff;                       /* bg-background on this plane  */
}

html.theme-soda .soda-catalog-card__shell {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-width: 0;
    border-radius: var(--t21-radius-md);
    overflow: hidden;                       /* source `overflow-hidden`     */
    background: #fff;
    box-shadow: var(--t21-shadow-xl);       /* shadow-xl                    */
    /* `transition-all duration-300`. Tailwind's default timing function is
       cubic-bezier(.4, 0, .2, 1) — the same curve Framer calls easeInOut. */
    transition: box-shadow var(--t21-dur-300) var(--t21-ease-in-out);
}

/* THE SOURCE'S SHADOW GETS SMALLER ON HOVER (shadow-xl -> shadow-lg). Ported
   as written. Focus is the equal state here, as it already is for the arc and
   the title wipe, so the card reached by Tab is the card reached by pointer. */
html.theme-soda .soda-catalog-card:hover .soda-catalog-card__shell,
html.theme-soda .soda-catalog-card:focus-within .soda-catalog-card__shell {
    box-shadow: var(--t21-shadow-lg);       /* hover:shadow-lg              */
}

/* The source's meta row is `flex items-center gap-2 mt-1`. Its own contents —
   a star rating, a review count and a "Free shipping" flag — have no U Jabka
   authority, so the row carries the two real facts this catalogue does own:
   the category, and the availability in the source's right-aligned `ml-auto`
   slot (the emerald one, which is where a delivery/stock fact belongs). */
html.theme-soda .soda-catalog-card__meta {
    display: flex;
    align-items: center;
    gap: var(--t21-2);                      /* gap-2  = .5rem               */
    margin-top: var(--t21-1);               /* mt-1   = .25rem              */
}

html.theme-soda .soda-catalog-card__cat {
    margin: 0;
    font-size: var(--t21-fs-xs);            /* text-xs = .75rem / 1rem      */
    line-height: var(--t21-lh-xs);
    color: var(--ink-3);                    /* text-muted-foreground        */
}

html.theme-soda .soda-catalog-card .soda-card__name a {
    color: inherit;
}

/* Preview parity: the in-card detail affordance reads "Detail →". The glyph is
   generated, so it never reaches the accessibility tree twice — the link
   already carries an explicit aria-label. */
html.theme-soda .soda-catalog-card .soda-card__link::after {
    content: ' \2192';
    transition: transform var(--dur-fast) ease;
}

html.theme-soda .soda-catalog-card:hover .soda-card__link {
    opacity: 1;
}

/* ------------------------------------ content stack — <CardContent p-4>
   Source: `<CardContent className="p-4">` wrapping `<div className="space-y-3">`.
   `.soda-card__body`'s hairline separator is not in the source and goes. */
html.theme-soda .soda-catalog-card__content {
    display: flex;
    flex-direction: column;
    flex: 1;
    gap: var(--t21-3);                      /* space-y-3 = .75rem           */
    padding: var(--t21-4);                  /* p-4       = 1rem             */
    border-top: 0;
}

/* The title's own wrapper: `<div>` holding the h3 and the meta row. */
html.theme-soda .soda-catalog-card__head {
    min-width: 0;
}

/* `flex items-baseline gap-2`. The struck-through original price is a real
   node in the source and has NO U Jabka authority — there is no "was" price
   in the read model — so it is omitted rather than invented. */
html.theme-soda .soda-catalog-card__price-row {
    display: flex;
    align-items: baseline;
    gap: var(--t21-2);                      /* gap-2 = .5rem                */
}

html.theme-soda .soda-catalog-card__price {
    margin: 0;
    font-size: var(--t21-fs-lg);            /* text-lg = 1.125rem / 1.75rem */
    line-height: var(--t21-lh-lg);
    font-weight: 600;                       /* font-semibold                */
    /* .soda-price contributes letter-spacing: -.02em. Neither source puts a
       tracking utility on its price span, so the ported slot sheds it -- the
       same shed already applied to the ported card title and badge. */
    letter-spacing: normal;
}

/* `<div className="space-y-3">` of `space-y-1.5` option groups. */
html.theme-soda .soda-catalog-card__groups {
    display: flex;
    flex-direction: column;
    gap: var(--t21-3);
}

html.theme-soda .soda-catalog-card__group {
    display: flex;
    flex-direction: column;
    gap: var(--t21-1_5);                    /* space-y-1.5 = .375rem        */
}

/* The group label: `text-xs text-muted-foreground`. The on-order note has no
   slot in the source at all and borrows the same type rather than inventing
   one — it is a required truthful disclosure, not decoration. */
html.theme-soda .soda-catalog-card__group-label,
html.theme-soda .soda-catalog-card__note {
    margin: 0;
    font-size: var(--t21-fs-xs);            /* text-xs = .75rem / 1rem      */
    line-height: var(--t21-lh-xs);
    color: var(--ink-3);
}

/* `flex flex-wrap gap-2` — the source's size row. */
html.theme-soda .soda-catalog-card__chips {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--t21-2);
    margin: 0;
    padding: 0;
    list-style: none;
}

/* The size chip, verbatim: `min-w-[2.5rem] h-8 px-2 rounded-md text-xs
   font-medium bg-muted/60`. These carry facts, not choices — a used kus has
   exactly one configuration and one grade — so they are spans with no
   selected state and no hover affordance. */
html.theme-soda .soda-catalog-card__chip {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* The configuration chip still carries `.soda-card__spec` as a data hook,
       and that component's own top margin would drop it below its row. */
    margin: 0;
    gap: var(--t21-2);
    min-width: 2.5rem;                      /* min-w-[2.5rem], arbitrary in
                                               the source; no token exists  */
    height: var(--t21-size-8);              /* h-8  = 2rem                  */
    padding-inline: var(--t21-2);           /* px-2 = .5rem                 */
    border-radius: var(--t21-radius-md);    /* rounded-md                   */
    /* bg-muted/60 resolved on this card's white ground. */
    background: rgba(20, 22, 26, .06);
    font-size: var(--t21-fs-xs);
    line-height: var(--t21-lh-xs);
    font-weight: 500;                       /* font-medium                  */
    color: var(--ink-2);
    white-space: nowrap;
}

/* The battery meter rides inside a chip, so its track needs a ground that is
   visible on white — its component default is a light-on-dark alpha. */
html.theme-soda .soda-catalog-card__chip .soda-battery__bar {
    background: rgba(20, 22, 26, .12);
}

/* THE META ROW'S RIGHT-HAND SLOT. The source puts `Free shipping` here as
   `text-xs text-emerald-600 ml-auto`; U Jabka has no free-shipping flag on a
   product, and it does have availability, which is the honest occupant of a
   right-aligned green fact. Colour is never the only signal: every state
   carries its own words. */
html.theme-soda .soda-catalog-card__stock,
html.theme-soda .soda-catalog-card__availability {
    margin-left: auto;                      /* ml-auto                      */
    font-size: var(--t21-fs-xs);
    line-height: var(--t21-lh-xs);
    color: var(--success);                  /* text-emerald-600             */
    white-space: nowrap;
}

html.theme-soda .soda-catalog-card__availability--on_order {
    color: var(--warning);
}

html.theme-soda .soda-catalog-card__availability--out_of_stock,
html.theme-soda .soda-catalog-card__availability--hidden,
html.theme-soda .soda-catalog-card__availability--unknown {
    color: var(--ink-3);
}

/* --------------------------------------- footer — <CardFooter p-4 pt-0> */
html.theme-soda .soda-catalog-card__footer {
    padding: var(--t21-4);                  /* p-4                          */
    padding-top: 0;                         /* pt-0                         */
}

/* The honest counterpart of the CTA: the source's own button box, no action. */
html.theme-soda .soda-catalog-card__unavailable {
    margin: 0;
    text-align: center;
    font-size: var(--t21-fs-sm);
    line-height: var(--t21-lh-sm);
    color: var(--ink-3);
}

/* ------------------------------------------------------------ placeholders */
/* Only NEW products may show one — a concrete used kus is public exactly when
   it has its own photograph, so it never reaches this branch. A missing photo
   must degrade to a deliberate, styled stand-in, never to a broken <img>. */

html.theme-soda .soda-catalog-card__device {
    width: 58%;
    max-width: 130px;
    max-height: 78%;
    color: rgba(255, 255, 255, .10);
    filter: drop-shadow(0 18px 26px rgba(0, 0, 0, .45));
}

html.theme-soda .soda-catalog-card__ph {
    display: grid;
    place-items: center;
    width: 84px;
    height: 84px;
    /* An 84px glyph box, not product media — it takes the control corner, one
       step tighter than the card around it. (It used to be a circle; Vesper's
       own markers are squares, and this now meets them halfway.) */
    border-radius: var(--r-control);
    border: 1px solid var(--line);
    background: rgba(255, 255, 255, .04);
    color: var(--ink-3);
}

html.theme-soda .soda-catalog-card__ph .ico {
    width: 32px;
    height: 32px;
}

/* An unavailable card mutes its artwork whether that artwork is a photo or a
   stand-in; previously only the photo branch was muted. */
html.theme-soda .soda-card--muted .soda-catalog-card__device,
html.theme-soda .soda-card--muted .soda-catalog-card__ph {
    opacity: .55;
}

/* ------------------------------------------------------------ badge column
   Source: `<div className="absolute top-3 left-3 flex flex-col gap-2">`.
   It holds up to three Badges there — New, Best Seller and a discount
   percentage. U Jabka has ONE real badge per card (the read model's
   `badge.label`); a best-seller ranking and a discount percentage have no
   authority here and are not fabricated. */
html.theme-soda .soda-catalog-card__badges {
    position: absolute;
    top: var(--t21-3);                      /* top-3  = .75rem              */
    left: var(--t21-3);                     /* left-3 = .75rem              */
    z-index: 2;
    display: flex;
    flex-direction: column;                 /* flex-col                     */
    gap: var(--t21-2);                      /* gap-2  = .5rem               */
    /* The source's badges are not interactive; the media link keeps the whole
       tile clickable underneath them. */
    pointer-events: none;
}

/* shadcn <Badge>: `rounded-full border px-2.5 py-0.5 text-xs font-semibold`.
   The opaque ground is kept from the previous card: the media is now a
   cover-cropped photograph, and a translucent state tint over an arbitrary
   product colour is unreadable. */
html.theme-soda .soda-catalog-card .soda-badge--floating {
    position: static;
    top: auto;
    left: auto;
    border-radius: var(--t21-radius-full);  /* rounded-full                 */
    /* py-0.5 / px-2.5 — Tailwind's .5 and 2.5 steps; no token carries them. */
    padding: .125rem .625rem;
    border: 1px solid rgba(0, 0, 0, .12);
    font-size: var(--t21-fs-xs);            /* text-xs = .75rem / 1rem      */
    line-height: var(--t21-lh-xs);
    font-weight: 600;                       /* font-semibold                */
    letter-spacing: normal;
    text-transform: none;
    background: #fff;
    color: #14161a;
}

/* -------------------------------------------------------------- card media */

/* OWNER DEVIATION (2026-09-03) — THE FRAME IS SQUARE AGAIN.

   The source is `relative aspect-[3/4] overflow-hidden`, and that 3/4 frame is
   what the owner rejected: "na novem je obrazek moc velky." The card itself —
   max-w-sm, rounded-md, the shadow ramp, the badge column, the p-4 stack, the
   pointer arc — stays exactly as ported; ONLY the media's proportion returns to
   the pre-transplant catalogue's `aspect-ratio: 1 / 1`, which is what the owner
   means by "velikost obrazku ze stareho product listu".

   Everything else in this rule is the source's and is untouched. */
html.theme-soda .soda-catalog-card .soda-card__media {
    position: relative;
    display: block;
    aspect-ratio: 1 / 1;
}

/* ------------------------------------------------------------- empty state */

html.theme-soda .soda-catalog .soda-empty {
    margin-top: .4rem;
}

/* `.soda-empty` centres its copy, so its call to action has to centre too —
   `.soda-btn` is `width: fit-content`, which would otherwise pin it left. */
html.theme-soda .soda-catalog__empty-cta {
    margin-inline: auto;
}

/* ------------------------------------------------------------ battery meter */
/* The fill width is a bucketed class, not an inline style: the enforcing CSP
   has no `unsafe-inline`, so `style="width:96%"` would be dropped and every
   meter would read as empty. 5 % buckets are below visual discrimination at
   46px wide, and the exact percentage is always printed as text next to it. */

html.theme-soda .soda-battery__fill--p0 { width: 0%; }
html.theme-soda .soda-battery__fill--p5 { width: 5%; }
html.theme-soda .soda-battery__fill--p10 { width: 10%; }
html.theme-soda .soda-battery__fill--p15 { width: 15%; }
html.theme-soda .soda-battery__fill--p20 { width: 20%; }
html.theme-soda .soda-battery__fill--p25 { width: 25%; }
html.theme-soda .soda-battery__fill--p30 { width: 30%; }
html.theme-soda .soda-battery__fill--p35 { width: 35%; }
html.theme-soda .soda-battery__fill--p40 { width: 40%; }
html.theme-soda .soda-battery__fill--p45 { width: 45%; }
html.theme-soda .soda-battery__fill--p50 { width: 50%; }
html.theme-soda .soda-battery__fill--p55 { width: 55%; }
html.theme-soda .soda-battery__fill--p60 { width: 60%; }
html.theme-soda .soda-battery__fill--p65 { width: 65%; }
html.theme-soda .soda-battery__fill--p70 { width: 70%; }
html.theme-soda .soda-battery__fill--p75 { width: 75%; }
html.theme-soda .soda-battery__fill--p80 { width: 80%; }
html.theme-soda .soda-battery__fill--p85 { width: 85%; }
html.theme-soda .soda-battery__fill--p90 { width: 90%; }
html.theme-soda .soda-battery__fill--p95 { width: 95%; }
html.theme-soda .soda-battery__fill--p100 { width: 100%; }

/* The meter now IS one of the source's chips (see .soda-catalog-card__chip),
   so it needs no pill of its own — only a track that reads on white. */

/* ------------------------------------------------------------- breakpoints */

@media (max-width: 1180px) {
    html.theme-soda .soda-catalog__grid {
        grid-template-columns: repeat(3, 1fr);
    }
}

@media (max-width: 900px) {
    html.theme-soda .soda-catalog__grid {
        grid-template-columns: repeat(2, 1fr);
    }

    /* Label above the controls. `stretch` (not `flex-start`) is load-bearing:
       flex-start shrink-wraps each control to max-content, which is how a long
       pill row used to grow past the panel and get clipped instead of
       scrolling. */
    html.theme-soda .soda-catalog__row {
        align-items: stretch;
        flex-direction: column;
        gap: .5rem;
    }

    html.theme-soda .soda-catalog__row-label {
        width: auto;
    }
}

@media (max-width: 720px) {
    /* Filter rows become horizontally scrollable pill rows so a long model list
       never pushes the stock off the screen. The panel itself collapses (see
       js/soda/catalog.js); it is server-rendered open, so with JS off the rows
       are simply all visible. */
    html.theme-soda .soda-catalog__seg {
        max-width: 100%;
        overflow-x: auto;
        scrollbar-width: none;
    }

    html.theme-soda .soda-catalog__seg::-webkit-scrollbar {
        display: none;
    }

    /* Grow to fill the row when there is room, never shrink below the label —
       so a short segment reads as a full-width switch (preview's mobile `.seg`)
       and a long one still scrolls. */
    

    html.theme-soda .soda-catalog__filters-body {
        padding-inline: 1rem;
    }

    html.theme-soda .soda-catalog__cats {
        grid-template-columns: repeat(2, 1fr);
    }

    /* Preview's own mobile category tile. */
    html.theme-soda .soda-catalog__cats .soda-cat-card {
        min-height: 186px;
        padding: 1.3rem 1.15rem;
    }

    html.theme-soda .soda-catalog__cats .soda-cat-card__icon {
        width: 42px;
        height: 42px;
    }

    html.theme-soda .soda-catalog__cats .soda-cat-card__go {
        display: none;
    }
}

@media (max-width: 560px) {
    html.theme-soda .soda-catalog__grid {
        grid-template-columns: 1fr;
    }

    html.theme-soda .soda-catalog__results,
    html.theme-soda .soda-catalog__sidebar {
        padding: 1.25rem;
    }

    /* No card padding — the ported source card is padding-less. The SECOND
       aspect ratio is back with the square one, though: `max-w-sm` is 24rem,
       which a 390px viewport does not reach, so a one-column card really does
       run the full column and a square frame really would hand the photo most
       of a screen. That was the pre-transplant comment's reason and it still
       holds. (The later CATALOG 2.0 block repeats this override because that
       block supersedes this one; both are kept in step.) */
    html.theme-soda .soda-catalog-card .soda-card__media {
        aspect-ratio: 4 / 3;
    }
}

/* ===========================================================================
   CATALOG 2.0 — compact browse workspace

   The rules below intentionally supersede the earlier full-width filter rows.
   They remain scoped to the catalog page: global Soda tokens, buttons and card
   recipes remain untouched. The server still renders every control; this CSS
   only changes their information hierarchy.
   ========================================================================== */

/* --------------------------------------------------------- category layer */

/* No reservation, and nothing left to reserve for. The introductory block used
   to hold an eyebrow, a heading and a lead of three different lengths, and the
   category row below it could switch between them in place — so a fixed height
   was the only thing keeping the product grid from jumping. There is one line
   of type here now, and no way to change category without leaving the page. */
html.theme-soda .soda-catalog__head {
    margin-bottom: clamp(1.1rem, 2vw, 1.6rem);
}

/* ---------------------------------------------------------- page geometry */

/*
 * THE PLANE'S OWN GEOMETRY.
 *
 * The two columns are divided by a HAIRLINE rather than by a gutter, and each
 * owns its padding — the same "rules, not gaps" grammar the homepage band and
 * the featured cells use. The rule lives on the results column, not on the
 * sidebar, because the sidebar is sticky and a border on a sticky box stops
 * where the box does instead of running the full height of the plane.
 */
html.theme-soda .soda-catalog__workspace {
    display: grid;
    grid-template-columns: minmax(13.75rem, 17rem) minmax(0, 1fr);
    align-items: start;
    gap: 0;
}

html.theme-soda .soda-catalog__sidebar {
    position: sticky;
    top: 6.25rem;
    min-width: 0;
    padding: 2.222vw 1.667vw;
}

html.theme-soda .soda-catalog__results {
    min-width: 0;
    padding: 2.222vw;
    border-left: 1px solid rgba(0, 0, 0, .15);
}

/* -------------------------------------------------------- filter sidebar */

/* On the plane the filter panel is a ruled column, not a floating card: no
   box, no fill, no blur, and its open state is not an accent edge. The
   surrounding rule is drawn by the results column beside it. */
html.theme-soda .soda-catalog__filters {
    margin: 0;
    border: 0;
    border-radius: var(--r-none);
    background: transparent;
}

html.theme-soda .soda-catalog__filters[open] {
    border: 0;
}

html.theme-soda .soda-catalog__filters-summary {
    padding-inline: 0;
    border-bottom: 1px solid rgba(0, 0, 0, .15);
}

html.theme-soda .soda-catalog__filters-body {
    padding-inline: 0;
    border-top: 0;
}

html.theme-soda .soda-catalog__filters-summary {
    min-height: 3.1rem;
    padding: .78rem .95rem;
}

html.theme-soda .soda-catalog__filters-state {
    margin-left: auto;
    color: var(--ink-3);
    font-family: var(--font-main);
    font-size: .72rem;
    font-weight: 500;
}

html.theme-soda .soda-catalog__filters-body {
    gap: 0;
    padding: 0;
    border-top-color: var(--line);
}

html.theme-soda .soda-catalog__filter-form {
    display: flex;
    flex-direction: column;
    gap: .85rem;
    padding: .9rem;
}

html.theme-soda .soda-catalog__facet {
    min-width: 0;
    margin: 0;
    padding: 0;
    border: 0;
}

html.theme-soda .soda-catalog__facet legend {
    margin: 0 0 .46rem;
    padding: 0;
    color: var(--ink-2);
    font-family: var(--font-display);
    font-size: .77rem;
    font-weight: 650;
    letter-spacing: .03em;
}

html.theme-soda .soda-catalog__facet .soda-select {
    min-height: 2.7rem;
    padding: .58rem .68rem;
    border-radius: var(--r-control);
    font-size: .83rem;
}

html.theme-soda .soda-catalog__facet .soda-select:disabled {
    cursor: not-allowed;
    opacity: .5;
}

html.theme-soda .soda-catalog__state-options {
    display: grid;
    gap: .38rem;
}

html.theme-soda .soda-catalog__state-option {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center;
    gap: .5rem;
    min-height: 2.35rem;
    padding: .42rem .55rem;
    border: 1px solid var(--line);
    border-radius: var(--r-control);
    background: rgba(255, 255, 255, .02);
    color: var(--ink-2);
    font-size: .8rem;
    cursor: pointer;
    transition: border-color var(--dur-fast) ease, background var(--dur-fast) ease;
}

html.theme-soda .soda-catalog__state-option:hover {
    border-color: var(--line-hi);
    background: var(--panel-hi);
}

html.theme-soda .soda-catalog__state-option:has(input:checked) {
    border-color: rgba(var(--accent-rgb), .58);
    background: color-mix(in srgb, var(--accent) 12%, var(--panel));
    color: var(--ink);
}

/* ------------------------------------------------ the panel's own controls
 *
 * DRAWN, NOT INHERITED, and that is the whole fix.
 *
 * These boxes used to be UA-painted controls tinted with
 * `accent-color: var(--accent)`. Two leftovers met on this white panel:
 *
 *   1. tokens.css puts `color-scheme: dark` on the document, and a native
 *      radio under a dark scheme is a DARK disc with a pale rim. The filter
 *      panel is not on the dark ground — it is on .soda-plane, which is
 *      opaque white — so every unselected option carried a near-black box on
 *      paper, which is the "dark leftover" the panel reads as.
 *   2. --accent resolves to #000 on the plane, so the checked dot the UA drew
 *      was ink inside that same dark disc: selected and unselected differed by
 *      almost nothing.
 *
 * Both states are therefore drawn here instead of asked for: white ground,
 * ink outline, an ink mark when chosen. Colour is never the only signal —
 * the row around the control also changes its edge and its wash — and the
 * three states stay distinguishable in monochrome, because they differ by
 * whether the box is FILLED, not by which hue it is.
 *
 * Scoped to the filter panel: no shared component and no other page's
 * controls are touched.
 */
html.theme-soda .soda-catalog__filters input[type='radio'],
html.theme-soda .soda-catalog__filters input[type='checkbox'] {
    appearance: none;
    -webkit-appearance: none;
    /* An explicit light scheme for whatever the UA still paints for this
       control — the default focus ring, the forced-colors fallback — so none
       of it arrives dressed for the dark ground. */
    color-scheme: light;
    flex: none;
    width: 1rem;
    height: 1rem;
    margin: 0;
    border: 1px solid var(--ink);
    background: #fff;
    cursor: pointer;
    transition: background var(--dur-fast) ease, border-color var(--dur-fast) ease, box-shadow var(--dur-fast) ease;
}

html.theme-soda .soda-catalog__filters input[type='radio'] {
    border-radius: 50%;
}

html.theme-soda .soda-catalog__filters input[type='checkbox'] {
    border-radius: var(--r-mark);
}

/* CHOSEN: the box fills with ink and keeps a white ring inside its own edge,
   so a radio reads as a dot and a checkbox as a filled mark. `box-shadow`
   rather than a pseudo-element on purpose — an <input> is a replaced element
   and does not reliably render ::before. */
html.theme-soda .soda-catalog__filters input[type='radio']:checked,
html.theme-soda .soda-catalog__filters input[type='checkbox']:checked {
    border-color: var(--ink);
    background: var(--ink);
}

html.theme-soda .soda-catalog__filters input[type='radio']:checked {
    box-shadow: inset 0 0 0 3px #fff;
}

html.theme-soda .soda-catalog__filters input[type='checkbox']:checked {
    box-shadow: inset 0 0 0 2px #fff;
}

/* UNAVAILABLE: a muted outline and a muted fill, never an invisible control —
   the label beside it still says what it is. */
html.theme-soda .soda-catalog__filters input[type='radio']:disabled,
html.theme-soda .soda-catalog__filters input[type='checkbox']:disabled {
    border-color: rgba(0, 0, 0, .28);
    background: rgba(0, 0, 0, .06);
    cursor: not-allowed;
}

html.theme-soda .soda-catalog__filters input[type='radio']:disabled:checked,
html.theme-soda .soda-catalog__filters input[type='checkbox']:disabled:checked {
    background: rgba(0, 0, 0, .45);
}

/* FOCUS is the page's own ring, offset so it never sits on the outline it has
   to be told apart from. */
html.theme-soda .soda-catalog__filters input[type='radio']:focus-visible,
html.theme-soda .soda-catalog__filters input[type='checkbox']:focus-visible {
    outline: var(--focus-ring) solid var(--focus);
    outline-offset: var(--focus-offset);
}

html.theme-soda .soda-catalog__state-option small {
    color: var(--ink-3);
    font-size: .7rem;
    font-variant-numeric: tabular-nums;
}

html.theme-soda .soda-catalog__facet-hint {
    margin: -.15rem 0 .05rem;
    color: var(--ink-3);
    font-size: .72rem;
    line-height: 1.4;
}

html.theme-soda .soda-catalog__filter-actions {
    display: grid;
    gap: .6rem;
    padding-top: .15rem;
}

html.theme-soda .soda-catalog__filter-submit {
    width: 100%;
    min-height: 2.65rem;
    justify-content: center;
    padding: .65rem .85rem;
    font-size: .82rem;
}

html.theme-soda .soda-catalog__clear-all {
    color: var(--ink-3);
    font-size: .76rem;
    text-decoration: underline;
    text-decoration-color: rgba(var(--accent-rgb), .45);
    text-underline-offset: .18em;
}

html.theme-soda .soda-catalog__clear-all:hover {
    color: var(--accent);
}

html.theme-soda .soda-catalog__secondary-facets {
    margin: 0 .9rem .9rem;
    padding-top: .72rem;
    border-top: 1px solid var(--line);
}

html.theme-soda .soda-catalog__secondary-facets > summary {
    color: var(--ink-3);
    font-family: var(--font-display);
    font-size: .74rem;
    font-weight: 600;
    cursor: pointer;
}

html.theme-soda .soda-catalog__quick-facet {
    display: grid;
    gap: .45rem;
    margin-top: .8rem;
}

html.theme-soda .soda-catalog__quick-facet > span {
    color: var(--ink-3);
    font-family: var(--font-display);
    font-size: .7rem;
    font-weight: 600;
    letter-spacing: .09em;
    text-transform: uppercase;
}

html.theme-soda .soda-catalog__quick-facet .soda-pill {
    min-height: 2rem;
    padding: .38rem .65rem;
    font-size: .73rem;
}

html.theme-soda .soda-catalog__note {
    margin: 0 .9rem .9rem;
    font-size: .74rem;
}

/* ---------------------------------------------------------- result header */

html.theme-soda .soda-catalog__results-head {
    display: flex;
    align-items: end;
    justify-content: space-between;
    gap: 1rem;
    margin: 0 0 1rem;
}

html.theme-soda .soda-catalog__results-head h2 {
    margin: 0;
    color: var(--ink);
    font-family: var(--font-display);
    font-size: clamp(1.25rem, 2vw, 1.55rem);
    font-weight: 650;
    letter-spacing: -.025em;
}

html.theme-soda .soda-catalog__results-head .soda-catalog__count {
    margin: .25rem 0 0;
}

html.theme-soda .soda-catalog__sort-form {
    display: grid;
    gap: .35rem;
    min-width: min(100%, 18rem);
}

html.theme-soda .soda-catalog__sort-form > label {
    color: var(--ink-3);
    font-family: var(--font-display);
    font-size: .72rem;
    font-weight: 600;
    letter-spacing: .08em;
    text-transform: uppercase;
}

html.theme-soda .soda-catalog__sort-control {
    display: flex;
    gap: .45rem;
}

html.theme-soda .soda-catalog__sort-control .soda-select {
    flex: 1 1 auto;
    min-width: 0;
    min-height: 2.6rem;
    padding: .55rem .65rem;
    font-size: .82rem;
}

html.theme-soda .soda-catalog__sort-submit {
    flex: none;
    min-height: 2.6rem;
    padding: .55rem .8rem;
    font-size: .78rem;
}

/* The button is retained as the plain GET fallback.  Enhanced filtering is
   immediate on change, so it must not occupy space or invite a second submit. */
html.theme-soda.js-soda .soda-catalog__sort-submit {
    display: none;
}

/* This remains a normal GET submit for non-enhanced browsing. */
html.theme-soda.js-soda .soda-catalog__filter-submit {
    display: none;
}

html.theme-soda .soda-catalog__active-filters {
    display: flex;
    align-items: start;
    gap: .55rem;
    margin: 0 0 1.1rem;
    padding: .72rem .8rem;
    border: 1px solid var(--line);
    border-radius: var(--r-control);
    background: rgba(255, 255, 255, .025);
}

html.theme-soda .soda-catalog__active-filters > span {
    flex: none;
    padding-top: .23rem;
    color: var(--ink-3);
    font-family: var(--font-display);
    font-size: .72rem;
    font-weight: 600;
}

html.theme-soda .soda-catalog__active-filters > div {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .38rem .5rem;
}

html.theme-soda .soda-catalog__active-filter {
    display: inline-flex;
    align-items: center;
    gap: .38rem;
    min-height: 1.85rem;
    padding: .28rem .55rem;
    border: 1px solid rgba(var(--accent-rgb), .38);
    border-radius: var(--r-mark);
    background: color-mix(in srgb, var(--accent) 10%, var(--panel));
    color: var(--ink-2);
    font-size: .73rem;
    text-decoration: none;
}

html.theme-soda .soda-catalog__active-filter:hover {
    border-color: var(--accent);
    color: var(--ink);
}

html.theme-soda .soda-catalog__active-filter span {
    color: var(--accent);
    font-size: 1rem;
    line-height: .8;
}

/* -------------------------------------------------------------- card CTAs */

html.theme-soda .soda-catalog__grid {
    /* Explicitly fixed desktop columns: a sparse category must not turn one
       card into a billboard. */
    grid-template-columns: repeat(3, minmax(0, 1fr));
    align-items: stretch;
}

/* ------------------------------------------------------- filter motion

   The grid is the box the filter transition measures and animates (catalog.js
   -> filter-motion.js), so it has to be able to hold a card that has already
   left the layout.

   `align-content: start` is what makes the height animation honest. While the
   grid's height tweens it is briefly TALLER than its rows need, and a grid's
   default content alignment would hand that surplus to the rows — every card
   stretching and snapping back over the 300ms. Rows sized by their content is
   also the right resting value, so this changes nothing when nothing moves.
   -------------------------------------------------------------------------- */

html.theme-soda .soda-catalog__grid {
    position: relative;
    align-content: start;
}

/* `.soda-card` is `display: flex`, which beats the UA's [hidden] rule — the
   same trap the carousel arrows and the homepage's cells document. Without
   this, a popped-out card would still occupy a grid slot. */
html.theme-soda .soda-catalog-card[hidden] {
    display: none;
}

/* popLayout: the exiting card is pinned at the coordinates it stood on and
   fades there, out of the flow, so the survivors reflow and slide at once.
   filter-motion.js writes left/top/width/height and removes all of it when the
   300ms is over. */
html.theme-soda .soda-catalog-card.is-flip-out[hidden] {
    display: flex;
    position: absolute;
    margin: 0;
    pointer-events: none;
}

html.theme-soda .soda-catalog-card .soda-card__media {
    min-height: 0;
    /* No padding and no fill — the source's media div is nothing but a clipped
       frame; the shell above owns the rounded corners now, so the tile itself
       is square-cornered. The white ground is the source's `bg-background` and
       is what the contained photo now letterboxes against on the narrow 4/3
       frame; the pre-transplant halo gradient is NOT coming back, because this
       card is white and a tinted arc behind a white-ground render reads as a
       box drawn inside the tile. */
    padding: 0;
    border-radius: 0;
    overflow: hidden;
    background: #fff;
}

/* OWNER DEVIATION (2026-09-03) — CONTAIN, NOT COVER.

   The source is `object-cover w-full h-full`, and cover is the half of "moc
   velky" that a smaller frame alone does not fix: cropping a 600x600 render to
   fill a frame paints it at max(fw/nw, fh/nh), i.e. LARGER than the frame in
   one axis, and throws the overflow away. Measured on the 3/4 frame at 1440:
   the 333x444 tile painted the render at 444x444 and cut 111px of phone off
   the sides.

   THE PRE-TRANSPLANT RULE IS RESTORED VERBATIM, and it is `100%` — not the
   84% that `.soda-card__media img` in components.css uses. That 84% inset is
   the DARK homepage card's recipe, where the product floats on a tinted ground
   under a drop shadow; the catalogue card overrode it to 100%/padding-0 on
   purpose, because these renders arrive on white and the tile is white, so an
   inset only draws an empty margin inside the tile. `contain` never overflows,
   so 100% is a ceiling, not a stretch: a square render fills the square frame
   exactly, and any other ratio letterboxes inside it.

   Measured against the owner's reference (localhost/ujabka @ 1ca2635, the old
   product list): there the img box was 309.38 x 309.38 in a 309.38 x 309.38
   square frame — the render painted at exactly 100 percent of the frame, the
   same relationship this rule reproduces. The absolute number is 333.34 here
   rather than 309.38, and that is the PORTED CARD's doing, not this rule's:
   the source <Card> has no padding, so its media runs edge to edge inside the
   border, while the old card inset it by `.833vw` (11.99px at 1440). That
   geometry belongs to the card the owner approved and is deliberately left
   alone. */
html.theme-soda .soda-catalog-card .soda-card__media img {
    width: 100%;
    height: 100%;
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
    object-position: center;
    /* The drop shadow was drawn for a product floating on the dark ground; on a
       white tile it is a smudge. */
    filter: none;
}

/* The whole media frame is the detail link. The source's <motion.img> is the
   only child of its media div; here that child is an anchor wrapping the
   image, because a catalogue tile navigates and the source's demo did not. */
html.theme-soda .soda-catalog-card__media-link {
    display: grid;
    /* EXPLICIT minmax(0, 1fr) TRACKS, and they are load-bearing — the same trap
       detail.css documents for the gallery stage. With an auto-sized row the
       photo's `height: 100%` resolves against an indefinite track and falls
       back to the image's own intrinsic height, so `object-fit` had no frame to
       resolve against: a landscape render sat as a strip in the middle of the
       tile instead of being fitted to it. Still true after the switch back to
       `contain` — contain needs a definite box exactly as much as cover does,
       it just letterboxes inside it instead of cropping out of it. */
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: minmax(0, 1fr);
    place-items: center;
    width: 100%;
    height: 100%;
    color: inherit;
    text-decoration: none;
}

/* Everything the link can hold — the photograph, the accessory glyph, the
   device outline — occupies the same single cell, so no branch can open an
   implicit second row and shrink the definite one above back to auto. */
html.theme-soda .soda-catalog-card__media-link > * {
    grid-area: 1 / 1;
}

html.theme-soda .soda-catalog-card__media-link:focus-visible {
    outline-offset: -4px;
}

/* THE FOOTER ROW. The source's CardFooter holds ONE `w-full` Button. The
   Detail affordance beside it is an existing, diagnostically pinned
   interaction, so the row holds two controls at the source's own button
   geometry rather than losing one of the two real actions. `gap-2` is the
   source's own spacing unit throughout the card. */
html.theme-soda .soda-catalog-card__actions {
    display: grid;
    grid-template-columns: minmax(0, .92fr) minmax(0, 1.08fr);
    gap: var(--t21-2);                      /* gap-2 = .5rem                */
    margin-top: 0;
}

html.theme-soda .soda-catalog-card__actions .soda-btn,
html.theme-soda .soda-catalog-card__actions .soda-card__form {
    width: 100%;
    min-width: 0;
    margin: 0;
}

/* shadcn <Button> default size: `h-10 px-4 py-2 rounded-md text-sm
   font-medium`, with `inline-flex items-center justify-center`. */
html.theme-soda .soda-catalog-card__actions .soda-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0;
    min-height: 2.5rem;                     /* h-10 = 2.5rem                */
    padding: .5rem 1rem;                    /* py-2 px-4                    */
    border-radius: var(--t21-radius-md);    /* rounded-md                   */
    font-size: var(--t21-fs-sm);            /* text-sm = .875rem / 1.25rem  */
    line-height: var(--t21-lh-sm);
    font-weight: 500;                       /* font-medium                  */
    text-align: center;
}

/* `<ShoppingCart className="mr-2 h-4 w-4" />` — the icon and its 8px gutter. */
html.theme-soda .soda-catalog-card__add-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    margin-right: var(--t21-2);             /* mr-2 = .5rem                 */
}

/* The inline lucide glyphs carry their pixel size as attributes so they are
   right with no stylesheet; these restate the same number in rem so they ride
   the artboard ladder exactly as `h-4 w-4` would. */
html.theme-soda .soda-catalog-card .soda-icon {
    flex: none;
}

html.theme-soda .soda-catalog-card .soda-icon--4 {
    width: var(--t21-size-4);               /* h-4 w-4 = 1rem               */
    height: var(--t21-size-4);
}

html.theme-soda .soda-catalog-card__detail {
    color: var(--ink-2);
}

html.theme-soda .soda-catalog-card__add {
    background: var(--btn-bg);
}

html.theme-soda .soda-catalog-card__choose {
    background: color-mix(in srgb, var(--accent) 12%, var(--btn-bg));
    border-color: rgba(var(--accent-rgb), .48);
}

html.theme-soda .soda-catalog-card__unavailable {
    display: grid;
    place-items: center;
    min-height: 2.5rem;
    margin: 0;
    padding: .5rem 1rem;
}

/* "Vice konfiguraci" is a state, not a stock level, so it reads as ink. */
html.theme-soda .soda-catalog-card__availability--choice {
    color: var(--ink-2);
}

/* ------------------------------------------ the add button's three states
   Source: idle = ShoppingCart + "Add to Cart"; adding = a spinning Loader2 +
   "Adding..."; added = Check + "Added to Cart", held for 2000ms.

   U Jabka's add is a REAL server POST behind a PRG redirect, not client
   state: the "added" frame has no referent here because the browser has
   already left the page by then, and the server is the only thing allowed to
   say a line reached the cart. So the idle and adding frames are ported and
   the added frame is not simulated. The spinner exists ONLY while a submit is
   in flight, so nothing on this page animates at rest. */
html.theme-soda .soda-catalog-card__spinner {
    display: none;
}

html.theme-soda .soda-catalog-card__add.is-adding .soda-icon:not(.soda-catalog-card__spinner) {
    display: none;
}

html.theme-soda .soda-catalog-card__add.is-adding .soda-catalog-card__spinner {
    display: inline-block;
    /* `animate-spin` = 1s linear infinite. */
    animation: soda-t21-spin 1s linear infinite;
}

@keyframes soda-t21-spin {
    to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
    html.theme-soda .soda-catalog-card__add.is-adding .soda-catalog-card__spinner {
        animation: none;
    }
}

/* -------------------------------------------------------------- responsive */

@media (max-width: 1080px) {
    html.theme-soda .soda-catalog__workspace {
        grid-template-columns: minmax(12.75rem, 14.5rem) minmax(0, 1fr);
        gap: 1.25rem;
    }

    html.theme-soda .soda-catalog__grid {
        grid-template-columns: repeat(3, minmax(0, 1fr));
    }
}

@media (max-width: 900px) {
    /* A compact category lead keeps the results anchor consistent. Full
       filtering moves into the enhanced sheet below instead of reserving its
       expanded height in the normal tablet flow. */
    html.theme-soda .soda-catalog__workspace {
        grid-template-columns: 1fr;
    }

    html.theme-soda .soda-catalog__grid {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }

    html.theme-soda .soda-catalog__results {
        padding: 1.5rem;
        border-left: 0;
        border-top: 1px solid rgba(0, 0, 0, .15);
    }

    html.theme-soda .soda-catalog__sidebar {
        padding: 1.5rem;
        position: static;
    }







    html.theme-soda .soda-catalog__secondary-facets,
    html.theme-soda .soda-catalog__note {
        margin-inline: .9rem;
    }

    html.theme-soda .soda-catalog__results-head {
        align-items: stretch;
        flex-direction: column;
    }

    html.theme-soda .soda-catalog__sort-form {
        width: 100%;
    }

    html.theme-soda .soda-catalog__active-filters {
        align-items: stretch;
        flex-direction: column;
    }

    html.theme-soda .soda-catalog__grid {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }

    /* The dialog treatment only activates once enhancement is known to be
       live. With JavaScript blocked, the server-open details stays a normal
       inline form rather than trapping a visitor in a fixed panel. */
    html.theme-soda.js-soda .soda-catalog__filters[open] {
        position: fixed;
        z-index: 1001;
        inset: auto max(.75rem, calc((100vw - 42rem) / 2)) .75rem;
        width: auto;
        max-height: calc(100dvh - 1rem);
        overflow-y: auto;
        overflow-x: hidden;
        /*
         * THE SHEET HAS TO STATE ITS OWN SURFACE.
         *
         * As a ruled column the panel is deliberately `background: transparent`
         * (see the CATALOG 2.0 filter sidebar block) because the PLANE under it
         * is the surface — a second fill there would be the box-in-a-box the
         * plane rules out. Lifting it to `position: fixed` takes the panel out
         * of the plane's flow but NOT out of the plane's token scope, so both
         * that transparent ground and --glass-bg/--panel (all transparent on
         * .soda-plane) stayed in force with nothing behind them: the product
         * grid and the results header read straight through the open filters.
         *
         * So the sheet carries the plane's own recipe explicitly — opaque
         * white, one hairline, the card corner — rather than inheriting a
         * treatment that only makes sense in the flow.
         */
        background: #fff;
        border: 1px solid rgba(0, 0, 0, .15);
        border-radius: var(--r-card);
        /* Only the sheet scrolls. The page behind it is already locked by
           .soda-catalog-filters-open; this stops a flick past either end of
           the form from chaining out to it anyway. */
        overscroll-behavior: contain;
        box-shadow: 0 1.5rem 4rem rgba(0, 0, 0, .42);
    }

    /* The summary is the sheet's header and the control that closes it, so it
       is the one strip the scrolling form passes under. It needs the same
       opaque ground for the same reason: --glass-bg is transparent on the
       plane, and the plane also neutralises backdrop-filter for everything it
       holds, so the old blur was never painted here. */
    html.theme-soda.js-soda .soda-catalog__filters[open] .soda-catalog__filters-summary {
        position: sticky;
        top: 0;
        z-index: 1;
        background: #fff;
        border-radius: var(--r-card) var(--r-card) 0 0;
    }

    /* The opener leaves the flow when it becomes the sheet, so the column it
       stood in has to keep its height — otherwise the whole results section
       slides up behind the backdrop on open and back down on close. One
       summary's worth of space, which is exactly what left. Boxes are
       border-box here, so the reservation carries the column's own padding
       (1.5rem, set above) alongside the summary's 3.1rem. */
    html.theme-soda.js-soda.soda-catalog-filters-open .soda-catalog__sidebar {
        min-height: calc(3.1rem + 3rem);
    }

    html.theme-soda.soda-catalog-filters-open body {
        overflow: hidden;
    }

    /* Let the fixed sheet join the page stacking context while it is open.
       The catalogue's usual z-index would otherwise trap the sheet below the
       body-level backdrop, even though the sheet itself is z-index: 1001. */
    html.theme-soda.js-soda.soda-catalog-filters-open .soda-section.soda-catalog {
        z-index: auto;
    }

    html.theme-soda .soda-catalog__filter-backdrop {
        position: fixed;
        z-index: 1000;
        inset: 0;
        background: rgba(4, 7, 11, .56);
    }
}

@media (max-width: 560px) {
    html.theme-soda .soda-catalog__grid {
        grid-template-columns: 1fr;
    }

    /* A full-width card would otherwise give the photo a whole screen height
       — `max-w-sm` (24rem) is wider than a 390px viewport, so it cannot cap
       anything here. This is the rule that actually wins on the narrow
       viewport; the earlier 560px block carries the same declaration. */
    html.theme-soda .soda-catalog-card .soda-card__media {
        aspect-ratio: 4 / 3;
    }

    html.theme-soda .soda-catalog-card__actions {
        grid-template-columns: minmax(0, .88fr) minmax(0, 1.12fr);
    }
}


/* ================================================ product card — Info Card

   The catalogue product card's one interaction, ported from 21st
   @maxim.bort.devel/info-card. Read from that component's source rather than
   from its picture, it does exactly two things, and both are here.

   1. THE BORDER FOLLOWS THE POINTER. Not a clock — the pointer. The source
      measures the cursor against the card's own centre and writes the result
      as an angle:

        const x = e.clientX - rect.left - rect.width / 2
        const y = e.clientY - rect.top  - rect.height / 2
        border.style.setProperty("--rotation", `${Math.atan2(y, x)}rad`)

      and draws the edge as

        conic-gradient(from var(--rotation),
          borderColor 0deg, borderColor 90deg, borderBg 90deg, borderBg 360deg)

      — a quarter-turn arc whose trailing end sits under the cursor and which
      sweeps around the edge as the cursor moves. The card this replaced ran a
      fixed 3.2s loop, which is the generic version of the idea: it moves, but
      it does not answer you. catalog.js writes the angle; the leave resets it
      to 0deg exactly as the source does.

      WHAT IS NOT TAKEN is its 3px #DAFF3E neon on #242424. The arc here is the
      one-pixel ink ring the card already had, at 45 percent, sitting on the
      hairline that is already there. Same mechanic, Vesper's voice.

   2. THE TITLE WIPE, and this is the "staged" part the border alone cannot
      carry. The source's title holds a coloured block clipped to nothing at its
      own centre line:

        clipPath: hovered ? "polygon(0 0, 100% 0, 100% 100%, 0% 100%)"
                          : "polygon(0 50%, 100% 50%, 100% 50%, 0 50%)"
        transition: "all cubic-bezier(.1,.5,.5,1) 0.4s"

      while the title's own colour crosses to the inverse over 0.3s. The block
      opens from the middle outward and the words turn to paper as it arrives.
      Both timings below are the source's, verbatim.

   WHAT THE SOURCE DOES NOT DO, and so neither does this: no media zoom, no
   overlay, no lightbox, no lift, no shadow, no scale. The information on the
   card is the same information at rest and in hand; the motion elevates the
   title, it does not reveal anything that was hidden. Nothing here is
   hover-only.

   FOCUS IS THE EQUAL STATE. Every rule below fires on :focus-within too, so
   reaching the card with Tab produces the same card. There is no pointer then,
   so the arc rests at its 0deg position — which is the source's own default.

   REDUCED MOTION keeps both states and removes both animations: the wipe and
   the colour swap arrive instantly, and the travelling arc is not drawn at all
   (catalog.js does not track a pointer it would not animate).
   -------------------------------------------------------------------------- */

@property --soda-card-angle {
    syntax: '<angle>';
    /* MUST INHERIT. card-pointer.js writes the angle on the CARD, but the arc
       is drawn by the card's ::before — and a non-inheriting registered
       property never reaches a pseudo-element. With `inherits: false` the arc
       rendered at its initial 0deg forever: the "stuck in the top-right
       corner" defect, invisible to a test that only read the variable off the
       card itself. (The upstream paints the gradient on the element that
       receives the write, so the flag never mattered there.) */
    inherits: true;
    initial-value: 0deg;
}

/* THE RING WAS BEING CLIPPED AWAY, and had been since it was introduced.
   `.soda-card` is `overflow: hidden`, so a ::before at inset -1px — which is
   ENTIRELY outside the padding box — was cut off in full: the effect computed,
   measured as applied, and painted nothing on a single catalogue card. Only the
   homepage's `.soda-prod__item`, which is not a `.soda-card`, ever showed it.

   THE 21ST TRANSPLANT REOPENED EXACTLY THIS TRAP: product-list.md's Card is
   `overflow-hidden`, and putting that on this element would delete the arc a
   second time. The clip therefore lives on `.soda-catalog-card__shell` — the
   element the source's own classes describe — and this frame stays unclipped
   so the arc has somewhere to paint. Nothing about the card's silhouette
   changes: the shell carries the same `rounded-md`. */
html.theme-soda .soda-catalog-card {
    overflow: visible;
    transition: border-color var(--dur) var(--ease);
}

html.theme-soda .soda-catalog-card:hover,
html.theme-soda .soda-catalog-card:focus-within {
    border-color: rgba(0, 0, 0, .34);
    /* Owner decision (2026-08-26): NO grey wash on hover. The shared
       .soda-card:hover rule paints var(--panel-hi) over the card; on this
       card the hover carries exactly two things — the pointer-border arc and
       the title transition — so the ground stays the resting white. */
    background: #fff;
}

html.theme-soda .soda-catalog-card::before {
    content: '';
    position: absolute;
    /* One pixel outside the box, so the ring lands ON the border rather than
       beside it, and one step rounder to match that offset. */
    inset: -1px;
    z-index: 2;
    /* Follows the card's radius, which is now the source's `rounded-md`. */
    border-radius: calc(var(--t21-radius-md) + 1px);
    padding: 1px;
    /* THE ARC SITS ON THE CURSOR, and the `+ 45deg` is two corrections in one
       number. Both were measured with tools/dev-arc-probe.mjs, which samples
       the painted ring pixel by pixel and reports the arc's angular centre
       against the pointer's own angle — eyeballing a one-pixel ring is what
       got this wrong twice.

         +90deg  TWO DIFFERENT ZEROES. Math.atan2(dy, dx) measures from the
                 +X axis (0 = 3 o'clock); a conic gradient measures from 12
                 o'clock, clockwise. Feeding one into the other unconverted
                 painted the arc a constant quarter-turn away from the hand —
                 the probe read -90.0deg at all eight compass points, which
                 is a unit bug, not a taste question. (The upstream carries
                 the same mismatch; its thick neon wedge on a square card
                 simply hides it.)
         -45deg  HALF THE SPAN, so the cursor lands in the arc's MIDDLE
                 rather than at its leading end, and the highlight falls off
                 evenly to both sides.

       Contrast comes from the same place the source's does — the arc against
       the rest of the edge: #DAFF3E on #242424 there, near-solid ink on a
       34-percent hairline here. The soft ends are anti-aliasing, not a
       design: a hard stop on a one-pixel ring reads as a nick. */
    background: conic-gradient(from calc(var(--soda-card-angle) + 45deg),
        transparent 0deg,
        rgba(0, 0, 0, .12) 4deg,
        rgba(0, 0, 0, .8) 8deg 82deg,
        rgba(0, 0, 0, .12) 86deg,
        transparent 90deg 360deg);
    /* Keep the border band, drop the middle: the ring is the difference between
       the padding box and the content box. */
    -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    -webkit-mask-composite: xor;
    mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
    mask-composite: exclude;
    opacity: 0;
    transition: opacity var(--dur) var(--ease);
    pointer-events: none;
}

html.theme-soda .soda-catalog-card:hover::before,
html.theme-soda .soda-catalog-card:focus-within::before {
    opacity: 1;
}

/* -- the title wipe ------------------------------------------------------- */

/* Source title: `<h3 className="font-medium line-clamp-1">`. Under Tailwind's
   preflight an h3 inherits its size and weight, so this is text-base at
   font-weight 500 — not the display face the card used to carry. */
html.theme-soda .soda-catalog-card .soda-card__name {
    position: relative;
    margin: 0;
    font-family: inherit;
    font-size: var(--t21-fs-base);          /* text-base = 1rem / 1.5rem    */
    line-height: var(--t21-lh-base);
    font-weight: 500;                       /* font-medium                  */
    letter-spacing: normal;
}

/* `line-clamp-1` lives on the TEXT, not on the h3: the wipe is an absolutely
   positioned sibling that deliberately bleeds past the text box, and
   line-clamp's own `overflow: hidden` on the h3 would shear that overhang off. */
html.theme-soda .soda-catalog-card .soda-card__name a {
    position: relative;
    z-index: 1;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 1;
    line-clamp: 1;
    overflow: hidden;
    transition: color .3s ease;
}

html.theme-soda .soda-card__name-wipe {
    position: absolute;
    /* The source bleeds its block past the text box by a few pixels; the same
       overhang here reads as a marker stroke rather than a table cell. */
    inset: -.15rem -.4rem;
    z-index: 0;
    border-radius: var(--r-mark);
    background: var(--ink);
    clip-path: polygon(0 50%, 100% 50%, 100% 50%, 0 50%);
    transition: clip-path .4s cubic-bezier(.1, .5, .5, 1);
    pointer-events: none;
}

html.theme-soda .soda-catalog-card:hover .soda-card__name-wipe,
html.theme-soda .soda-catalog-card:focus-within .soda-card__name-wipe {
    clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%);
}

html.theme-soda .soda-catalog-card:hover .soda-card__name a,
html.theme-soda .soda-catalog-card:focus-within .soda-card__name a {
    /* The card is white and the wipe is ink, so the words become the card. */
    color: #fff;
}

@media (prefers-reduced-motion: reduce) {
    /* Both states survive; both animations go. */
    html.theme-soda .soda-catalog-card::before {
        display: none;
    }

    html.theme-soda .soda-card__name-wipe,
    html.theme-soda .soda-catalog-card .soda-card__name a {
        transition: none;
    }
}

/* ============================================== category carousel (straight)

   THE CATEGORY LEVEL of the catalogue, corrected per the owner's 2026-08-26
   direction: the card-fan arrangement is retired. The categories are a
   COMPACT, STRAIGHT horizontal row of small cards; what remains of the
   carousel is its behaviour, not its curvature:

     swipe    the track is an ordinary horizontally scrollable list with
              scroll-snap, so wheel, trackpad, touch and Tab all move it
              with no script at all;
     arrows   catfan.js reveals the arrow pair only on REAL overflow and
              steps exactly one card per press, landing on the snap position
              (the same contract as the model carousel below);
     active   the current category is marked in place — aria-current, a
              brighter edge, a heavier name with the filled mark;
     keyboard Left/Right/Home/End move focus between the cards.

   HIERARCHY: this row answers "which catalogue", the model row underneath
   answers "which iPhone". The two stay visually distinct — vertical
   media-led category cards against the model row's horizontal chips. */

html.theme-soda .soda-catfan {
    position: relative;
    margin: .833vw 0 1.111vw;
}

html.theme-soda .soda-catfan__arrows {
    position: absolute;
    top: -2.15rem;
    right: 0;
    display: flex;
    gap: .375rem;
}

/* `display: flex` beats the UA's [hidden] rule — the same trap the model
   arrows document. Without this the pair renders even with nothing to scroll. */
html.theme-soda .soda-catfan__arrows[hidden] {
    display: none;
}

html.theme-soda .soda-catfan__arrow {
    display: grid;
    place-items: center;
    width: 1.9rem;
    height: 1.9rem;
    padding: 0;
    border: 1px solid var(--btn-border);
    border-radius: var(--r-control);
    background: none;
    color: var(--ink);
    cursor: pointer;
    transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease), opacity var(--dur) var(--ease);
}

html.theme-soda .soda-catfan__arrow:hover:not(:disabled) {
    background: var(--hover-wash);
    border-color: var(--line-hi);
}

html.theme-soda .soda-catfan__arrow:disabled {
    opacity: .35;
    cursor: default;
}

html.theme-soda .soda-catfan__track {
    display: flex;
    /* Centred while the row fits; `safe` keeps the start reachable once the
       row overflows into a scroll. */
    justify-content: safe center;
    gap: .75rem;
    margin: 0;
    padding: 0 0 .35rem;
    list-style: none;
    overflow-x: auto;
    scroll-snap-type: x proximity;
    scroll-behavior: smooth;
    scrollbar-width: none;
}

html.theme-soda .soda-catfan__track::-webkit-scrollbar {
    display: none;
}

@media (prefers-reduced-motion: reduce) {
    html.theme-soda .soda-catfan__track {
        scroll-behavior: auto;
    }
}

html.theme-soda .soda-catfan__cell {
    flex: 0 0 auto;
    margin: 0;
    scroll-snap-align: start;
}

html.theme-soda .soda-catfan__card {
    position: relative;
    display: flex;
    flex-direction: column;
    width: 8.75rem;
    border: 1px solid var(--line);
    border-radius: var(--r-card);
    background: #101013;
    color: var(--ink);
    text-decoration: none;
    overflow: hidden;
    transition: border-color var(--dur) var(--ease);
}

html.theme-soda a.soda-catfan__card:hover {
    border-color: var(--line-hi);
}

html.theme-soda .soda-catfan__card:focus-visible {
    outline: var(--focus-ring) solid var(--focus);
    outline-offset: var(--focus-offset);
}

/* Every product render arrives on white and needs that ground to read —
   the same rule the model cards follow. */
html.theme-soda .soda-catfan__media {
    position: relative;
    display: grid;
    place-items: center;
    aspect-ratio: 4 / 3;
    background: #fff;
    overflow: hidden;
}

html.theme-soda .soda-catfan__media img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: contain;
    background: #fff;
}

html.theme-soda .soda-catfan__glyph {
    display: grid;
    place-items: center;
    width: 100%;
    height: 100%;
    color: rgba(0, 0, 0, .35);
}

html.theme-soda .soda-catfan__glyph svg {
    width: 34%;
    height: 34%;
}

html.theme-soda .soda-catfan__label {
    display: flex;
    flex-direction: column;
    gap: .05rem;
    padding: .45rem .6rem .55rem;
}

html.theme-soda .soda-catfan__name {
    font-family: var(--font-display);
    font-size: .9rem;
    font-weight: 400;
    line-height: 1.2;
    color: var(--ink);
    white-space: nowrap;
}

html.theme-soda .soda-catfan__note {
    font-size: .75rem;
    line-height: 1.2;
    color: var(--ink-faint);
    white-space: nowrap;
}

/* The current category: shape and weight, never colour alone. */
html.theme-soda .soda-catfan__card.is-current {
    border-color: rgba(255, 255, 255, .55);
}

html.theme-soda .soda-catfan__card.is-current .soda-catfan__name {
    font-weight: 500;
}

html.theme-soda .soda-catfan__card.is-current .soda-catfan__name::after {
    content: '';
    display: inline-block;
    width: .4em;
    height: .4em;
    margin-left: .45em;
    background: currentColor;
}

/* An upcoming category is a plain card: no pointer, muted note, honest. */
html.theme-soda .soda-catfan__card--upcoming {
    cursor: default;
}

html.theme-soda .soda-catfan__card--upcoming .soda-catfan__media {
    opacity: .8;
}

@media (max-width: 639.98px) {
    html.theme-soda .soda-catfan__card {
        width: 7.75rem;
    }
}

/* ================================================= model carousel (filter)

   The catalogue's model filter. It used to be a row of text buttons: correct,
   operable and visually dead — a list of words where the thing being chosen is
   an object with a shape and a colour.

   Now each model is an image card, and it is still exactly the same filter:
   real links to real catalogue URLs, the same model ids, the same complete
   <select> beside it in the sidebar. It is a control, so it stays STRAIGHT and
   aligned — the reference's fan layout makes you interpret a card's position
   before you can click it, which is the wrong trade for a filter.

   IT NOW SITS ABOVE THE PLANE, in the page head directly under the headline,
   and it is the ONLY model control on the page — the sidebar's <select> is
   gone. So it is styled for the dark ground rather than for white: the shell
   takes the page's own hairline and ink tokens, which outside .soda-plane are
   already the light-on-dark ones, and only the MEDIA box stays white, because
   every product render arrives on white and needs that ground to read.

   OVERFLOW IS THE NATIVE ONE. The track is an ordinary horizontally scrollable
   list, so wheel, trackpad, touch and keyboard all work with no script at all.
   The arrow buttons are an enhancement ui.js reveals only when the track
   actually overflows, and they move the same scrollLeft a finger would.

   Selected state is carried three ways — aria-current, a heavier label and a
   filled square mark that is present or absent — so it survives monochrome and
   colour-blindness.
   -------------------------------------------------------------------------- */

/* A ROW, not a stack. The arrows used to sit on a line of their own beside the
   "Model iPhonu" label; with the label gone that line would have been a band of
   empty space above the cards, and reserving it or not reserving it were both
   wrong — not reserving it means revealing the arrows pushes the whole row
   down. Beside the track they cost no vertical space at all, they are where a
   carousel's controls are expected to be, and appearing only shortens the
   track. No rule underneath either: the plane's own top edge is the division
   between the head and the workspace. */
html.theme-soda .soda-models {
    display: flex;
    align-items: center;
    gap: .75rem;
    margin: 0 0 clamp(1.35rem, 2.8vw, 2.25rem);
    padding: 0;
}

/* The head is the arrows and nothing else — the "Model iPhonu" label went with
   the rest of the head's copy. The <nav> keeps its aria-label, so the row is
   still named for assistive technology. */
html.theme-soda .soda-models__head {
    order: 2;
    flex: none;
    display: flex;
    align-items: center;
}

html.theme-soda .soda-models__arrows {
    display: flex;
    gap: .35rem;
}

/* `display: flex` beats the UA's [hidden] rule, so the attribute ui.js toggles
   would have done nothing on its own. */
html.theme-soda .soda-models__arrows[hidden] {
    display: none;
}

html.theme-soda .soda-models__arrow {
    display: grid;
    place-items: center;
    width: 2rem;
    height: 2rem;
    padding: 0;
    border: 1px solid var(--line);
    border-radius: var(--r-control);
    background: transparent;
    color: var(--ink-2);
    font-size: .9rem;
    line-height: 1;
    cursor: pointer;
    transition: border-color var(--dur) var(--ease), color var(--dur) var(--ease);
}

html.theme-soda .soda-models__arrow:hover:not(:disabled) {
    border-color: var(--ink);
    color: var(--ink);
}

/* Disabled at an end of the track: still in the tab order is pointless, but it
   must not silently vanish either, so it dims and stops responding. */
html.theme-soda .soda-models__arrow:disabled {
    opacity: .35;
    cursor: default;
}

html.theme-soda .soda-models__track {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    gap: .5rem;
    margin: 0;
    padding: 0 0 .35rem;
    list-style: none;
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x proximity;
    scroll-behavior: smooth;
    /* Only the track scrolls sideways; the page never does. */
    overscroll-behavior-x: contain;
    scrollbar-width: thin;
}

@media (prefers-reduced-motion: reduce) {
    html.theme-soda .soda-models__track {
        scroll-behavior: auto;
    }
}

html.theme-soda .soda-models__cell {
    flex: 0 0 auto;
    scroll-snap-align: start;
}

/* The render sits BESIDE the label rather than above it. A stacked card is the
   obvious shape and it is 60 px too tall for the reserved slot; laid out this
   way the whole row fits inside a reservation the accessory page also has to
   live with, and it matches the brief's "smaller horizontal image-card"
   directly. */
html.theme-soda .soda-model-card {
    position: relative;
    display: flex;
    align-items: center;
    gap: .5rem;
    width: 10.5rem;
    padding: .4rem .6rem .4rem .4rem;
    border: 1px solid var(--line);
    border-radius: var(--r-control);
    background: var(--panel);
    color: inherit;
    text-decoration: none;
    transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease);
}

html.theme-soda .soda-model-card:hover {
    border-color: var(--line-hi);
    background: var(--panel-hi);
}

html.theme-soda .soda-model-card__media {
    display: grid;
    place-items: center;
    flex: none;
    width: 3.25rem;
    aspect-ratio: 1 / 1;
    border-radius: var(--r-mark);
    /* The renders arrive on white and this ground is dark. */
    background: #fff;
    overflow: hidden;
}

html.theme-soda .soda-model-card__media img {
    width: 100%;
    height: 100%;
    object-fit: contain;
}

/* THE INTERACTIVE CARD, which in the fan reference is a lift plus a 1.08 scale
   with the neighbours pushed aside. The neighbour spread is a property of the
   arc and goes with the arc; the card's own response stays, as the move the
   approved gallery card already makes — the media scales inside its own box.
   Hover is never the only way to anything here: the card is a link, the state
   is `aria-current`, and coarse pointers get none of this. */
html.theme-soda .soda-model-card__media img,
html.theme-soda .soda-model-card__device {
    transition: transform var(--dur) var(--ease);
}

@media (hover: hover) and (prefers-reduced-motion: no-preference) {
    html.theme-soda .soda-model-card:hover .soda-model-card__media img,
    html.theme-soda .soda-model-card:hover .soda-model-card__device,
    html.theme-soda .soda-model-card:focus-visible .soda-model-card__media img,
    html.theme-soda .soda-model-card:focus-visible .soda-model-card__device {
        transform: scale(1.08);
    }
}

@media (prefers-reduced-motion: reduce) {
    html.theme-soda .soda-model-card__media img,
    html.theme-soda .soda-model-card__device {
        transition: none;
    }
}

html.theme-soda .soda-model-card__device {
    width: 44%;
    height: auto;
    color: rgba(0, 0, 0, .22);
}

html.theme-soda .soda-model-card__foot {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: .35rem;
    flex: 1;
    min-width: 0;
}

html.theme-soda .soda-model-card__name {
    font-size: var(--text-small);
    line-height: 1.25;
    color: var(--ink-2);
}

html.theme-soda .soda-model-card__count {
    font-size: var(--text-label);
    font-variant-numeric: tabular-nums;
    color: var(--ink-3);
}

/* The selected mark: a filled bar under the card, drawn or not drawn. */
html.theme-soda .soda-model-card__mark {
    position: absolute;
    left: .4rem;
    bottom: -1px;
    width: 0;
    height: 2px;
    background: var(--ink);
    transition: width var(--dur) var(--ease);
}

html.theme-soda .soda-model-card.is-current {
    border-color: var(--line-hi);
}

html.theme-soda .soda-model-card.is-current .soda-model-card__name {
    font-weight: 600;
    color: var(--ink);
}

html.theme-soda .soda-model-card.is-current .soda-model-card__mark {
    width: calc(100% - .8rem);
}

@media (prefers-reduced-motion: reduce) {
    html.theme-soda .soda-model-card,
    html.theme-soda .soda-model-card__mark {
        transition: none;
    }
}

@media (max-width: 1023.98px) {
    html.theme-soda .soda-model-card {
        width: 9.5rem;
    }

    /* Touch scrolls the track; the arrows are a pointer affordance. The head
       goes with them so it cannot leave its gap behind, and the arrows say so
       themselves rather than relying on an ancestor to say it for them. */
    html.theme-soda .soda-models__head,
    html.theme-soda .soda-models__arrows {
        display: none;
    }
}

/* The carousel now sits above the plane, in the page head, so there is nothing
   left to reserve: it is rendered for iPhones and simply absent for
   accessories, and no product grid moves because of it either way. */
@media (max-width: 900px) {
    html.theme-soda .soda-models {
        gap: 0;
        margin: 0 0 1.25rem;
        padding: 0;
    }

    html.theme-soda .soda-models__head {
        display: none;
    }

    html.theme-soda .soda-models__track {
        scrollbar-width: none;
    }

    }
