/* Geist, self-hosted. Generated by packages/ui/scripts/kit.mjs - do not edit.
   Link this BEFORE tokens.css. Inside the monorepo you do not need it: next/font loads the
   same family and sets the same two variables. */

@font-face {
  font-family: 'Geist';
  src: url('/s/geist-a369fcf5628ea2aa.woff2') format('woff2-variations');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: 'Geist Mono';
  src: url('/s/geist-mono-fba8f577f38a2bbc.woff2') format('woff2-variations');
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}
:root {
  --font-geist-sans: 'Geist';
  --font-geist-mono: 'Geist Mono';
}

/* mixtape.works design tokens — generated from packages/ui/src/tokens.css.
   DO NOT EDIT. Run `pnpm --filter @mixtape/ui kit` after changing the source.

   This is the plain-CSS build, for products with no Tailwind and no build step. It is the
   same palette, the same type, the same radii and the same motion as every other product -
   that is the entire point of it. Add a colour to the source, never here, and never to a
   product's own stylesheet. */

:root {
  --font-sans: var(--font-geist-sans), var(--font-script, ui-sans-serif), system-ui, sans-serif;
  --font-mono: var(--font-geist-mono), ui-monospace, "SF Mono", Menlo, monospace;
  --radius-sm: 8px;
  --radius-md: 12px;
  --radius-lg: 18px;
  --radius-xl: 26px;
  --ease-out-soft: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
  --animate-rise: rise 380ms cubic-bezier(0.22, 1, 0.36, 1) both;
  --animate-fade: fade 200ms cubic-bezier(0.22, 1, 0.36, 1) both;
  --animate-pop: pop 200ms cubic-bezier(0.34, 1.56, 0.64, 1) both;
  --animate-pulse-soft: pulse-soft 2.4s cubic-bezier(0.22, 1, 0.36, 1) infinite;
}
/* mixtape.works design tokens — one system, every product.

   Two layers on purpose:
   1. `@theme inline` maps Tailwind utilities onto CSS variables *by reference*, so
      `bg-canvas` resolves at paint time rather than being frozen at build time.
   2. The palettes below re-point those variables. Swap `data-theme` on <html> and the
      whole app re-themes with no re-render and no flash.

   Add a colour here, never in an app. If a product needs its own accent, override
   `--c-accent*` on a wrapper element; everything else inherits. */



/* ── The page's width, for every mixtape page ──────────────────────────────
   One number: the landing column, a product's dashboard, Studio and the account all stop
   here. 90rem is 1440px at the default size, so a 1440 screen is filled edge to edge (less
   the gutters) and a wider one keeps the column at 1440 rather than stretching lines across
   it (Chris, 2026-09-21: "every page including dashboards on 1440px screens should fill out
   the whole width content-wise (for bigger windows dont)"). It was 1200px on the landing
   layer, 80rem in Studio and 72rem in the account, and none of them agreed. */
:root { --page-max: 90rem; }

/* ── Small text and field edges pass WCAG AA on every surface (2026-11-02, Chris's yes) ──────────
   --c-ink-subtle is the small text's colour (hints, column subs, captions, placeholders) and --mx-label
   (app.css) is a tenth of the way from it to --c-ink, so both have to read at 4.5:1 (WCAG 1.4.3) on the
   canvas, a card, surface-2 and the sunk surface. They were 3.0-3.3:1 on paper (labels 3.5-3.9:1).
   --c-line-field is the edge of a box you type into or choose in (fields, selects, the sort bar, the
   newsletter box): 3:1 against the field's own canvas and the card round it (WCAG 1.4.11). It is its own
   token because --c-line-strong (1.4-1.6:1) also draws ghost buttons, chips, tables and dividers, which
   are named by their words and stay as light as they were. Same hue, only the lightness moved (OKLCH):
     light  subtle #868d99 -> #666d79  4.65-5.21:1   label 5.25-5.88:1   field #8f8a81 3.06-3.43:1
     dark   subtle #6d7583 -> #7c8492  4.62-5.57:1   label 5.34-6.44:1   field #606773 3.06-3.69:1
   The phone sheet re-points them for its own surfaces (app.css, --mx-sheet-*). */

/* ── Dark: the studio default and the brand's home ─────────────────────── */
:root,
:root[data-theme='dark'] {
  color-scheme: dark;

  --c-canvas: #0a0b0d;
  --c-surface: #101216;
  --c-surface-2: #171a21;
  --c-line: #23272f;
  --c-line-strong: #323843;
  --c-line-field: #606773;

  --c-ink: #eceef1;
  --c-ink-muted: #a3abb6;
  --c-ink-subtle: #7c8492;

  --c-accent: #f2b544;
  --c-accent-strong: #ffc85c;
  --c-accent-ink: #0a0b0d;
  --c-accent-soft: #f2b5441f;

  --c-ok: #3ecf8e;
  --c-rec: #f0604d;
  /* A destructive button's fill: the light theme's red in BOTH themes, because the dark theme's
     #f0604d carries a white label at only 3.2:1. White on #c3402c is 5.1:1, on the hover 6.6:1,
     and the button still stands at 3.8:1 against the dark page (2026-10-05, task #1304). Defined
     here once; the light blocks inherit it. */
  --c-danger: #c3402c;
  --c-danger-strong: #a8331f;
  --c-danger-ink: #ffffff;
  --c-warn: #e9b949;
  --c-go: #6fbf5e;
  --c-go-strong: #5aa84a;
  --c-go-ink: #0a0b0d;
  --c-idea: #b892f6;
  --c-plan: #4cc4e0;
  --c-build: #5b8def;
  --c-review: #f0916d;
  --c-surface-sunk: #0c0d10;

  --select-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23a3abb6' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  /* Elevation for a window over the page (a confirm, a dialog): a colour, never #000 - deep in the dark, ink on paper. */
  --shadow-lift: 0 24px 60px color-mix(in srgb, var(--c-canvas) 55%, transparent);
}

/* ── Light: warm paper, not clinical white ─────────────────────────────── */
:root[data-theme='light'] {
  color-scheme: light;

  --c-canvas: #fbfaf8;
  --c-surface: #ffffff;
  --c-surface-2: #f4f2ee;
  --c-line: #e6e2db;
  --c-line-strong: #d3cec4;
  --c-line-field: #8f8a81;

  --c-ink: #17191c;
  --c-ink-muted: #565c66;
  --c-ink-subtle: #666d79;

  /* The brand orange, on paper. Two corrections, in order.
     First, the light accent used to be a dark brown-gold chosen so *white* text could sit on
     it; dark ink on a brighter amber - the relationship dark mode already has - reads better
     and looks like the brand. Then on 2026-09-16 Chris said the buttons and badges still
     looked brownish here, and the cause was hue rather than lightness: #c8871f is a
     low-chroma gold, and a filled patch of it reads as mustard. #d97706 is the same
     lightness at a purer orange, so it costs nothing - it is fractionally better as text
     (3.05:1 against paper, up from 2.90) while dark ink on it stays at 5.7:1. Small accent
     labels remain the deliberate cost of one token doing both jobs; splitting fill from text
     would mean touching 73 call sites to buy a tenth of a ratio. */
  --c-accent: #d97706;
  --c-accent-strong: #b45309;
  --c-accent-ink: #1c1405;
  --c-accent-soft: #d977061f;

  --c-ok: #17864f;
  --c-rec: #c3402c;
  --c-warn: #a67908;
  --c-go: #3f8f31;
  --c-go-strong: #2f7524;
  --c-go-ink: #ffffff;
  --c-idea: #7c4dcc;
  --c-plan: #0f7d99;
  --c-build: #2f5fd0;
  --c-review: #c2551f;
  --c-surface-sunk: #f1f2f4;

  --select-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23565c66' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
  /* Elevation for a window over the page (a confirm, a dialog): a colour, never #000 - deep in the dark, ink on paper. */
  --shadow-lift: 0 24px 60px color-mix(in srgb, var(--c-ink) 22%, transparent);
}

/* Follow the OS when the user hasn't chosen. `data-theme` always wins. */
@media (prefers-color-scheme: light) {
  :root:not([data-theme]) {
    color-scheme: light;

    --c-canvas: #fbfaf8;
    --c-surface: #ffffff;
    --c-surface-2: #f4f2ee;
    --c-line: #e6e2db;
    --c-line-strong: #d3cec4;
    --c-line-field: #8f8a81;

    --c-ink: #17191c;
    --c-ink-muted: #565c66;
    --c-ink-subtle: #666d79;

    --c-accent: #d97706;
    --c-accent-strong: #b45309;
    --c-accent-ink: #1c1405;
    --c-accent-soft: #d977061f;

    --c-ok: #17864f;
    --c-rec: #c3402c;
    --c-warn: #a67908;
    --c-go: #3f8f31;
    --c-go-strong: #2f7524;
    --c-go-ink: #ffffff;
    --c-idea: #7c4dcc;
    --c-plan: #0f7d99;
    --c-build: #2f5fd0;
    --c-review: #c2551f;
    --c-surface-sunk: #f1f2f4;

    --select-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23565c66' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
    --shadow-lift: 0 24px 60px color-mix(in srgb, var(--c-ink) 22%, transparent);
  }
}

/* ── Phones in dark: the page is black ───────────────────────────────────
   Chris, 2026-09-22: "on mobile only, when on dark mode, the bg color of the page (outside of
   our cards/boxes etc) should be totally black". A phone screen is mostly OLED, where black is
   the panel switched off and the cards read as lit objects on it; on a desktop monitor the
   near-black stays. Only the canvas moves - surfaces, lines and ink keep their values, so the
   cards look exactly as they do everywhere else. Both ways a page can be dark: chosen
   (data-theme) and followed from the OS (no stamp). syncThemeColor reads --c-canvas, so the
   status bar follows by itself. */
@media (max-width: 767.98px) {
  :root[data-theme='dark'] { --c-canvas: #000; }
}
@media (max-width: 767.98px) and (prefers-color-scheme: dark) {
  :root:not([data-theme]) { --c-canvas: #000; }
}

@keyframes rise {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}
@keyframes fade {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}
@keyframes pop {
  from {
    opacity: 0;
    transform: scale(0.94);
  }
  to {
    opacity: 1;
    transform: none;
  }
}
@keyframes pulse-soft {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.45;
  }
}

@layer base {
  html {
    -webkit-tap-highlight-color: transparent;
    /* The root element, not just body: any moment where the page is between renders paints
       this, and without it the browser falls back to `color-scheme` - which is dark by
       default here, so light mode flashed dark. */
    background-color: var(--c-canvas);
  }

  /* Interface scale. The middle step is the size the UI is designed at, so it sets no
     attribute at all; the other two nudge it either way. A phone gets a smaller spread
     because its layout already runs close to the screen edges.

     The root's font size, not `zoom` on the body - and the difference is not cosmetic.
     **CSS `zoom` breaks `position: fixed` on iOS**: a bar with `bottom: 0` under a zoomed
     body resolves against a scaled coordinate space and lands short of the bottom edge by
     the difference, so at 0.94 on a 932pt screen the tab bar floated about 55pt up the
     screen. It looked correct on any page long enough to scroll, because scrolling drags a
     mis-positioned fixed element back into place, which is what made it look like a
     content-height problem for weeks. The docked chat card is fixed too, and had the same
     flaw waiting.

     Every size and every spacing step in this system is already in `rem` - `--spacing` is
     .25rem, the type scale is rem throughout - so moving the root font size scales the whole
     interface with no zoom anywhere and no coordinate space to get wrong. Values written in
     px deliberately stay put: a 16px field has to stay 16px or iOS zooms the page on focus,
     and a touch target should not shrink because somebody likes small text. */
  :root[data-scale='s'] {
    font-size: 15px;
  }
  :root[data-scale='l'] {
    font-size: 17.25px;
  }

  @media (min-width: 768px) {
    :root[data-scale='s'] {
      font-size: 14.4px;
    }
    :root[data-scale='l'] {
      font-size: 18.4px;
    }
  }

  /* The look a product's owner chose for its pages (kit/look.ts, DESIGN.md "Look settings") - a shop
     owner's corners and line weight in Storefront Ninja first (2026-11-02). Only a value that differs
     from the default sets anything, and the default is no attribute at all, so a page that chose
     nothing is exactly what it was. The tokens are read with the product's own value as the fallback -
     `var(--mx-control-radius, <own>)`, `var(--mx-stroke, <own>)`, `var(--mx-icon-stroke, <own>)` - and the
     kit's own controls and cards do the same (app.css). On <html>, or on any element for its part of a
     page (a preview pane): custom properties inherit. 'round' and 'regular' are "the product's own". */
  [data-mx-radius='square'] { --mx-control-radius: 0px; }
  [data-mx-radius='slight'] { --mx-control-radius: 2px; }
  [data-mx-radius='soft'] { --mx-control-radius: 4px; }
  [data-mx-radius='gentle'] { --mx-control-radius: 6px; }
  [data-mx-radius='rounder'] { --mx-control-radius: 12px; }
  [data-mx-radius='roundest'] { --mx-control-radius: 18px; }
  [data-mx-radius='pill'] { --mx-control-radius: 999px; }
  /* In order, smallest first (kit/look.ts): a product may offer these as a slider. 'none' draws no
     borders at all; an icon keeps a thin line, because an icon at 0 is no icon. */
  [data-mx-stroke='none'] { --mx-stroke: 0px; --mx-icon-stroke: 1.25; }
  [data-mx-stroke='thin'] { --mx-stroke: 1px; --mx-icon-stroke: 1.25; }
  [data-mx-stroke='medium'] { --mx-stroke: 1.5px; --mx-icon-stroke: 2; }
  [data-mx-stroke='bold'] { --mx-stroke: 2px; --mx-icon-stroke: 2.25; }
  [data-mx-stroke='heavy'] { --mx-stroke: 3px; --mx-icon-stroke: 2.75; }

  /* Colour changes are animated so the theme toggle dissolves rather than jump-cuts.
     Scoped to colour only, so transforms and animations stay crisp. */
  body,
  header,
  main,
  aside,
  nav,
  section,
  article,
  li,
  input,
  textarea,
  select,
  button {
    transition:
      background-color 380ms cubic-bezier(0.22, 1, 0.36, 1),
      border-color 380ms cubic-bezier(0.22, 1, 0.36, 1),
      color 380ms cubic-bezier(0.22, 1, 0.36, 1);
  }

  /* --color-* exist only inside Tailwind's @theme (Studio); the published tokens.css drops that block, so
     every product had no ring at all until these fell back to the --c-* tokens (2026-11-02). */
  ::selection {
    background: var(--color-accent, var(--c-accent));
    color: var(--color-accent-ink, var(--c-accent-ink));
  }

  /* Anything focusable gets the same visible ring — keyboard and switch users included. */
  :focus-visible {
    outline: 2px solid var(--color-accent, var(--c-accent));
    outline-offset: 2px;
    border-radius: var(--radius-sm);
  }

  /* Honour the OS setting. Motion is decoration in an app; it is never load-bearing.
     A marketing page opts out with `data-motion="always"` on <html>, because there the
     motion IS the product - and because older Windows machines have animations disabled
     globally, so honouring the setting there shows a stranger a dead page. Owner's standing
     decision. Scoping it here rather than trying to undo it further down is deliberate:
     `animation-duration: revert` rolls back to the user-agent value, which is 0s, so an
     "un-disable" rule would disable the animations just as thoroughly. */
  @media (prefers-reduced-motion: reduce) {
    :root:not([data-motion='always']) *,
    :root:not([data-motion='always']) *::before,
    :root:not([data-motion='always']) *::after {
      animation-duration: 0.01ms !important;
      animation-iteration-count: 1 !important;
      transition-duration: 0.01ms !important;
      scroll-behavior: auto !important;
    }
  }

  /* The page's own "no animation" (kit/look.ts motion: off - a shop owner's choice): the same as the
     OS setting above, whatever the OS says. On <html>, or on any element for its part of the page. */
  [data-motion='off'],
  [data-motion='off'] *,
  [data-motion='off'] *::before,
  [data-motion='off'] *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* The shared settings element (elements/settings.ts) paints nothing until its script has
   defined it. Hold its exact footprint meanwhile - 44px square, pulled flush with the edge -
   so the header does not shift sideways the moment the cog appears. */
mixtape-settings:not(:defined) {
  display: inline-block;
  width: 44px;
  height: 44px;
  margin-inline-end: var(--mx-cog-edge, -0.5rem);
}

/* Native <select> anchors its arrow to the padding box, so adding padding-right only
   squeezes the label — it never moves the arrow off the border. Suppress the native
   control and draw our own chevron at a fixed inset instead. Colour comes from a
   per-theme variable because a data: URI can't read currentColor. */


/* Minimum comfortable touch target — 44px is the Apple/WCAG floor. */


/* Physical feedback on tap. Cheap, and it makes the whole app feel alive. */


/* Children rise in sequence; set --i on each child for the stagger. */


/* iOS safe areas. `viewport-fit=cover` deliberately extends the page under the notch and
   the home indicator so backgrounds reach the screen edges - these keep *content* out of
   them. Applied to the sticky header and the fixed tab bar, never to <html>, because
   padding there breaks sticky positioning. */




/* The tab bar's own version: enough to clear a home indicator, not a whole band of it.
   Four items with labels already stand well clear, and the full inset left a strip of empty
   canvas under them that read as the bar floating rather than sitting on the edge. */
/* While the keyboard is up, everything pinned to the bottom stands aside. There is nothing
   to gain from a tab bar and a usage dock squeezed between the text you are writing and the
   keyboard you are writing it with. */
:root[data-kb] [data-overlay-bar] {
  display: none;
}

/* ...and the thing you are typing into comes to the keyboard, rather than the page being
   scrolled until it happens to be visible.

   Four rounds went into scrolling that card into the right place: correcting the browser,
   then racing it, then giving up and trusting it - and it still left the field under the
   keyboard on a phone. Docking removes the question. `bottom: var(--kb)` is the top of the
   keyboard on both platforms: zero on Android, where the layout viewport already shrank,
   and the keyboard's height on iOS, where it did not.

   `--kb-bar` is the editor toolbar Android floats over the page - arrows and a Done tick -
   which no measurement includes and no attribute refuses. It is reserved *inside* the card
   rather than by lifting the card, so the strip it covers is the card's own background
   instead of the conversation showing through underneath. Zero on iOS, which draws no such
   bar for a `contenteditable`. See Keyboard.tsx. */
:root[data-kb] [data-docks-to-keyboard] {
  position: fixed;
  left: 0;
  right: 0;
  bottom: var(--kb, 0px);
  padding-bottom: var(--kb-bar, 0px);
  z-index: 45;
  border-radius: 0;
  border-left: 0;
  border-right: 0;
  max-height: 100%;
}

/* A field you can tap without the page jumping.

   iOS zooms the whole page when you focus a control whose text is under 16px, and every
   measurement taken afterwards is in a scaled viewport - which is how the chat card once
   ended up somewhere else entirely. 16px on a phone is the guard.

   The trigger is the *device*, not the window width, and tying it to a `sm:` breakpoint got
   that wrong: a phone in landscape and a tablet are both wider than any breakpoint and both
   still zoom. So ask what is pointing at the screen. A mouse gets the smaller, tidier text;
   anything you touch gets 16px, at any width. */
.tap-text {
  font-size: 13px;
}
@media (pointer: coarse) {
  .tap-text {
    font-size: 16px;
  }

  /* No compensation for the interface scale here any more. It used to be a `zoom` on the
     body, which rendered this 16px at 15.04 and let iOS zoom the page anyway; the scale is
     the root font size now and this is px, so 16 stays 16 at every setting. */
}

/* A strip that scrolls sideways in step with another one, so only one of them should show
   a scrollbar. The board's headings mirror the columns below them; two bars would be two
   controls for one axis. */
[data-no-scrollbar] {
  scrollbar-width: none;
}
[data-no-scrollbar]::-webkit-scrollbar {
  display: none;
}

/* Chrome that gets out of the way.

   Scrolling down is the one time the two bottom bars are worth less than the rows they are
   covering, so they leave - and come back the moment you scroll up, which is when you are
   looking for them. Only past a threshold, so a short nudge does not flap them. Never while
   the keyboard is up: those bars are already hidden then, and a transform on top of that is
   a second answer to a settled question.

   The header goes too, but only on a phone: on a desktop it carries the nav and there is
   screen to spare. It is `sticky`, so by the time it leaves - a hundred and twenty pixels
   down - the place it holds in the flow is already scrolled past, and nothing opens a gap
   where it was. The board's sticky column headings move up to take its place, or they would
   hang below a bar that is no longer there. */
[data-chrome-bar] {
  transition: transform 220ms var(--ease-out-soft);
}
:root[data-chrome-away] [data-chrome-bar='bottom'] {
  transform: translateY(100%);
}
@media (max-width: 767px) {
  :root[data-chrome-away] header[data-chrome-bar] {
    transform: translateY(-100%);
  }
  /* Up into the space the header left - all the way, and filling the notch itself rather
     than starting under it. Stopping at the inset leaves a band of status bar with the
     board scrolling behind it, which is what the header had been covering. */
  :root[data-chrome-away] [data-board-headings] {
    top: 0;
    padding-top: env(safe-area-inset-top);
  }
}
@media (prefers-reduced-motion: reduce) {
  [data-chrome-bar] {
    transition: none;
  }
}



/* Landscape on a notched phone puts the cutout on one side. */


/* The studio floor. Two-frame sprite animation done the pixel-art way: cross-fade nothing,
   just swap which frame is visible on a step timing function, so it reads as real frames
   rather than a smooth tween. */
@keyframes bob {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-1.5%); }
}
@keyframes frame-a {
  0%, 49.9% { opacity: 1; }
  50%, 100% { opacity: 0; }
}
@keyframes frame-b {
  0%, 49.9% { opacity: 0; }
  50%, 100% { opacity: 1; }
}
.frame-a { animation: frame-a 0.44s steps(1, end) infinite; }
.frame-b { animation: frame-b 0.44s steps(1, end) infinite; }

/* The two frames are sibling <g> elements at the same coordinates inside one SVG, so they
   already overlap - opacity alone swaps which one you see. CSS positioning does not apply
   to SVG groups, so do not reach for it here. */

/* Somebody who does not want motion still gets the colour and the bubble, which carry the
   same information - the animation is emphasis, never the only signal. */
@media (prefers-reduced-motion: reduce) {
  .frame-a, .frame-b { animation: none; }
  .frame-b { opacity: 0; }
}

/* ── No zoom-lurch when a field is focused ──────────────────────────────────────────────
 *
 * iOS Safari zooms the whole page in when you tap into a control whose text is smaller
 * than 16px, and zooms back out when you leave it. Every text field in this app is 14px,
 * so typing anywhere meant the page jumping in and out.
 *
 * The usual fix - `maximum-scale=1, user-scalable=no` in the viewport meta - takes
 * pinch-zoom away from everybody permanently to solve a typing annoyance. Giving the
 * controls the 16px Safari is asking for costs nothing and leaves zoom alone.
 *
 * Deliberately unlayered: Tailwind's utilities live in a cascade layer, and anything
 * layered loses to unlayered rules regardless of order - which is the only way to beat the
 * `text-sm` that every control here carries.
 */
@media (pointer: coarse) {
  input:not([type='checkbox']):not([type='radio']):not([type='color']):not([type='range']),
  select,
  textarea {
    font-size: 16px;
  }

  /* The pre-divided 17.05px that used to sit here is gone with the `zoom` it compensated
     for. 16px is 16px at every interface scale now. */
}

/* An always-dark surface - the photo viewer - and what sits on it, the same in both themes. Named
   here so the app layer names no colour of its own (the gate's colour check reads a product's
   vendored app.css; RentKeeper went red on the viewer's literal #050505/#fff, 2026-10-05). */
:root {
  --c-scrim: #050505;
  --c-scrim-ink: #ffffff;
}

/* ── The base every page wears ──────────────────────────────────────────────
   The landing and app layers are written for border-box, a body with no margin, and the kit's
   face on the body - and until 2026-10-05 the kit never said so: Dayrule wrote it into its own
   sheet, and RentKeeper did not, so it went live in the browser's Times with every box a little
   wider than its column. Here once, for every product. `:where()` keeps it at zero
   specificity: a product's own body rule, Tailwind's preflight (Studio) or a class still wins. */
:where(*, *::before, *::after) { box-sizing: border-box; }
/* `hidden` always hides. The browser's own [hidden] rule loses to any layout that sets display - a
   table restacked as rows on a phone (`tr { display: block }`) showed every row the kit's filter had
   hidden, while the laptop showed two (2026-10-05). The kit's filter, sort bar and photo picker all
   hide with the attribute, so it has to win. */
[hidden]:not([hidden='until-found']) { display: none !important; }
:where(body) {
  margin: 0;
  background: var(--c-canvas);
  color: var(--c-ink);
  font-family: var(--font-sans);
  -webkit-font-smoothing: antialiased;
}
/* The element a background animation goes behind when the page is not a landing (kit/scene.ts, a shop's
   [data-mx-scenes]): a stacking context, as `.lp` is (landing.css), so the scene's fixed layer at z-index -1
   paints over the element's own background and under everything in it - not under the whole document. */
[data-mx-scenes] { position: relative; z-index: 0; }

/* ── Every script of the kit's 42 languages (mixtape-i18n.mjs LANGUAGES, 2026-10-05) ──────────
   The device's own fonts for the scripts no design downloads, in the regional form the page's
   language asks for: one Han character is drawn differently in Japan, Korea, mainland China and
   Taiwan, and only lang tells the browser which. */
:lang(ja) { --font-script: 'Hiragino Sans', 'Hiragino Kaku Gothic ProN', 'Yu Gothic UI', 'Yu Gothic', Meiryo, 'Noto Sans JP', 'Noto Sans CJK JP'; }
:lang(ko) { --font-script: 'Apple SD Gothic Neo', 'Malgun Gothic', 'Noto Sans KR', 'Noto Sans CJK KR'; }
:lang(zh-Hans) { --font-script: 'PingFang SC', 'Microsoft YaHei', 'Noto Sans SC', 'Noto Sans CJK SC'; }
:lang(zh-Hant) { --font-script: 'PingFang TC', 'Microsoft JhengHei', 'Noto Sans TC', 'Noto Sans CJK TC'; }
:lang(th) { --font-script: Thonburi, 'Leelawadee UI', 'Noto Sans Thai'; }
:lang(hi) { --font-script: 'Kohinoor Devanagari', 'Nirmala UI', 'Noto Sans Devanagari'; }
:lang(ar) { --font-script: 'Geeza Pro', 'Segoe UI', 'Noto Sans Arabic'; }

/* Letter-spacing tears Arabic's joined letters apart and breaks Hindi's and Thai's stacked marks,
   and spreads Chinese, Japanese and Korean thin: the kit's spaced capitals (labels, eyebrows,
   badges) and tight display headings are Latin habits. In these scripts, the font's own spacing. */
:where(:lang(ja), :lang(ko), :lang(zh-Hans), :lang(zh-Hant), :lang(th), :lang(hi), :lang(ar)) * { letter-spacing: normal !important; }
/* Marks above and below the line need room a Latin heading's tight line-height does not give. */
:where(:lang(th), :lang(hi), :lang(ar)) :is(h1, h2, h3, .lp-h1, .lp-h2, .lp-h3, .mx-h1, .mx-h2) { line-height: 1.4 !important; }
/* Korean breaks lines between words, as English does - not inside one. */
:lang(ko) { word-break: keep-all; }
:where(:lang(ja), :lang(zh-Hans), :lang(zh-Hant)) { line-break: strict; }

/* Right to left (Arabic): the kit lays out with start and end, and these are the few things that
   only have a direction - a slide, an arrow. --mx-dir reaches inside the menus' shadow roots,
   which cannot see the page's dir attribute. */
:root { --mx-dir: 1; --mx-end: right; }
[dir='rtl'] { --mx-dir: -1; --mx-end: left; }

/* accent.css — Storefront Ninja's rose, the product's only colour decision.
 *
 * Four tokens, set once, straight after static/kit/tokens.css. Every button, badge, pill, focus ring
 * and chart in the kit reads them, so nothing else in this product ever names a colour.
 *
 * The dark rule repeats tokens.css's own selector list on purpose: a bare `:root` loses to its
 * `:root[data-theme="dark"]`, and the product would silently keep mixtape's orange in dark mode.
 *
 * Measured, not guessed (WCAG contrast):
 *   #fb7185 on the dark canvas #0a0b0d ........ 7.32:1   (bar: 3:1)
 *   #4c0519 on #fb7185 ........................ 5.81:1   (bar: 4.5:1, every button label)
 *   #e11d48 on the light canvas #fbfaf8 ....... 4.50:1
 *   #ffffff on #e11d48 ........................ 4.70:1
 * The icon mark uses the dark accent; tools/draw-icons.mjs keeps the same hex and
 * icons.test.mjs fails if the two ever drift apart.
 */

:root,
:root[data-theme='dark'] {
  --c-accent: #fb7185;
  --c-accent-strong: #fda4af;
  --c-accent-ink: #4c0519;
  --c-accent-soft: #fb71851f;
}

:root[data-theme='light'] {
  --c-accent: #e11d48;
  --c-accent-strong: #be123c;
  --c-accent-ink: #fff;
  --c-accent-soft: #e11d481f;
}

@media (prefers-color-scheme: light) {
  :root:not([data-theme]) {
    --c-accent: #e11d48;
    --c-accent-strong: #be123c;
    --c-accent-ink: #fff;
    --c-accent-soft: #e11d481f;
  }
}

/* mixtape.works landing layer — the "spiced up" half of the design system.

   `tokens.css` is the system every product shares: the palette, the type, the radii, the
   motion vocabulary. It is deliberately quiet, because most of what we build is an app
   somebody works in all day. A landing page has the opposite job - it has about four
   seconds to look like a company worth buying from - so this file adds the loud half:
   full-bleed bands, glass surfaces, an animated ribbon, bento cards with live vignettes,
   and DOM screencasts instead of video files.

   Two rules keep the two halves one system:
   1. **Every colour here comes from `--c-*`.** No landing page invents a palette. Swap
      `data-theme` and this re-themes with everything else, because there is nothing to
      re-theme separately.
   2. **It is plain CSS.** Our Next apps get it through `@mixtape/ui/landing.css`; a product
      that is a bare Node server with no build step gets the identical file out of
      https://mixtape.works/kit/. One file, one look, both worlds.

   Put a `.lp` class on the page root and everything below is in scope.

   Removing the ribbon: delete the `.lp-wave` block here, `landing/wave.js`, and the two
   markup blocks. Nothing else depends on it. */

/* ── Landing-only variables ──────────────────────────────────────────────
   Hairlines are alpha, never a flat grey: a line at 8% black picks up whatever sits
   behind it, which on a page made of gradients and glass is the difference between a
   border and a stripe. The veil is the one alternate band background. */
.lp {
  --lp-hair: rgba(255, 255, 255, 0.10);
  --lp-hair-strong: rgba(255, 255, 255, 0.16);
  --lp-veil: rgba(23, 26, 33, 0.55);
  --lp-glass: color-mix(in srgb, var(--c-surface) 65%, transparent);
  --lp-glass-up: color-mix(in srgb, var(--c-surface) 78%, transparent);
  --lp-glass-down: color-mix(in srgb, var(--c-surface) 48%, transparent);
  --lp-tile: color-mix(in srgb, var(--c-accent) 7%, var(--c-surface-2));
  /* The chosen half of a segmented switch (Yearly | Monthly). In dark the plain surface is the
     track's own colour, so the chosen one did not read as chosen (Chris, 2026-10-15): lifted. */
  --lp-seg-on: color-mix(in srgb, var(--c-ink) 18%, var(--c-surface));
  --lp-header-h: 3.6875rem;
  --lp-gutter: 1.5rem;
  --lp-max: var(--page-max, 90rem);
  /* Ribbon strength. Dark is the brand's home, so it is tuned there first - and the light
     value is lower rather than higher, which is the opposite of what the reference site
     needed. Our paper is #fbfaf8, much lighter than theirs, so the same saturated band
     reads twice as loud on it and starts competing with the copy instead of sitting behind
     it. Measured on the real page, not guessed. */
  --lp-wave-opacity: 0.34;
  --lp-dots: #d9c6a8;
  --lp-dots-blend: screen;
  --lp-dots-opacity: 0.10;
}

:root[data-theme='light'] .lp {
  --lp-hair: rgba(0, 0, 0, 0.08);
  --lp-hair-strong: rgba(0, 0, 0, 0.13);
  --lp-veil: rgba(255, 255, 255, 0.62);
  --lp-seg-on: var(--c-surface);
  --lp-wave-opacity: 0.3;
  --lp-dots: #7a5a24;
  --lp-dots-blend: multiply;
  --lp-dots-opacity: 0.09;
}

@media (prefers-color-scheme: light) {
  :root:not([data-theme]) .lp {
    --lp-hair: rgba(0, 0, 0, 0.08);
    --lp-hair-strong: rgba(0, 0, 0, 0.13);
    --lp-veil: rgba(255, 255, 255, 0.62);
    --lp-seg-on: var(--c-surface);
    --lp-wave-opacity: 0.3;
    --lp-dots: #7a5a24;
    --lp-dots-blend: multiply;
    --lp-dots-opacity: 0.09;
  }
}

/* ── Page frame ──────────────────────────────────────────────────────────
   Full-bleed bands are `width: 100vw` children of a centred container, so they push past
   the viewport edge by a scrollbar's width unless something clips. Clip at `html`, not at
   the container: the primary button's glow and the CTA band's shadow are *meant* to spill
   past their own box. */
html:has(.lp) { overflow-x: clip; }

.lp {
  position: relative;
  /* A stacking context, so the fixed background layers below can sit at z-index -1 inside
     the page and still paint behind the content rather than behind the whole document. */
  z-index: 0;
}

.lp-container {
  width: 100%;
  max-width: var(--lp-max);
  margin-inline: auto;
  padding-inline: var(--lp-gutter);
}

.lp-bleed {
  width: 100vw;
  margin-inline: calc(50% - 50vw);
  position: relative;
}

/* ── The three background layers ─────────────────────────────────────────
   All fixed, all inert, all behind the content. Never `background-attachment: fixed` on
   any of them: it forces Chrome onto main-thread scrolling for the whole document, which
   is how a page with no heavy content still scrolls like it does. */
.lp-bg {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  overflow: hidden;
}

.lp-bg__wash {
  position: absolute;
  inset: 0;
  background: linear-gradient(
    135deg,
    color-mix(in srgb, var(--c-idea) 14%, transparent) 0%,
    color-mix(in srgb, var(--c-review) 12%, transparent) 26%,
    color-mix(in srgb, var(--c-rec) 11%, transparent) 52%,
    color-mix(in srgb, var(--c-accent) 14%, transparent) 78%,
    color-mix(in srgb, var(--c-accent-strong) 12%, transparent) 100%
  );
}

/* 2rem grid of 1.5px dots. Blended rather than drawn flat, so it reads as texture in the
   light and as a faint star-field in the dark instead of as a grey mesh in both. */
.lp-bg__dots {
  position: absolute;
  inset: 0;
  opacity: var(--lp-dots-opacity);
  mix-blend-mode: var(--lp-dots-blend);
  background-image: radial-gradient(circle at 1.5px 1.5px, var(--lp-dots) 1.5px, transparent 0);
  background-size: 2rem 2rem;
}

/* ── The ribbon ──────────────────────────────────────────────────────────
   Strength lives on the wrapper's opacity, never on the canvas: the canvas fades itself in
   once (see `--drawn`) and a second opacity on the same element would fight that. */
.lp-wave {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  overflow: hidden;
  opacity: var(--lp-wave-opacity);
  /* The cheap fallback, and the only thing phones ever get. It paints during LCP, so it is
     two radial gradients and nothing else - no mask, no color-mix. */
  background:
    radial-gradient(48% 42% at 78% 12%, rgba(242, 181, 68, 0.30), transparent 70%),
    radial-gradient(40% 38% at 96% 34%, rgba(240, 96, 77, 0.20), transparent 72%);
}

.lp-wave__canvas {
  display: block;
  width: 100%;
  height: 100%;
  opacity: 0;
  transition: opacity 0.7s ease;
}

.lp-wave--drawn .lp-wave__canvas { opacity: 1; }

/* Frosted ends instead of a faded ribbon: two composited strips cost one blur each, where
   fading the geometry would mean a multi-tap read per pixel in the shader. */
.lp-wave__frost {
  position: absolute;
  top: 0;
  bottom: 0;
  -webkit-backdrop-filter: blur(2.125rem);
  backdrop-filter: blur(34px);
  pointer-events: none;
}
.lp-wave__frost--left {
  left: 0;
  width: 42vw;
  -webkit-mask-image: linear-gradient(90deg, #000 0%, rgba(0, 0, 0, 0.82) 42%, transparent 100%);
  mask-image: linear-gradient(90deg, #000 0%, rgba(0, 0, 0, 0.82) 42%, transparent 100%);
}
.lp-wave__frost--right {
  right: 0;
  width: 22vw;
  -webkit-mask-image: linear-gradient(270deg, #000 0%, rgba(0, 0, 0, 0.82) 42%, transparent 100%);
  mask-image: linear-gradient(270deg, #000 0%, rgba(0, 0, 0, 0.82) 42%, transparent 100%);
}

/* ── Bands ───────────────────────────────────────────────────────────────
   Exactly two section backgrounds, strictly alternating down the page: transparent, then
   the veil. Add or remove a section and fix the alternation - it is the page's rhythm, and
   two tinted bands in a row read as a mistake nobody can name. */
.lp-band {
  width: 100vw;
  margin-inline: calc(50% - 50vw);
  padding-block: 4.5rem;
  position: relative;
}
.lp-band--tint { background: var(--lp-veil); }
.lp-band--tight { padding-block: 3rem; }
/* The hero sets its own vertical rhythm, including the clearance for the fixed header, so
   the band around it contributes none. Without this the top of the page carries the band's
   4.5rem *and* the hero's 4rem *and* the header's height, and the headline sits a third of
   the way down the screen. */
/* Two classes, so the tablet/phone rule `.lp-band { padding-block: 3rem }` further down
   cannot win on source order: it did until 2026-09-21, and every phone got 3rem of empty
   band above the hero's own header clearance - a gap under the bar Chris spotted at once. */
.lp-band.lp-band--hero { padding-block: 0; }

/* ── Boxed tinted bands (Chris 2026-10-21: "lets keep it (for now)") ─────
   On a tablet and a desktop a tinted band no longer fills the page's width: its content sits in
   one big rounded box, the content's width, wearing the veil the band used to. A phone keeps the
   full-width band. Everything about it is this one block.
   TO REVERT to full-width tinted bands: delete this block, rebuild the kit, re-vendor
   landing.css into every product and publish them (DESIGN.md "Bands"). Nothing else depends on it.
   Two classes, so the tablet/phone `.lp-band { padding-block: 3rem }` further down cannot win. */
@media (min-width: 576px) {
  .lp-band.lp-band--tint { background: none; padding-block: 0.75rem; }
  .lp-band--tint > .lp-container {
    width: min(var(--lp-max), calc(100% - 2 * var(--lp-gutter)));
    padding-block: 3rem;
    border-radius: 24px;
    background: var(--lp-veil);
  }
  .lp-band--tint.lp-band--tight > .lp-container { padding-block: 2rem; }
  /* A closing call to action that IS the band's container (<div class="lp-container lp-final">)
     gets the box's room too: `.lp-final:first-child { padding-block: 0 }` below leaves the band
     to pad it, and a boxed band does not - its heading sat on the box's top edge (2026-10-21). */
  .lp-band--tint > .lp-container.lp-final { padding-block: 3rem; }
}
@media (min-width: 992px) {
  .lp-band.lp-band--tint { padding-block: 1rem; }
  .lp-band--tint > .lp-container { padding-block: 4rem; border-radius: 28px; }
  .lp-band--tint.lp-band--tight > .lp-container { padding-block: 2.5rem; }
  .lp-band--tint > .lp-container.lp-final { padding-block: 4rem; }
}

/* A document page - privacy, terms, refunds, help - in the landing frame: the fixed header's
   clearance, a reading measure, text from the left. Every product used to invent its own
   (Dayrule `section.page`, Storefront `.sn-doc`), and RentKeeper's did it by padding the kit's
   own .lp-container, which also pushed its landing hero 130px down on phones (2026-10-05).
   <section class="lp-doc"><div class="lp-container"> ... </div></section> */
.lp-doc { padding-block: calc(var(--lp-header-h) + 3rem) 4rem; }
.lp-doc > .lp-container { max-width: 46rem; }
/* A document reads as paragraphs and blocks with room between them; the first card or grid after the
   text gets a little more, so it does not sit on the line above it (RentKeeper's privacy page). */
.lp-doc > .lp-container > * + * { margin-top: 1rem; }
.lp-doc > .lp-container > * + :is(.lp-bento, .lp-grid, .lp-faq, .lp-card, .mx-panel) { margin-top: 2rem; }
@media (max-width: 575.98px) { .lp-doc { padding-block: calc(var(--lp-header-h) + 2rem) 3rem; } }

/* ── Type ────────────────────────────────────────────────────────────────
   One display treatment shared by the hero and every section title, so the page has a
   single voice. Geist is the system font - we do not add a second family for landing
   pages; the display weight and the negative tracking are what make it read as display. */
.lp-eyebrow {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  /* min-height, not height. A two-word English eyebrow fits on one line at any width; a
     Hungarian one does not, and a fixed-height pill puts its second line outside its own
     background. The padding is what centres the text now, and every pill on a page still
     matches because they all start from the same minimum. */
  min-height: 1.8125rem;
  padding: 0.3125rem 0.8125rem 0.25rem;
  box-sizing: border-box;
  line-height: 1.25;
  text-align: center;
  text-wrap: balance;
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--c-accent);
  background: var(--c-accent-soft);
  border: 1px solid color-mix(in srgb, var(--c-accent) 22%, transparent);
  border-radius: 999px;
  align-self: flex-start;
  /* As wide as its words, never as wide as its container: a grid or a stretched flex column
     would otherwise give it the whole row. Once it wraps (a Hungarian eyebrow on a phone) the
     browser still sizes the box to the full row however short the balanced lines are - the kit
     script measures the lines and narrows it (kit/pills.ts). */
  width: fit-content;
  max-width: 100%;
  justify-self: start;
  /* Its own composited layer. Sections above have fractional heights, so pills land on
     sub-pixel offsets and rasterise differently from one another - which reads as pills of
     visibly different heights. In a layer each one rasterises in integer local coordinates
     and Chrome pixel-aligns it when compositing. */
  transform: translateZ(0);
}

.lp-eyebrow__dot {
  width: 0.375rem;
  height: 0.375rem;
  border-radius: 50%;
  background: var(--c-accent);
  animation: lp-dot 2.4s ease-in-out infinite;
}

.lp-h1 {
  margin: 0;
  font-size: clamp(2.5rem, 6.2vw, 4.5rem);
  font-weight: 800;
  line-height: 1.04;
  letter-spacing: -0.035em;
  text-wrap: balance;
  color: var(--c-ink);
}
/* The second line goes on its own row on purpose, rather than being left to wrap. */
.lp-h1 span { display: block; }

.lp-h2 {
  margin: 0.875rem 0 0;
  font-size: clamp(2rem, 2.5vw, 2.5rem);
  font-weight: 700;
  line-height: 1.08;
  letter-spacing: -0.035em;
  text-wrap: balance;
  color: var(--c-ink);
}

.lp-lead {
  margin: 0.875rem 0 0;
  font-size: 1.125rem;
  line-height: 1.55;
  color: var(--c-ink-muted);
  text-wrap: pretty;
}

.lp-sub {
  margin: 1.5rem 0 0;
  max-width: 720px;
  font-size: 1.125rem;
  line-height: 1.55;
  color: color-mix(in srgb, var(--c-ink) 85%, transparent);
  text-wrap: pretty;
}

.lp-trust {
  margin: 1.5rem 0 0;
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: color-mix(in srgb, var(--c-ink) 52%, transparent);
}

.lp-bullets {
  margin: 1.5rem 0 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 0.85rem;
}
.lp-bullets li {
  position: relative;
  display: grid;
  grid-template-columns: 1.25rem 1fr;
  gap: 0.7rem;
  align-items: start;
  font-size: 1.125rem;
  line-height: 1.5;
  color: var(--c-ink-muted);
  text-wrap: pretty;
}
.lp-bullets li::before {
  content: "";
  margin-top: 0.28em;
  width: 1.25rem;
  height: 1.25rem;
  border-radius: 50%;
  background: var(--c-accent-soft);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--c-accent) 25%, transparent);
}
/* The tick itself is a mask rather than an icon font or an <svg> per bullet: one data URI,
   no network, and it takes its colour from the token like everything else. */
.lp-bullets li::after {
  content: "";
  position: absolute;
  inset-inline-start: 0;
  margin-top: 0.28em;
  width: 1.25rem;
  height: 1.25rem;
  background: var(--c-accent);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 12.5 4 4 8-8.5'/%3E%3C/svg%3E") center / 0.8125rem 0.8125rem no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 12.5 4 4 8-8.5'/%3E%3C/svg%3E") center / 0.8125rem 0.8125rem no-repeat;
}

/* ── Buttons ─────────────────────────────────────────────────────────────
   Primary and secondary share exactly one geometry. The secondary subtracts its border
   from its padding, so the two are the same height to the pixel when they sit side by
   side - which they almost always do. */
.lp-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.55rem;
  padding: 1.15rem 1.75rem;
  border-radius: 12px;
  font-size: 16px;
  font-weight: 700;
  line-height: 1;
  text-decoration: none;
  cursor: pointer;
  border: none;
}

.lp-btn--primary {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  color: var(--c-accent-ink);
  /* A → B → C → B → A, so the two ends of the gradient match and the loop has no seam.
     The middle stop is `--c-accent-shine`, a neighbour of the accent's own hue. Unset, it is
     mixtape's amber mixed toward red, which stays warm. Mixing ANY accent with red is not safe:
     Dayrule's teal went brown-olive (Chris, 2026-09-30, "the brown... im not a fan of"), as
     `--c-go` green once did against warm paper. A product sets its shine next to its accent;
     a design's accent gets one from kit/design.ts shineFor(). Check a gradient in BOTH themes,
     because the one that fails is never the one you designed it in.
     app.css `.mx-cta` is this same button for an app screen (Studio's Continue): change both. */
  background: linear-gradient(
    120deg,
    var(--c-accent-strong) 0%,
    var(--c-accent) 25%,
    var(--c-accent-shine, color-mix(in srgb, var(--c-accent) 62%, var(--c-rec))) 50%,
    var(--c-accent) 75%,
    var(--c-accent-strong) 100%
  );
  background-size: 200% 100%;
  animation: lp-flow 9s linear infinite, lp-breathe 3.6s ease-in-out infinite;
  box-shadow: 0 0 24px color-mix(in srgb, var(--c-accent) 35%, transparent);
  transition: transform 0.2s ease, box-shadow 0.3s ease;
}
.lp-btn--primary::after {
  content: "";
  position: absolute;
  inset: 0;
  /* Above the gradient, below the label. A positioned pseudo-element at z-index 0 paints
     after inline text, so the label has to be a span with a layer of its own - a bare text
     node cannot be raised above this and the sweep would wash over the words. */
  z-index: 0;
  background: linear-gradient(115deg, transparent 38%, rgba(255, 255, 255, 0.18) 50%, transparent 62%);
  transform: translateX(-130%);
  animation: lp-sheen 5.5s ease-in-out infinite;
  pointer-events: none;
}
.lp-btn--primary:hover,
.lp-btn--primary:focus-visible {
  transform: translateY(-2px);
  /* The glow animation is dropped so the stronger static shadow can win - a running
     box-shadow animation outranks a hover rule for as long as it is running. */
  animation: lp-flow 9s linear infinite;
  box-shadow:
    0 0 44px color-mix(in srgb, var(--c-accent) 60%, transparent),
    0 0 0 0.375rem color-mix(in srgb, var(--c-accent) 18%, transparent);
}
.lp-btn--primary:active { transform: translateY(0); }

.lp-btn__label {
  position: relative;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
}
.lp-btn__arrow { animation: lp-nudge 4.5s ease-in-out infinite; }
.lp-btn--primary:hover .lp-btn__arrow,
.lp-btn--primary:focus-visible .lp-btn__arrow {
  animation: none;
  transform: translateX(3px);
  transition: transform 0.2s ease;
}

.lp-btn--ghost {
  padding: calc(1.15rem - 1px) calc(1.75rem - 1px);
  color: var(--c-ink);
  background: var(--lp-glass);
  border: 1px solid var(--lp-hair-strong);
  -webkit-backdrop-filter: blur(0.75rem);
  backdrop-filter: blur(12px);
  transition: background 0.2s ease, border-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
}
.lp-btn--ghost:hover,
.lp-btn--ghost:focus-visible {
  background: var(--lp-glass-up);
  border-color: color-mix(in srgb, var(--c-accent) 45%, transparent);
  transform: translateY(-2px);
  box-shadow: 0 10px 30px -18px var(--c-accent);
}

/* A pill, for the "reveal" affordances and the sticky heads. */
.lp-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.6rem 1.05rem;
  border-radius: 999px;
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--c-ink);
  background: var(--lp-glass);
  border: 1px solid var(--lp-hair);
  -webkit-backdrop-filter: blur(0.75rem);
  backdrop-filter: blur(12px);
  cursor: pointer;
  transition: border-color 0.2s ease, background 0.2s ease;
}
.lp-pill:hover { border-color: color-mix(in srgb, var(--c-accent) 40%, transparent); }
.lp-pill__chev { transition: transform 0.25s var(--ease-out-soft); }
.lp-pill[aria-expanded='true'] .lp-pill__chev { transform: rotate(180deg); }

/* ── Header ──────────────────────────────────────────────────────────────
   Transparent until the page moves, so the ribbon runs unbroken behind it on first paint.
   A tiny scroll watcher flips `.is-scrolled` on the element; everything else is CSS. */
.lp-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 50;
  height: var(--lp-header-h);
  display: flex;
  align-items: center;
  background: transparent;
  border-bottom: 1px solid transparent;
  transition: background 0.25s ease, border-color 0.25s ease, backdrop-filter 0.25s ease;
}
.lp-header.is-scrolled {
  background: var(--lp-glass);
  border-bottom-color: var(--lp-hair);
  -webkit-backdrop-filter: blur(0.75rem);
  backdrop-filter: blur(12px);
}
.lp-header__inner {
  position: relative;
  width: 100%;
  max-width: var(--lp-max);
  margin-inline: auto;
  padding-inline: var(--lp-gutter);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}
/* Absolutely centred on the page rather than flex-centred between the two clusters: the
   left lockup and the right actions are never the same width, so flex centring puts the
   nav visibly off the page's true centre line. */
.lp-header__nav {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 1.75rem;
}
.lp-header__nav a {
  font-size: 0.9062rem;
  font-weight: 600;
  color: var(--c-ink);
  opacity: 0.72;
  text-decoration: none;
  transition: color 0.2s ease, opacity 0.2s ease;
}
.lp-header__nav a:hover { opacity: 1; color: var(--c-accent); }
.lp-header__right { display: flex; align-items: center; gap: 0.75rem; }
/* Studio pulls the cog flush with the header edge; here something follows it. */
.lp-header__right mixtape-settings { --mx-cog-edge: 0; }
/* Two round 44px targets side by side read further apart than two glyphs should: 4px closer. */
.lp-header__right mixtape-settings + mixtape-profile { margin-inline-start: -0.25rem; }

/* Anything a header link scrolls to needs room to land under the fixed bar. */
.lp [id] { scroll-margin-top: calc(var(--lp-header-h) + 1.25rem); }

/* ── Hero ────────────────────────────────────────────────────────────────
   The gaps above and below the button row are equal on purpose; an unequal pair is the
   most common way a centred hero looks subtly wrong. */
.lp-hero {
  padding-block: calc(var(--lp-header-h) + 4rem) 4rem;
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
}
.lp-hero .lp-eyebrow { align-self: center; justify-self: center; margin-bottom: 1.5rem; }
/* THE PILL IS THE HERO'S (Chris, 2026-09-28: "only the hero cta can have a pill, all other
   section's headings dont need a pill"). A section's title stands on its own; an eyebrow above
   every one of them is a row of identical badges saying nothing the title does not. Kept out
   here rather than trusted to every product's markup, so a page written before the rule - or
   by a Builder that missed it - still shows one pill, at the top. */
.lp-eyebrow:not(.lp-hero .lp-eyebrow) { display: none; }
.lp-hero__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.75rem;
  margin-top: 1.5rem;
}

/* The demo directly under the hero reads as part of it, so the gap above is small and the
   container gutter is cancelled - it sits flush with the content edge. */
.lp-hero-demo { padding-block: 1.25rem calc(3.5rem + 1.25rem); }

/* ── Split spotlight ─────────────────────────────────────────────────────
   7/12 demo, 5/12 copy, alternating sides down the page. */
.lp-spotlight {
  display: grid;
  grid-template-columns: 7fr 5fr;
  gap: 3rem;
  align-items: center;
}
.lp-spotlight--flip { grid-template-columns: 5fr 7fr; }
.lp-spotlight--flip .lp-spotlight__copy { order: -1; }
.lp-spotlight__demo { max-width: 75%; }
.lp-spotlight:not(.lp-spotlight--flip) .lp-spotlight__demo { margin-inline-end: auto; }
.lp-spotlight--flip .lp-spotlight__demo { margin-inline-start: auto; }

/* ── Sticky-head bento ───────────────────────────────────────────────────*/
.lp-grid {
  display: grid;
  grid-template-columns: 360px 1fr;
  gap: 44px;
  align-items: start;
}
.lp-grid--flip { grid-template-columns: 1fr 360px; }
.lp-grid--flip .lp-grid__head { order: 2; }
.lp-grid__head {
  position: sticky;
  top: calc(var(--lp-header-h) + 1.5rem);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}
.lp-grid__head .lp-pill { margin-top: 1.5rem; }

.lp-bento { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.75rem; }
/* Three across on a desktop, for a set of exactly three - two to a row would leave the third
   alone on its own line. Below the breakpoint the bento's own rules apply. */
@media (min-width: 992px) {
  .lp-bento.lp-bento--3 { grid-template-columns: repeat(3, 1fr); }
}
/* Four across on a wide screen, for a list that grows (mixtape.works' products, Chris 2026-10-05):
   a last card on its own stays one column wide - never stretched across the row. */
@media (min-width: 1200px) {
  .lp-bento.lp-bento--4 { grid-template-columns: repeat(4, 1fr); }
}
.lp-card {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  min-width: 0;
  padding: 1.125rem;
  border-radius: 16px;
  background: var(--lp-glass);
  border: 1px solid var(--lp-hair);
  -webkit-backdrop-filter: blur(0.75rem);
  backdrop-filter: blur(12px);
}
.lp-card--wide { grid-column: span 2; }
.lp-card--up { background: var(--lp-glass-up); }
.lp-card--down { background: var(--lp-glass-down); }
.lp-card--row { flex-direction: row; align-items: center; gap: 1rem; padding: 1.125rem 1.25rem; }
/* The margins are set, not left to the element: a product that marks its card titles up as the
   headings they are (<h3>, which search engines read) got the browser's 1em above and below,
   and Storefront Ninja's bento had a gap under every title that Dayrule's <div>s did not
   (Chris, 2026-10-01). Same look whatever the element. */
.lp-card__title { margin: 0; font-size: 1.0938rem; font-weight: 700; color: var(--c-ink); }
.lp-card__text { margin: 2px 0 0; font-size: 0.9688rem; line-height: 1.5; color: var(--c-ink-muted); text-wrap: pretty; }

/* The tile a vignette lives in. Fixed height, clipped, tinted with the accent so the row
   of them reads as one material. */
/* Everything inside a tile sizes border-box, whatever the host page's reset says: these
   vignettes ship to products that have their own CSS and some of them do not set it. */
.lp-viz,
.lp-viz * { box-sizing: border-box; }

.lp-viz {
  height: 6.25rem;
  border-radius: 11px;
  background: var(--lp-tile);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  position: relative;
}

/* ── Reveal grid ─────────────────────────────────────────────────────────*/
.lp-reveal { display: none; margin-top: 1.375rem; }
.lp-reveal.is-open { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.75rem; }
.lp-reveal__item {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.25rem 0.625rem;
  align-items: center;
  padding: 0.875rem;
  border-radius: 14px;
  background: var(--lp-glass);
  border: 1px solid var(--lp-hair);
}
.lp-reveal__name { font-size: 0.9062rem; font-weight: 700; color: var(--c-ink); }
.lp-reveal__text { grid-column: 1 / -1; font-size: 0.8125rem; line-height: 1.45; color: var(--c-ink-muted); }

/* ── Centred section head, and the fineprint under an offer ──────────────*/
.lp-head-centre { max-width: 760px; margin: 0 auto 2.25rem; text-align: center; }
.lp-head-centre .lp-eyebrow { align-self: center; }
.lp-head-centre .lp-h2 { margin-top: 0; }
.lp-head-centre .lp-lead { margin-inline: auto; }
/* A section title written straight into its band's container, without the .lp-head-centre block
   (Hotel, 2026-10-06 - Chris: "gap issues below headings": "What it does" and "How it works" sat
   on their cards, flush left over a centred page). It reads as that block does: centred, its
   paragraph centred under it, and the block's gap before what follows. The closing CTA
   (.lp-final) and the sticky-head grid (.lp-grid__head) keep their own. */
/* A document page's title and lead too (.lp-doc): its body stays left, but its heading reads like every
   other heading on the site (Storefront Ninja mirrored this rule for its own document pages, 2026-10-09 -
   one rule for every product's privacy, terms and help pages instead). */
:is(.lp-band, .lp-doc) > .lp-container:not(.lp-final) > .lp-h2 {
  max-width: 760px;
  margin: 0 auto 2.25rem;
  text-align: center;
}
:is(.lp-band, .lp-doc) > .lp-container:not(.lp-final) > .lp-h2:has(+ :is(.lp-lead, .lp-sub)) { margin-bottom: 0; }
:is(.lp-band, .lp-doc) > .lp-container:not(.lp-final) > .lp-h2 + :is(.lp-lead, .lp-sub) {
  max-width: 760px;
  margin: 0.875rem auto 2.25rem;
  text-align: center;
}
/* A section that centres its heading centres what follows it. A list flush to the left margin
   under a centred heading reads as a stray block that lost its own heading - the list is
   centred as a block, its items still left-aligned against each other so the ticks line up. */
.lp-container:has(> .lp-head-centre) > .lp-bullets {
  width: fit-content;
  max-width: 100%;
  margin-inline: auto;
}
/* And they stand further off than a hero's do. A hero's buttons follow straight on from the
   sentence that set them up; a closing offer has said its piece - the eyebrow, the heading,
   the lead and usually the small print - and the button is the answer to all of it, so it
   gets air above it rather than reading as one more line of the paragraph (Chris, 2026-09-22). */
.lp-container:has(> .lp-head-centre) > .lp-hero__actions,
.lp-container:has(> .lp-head-centre) > .lp-actions {
  justify-content: center;
  margin-top: 3.25rem;
}
.lp-container:has(> .lp-head-centre) > .lp-fine { text-align: center; }
.lp-fine {
  margin: 0.875rem 0 0;
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: 0.8125rem;
  line-height: 1.55;
  color: var(--c-ink-muted);
}

/* A whole card that is a link. Colour and underline come off; the lift stays. */
a.lp-card {
  text-decoration: none;
  color: inherit;
  transition: border-color 0.2s ease, transform 0.2s ease;
}
a.lp-card:hover {
  border-color: color-mix(in srgb, var(--c-accent) 55%, transparent);
  transform: translateY(-2px);
}
a.lp-card .lp-card__cta { align-self: flex-start; width: auto; }

/* ── A product, in its own colour ──────────────────────────────────────────
   Where the company lists what it makes (mixtape.works, the account's product list), each
   card wears the product's accent instead of mixtape's orange: the mark, a wash in the
   corner, the hover edge and the button all say which product this is before the name does.
   The page sets --p-dark and --p-light on the card (the product's own two accents); this
   re-points --c-accent inside the card, so the kit's own rules paint with it. Same selector
   list as tokens.css, so an explicit theme choice and the OS both win the way they do there. */
.lp-product { --c-accent: var(--p-dark); }
:root[data-theme='light'] .lp-product { --c-accent: var(--p-light); }
@media (prefers-color-scheme: light) {
  :root:not([data-theme]) .lp-product { --c-accent: var(--p-light); }
}
.lp-product {
  background-image: radial-gradient(150% 130% at 0% 0%, color-mix(in srgb, var(--c-accent) 16%, transparent), transparent 72%);
}
.lp-product__head { display: flex; align-items: center; gap: 0.75rem; }
/* The bare mark in the product's accent, never on a tile - see .mx-brand svg in app.css. */
.lp-product__icon { width: 2rem; height: 2rem; flex: none; color: var(--c-accent); }
.lp-product .lp-card__title { font-size: 1.375rem; letter-spacing: -0.01em; }
/* What the product gets done, in one bold line above how it works, and what its free plan
   holds, in the product's own colour just above the button (mixtape.works, 2026-10-14). */
.lp-product__headline { margin: 0; font-size: 1.0625rem; font-weight: 600; line-height: 1.35; color: var(--c-ink); text-wrap: balance; }
.lp-product__free { margin: 0; font-size: 0.8125rem; font-weight: 600; color: var(--c-accent); }

/* The button fills with the product's colour from the left, and the arrow steps right as the
   fill reaches it (Chris, 2026-09-21). The fill is the product's LIGHT-theme accent in both
   themes: it is the deeper of the two, so the white label reads on it either way (Voks
   5.7:1, Dayrule 5.5:1), where the bright dark-theme accents would put white on pastel.
   The whole card is the link, so hovering or focusing the card is what starts it. */
.lp-product .lp-card__cta {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  color: var(--c-accent);
  border-color: color-mix(in srgb, var(--c-accent) 40%, transparent);
  transition: color 0.3s ease 0.08s, border-color 0.45s ease;
}
.lp-product .lp-card__cta::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--p-light);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 0.45s cubic-bezier(0.65, 0, 0.35, 1);
}
.lp-product__arrow { display: inline-block; transition: transform 0.2s ease; }
.lp-product:hover .lp-card__cta,
.lp-product:focus-visible .lp-card__cta { color: #fff; border-color: var(--p-light); }
.lp-product:hover .lp-card__cta::before,
.lp-product:focus-visible .lp-card__cta::before { transform: scaleX(1); }
/* The fill crosses the arrow about 0.3s in; the arrow waits for it, then steps. */
.lp-product:hover .lp-product__arrow,
.lp-product:focus-visible .lp-product__arrow { transform: translateX(0.25rem); transition: transform 0.22s ease 0.3s; }
@media (prefers-reduced-motion: reduce) {
  .lp-product .lp-card__cta,
  .lp-product .lp-card__cta::before,
  .lp-product__arrow { transition: none; }
}

/* ── Three-card comparison ───────────────────────────────────────────────*/
.lp-compare {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.875rem;
  max-width: 980px;
  margin-inline: auto;
}
.lp-compare--two { grid-template-columns: repeat(2, 1fr); max-width: 820px; }
/* Free plus three paid plans (2026-10-13: every product is free forever, with paid plans by size). Four in a row
   on a wide screen, two by two in between, one column on a phone like the others. */
.lp-compare--four { grid-template-columns: repeat(4, 1fr); max-width: 1200px; }
@media (max-width: 1180px) { .lp-compare--four { grid-template-columns: repeat(2, 1fr); max-width: 820px; } }
.lp-compare .lp-card { position: relative; height: 100%; }
/* The emphasised one. `margin-top: auto` on the CTA is what aligns the buttons across all
   three when the copy above them is different lengths. */
.lp-compare .lp-card--pick {
  background: var(--lp-glass-up);
  border-color: color-mix(in srgb, var(--c-accent) 55%, transparent);
  box-shadow: 0 10px 30px -18px var(--c-accent);
}
.lp-card__badge {
  position: absolute;
  top: 0.875rem;
  inset-inline-end: 0.875rem;
  /* The letter-spacing also follows the LAST letter, so the end side gives that much back and the
     word sits in the middle of its pill. */
  padding-block: 3px;
  padding-inline: 0.5625rem calc(0.5625rem - 0.07em);
  border-radius: 999px;
  font-size: 0.6562rem;
  font-weight: 700;
  line-height: 1.4;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--c-accent-ink);
  background: var(--c-accent);
}
/* Capitals have no descenders, so a line box centres them high. Where the browser can, the box is cut to
   the capitals and the pill pads them evenly. */
@supports (text-box: trim-both cap alphabetic) {
  .lp-card__badge { text-box: trim-both cap alphabetic; padding-block: 0.375rem; }
}
.lp-card__cta { margin-top: auto; width: 100%; }

/* ── Plan cards + segmented toggle ───────────────────────────────────────*/
.lp-seg {
  display: inline-flex;
  padding: 3px;
  border-radius: 999px;
  background: var(--lp-glass-down);
  border: 1px solid var(--lp-hair);
}
.lp-seg button {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem 1.1rem;
  border: none;
  border-radius: 999px;
  background: transparent;
  color: var(--c-ink-muted);
  font-size: 0.875rem;
  font-weight: 600;
  cursor: pointer;
}
.lp-seg button[aria-pressed='true'] {
  background: var(--lp-seg-on, var(--c-surface));
  color: var(--c-ink);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
}
.lp-seg__badge {
  padding: 2px 0.4375rem;
  border-radius: 999px;
  font-size: 0.6875rem;
  font-weight: 700;
  color: var(--c-go-ink);
  background: var(--c-go);
}
.lp-was { margin-inline-end: 0.4rem; color: var(--c-ink-subtle); text-decoration: line-through; }
.lp-plan__list { margin: 1rem 0 0; padding: 1rem 0 0; list-style: none; border-top: 1px solid var(--lp-hair); }
.lp-plan__fine { margin-top: 0.75rem; font-family: var(--font-mono, ui-monospace, monospace); font-size: 0.75rem; color: var(--c-ink-subtle); }
/* One card shows one variant at a time; the wrapper's data attribute picks it. */
[data-mode='a'] .lp-var--b, [data-mode='b'] .lp-var--a { display: none; }

/* ── FAQ ─────────────────────────────────────────────────────────────────
   Native <details>. No JavaScript, keyboard and screen-reader correct for free. */
.lp-faq { max-width: 760px; margin-inline: auto; display: flex; flex-direction: column; gap: 0.625rem; }
/* The questions sit in a centred column, so their heading does too - whether a product wraps it
   in .lp-head-centre or writes it straight into the container (Chris, 2026-09-28: "the heading
   above the q&a sections should always be in the middle"). */
.lp-container:has(> .lp-faq) > :is(.lp-h2, .lp-lead, .lp-eyebrow) { text-align: center; margin-inline: auto; }
.lp-container:has(> .lp-faq) > .lp-h2 { margin-bottom: 2.25rem; }
.lp-faq details {
  padding: 0 1.125rem;
  border-radius: 14px;
  background: var(--lp-glass);
  border: 1px solid var(--lp-hair);
  -webkit-backdrop-filter: blur(0.75rem);
  backdrop-filter: blur(12px);
}
.lp-faq summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 1.125rem 0;
  font-size: 1.125rem;
  font-weight: 600;
  color: var(--c-ink);
  cursor: pointer;
  list-style: none;
}
.lp-faq summary::-webkit-details-marker { display: none; }
.lp-faq summary::after {
  content: "+";
  flex: 0 0 auto;
  font-size: 1.375rem;
  font-weight: 400;
  line-height: 1;
  color: var(--c-ink-subtle);
  transition: transform 0.25s var(--ease-out-soft);
}
.lp-faq details[open] summary::after { transform: rotate(45deg); }
.lp-faq p { margin: 0 0 1.125rem; font-size: 1.0312rem; line-height: 1.6; color: var(--c-ink-muted); text-wrap: pretty; }

/* ── Final CTA ───────────────────────────────────────────────────────────*/
.lp-final { padding-block: 3.5rem; text-align: center; }
/* The divider and the extra padding set a closing call-to-action apart from the section it
   SHARES a band with - so only when something comes before it. One that opens its own band is
   set apart by the band already; with them it showed a hairline floating a band's padding below
   the edge. That was fixed once for one markup (.lp-container > .lp-final, mixtape.works
   2026-09-21) and came straight back on Storefront Ninja, which writes the two classes on one
   element (2026-09-28) - so the rule now asks what comes before, not how it is nested. */
.lp-final:not(:first-child) { border-top: 1px solid var(--lp-hair); }
.lp-final:first-child { padding-block: 0; }
.lp-final .lp-h2 { margin-top: 0; }
/* Its sentence centred under the heading, whichever text style it wears: .lp-sub has a max-width
   and no auto margins, so in a centred closing section it hung at the left (Hotel, 2026-10-06). */
.lp-final :is(.lp-lead, .lp-sub) { max-width: 720px; margin-inline: auto; }

/* ── Footer ──────────────────────────────────────────────────────────────
   `position: relative; z-index: 2`, and `.lp` is z-index 0 with its own stacking context.
   Without the footer's own layer the fixed ribbon inside the page paints straight over it. */
.lp-footer {
  position: relative;
  z-index: 2;
  padding-block: 3rem;
  background: var(--lp-glass);
  /* No rule on top: the glass and the band above it meet without a line (Chris, 2026-10-21). */
  -webkit-backdrop-filter: blur(0.75rem);
  backdrop-filter: blur(12px);
}

/* LEGACY - the one-row footer every landing page drew before the kit's own (below). Kept only until each product
   and mixtape.works has moved to landingFooter(); then this block and .lp-footer__links can go.
   The studio line on one side, the page links on the other, wrapping on a phone. Every link a 44px target on
   touch - they were 21px tall on a 390px phone (Hotel, 2026-10-07). */
.lp-footer__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem 1.5rem;
  font-size: 0.8125rem;
  color: var(--c-ink-subtle);
}
.lp-footer__row p { margin: 0; }
.lp-footer__links { display: flex; flex-wrap: wrap; gap: 0 1.25rem; }
.lp-footer a { color: inherit; text-underline-offset: 3px; }
.lp-footer a:hover { color: var(--c-ink); }
@media (pointer: coarse) {
  .lp-footer a { display: inline-flex; align-items: center; min-height: 44px; }
}

/* ── Landing footer ──────────────────────────────────────────────────────
   The kit's own (Chris, 2026-10-21): server/mixtape-footer.mjs landingFooter(), React LandingFooter.
   Four columns on a desktop - the product with its one-liner and "A product of mixtape.works", the header's
   links, help and legal, the newsletter - two by two on a tablet, one on a phone; below them the copyright and
   four placeholder social icons.
   Its ground follows the section above it: a fill (the tinted bands' veil) after a plain section, none right
   after a tinted band, so two tints never touch and the page never ends on two plain grounds. `lp-footer--plain`
   / `lp-footer--fill` (afterTint) override what the page says. */
.lp-footer--kit {
  padding-block: 3.5rem 2rem;
  background: var(--lp-veil);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  font-size: 0.875rem;
  line-height: 1.5;
  color: var(--c-ink-muted);
}
:is(main:has(> .lp-band--tint:last-of-type), .lp-band--tint) + .lp-footer--kit:not(.lp-footer--fill),
.lp-footer--kit.lp-footer--plain { background: transparent; }
.lp-footer__grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 2.25rem; }
@media (min-width: 576px) {
  .lp-footer__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 2.5rem 2rem; }
}
@media (min-width: 992px) {
  .lp-footer__grid { grid-template-columns: minmax(0, 1.45fr) minmax(0, 0.85fr) minmax(0, 0.95fr) minmax(0, 1.55fr); gap: 2.5rem; }
  .lp-footer__grid--three { grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr) minmax(0, 1fr); }
}
/* Zero specificity, so every class below sets its own margins over it. */
:where(.lp-footer--kit) p { margin: 0; }
/* The product: its mark beside its name and one-liner; the studio line under the two. */
.lp-footer__id { display: flex; align-items: center; font-size: 1rem; line-height: normal; }
/* The mark and name are the header's own lock-up (.mx-brand / .mx-logo, app.css), at the header's 1rem, not a footer copy
   of their own (Chris, 2026-10-26). The one-liner under them, from the column's own edge (Chris, 2026-10-21). */
.lp-footer__line { max-width: 22rem; margin-top: 0.75rem; }
.lp-footer__of { margin-top: 1rem; color: var(--c-ink-subtle); font-size: 0.8125rem; }
.lp-footer__of a { text-decoration: underline; }
.lp-footer__head {
  margin-bottom: 0.875rem;
  color: var(--c-ink-subtle);
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: 0.6875rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
/* Arabic is joined script: the monospace face draws it letter by letter, and spacing breaks it apart - so its
   headings take the page's own face, unspaced. */
.lp-footer--kit:lang(ar) .lp-footer__head { font-family: inherit; letter-spacing: 0; text-transform: none; }
.lp-footer__list { display: grid; gap: 0.5rem; margin: 0; padding: 0; list-style: none; }
/* On a touch screen the old footer's 44px link rows (above) spread the lists far apart (Chris, 2026-10-21: "a big gap
   between all the links"): here the rows are 2rem, touching, which keeps a fair target and a tight list. */
@media (pointer: coarse) {
  .lp-footer--kit .lp-footer__list { gap: 0; }
  .lp-footer--kit .lp-footer__list a { min-height: 2rem; }
}
.lp-footer--kit a { color: var(--c-ink-muted); text-decoration-color: color-mix(in srgb, currentColor 35%, transparent); }
.lp-footer__list a { text-decoration: none; }
.lp-footer--kit a:hover { color: var(--c-ink); }
/* The newsletter: the address and its button on one row, the mixtape box and the small print under it. */
.lp-footer__lead { margin-bottom: 0.875rem; }
.lp-footer__news { position: relative; margin: 0; }
.lp-footer__label { display: block; margin-bottom: 0.375rem; color: var(--c-ink-muted); font-size: 0.8125rem; }
.lp-footer__field { display: flex; gap: 0.5rem; }
.lp-footer__input {
  flex: 1;
  min-width: 0;
  height: 2.75rem;
  padding: 0 0.875rem;
  border: 1px solid var(--c-line-field);
  border-radius: 10px;
  background: var(--c-surface);
  color: var(--c-ink);
  font: inherit;
  font-size: 0.9375rem;
}
.lp-footer__input::placeholder { color: var(--c-ink-subtle); opacity: 1; }
.lp-footer__input:focus-visible { outline: 2px solid var(--c-accent); outline-offset: 1px; }
/* The button the field's own height, so the two read as one control. */
.lp-btn.lp-footer__go { flex: none; height: 2.75rem; min-height: 0; padding-block: 0; padding-inline: 1.125rem; font-size: 0.875rem; }
.lp-footer__check { display: flex; align-items: flex-start; gap: 0.5rem; margin-top: 0.75rem; font-size: 0.8125rem; line-height: 1.4; cursor: pointer; }
.lp-footer__check input { flex: none; width: 1rem; height: 1rem; margin: 0.1rem 0 0; accent-color: var(--c-accent); }
/* The honeypot: a field no person sees or reaches, which a form-filling bot does. */
.lp-footer__hp { position: absolute; inset-inline-start: -9999px; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
.lp-footer__fine { margin-top: 0.625rem; color: var(--c-ink-subtle); font-size: 0.75rem; }
.lp-footer__bottom {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin-top: 3rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--lp-hair);
  color: var(--c-ink-subtle);
  font-size: 0.8125rem;
}
/* Placeholders until the accounts exist: no links, nothing to press. */
.lp-footer__social { display: flex; gap: 1rem; color: var(--c-ink-subtle); }
.lp-footer__icon { display: inline-flex; width: 1rem; height: 1rem; }
.lp-footer__icon svg { width: 100%; height: 100%; }

/* ── Demo frame ──────────────────────────────────────────────────────────
   The scale-to-fit scaffolding every DOM screencast uses. Three elements, and which one
   carries the transform is not a matter of taste: iOS Safari fails to render a transformed
   element clipped by its own box, so the transform goes on the CHILD and a plain parent
   does the clipping.

   Never put `will-change: transform` on the stage - it rasterises at natural size and every
   glyph blurs the moment the scale goes above 1.

   The frame draws no box of its own, and a stage paints no ground: the phone, the browser
   window, whatever the demo shows, stand on the page itself (Chris, 2026-10-20, FleetFault:
   "remove that area around the two screens"). The frame still clips, so a stage keeps what it
   draws clear of its edges by more than its shadow reaches: --lp-demo-shadow, the one shadow
   for a demo's devices, fades out within about 40px at the sides and bottom (the page's
   --shadow-lift, a 60px blur, would end in a hard line there). */
.lp-demo {
  --lp-demo-shadow: 0 12px 32px rgb(0 0 0 / 0.24);
  width: 100%;
}
.lp-demo__fit {
  position: relative;
  width: 100%;
  overflow: hidden;
}
.lp-demo__stage {
  position: absolute;
  top: 0;
  left: 0;
  transform-origin: top left;
}
/* The fake cursor: an empty `<span class="lp-cursor" hidden></span>` in the stage, which demo.js
   fills with the kit's two shapes - the arrow, and a pointing finger wherever a finger is what
   would really touch the screen: over a demo's phone (demo.js sets .is-touch while the cursor's
   point is on a .lp-phone), and everywhere on a touch screen, since nobody watching on a phone
   has an arrow (Chris, 2026-10-20). Either one's point is where moveTo() puts it, and the click
   ring opens round that point. Inside a stage, so px. */
.lp-cursor {
  position: absolute;
  top: 0;
  left: 0;
  width: 24px;
  height: 24px;
  z-index: 90;
  pointer-events: none;
  filter: drop-shadow(0 3px 5px rgba(0, 0, 0, 0.42));
  transition: transform 0.45s cubic-bezier(0.5, 0.05, 0.2, 1);
}
.lp-cursor svg { position: absolute; inset: 0; width: 24px; height: 24px; overflow: visible; }
/* The theme's ink with a canvas edge, so it reads on either ground. */
.lp-cursor path { fill: var(--c-ink); stroke: var(--c-canvas); stroke-width: 1.4; stroke-linejoin: round; stroke-linecap: round; }
.lp-cursor .lp-cursor__knuckles { fill: none; }
.lp-cursor__hand { display: none; }
.lp-cursor.is-click::after {
  content: "";
  position: absolute;
  left: -8.5px;
  top: -8.5px;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  border: 2px solid var(--demo-accent, var(--c-accent));
  /* Held at its faded last frame: demo.js only restarts the class, so without it the ring
     came back round the cursor after every click (FleetFault, 2026-10-20). */
  animation: lp-ripple 0.55s ease-out forwards;
}
.lp-cursor.is-touch .lp-cursor__arrow { display: none; }
.lp-cursor.is-touch .lp-cursor__hand { display: block; }
/* The fingertip (8, 2 in its box) on the point, not the box's corner. */
.lp-cursor.is-touch { margin: -2px 0 0 -8px; }
.lp-cursor.is-touch.is-click::after { left: -2px; top: -8px; }
@media (pointer: coarse) {
  .lp-cursor .lp-cursor__arrow { display: none; }
  .lp-cursor .lp-cursor__hand { display: block; }
  .lp-cursor { margin: -2px 0 0 -8px; }
  .lp-cursor.is-click::after { left: -2px; top: -8px; }
}

/* ── Demo phone ──────────────────────────────────────────────────────────
   The phone a demo shows: `<div class="lp-phone"><div class="lp-phone__screen">...`. The kit
   draws it - a thin bezel (4px, Chris 2026-10-20: "lower the border of the phone by more than
   half"), the front camera over whatever the screen shows, the demo shadow; the product places
   and sizes it (position, left, top, width, height) and fills the screen. */
.lp-phone {
  position: absolute;
  padding: 4px;
  border-radius: 44px;
  background: color-mix(in srgb, var(--c-ink) 16%, var(--c-surface));
  box-shadow: inset 0 0 0 1px var(--lp-hair-strong, var(--c-line-strong)), var(--lp-demo-shadow);
}
.lp-phone__screen {
  position: relative;
  height: 100%;
  overflow: hidden;
  border-radius: 40px;
  background: var(--c-canvas);
}
.lp-phone__screen::before {
  content: "";
  position: absolute;
  z-index: 30;
  top: 10px;
  left: 50%;
  width: 84px;
  height: 22px;
  margin-left: -42px;
  border-radius: 12px;
  background: var(--c-scrim, #050505);
}
.lp-pressed { filter: brightness(0.92); }

/* ── Vignette recipes ────────────────────────────────────────────────────
   Abstract, never screenshots: a few spans, pure CSS, looping forever. Siblings are
   staggered with NEGATIVE delays so the whole row is already mid-motion on first paint -
   a vignette that starts from zero when you scroll to it looks like a page still loading. */
@keyframes lp-dot {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--c-accent) 40%, transparent); }
  50% { box-shadow: 0 0 0 0.375rem color-mix(in srgb, var(--c-accent) 0%, transparent); }
}
@keyframes lp-flow { 0% { background-position: 200% 50%; } 100% { background-position: 0% 50%; } }
@keyframes lp-breathe {
  0%, 100% { box-shadow: 0 0 1.5rem color-mix(in srgb, var(--c-accent) 35%, transparent); }
  50% { box-shadow: 0 0 2.625rem 0.25rem color-mix(in srgb, var(--c-accent) 55%, transparent); }
}
@keyframes lp-sheen {
  0%, 62% { transform: translateX(-130%); }
  78%, 100% { transform: translateX(130%); }
}
@keyframes lp-nudge {
  0%, 78%, 100% { transform: translateX(0); }
  84% { transform: translateX(0.25rem); }
  90% { transform: translateX(0); }
  95% { transform: translateX(2px); }
}
@keyframes lp-ripple {
  from { transform: scale(0.3); opacity: 1; }
  to { transform: scale(1.6); opacity: 0; }
}
@keyframes lp-grow { from { transform: scaleY(0.25); } to { transform: scaleY(1); } }
@keyframes lp-shimmer { 0% { transform: translateX(-130%); } 60%, 100% { transform: translateX(260%); } }
@keyframes lp-slide-in {
  0%, 12% { opacity: 0; transform: translateY(0.75rem); }
  22%, 78% { opacity: 1; transform: translateY(0); }
  90%, 100% { opacity: 0; transform: translateY(-0.25rem); }
}
@keyframes lp-walk {
  0%, 13% { background: var(--c-accent-soft); color: var(--c-accent); }
  19%, 100% { background: transparent; color: var(--c-ink-subtle); }
}
@keyframes lp-cycle {
  0%, 26% { opacity: 1; transform: translateY(0); }
  33%, 93% { opacity: 0; transform: translateY(-0.5rem); }
  100% { opacity: 1; transform: translateY(0); }
}
@keyframes lp-blip {
  0%, 100% { transform: scale(1); opacity: 1; }
  50% { transform: scale(1.7); opacity: 0.45; }
}
@keyframes lp-marquee { from { transform: translateX(0); } to { transform: translateX(-50%); } }
@keyframes lp-hop {
  0%, 80%, 100% { transform: translate(0, 0); }
  85% { transform: translate(0.375rem, -0.3125rem); }
}
@keyframes lp-type { from { transform: scaleX(0.86); } to { transform: scaleX(1); } }
@keyframes lp-confirm {
  0%, 38% { opacity: 0; transform: scale(0.6); }
  55%, 96% { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(0.6); }
}

/* The parts the vignettes are built from. */
.lp-viz-bar { border-radius: 3px; background: color-mix(in srgb, var(--c-ink) 22%, transparent); }
/* `width` is not optional. The bars are `flex: 1` with no basis, so inside a tile that
   centres its child on the main axis the row collapses to zero and the tile renders empty -
   which looks exactly like a vignette nobody wrote, not like a bug. */
.lp-viz-bars { display: flex; align-items: flex-end; gap: 0.5rem; height: 3.875rem; width: 100%; max-width: 260px; }
.lp-viz-bars > i {
  flex: 1;
  border-radius: 5px 5px 2px 2px;
  transform-origin: bottom;
  background: color-mix(in srgb, var(--c-accent) 45%, var(--c-surface-2));
  animation: lp-grow 2.4s ease infinite alternate;
}
.lp-viz-bars > i:nth-child(1) { height: 44%; animation-delay: -0.2s; }
.lp-viz-bars > i:nth-child(2) { height: 62%; animation-delay: -0.7s; }
.lp-viz-bars > i:nth-child(3) { height: 50%; animation-delay: -1.1s; }
.lp-viz-bars > i:nth-child(4) { height: 78%; animation-delay: -1.5s; }
.lp-viz-bars > i:nth-child(5) { height: 96%; background: var(--c-accent); animation-delay: -1.9s; }

.lp-viz-chips { display: flex; flex-wrap: wrap; gap: 0.375rem; justify-content: center; padding: 0 0.75rem; }
.lp-viz-chips > span {
  padding: 0.25rem 0.5625rem;
  border-radius: 999px;
  font-size: 0.7188rem;
  font-weight: 600;
  color: var(--c-ink-subtle);
  animation: lp-walk 6s linear infinite;
}
.lp-viz-chips > span:nth-child(2) { animation-delay: -1s; }
.lp-viz-chips > span:nth-child(3) { animation-delay: -2s; }
.lp-viz-chips > span:nth-child(4) { animation-delay: -3s; }
.lp-viz-chips > span:nth-child(5) { animation-delay: -4s; }
.lp-viz-chips > span:nth-child(6) { animation-delay: -5s; }

.lp-viz-cycle { position: relative; width: 100%; height: 100%; }
.lp-viz-cycle > span {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.625rem;
  font-weight: 700;
  color: var(--c-ink);
  animation: lp-cycle 6s ease infinite;
}
.lp-viz-cycle > span:nth-child(2) { animation-delay: -4s; }
.lp-viz-cycle > span:nth-child(3) { animation-delay: -2s; }

.lp-viz-rows { display: flex; flex-direction: column; gap: 0.3125rem; width: 100%; padding-inline: 0.875rem; }
.lp-viz-rows > span {
  display: flex;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.25rem 0.625rem;
  border-radius: 7px;
  font-size: 0.7188rem;
  font-weight: 600;
  white-space: nowrap;
  color: var(--c-ink-muted);
  background: var(--c-surface);
  border: 1px solid var(--lp-hair);
}
.lp-viz-rows > span.is-new { animation: lp-slide-in 4.5s ease infinite; }

.lp-viz-shimmer { position: relative; overflow: hidden; }
.lp-viz-shimmer::after {
  content: "";
  position: absolute;
  inset: 0;
  width: 40%;
  background: linear-gradient(100deg, transparent, color-mix(in srgb, var(--c-accent) 30%, transparent), transparent);
  animation: lp-shimmer 3.2s ease-in-out infinite;
}

.lp-viz-marquee { width: max-content; display: flex; gap: 0.625rem; animation: lp-marquee 32s linear infinite; }
.lp-viz-marquee--slow { animation-duration: 40s; animation-direction: reverse; }
.lp-viz:hover .lp-viz-marquee { animation-play-state: paused; }

.lp-blip { width: 8px; height: 8px; border-radius: 50%; background: var(--c-accent); flex: 0 0 auto; animation: lp-blip 2.4s ease infinite; }

/* ── Responsive ──────────────────────────────────────────────────────────
   One real breakpoint. Below it: every split stacks, sticky heads go static, and the three
   expensive effects - the ribbon, backdrop blur and header translucency - all switch off.
   Blur is not a taste call on a phone: it breaks fixed positioning during touch scroll and
   costs GPU the device does not have. */
@media (max-width: 991.98px) {
  .lp-wave { display: none; }
  .lp-band { padding-block: 3rem; }
  .lp-spotlight,
  .lp-spotlight--flip,
  .lp-grid,
  .lp-grid--flip { grid-template-columns: 1fr; gap: 2rem; }
  .lp-spotlight__demo,
  .lp-spotlight--flip .lp-spotlight__demo { max-width: 100%; margin-inline: 0; }
  .lp-spotlight--flip .lp-spotlight__copy { order: 0; }
  .lp-grid__head { position: static; }
  .lp-header__nav { display: none; }
  .lp-compare,
  .lp-compare--two,
  .lp-compare--four,
  .lp-reveal.is-open { grid-template-columns: 1fr; max-width: 460px; margin-inline: auto; }

  /* Kill every glass surface at once. */
  .lp-header.is-scrolled,
  .lp-card,
  .lp-reveal__item,
  .lp-faq details,
  .lp-btn--ghost,
  .lp-pill,
  .lp-footer:not(.lp-footer--kit),
  .lp-seg { -webkit-backdrop-filter: none; backdrop-filter: none; background: var(--c-surface); }
  .lp-card--down { background: var(--c-surface); }
  .lp-card--up { background: var(--c-surface-2); }

  /* When the bento grid stacks, a button inside the sticky head would land ABOVE the cards
     it is meant to follow. `display: contents` promotes the head's children to grid items,
     so the button can be ordered below the bento on its own. The head's gutters move onto
     the children, because a `display: contents` box has none. */
  .lp-grid__head { display: contents; }
  .lp-grid { row-gap: 0; }
  .lp-grid__head > * { margin-bottom: 0.875rem; }
  .lp-grid__head > .lp-pill { order: 1; margin-top: 1.375rem; margin-bottom: 0; align-self: start; }
  .lp-bento { margin-bottom: 0; }
}

/* A touch device of any width. Dropping the blur is not enough on its own: a 65%
   translucent surface with nothing blurring behind it is a window, and the page scrolls
   visibly through the header. Whatever turns the blur off has to make the surface opaque
   in the same breath. */
@media (hover: none) and (pointer: coarse) {
  .lp-card,
  .lp-reveal__item,
  .lp-faq details,
  .lp-btn--ghost,
  .lp-pill,
  .lp-seg,
  .lp-footer:not(.lp-footer--kit),
  .lp-header.is-scrolled {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    background: var(--c-surface);
  }
  .lp-card--down { background: var(--c-surface); }
  .lp-card--up { background: var(--c-surface-2); }
}

@media (max-width: 575.98px) {
  /* A phone needs the header's clearance and a breath, not a desktop's 4rem of air. */
  .lp-hero { padding-block: calc(var(--lp-header-h) + 2.25rem) 3rem; }
  .lp { --lp-gutter: 1.75rem; }
  .lp-bento { grid-template-columns: 1fr; }
  .lp-card--wide { grid-column: auto; }
  .lp-grid__head,
  .lp-spotlight__copy { text-align: center; }
  .lp-grid__head .lp-eyebrow,
  .lp-spotlight__copy .lp-eyebrow { align-self: center; }
  /* The list centres as a block; the lines inside it stay left-aligned, because centred
     multi-line body copy is much harder to read than people expect. */
  .lp-bullets { display: inline-flex; text-align: start; }
  .lp-demo { margin-inline: calc(var(--lp-gutter) * -1); width: calc(100% + var(--lp-gutter) * 2); }
}

/* ── Right to left ────────────────────────────────────────────────────────────────────────── */
/* Arrows point the way the page reads, and step that way on hover. */
/* `scale`, not `transform`: it composes with the arrows' own nudge, and turns the nudge the same
   way - an arrow that points left steps left. */
[dir='rtl'] :is(.lp-product__arrow, .lp-btn__arrow) { display: inline-block; scale: -1 1; }
/* The scripted demos are a picture of a screen, with a cursor at fixed points: drawn as they are. */
[dir='rtl'] .lp-demo__stage { direction: ltr; }


/* ── "Made with mixtape.works" ────────────────────────────────────────────
   The one line every page a FREE plan shows the public carries (a shop, a board, a guest page, a report form),
   gone on any paid plan (2026-10-13). Quiet on purpose: it is a thank-you, not an ad. */
.mx-made-with {
  display: block;
  margin: 1.5rem auto 0.75rem;
  text-align: center;
  font-size: 0.75rem;
  color: var(--c-ink-subtle);
}
.mx-made-with a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
.mx-made-with a:hover { color: var(--c-ink); }

/* ── The pricing band drawn by server/mixtape-pricing.mjs (2026-10-13) ───────────────────────────────────────
   Free plus paid plans by size, yearly first. Every card is a column, so the buttons line up at the foot. */
.mx-plan__seg { display: flex; justify-content: center; margin-bottom: 1.5rem; }
.mx-plan { display: flex; flex-direction: column; min-width: 0; }
.mx-plan__size { margin: 0.25rem 0 0; color: var(--c-ink-muted); font-size: 0.875rem; }
.mx-plan__price { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 6px; margin: 0.625rem 0 0; color: var(--c-ink); line-height: 1.1; font-variant-numeric: tabular-nums; }
.mx-plan__price strong { font-size: 2.125rem; font-weight: 800; letter-spacing: -0.03em; }
.mx-plan__price .lp-was { margin-inline-end: 0; font-size: 1.25rem; font-weight: 600; }
.mx-plan__per { font-size: 0.9375rem; font-weight: 500; color: var(--c-ink-muted); }
/* Close under the price it explains (Chris, 2026-10-21: the gap was too big, then "even closer, like 5px up"). Free has no billing to explain, but
   keeps the line's height (--none), so every card's list starts at the same height. */
.mx-plan__billed { display: flex; flex-wrap: wrap; align-items: center; gap: 0.375rem; margin: calc(0.125rem - 5px) 0 0; color: var(--c-ink-muted); font-size: 0.875rem; }
.mx-plan__billed--none { visibility: hidden; }
.mx-plan .lp-plan__list { display: grid; gap: 0.5rem; margin-bottom: 1.25rem; }
.mx-plan .lp-plan__list li { position: relative; padding-inline-start: 1.375rem; color: var(--c-ink); font-size: 0.9375rem; }
.mx-plan .lp-plan__list li::before { content: '✓'; position: absolute; inset-inline-start: 0; color: var(--c-accent); font-weight: 700; }
.mx-plan .lp-card__cta { margin-top: auto; }
.mx-plan .lp-card__cta:disabled { opacity: 0.55; cursor: not-allowed; animation: none; box-shadow: none; }
.mx-plan__fine { max-width: 46rem; margin-inline: auto; text-align: center; }
/* The "Launch deal" badge has its own row above the title on every card (empty on Free, hidden while Monthly is
   chosen), so the titles and prices line up across the cards whichever is shown. */
/* The row keeps its height on the Free card too (no badge), so the titles line up; the badge is
   centred in it at its own height, never stretched to fill it (a stretched pill put its word at the top). */
.mx-plan__flag { display: flex; align-items: center; min-height: 1.5rem; margin-bottom: 0.5rem; }
.mx-plan__flag .lp-card__badge { position: static; }

/* app.css — the dashboard layer: the frame and the furniture every signed-in mixtape page
   wears, as plain CSS classes.
 *
 * Why this file exists. The React products (Studio, mixtape.works) had these as components
 * in src/components — AppShell, Card, Panel, Button, Field, Badge — written in Tailwind
 * utilities. A product that is not React could not use a single one of them, so Dayrule and
 * qr-voks each grew their own, which is the one thing we said we would never do. These
 * classes are the definition; the React components render them, so there is one design and
 * two ways to reach it.
 *
 * Load after tokens.css. Every colour, radius and font here is a token: restyle a product by
 * setting --c-accent and friends, never by forking this file. landing.css is the other half,
 * for the pages a stranger sees; this one is for the pages a signed-in person works in.
 *
 * The frame, as the AppShell built it and this keeps it:
 *   - a sticky top bar, 3.5rem, blurred, with the brand, the nav and the gear
 *   - on a phone the nav moves to a fixed bottom tab bar, where a thumb is
 *   - one content column, 1120px, with its own padding
 * The furniture: .mx-card, .mx-panel, .mx-btn, .mx-field, .mx-table, .mx-badge, .mx-stat.
 */

/* Labels - the mono caps on a panel header, a field, a column, a stat.
 *
 * They were --c-ink-subtle at 10.5-0.6875rem, which is a whisper: Chris asked for a point bigger and
 * ten per cent more visible. One mix, not two colours: a tenth of the way from subtle towards
 * the page's own ink brightens it on a dark theme and darkens it on a light one, by
 * construction, and every product that loads this file moves with it. Since 2026-11-02 --c-ink-subtle reads at
 * 4.5:1 or better on every surface (tokens.css), so the label does too: 5.3-5.9:1 on paper, 5.3-6.4:1 in the dark. */
:root {
  --mx-label: color-mix(in srgb, var(--c-ink-subtle) 90%, var(--c-ink));
  --mx-label-size: 0.75rem;
  --mx-label-size-sm: 0.7188rem;
}

/* ── The frame ───────────────────────────────────────────────────────────*/

.mx-app {
  display: flex;
  min-height: 100vh;
  min-height: 100dvh;
  flex-direction: column;
  background: var(--c-canvas);
}

.mx-bar {
  position: sticky;
  top: 0;
  z-index: 40;
  border-bottom: 1px solid var(--c-line);
  background: color-mix(in srgb, var(--c-canvas) 85%, transparent);
  -webkit-backdrop-filter: blur(0.75rem);
  backdrop-filter: blur(0.75rem);
  padding-top: env(safe-area-inset-top);
}
.mx-bar__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  height: 3.5rem;
  width: 100%;
  max-width: var(--lp-max, var(--page-max, 90rem));
  margin-inline: auto;
  padding-inline: max(var(--lp-gutter, 1.5rem), env(safe-area-inset-left)) max(var(--lp-gutter, 1.5rem), env(safe-area-inset-right));
}
.mx-bar__left {
  display: flex;
  min-width: 0;
  align-items: center;
  gap: 1rem;
}
.mx-bar__right {
  display: flex;
  flex-shrink: 0;
  align-items: center;
  gap: 0.5rem;
}
/* The cog pulls itself flush with the bar's edge - unless the profile follows it, which then
   takes the edge (--mx-me-edge) and leaves the pair half a rem apart, as on mixtape.works.
   Global, not per bar: Studio's React header pairs them the same way. */
mixtape-settings:has(+ mixtape-profile) { --mx-cog-edge: 0; }
/* mixtape.works' own lock-up (components/Logo.tsx, and the kit landing footer for mixtape.works): the cassette in the
   accent and the wordmark with a quieter ".works". In rem, so it follows the interface size (Chris, 2026-10-26). */
.mx-logo { display: inline-flex; align-items: center; gap: 0.625rem; }
.mx-logo__mark { width: 1.75rem; height: 1.75rem; flex: none; color: var(--c-accent); }
.mx-logo__word { color: var(--c-ink); font-size: 0.9375rem; font-weight: 600; letter-spacing: -0.015em; line-height: 1.2; }
.mx-logo__tld { color: var(--c-ink-muted); }
.mx-logo__mark > svg { display: block; width: 100%; height: 100%; }
/* The brand lock-up: mark, wordmark, and the product badge next to it. */
.mx-brand {
  display: inline-flex;
  flex-shrink: 0;
  align-items: center;
  gap: 0.625rem;
  color: var(--c-ink);
  font-weight: 600;
  text-decoration: none;
}
/* The mark is the bare drawing in the product's accent - the way mixtape's own cassette mark
   sits in its orange. The rounded tile behind it belongs to the favicon and the home-screen
   icon only, where the OS needs a shape to put it on (Chris, 2026-09-21). A product's mark is
   an SVG drawn in currentColor. */
.mx-brand svg { width: 1.375rem; height: 1.375rem; flex: none; color: var(--c-accent); }
/* Who is signed in, next to the gear. A name, not an address: an address is a credential and
   reads as clutter. Hidden where the bar is tight - the gear menu still says. */
.mx-who {
  display: none;
  max-width: 18ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.8125rem;
  color: var(--c-ink-muted);
}
@media (min-width: 600px) { .mx-who { display: inline-block; } }

.mx-badge-app {
  border: 1px solid var(--c-line-strong);
  border-radius: 999px;
  padding: 2px 0.5rem;
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: 0.6875rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--mx-label);
}

/* ── Navigation: one vocabulary, two bars ────────────────────────────────
   The same destinations with the same icons, whether they are clicked in the top bar or
   thumbed in the bottom one. Desktop shows the top nav; a phone shows the tab bar. */

.mx-nav { display: none; align-items: center; gap: 2px; }
.mx-nav a {
  position: relative;
  display: flex;
  align-items: center;
  gap: 0.375rem;
  border-radius: var(--radius-sm);
  padding: 0.375rem 0.5rem;
  font-size: 0.875rem;
  color: var(--c-ink-muted);
  text-decoration: none;
  transition: color 0.15s ease;
}
.mx-nav a:hover { color: var(--c-ink); }
.mx-nav a svg { width: 1rem; height: 1rem; flex-shrink: 0; opacity: 0.7; }
.mx-nav a[aria-current='page'] { color: var(--c-ink); }
.mx-nav a[aria-current='page'] svg { color: var(--c-accent); opacity: 1; }
/* The 2px rule under the current page sits on the bar's own border. */
.mx-nav a[aria-current='page']::after {
  content: '';
  position: absolute;
  inset-inline: 0.5rem;
  bottom: -13px;
  height: 2px;
  border-radius: 999px;
  background: var(--c-accent);
}

.mx-tabs {
  position: fixed;
  inset-inline: 0;
  bottom: 0;
  z-index: 40;
  border-top: 1px solid var(--c-line);
  background: color-mix(in srgb, var(--c-canvas) 90%, transparent);
  -webkit-backdrop-filter: blur(0.75rem);
  backdrop-filter: blur(0.75rem);
  /* Clear of the home indicator, not a whole band of it - Studio's own bar (safe-b-tight): the
     full inset left an empty strip under the labels. 58px of tabs + 24 = Apple's own 83pt bar. */
  padding-bottom: max(calc(env(safe-area-inset-bottom) - 10px), 4px);
}
.mx-tabs ul {
  display: flex;
  align-items: stretch;
  justify-content: space-around;
  max-width: 35rem;
  /* margin 0: a product without a reset kept the browser's 1em above and below the list, and
     the bar stood 33px taller than its tabs (RentKeeper on Chris's iPhone, 2026-10-05). */
  margin: 0 auto;
  padding: 0 0.25rem;
  list-style: none;
}
.mx-tabs li { flex: 1; }
.mx-tabs a {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.25rem;
  border-radius: var(--radius-sm);
  padding: 0.5rem 0.25rem;
  font-size: 0.625rem;
  font-weight: 500;
  color: var(--c-ink-subtle);
  text-decoration: none;
}
.mx-tabs a svg { width: 1.375rem; height: 1.375rem; transition: transform 0.15s ease; }
.mx-tabs a[aria-current='page'] { color: var(--c-accent); }
.mx-tabs a[aria-current='page'] svg { transform: scale(1.1); }

.mx-main {
  min-width: 0;
  flex: 1;
  /* Clear of the tab bar on a phone, as tall as it really is (kit/frame.ts --mx-tabs-h: a wrapped
     label makes it taller); the bar is gone from `md` up. */
  padding-bottom: calc(var(--mx-tabs-h, calc(3.75rem + env(safe-area-inset-bottom))) + 0.75rem + var(--mx-fab-room, 0px));
}
@media (min-width: 768px) {
  .mx-nav { display: flex; }
  .mx-tabs { display: none; }
  .mx-main { padding-bottom: calc(2rem + var(--mx-fab-room, 0px)); }
}
/* The tab that stands for several pages (the sidebar's More, below) is lit like a page when the page
   open is one of its own: aria-current="true", "the current one of a set", not "this page". */
.mx-tabs a[aria-current='true'] { color: var(--c-accent); }
.mx-tabs a[aria-current='true'] svg { transform: scale(1.1); }

/* ── The phone (2026-11-03, Chris on Storefront's Analytics, iPhone: "buggy/unfinished") ─────────────────────────
   On a touch screen both bars are SOLID. backdrop-filter on a position: fixed / sticky box is what iOS Safari moves
   late during a momentum scroll - the tab bar was drawn above the bottom edge with the page's card showing under it -
   and the 85-90% see-through canvas let a line of the page ("stopped.") read through the top bar. A blur is for a
   mouse, where nothing is flung. */
@media (hover: none), (pointer: coarse) {
  .mx-bar, .mx-tabs { -webkit-backdrop-filter: none; backdrop-filter: none; background: var(--c-canvas); }
}
/* Below the tab bar, more of the bar: when iOS draws it above the true bottom for a moment (its toolbar sliding,
   the rubber band at the end of the page), the gap shows the bar's own colour, never the page carrying on. A fixed
   box's overflow adds nothing to the page's scroll height. */
.mx-tabs::after { content: ''; position: absolute; inset-inline: 0; top: 100%; height: 50vh; background: inherit; pointer-events: none; }
/* Native controls (checkboxes, a date picker's selected day, a range, a progress bar) in the product's colour, not
   the platform's blue. */
.mx-app { accent-color: var(--c-accent); }
/* The phone bar: one row of things the same size - the switcher's mark the avatar's 1.75rem, every icon a 2.25rem
   round target with a 44px reach on a touch screen, no box drawn round any of them. A ghost button in the bar
   loses its border here (a 44px outlined box stood taller than the bar's other things), and a button whose words
   are in .mx-btn__text becomes the icon button below 640px - the words stay in its aria-label and title. */
@media (max-width: 767.98px) {
  .mx-bar__right { gap: 0.25rem; }
  .mx-bar .mx-switch__mark { width: 1.75rem; height: 1.75rem; border-radius: 0.4375rem; }
  .mx-bar .mx-btn--ghost { border-color: transparent; }
  .mx-bar .mx-btn:is(.mx-btn--ghost, .mx-btn--quiet):has(> svg) { padding-inline: 0.5rem; color: var(--c-ink-muted); }
  .mx-bar .mx-btn:has(> svg) > svg { width: 1.25rem; height: 1.25rem; }
}
@media (max-width: 639.98px) {
  .mx-bar .mx-btn__text { display: none; }
  .mx-bar .mx-btn:has(> .mx-btn__text) {
    width: 2.25rem; height: 2.25rem; min-width: 44px; padding: 0; border-color: transparent; border-radius: 9999px;
    background: transparent; color: var(--c-ink-muted);
  }
}
/* The support chat's round button is on this page (elements/support.ts stamps it): the last row of the page keeps
   clear of it as well as of the tab bar. */
:root[data-mx-support-fab] { --mx-fab-room: 4.25rem; }
@media (max-width: 720px) { :root[data-mx-support-fab] { --mx-fab-room: 3.5rem; } }

/* ── Sidebar navigation: .mx-side (2026-11-02) ──────────────────────────────────────────────────
   For a product with a big admin (Storefront Ninja first, Shopiza's admin the model): every
   destination down the left, grouped, instead of four in the top bar. OPT-IN BY MARKUP - a
   <nav class="mx-side"> that is a direct child of .mx-app - so a product without one keeps its
   top-bar layout to the pixel. One list, three widths:
     - from 1200px: the sidebar, icon + label, a group's pages indented under it;
     - 768-1199px: an icon rail with the labels as tooltips. The toggle in the sidebar's foot
       switches between the rail and the full sidebar at any width from 768px, and the device
       remembers which (kit/side.ts, localStorage mixtape-side). boot.js stamps the choice (or the
       width's default) on <html data-mx-side> before paint, so nothing moves on load; with no
       script at all there is no attribute and the full sidebar shows, its groups plain disclosures;
     - a head (the shop or workspace the admin is of, with its menu) and a foot (the product's mark,
       name and plan, beside the toggle), both optional: with a head the sidebar runs the window's
       full height and the bar sits beside it ("The head and the foot" below);
     - under 768px: no sidebar. The kit's tab bar carries four of the items and a More tab
       (data-mx-side-more) that opens the whole list in the kit's overlay.
   Markup, words and icons: server/mixtape-sidenav.mjs. Behaviour: kit/side.ts. DESIGN.md
   "Sidebar navigation". */
:root {
  --mx-side-w: 15rem;
  --mx-side-rail: 4.25rem;
  /* A phone on its side is 844px wide - rail width - with the notch on one edge. */
  --mx-side-edge: env(safe-area-inset-left, 0px);
}
:root[dir='rtl'] { --mx-side-edge: env(safe-area-inset-right, 0px); }
.mx-side { display: none; }
/* Lists and disclosures with their own margins: products have no reset (see .mx-tabs ul). */
.mx-side__list,
.mx-side__sub { margin: 0; padding: 0; list-style: none; }
.mx-side__list { display: flex; flex-direction: column; gap: 2px; }
.mx-side__list li,
.mx-side__group { margin: 0; }
.mx-side__item {
  position: relative;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  width: 100%;
  min-height: 2.5rem;
  /* 15px in: a rail's 48px item centres its 18px icon there, so the icons do not move when the
     rail widens into the sidebar. */
  padding: 0.4375rem 0.75rem;
  padding-inline-start: 0.9375rem;
  border-radius: var(--radius-sm);
  color: var(--c-ink-muted);
  font-size: 0.875rem;
  font-weight: 500;
  line-height: 1.3;
  text-decoration: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background-color 0.15s ease, color 0.15s ease;
}
summary.mx-side__item { list-style: none; }
summary.mx-side__item::-webkit-details-marker { display: none; }
.mx-side__item > svg { width: 1.125rem; height: 1.125rem; flex: none; }
.mx-side__label { flex: 1 1 auto; min-width: 0; overflow-wrap: break-word; }
@media (hover: hover) {
  .mx-side__item:hover { background: var(--c-surface-2); color: var(--c-ink); }
}
.mx-side__item:focus-visible { outline: 2px solid var(--c-accent); outline-offset: -2px; }
/* A group's chevron: the kit's dropdown drawing as a mask, so it takes the row's colour. */
summary.mx-side__item::after {
  content: '';
  flex: none;
  width: 0.75rem;
  height: 0.75rem;
  background: currentColor;
  -webkit-mask: var(--select-chevron) center / contain no-repeat;
  mask: var(--select-chevron) center / contain no-repeat;
  opacity: 0.7;
  transition: transform 0.2s var(--ease-out-soft, ease);
}
.mx-side__group[open] > summary.mx-side__item::after { transform: rotate(180deg); }
/* The current page, in the product's accent; the group holding it says so too. */
.mx-side__item[aria-current='page'] { background: var(--c-accent-soft); color: var(--c-ink); font-weight: 600; }
.mx-side__item[aria-current='page'] > svg,
.mx-side__group:has([aria-current='page']) > summary > svg { color: var(--c-accent); }
.mx-side__group:has([aria-current='page']) > summary { color: var(--c-ink); }
.mx-side__item[aria-current='page']::before {
  content: '';
  position: absolute;
  inset-inline-start: 0;
  top: 0.5rem;
  bottom: 0.5rem;
  width: 3px;
  border-radius: 999px;
  background: var(--c-accent);
}
/* A group's pages start under its label, on a hairline down from its icon. */
.mx-side__sub { position: relative; display: flex; flex-direction: column; gap: 2px; margin: 2px 0 0.375rem; padding-inline-start: 2.0625rem; }
.mx-side__sub::before {
  content: '';
  position: absolute;
  inset-inline-start: 1.5rem;
  top: 0.25rem;
  bottom: 0.25rem;
  width: 1px;
  background: var(--c-line);
}
.mx-side__sub .mx-side__item { min-height: 2.25rem; padding-block: 0.375rem; padding-inline-start: 0.75rem; gap: 0.625rem; font-size: 0.8125rem; font-weight: 400; }
.mx-side__sub .mx-side__item > svg { width: 1rem; height: 1rem; }
.mx-side__sub .mx-side__item[aria-current='page'] { font-weight: 600; }
.mx-side__toggle { display: none; }

@media (min-width: 768px) {
  .mx-app:has(> .mx-side) { --mx-side-now: var(--mx-side-w); }
  .mx-side {
    position: fixed;
    z-index: 30;
    inset-inline-start: 0;
    /* Under the bar: its 3.5rem row, its hairline and the notch above it. */
    top: calc(3.5rem + 1px + env(safe-area-inset-top, 0px));
    bottom: 0;
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    width: calc(var(--mx-side-now, var(--mx-side-w)) + var(--mx-side-edge));
    margin: 0;
    padding-inline-start: var(--mx-side-edge);
    border-inline-end: 1px solid var(--c-line);
    background: var(--c-canvas);
    transition: width 0.2s var(--ease-out-soft, ease), box-shadow 0.2s ease;
  }
  .mx-side > .mx-side__list {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: 0.75rem 0.625rem;
    scrollbar-width: thin;
  }
  .mx-app:has(> .mx-side) > :is(.mx-main, .mx-footer) {
    margin-inline-start: calc(var(--mx-side-now) + var(--mx-side-edge));
    transition: margin 0.2s var(--ease-out-soft, ease);
  }
  /* The bar spans the window, so the brand sits over the sidebar and the menus at the far edge. */
  .mx-app:has(> .mx-side) .mx-bar__inner { max-width: none; }
}

/* The rail: the toggle in the foot chooses it at any width from 768px (boot.js stamps the choice, or the width's
   default - the rail up to 1199px, the full sidebar from 1200px - before paint). */
@media (min-width: 768px) {
  :root[data-mx-side='rail'] .mx-app:has(> .mx-side) { --mx-side-now: var(--mx-side-rail); }
  /* The toggle: only once a script can work it. 15px in, so its icon sits in the rail's column of icons. */
  :root[data-mx-side] .mx-side__toggle {
    display: flex;
    flex: none;
    align-items: center;
    box-sizing: border-box;
    width: 3rem;
    height: 2.75rem;
    margin: 0;
    padding: 0;
    padding-inline-start: 0.9375rem;
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--c-ink-muted);
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
  }
  :root[data-mx-side] .mx-side__toggle svg { width: 1.125rem; height: 1.125rem; flex: none; }
  :root[data-mx-side] .mx-side__toggle:focus-visible { outline: 2px solid var(--c-accent); outline-offset: -2px; }
  :root[data-mx-side='rail'] .mx-side__toggle svg + svg,
  :root[data-mx-side='open'] .mx-side__toggle svg:first-child { display: none; }
  /* The rail: the same items, their labels lifted out as tooltips. Nothing clips them, so the list
     does not scroll - a rail holds eight icons in 25rem. */
  :root[data-mx-side='rail'] .mx-side:not([data-peek]) > .mx-side__list { overflow: visible; }
  :root[data-mx-side='rail'] .mx-side:not([data-peek]) .mx-side__label {
    position: absolute;
    z-index: 1;
    inset-inline-start: calc(100% + 0.5rem);
    top: 50%;
    translate: 0 -50%;
    width: max-content;
    max-width: 14rem;
    padding: 0.3125rem 0.5rem;
    border-radius: var(--radius-sm);
    background: var(--c-ink);
    color: var(--c-canvas);
    font-size: 0.75rem;
    font-weight: 500;
    box-shadow: var(--shadow-lift);
    /* Faded, never hidden: the label is still the link's name to a screen reader. */
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.12s ease;
  }
  :root[data-mx-side='rail'] .mx-side:not([data-peek]) .mx-side__item:focus-visible > .mx-side__label { opacity: 1; }
  :root[data-mx-side='rail'] .mx-side:not([data-peek]) summary.mx-side__item::after,
  :root[data-mx-side='rail'] .mx-side:not([data-peek]) .mx-side__sub { display: none; }
  /* Its pages folded away, a group lights up for the page inside it. */
  :root[data-mx-side='rail'] .mx-side:not([data-peek]) .mx-side__group:has([aria-current='page']) > summary { background: var(--c-accent-soft); }
  /* A group pressed in the rail opens the whole sidebar over the page until a page is picked. */
  .mx-side[data-peek] { z-index: 39; width: calc(var(--mx-side-w) + var(--mx-side-edge)); box-shadow: var(--shadow-lift); }
}
@media (min-width: 768px) and (hover: hover) {
  :root[data-mx-side='rail'] .mx-side:not([data-peek]) .mx-side__item:hover > .mx-side__label { opacity: 1; }
  :root[data-mx-side] .mx-side__toggle:hover { background: var(--c-surface-2); color: var(--c-ink); }
}

/* ── The head and the foot (2026-11-02, Shopiza's sidebar the model) ─────────────────────────────
   The head says what this admin is OF - a shop, a workspace: its mark (a picture, or its initial on its colour,
   a rounded square), its name, one line under it, and a menu of what can be done with it (.mx-switch, a native
   <details>). The foot is the product's own mark and name with one line under it - the plan - beside the toggle.
   With a head the sidebar runs the window's full height, the head's row level with the bar beside it, so the
   product's name is said once, at the bottom. On a phone there is no sidebar: the product draws the same head in
   the bar (sideSwitch() in server/mixtape-sidenav.mjs), and More's menu ends with the foot's brand. */
.mx-switch { position: relative; min-width: 0; }
.mx-switch__btn {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  gap: 0.625rem;
  width: 100%;
  min-height: 2.75rem;
  padding: 0.3125rem 0.5rem;
  border-radius: var(--radius-sm);
  color: var(--c-ink);
  text-decoration: none;
  list-style: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background-color 0.15s ease;
}
span.mx-switch__btn { cursor: default; }
summary.mx-switch__btn::-webkit-details-marker { display: none; }
/* Up and down chevrons: the head opens a menu, it does not fold a section. */
summary.mx-switch__btn::after {
  content: '';
  flex: none;
  width: 1rem;
  height: 1rem;
  margin-inline-start: auto;
  background: currentColor;
  opacity: 0.55;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m7 15 5 5 5-5'/%3E%3Cpath d='m7 9 5-5 5 5'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m7 15 5 5 5-5'/%3E%3Cpath d='m7 9 5-5 5 5'/%3E%3C/svg%3E") center / contain no-repeat;
}
@media (hover: hover) {
  :is(summary, a).mx-switch__btn:hover { background: var(--c-surface-2); }
}
.mx-switch[open] > .mx-switch__btn { background: var(--c-surface-2); }
.mx-switch__btn:focus-visible { outline: 2px solid var(--c-accent); outline-offset: -2px; }
/* The mark: 2rem, a rounded square; a picture is fitted whole on the surface, an initial sits on its colour. */
.mx-switch__mark,
.mx-switch__rowmark { flex: none; box-sizing: border-box; display: block; width: 2rem; height: 2rem; border-radius: 0.5rem; }
.mx-switch__rowmark { width: 1.5rem; height: 1.5rem; border-radius: 0.375rem; }
img:is(.mx-switch__mark, .mx-switch__rowmark) { object-fit: contain; padding: 2px; background: var(--c-surface); box-shadow: inset 0 0 0 1px var(--c-line); }
:is(.mx-switch__mark, .mx-switch__rowmark) text { font-family: inherit; font-size: 15px; font-weight: 650; }
.mx-mark__ground { fill: var(--c-accent); }
.mx-mark__ink { fill: var(--c-accent-ink); }
.mx-switch__text { display: grid; flex: 1 1 auto; min-width: 0; line-height: 1.25; text-align: start; }
.mx-switch__name,
.mx-switch__detail { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mx-switch__name { font-size: 0.875rem; font-weight: 600; color: var(--c-ink); }
.mx-switch__detail { font-size: 0.75rem; color: var(--c-ink-subtle); }
/* The menu: under the head, at least as wide as it. */
.mx-switch__menu {
  position: absolute;
  z-index: 60;
  top: calc(100% + 0.25rem);
  inset-inline-start: 0;
  box-sizing: border-box;
  display: grid;
  gap: 1px;
  min-width: max(100%, 15rem);
  max-width: min(22rem, calc(100vw - 1.5rem));
  margin: 0;
  padding: 0.375rem;
  border: 1px solid var(--c-line);
  border-radius: var(--radius-md);
  background: var(--c-surface);
  box-shadow: var(--shadow-lift);
}
.mx-switch__heading {
  margin: 0.25rem 0.5rem 0.25rem;
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--mx-label-size-sm);
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--mx-label);
}
.mx-switch__sep { height: 0; margin: 0.25rem -0.375rem; border: 0; border-top: 1px solid var(--c-line); }
.mx-switch__form { display: block; margin: 0; }
.mx-switch__item {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  gap: 0.625rem;
  width: 100%;
  min-height: 2.5rem;
  margin: 0;
  padding: 0.375rem 0.5rem;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--c-ink);
  font: inherit;
  font-size: 0.875rem;
  text-align: start;
  text-decoration: none;
  cursor: pointer;
}
.mx-switch__item .mx-switch__name { font-weight: 500; }
.mx-switch__item > svg:not(.mx-switch__rowmark) { flex: none; width: 1rem; height: 1rem; color: var(--c-ink-muted); }
/* The row being worked in: a tick at its end in the accent, nothing to press. */
.mx-switch__item[aria-current] { cursor: default; }
.mx-switch__item[aria-current] > svg:last-child { color: var(--c-accent); }
@media (hover: hover) {
  :is(a, button).mx-switch__item:hover { background: var(--c-surface-2); }
}
.mx-switch__item:focus-visible { outline: 2px solid var(--c-accent); outline-offset: -2px; }

/* The foot: the product's mark and name, its plan under it, the toggle at the end. */
.mx-side__foot { display: none; }
.mx-side__brand {
  box-sizing: border-box;
  display: flex;
  flex: 1 1 auto;
  align-items: center;
  gap: 0.625rem;
  min-width: 0;
  min-height: 2.75rem;
  padding: 0.3125rem 0.5rem;
  border-radius: var(--radius-sm);
  color: var(--c-ink);
  text-decoration: none;
}
.mx-side__brandmark { display: grid; place-items: center; flex: none; width: 2rem; height: 2rem; color: var(--c-accent); }
.mx-side__brandmark > svg { width: 1.5rem; height: 1.5rem; }
@media (hover: hover) {
  a.mx-side__brand:hover { background: var(--c-surface-2); }
}
a.mx-side__brand:focus-visible { outline: 2px solid var(--c-accent); outline-offset: -2px; }
/* In the phone's menu (More), the foot closes the list under a hairline. */
.mx-side-menu .mx-side__foot { display: flex; margin-top: 0.5rem; padding-top: 0.5rem; border-top: 1px solid var(--c-line); }

/* The phone's bar holds the head instead of a brand. The name alone, one line; the mark 5px in, like the brand. */
.mx-bar .mx-switch { flex: 0 1 auto; }
.mx-bar .mx-switch__btn { padding-inline: 0.3125rem 0.375rem; }
.mx-bar .mx-switch__detail { display: none; }
.mx-bar .mx-switch__menu {
  position: fixed;
  top: calc(3.5rem + env(safe-area-inset-top, 0px));
  inset-inline-start: max(var(--lp-gutter, 0.75rem), env(safe-area-inset-left));
  /* The bar (backdrop-filter) is a fixed child's containing block: 100% would be the whole bar. */
  min-width: min(16rem, calc(100vw - 2 * var(--lp-gutter, 0.75rem)));
  max-width: calc(100vw - 2 * var(--lp-gutter, 0.75rem));
}

@media (min-width: 768px) {
  .mx-side__head {
    flex: none;
    box-sizing: border-box;
    display: flex;
    align-items: center;
    height: calc(3.5rem + 1px);
    padding: 0 0.625rem;
    border-bottom: 1px solid var(--c-line);
  }
  .mx-side__head > .mx-switch { flex: 1 1 auto; }
  .mx-side__foot {
    flex: none;
    display: flex;
    align-items: center;
    gap: 0.25rem;
    padding: 0.5rem 0.625rem calc(0.5rem + env(safe-area-inset-bottom, 0px));
  }
  .mx-side__brand ~ .mx-side__toggle,
  :root[data-mx-side] .mx-side__brand ~ .mx-side__toggle { width: 2.5rem; padding-inline-start: 0; justify-content: center; }
  .mx-side__foot:has(.mx-side__brand) { border-top: 1px solid var(--c-line); }
  /* With a head: the window's full height, and the bar beside it rather than over it. */
  .mx-app:has(> .mx-side > .mx-side__head) > .mx-side { top: 0; padding-top: env(safe-area-inset-top, 0px); }
  .mx-app:has(> .mx-side > .mx-side__head) > .mx-bar {
    margin-inline-start: calc(var(--mx-side-now) + var(--mx-side-edge));
    transition: margin 0.2s var(--ease-out-soft, ease);
  }
  /* The sidebar says whose admin this is (head) and whose product (foot): the bar repeats neither. */
  .mx-app:has(> .mx-side > .mx-side__head) > .mx-bar .mx-brand,
  .mx-app:has(> .mx-side) > .mx-bar .mx-switch { display: none; }
  /* An open menu rides above the bar, which it can reach from the rail. */
  .mx-side:has(.mx-switch[open]) { z-index: 45; }
  /* The rail: the marks alone, centred in it; the head's menu opens beside the rail, the foot stacks. */
  :root[data-mx-side='rail'] .mx-side:not([data-peek]) .mx-side__head { position: relative; }
  :root[data-mx-side='rail'] .mx-side:not([data-peek]) .mx-side__head > .mx-switch { position: static; flex: none; }
  :root[data-mx-side='rail'] .mx-side:not([data-peek]) :is(.mx-switch__btn, .mx-side__brand) { width: 3rem; justify-content: center; padding-inline: 0; }
  :root[data-mx-side='rail'] .mx-side:not([data-peek]) :is(.mx-switch__btn, .mx-side__brand) > .mx-switch__text,
  :root[data-mx-side='rail'] .mx-side:not([data-peek]) summary.mx-switch__btn::after { display: none; }
  :root[data-mx-side='rail'] .mx-side:not([data-peek]) .mx-side__head .mx-switch__menu { top: 0.375rem; inset-inline-start: calc(100% + 0.375rem); min-width: 15rem; }
  :root[data-mx-side='rail'] .mx-side:not([data-peek]) .mx-side__foot { flex-direction: column; align-items: flex-start; gap: 2px; }
  :root[data-mx-side='rail'] .mx-side:not([data-peek]) .mx-side__brand ~ .mx-side__toggle { width: 3rem; padding-inline-start: 0.9375rem; justify-content: flex-start; }
}
@media (prefers-reduced-motion: reduce) {
  .mx-app:has(> .mx-side > .mx-side__head) > .mx-bar { transition: none; }
}

/* A phone with no script: More is a link to the list, which then fills the screen. */
@media (max-width: 767.98px) {
  .mx-side:not([data-mx-ready]):target {
    position: fixed;
    inset: 0;
    z-index: 50;
    display: block;
    margin: 0;
    padding: calc(env(safe-area-inset-top, 0px) + 1rem) 1rem calc(env(safe-area-inset-bottom, 0px) + 1rem);
    overflow-y: auto;
    background: var(--c-canvas);
  }
}
/* The whole list in the overlay, opened from More (kit/side.ts). */
.mx-side-menu .mx-side__item { min-height: 2.75rem; font-size: 0.9375rem; }
.mx-side-menu .mx-side__sub .mx-side__item { min-height: 2.75rem; font-size: 0.875rem; }
@media (prefers-reduced-motion: reduce) {
  .mx-side,
  .mx-app:has(> .mx-side) > :is(.mx-main, .mx-footer),
  summary.mx-side__item::after,
  .mx-side__label { transition: none; }
}
@media print {
  .mx-side { display: none; }
  .mx-app:has(> .mx-side) > :is(.mx-main, .mx-footer) { margin-inline-start: 0; }
}

/* The signed-in frame's footer (2026-10-07): the studio line, Privacy, support. Its own class, so
   no product borrows the landing one (Hotel's .lp-footer on /hotel, eight runs asking for this).
   After .mx-main, it clears the phone's tab bar itself and .mx-main stops padding for it. */
.mx-footer {
  border-top: 1px solid var(--c-line);
  padding: 1.25rem max(var(--lp-gutter, 1.5rem), env(safe-area-inset-left))
    calc(var(--mx-tabs-h, calc(3.75rem + env(safe-area-inset-bottom))) + 0.75rem + var(--mx-fab-room, 0px)) max(var(--lp-gutter, 1.5rem), env(safe-area-inset-right));
  font-size: 0.8125rem;
  color: var(--c-ink-subtle);
}
.mx-main:has(+ .mx-footer) { padding-bottom: 1.5rem; }
/* The quiet footer (server/mixtape-footer.mjs; Chris, 2026-10-14: "much more minimal"): three small links on
   one centred line - privacy, help, mixtape.works - no rule above it, no underlines, nothing that reads as a
   paragraph. The product's name is in the bar already. */
.mx-footer--quiet { border-top: 0; padding-top: 0.5rem; font-size: 0.75rem; }
.mx-footer--quiet p { display: flex; flex-wrap: wrap; justify-content: center; gap: 0 1.25rem; margin: 0; }
.mx-footer--quiet a { text-decoration: none; }
.mx-footer__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.25rem 1.5rem;
  max-width: var(--lp-max, var(--page-max, 90rem));
  margin: 0 auto;
}
.mx-footer__row p { margin: 0; }
.mx-footer__links { display: flex; flex-wrap: wrap; gap: 0 1.25rem; }
.mx-footer a { color: inherit; text-underline-offset: 3px; }
.mx-footer a:hover { color: var(--c-ink); }
@media (pointer: coarse) {
  .mx-footer a { display: inline-flex; align-items: center; min-height: 44px; }
}
@media (min-width: 768px) {
  .mx-footer { padding-bottom: calc(1.5rem + var(--mx-fab-room, 0px)); }
}
@media print {
  .mx-footer { display: none; }
}

/* ── The content column ──────────────────────────────────────────────────
   The same width as a landing page's, and the same gutter. Every page we ship - the one a
   stranger lands on and the one they work in after signing in - holds its content in a column
   of exactly one width, or the product reads as two products. */

.mx-page {
  width: 100%;
  max-width: var(--lp-max, var(--page-max, 90rem));
  margin-inline: auto;
  padding: 1.5rem max(var(--lp-gutter, 1.5rem), env(safe-area-inset-left)) 0
    max(var(--lp-gutter, 1.5rem), env(safe-area-inset-right));
}
@media (min-width: 768px) { .mx-page { padding-top: 2rem; } }
/* On a phone the page's edge sits a little closer than one panel is to the next (Chris,
   2026-09-22: first "the same as the padding between the boxes", then "like 2px even
   smaller") - the screen's narrowest dimension was being spent on margin. The bar follows, so
   the logo stays over the panels' edge. The landing layer keeps its own, roomier gutter: that
   is text. */
@media (max-width: 767.98px) {
  .mx-bar, .mx-page, .mx-footer { --lp-gutter: 0.75rem; }
}
/* On a phone the brand and the profile button read as pushed out against the edges, so each comes
   5px in (Chris, 2026-10-14). Only those two: a relative offset moves the drawing, not the layout,
   so the cog and everything else on the bar stay exactly where they were. Studio's React header
   (AppShell.tsx) does the same. */
@media (max-width: 767.98px) {
  .mx-bar .mx-brand { position: relative; inset-inline-start: 5px; }
  .mx-bar mixtape-profile { inset-inline-end: 5px; }
}

.mx-page__inner { min-width: 0; }
/* THE PAGE IS A STACK, and it keeps the same rhythm as the stacks inside it.
   Everything a page holds - a panel, a row of columns, a notice - sits .875rem from the
   block above it, the same gap .mx-cols and .mx-stack leave between their own children.
   Without this a full-width panel followed by a row of columns touched it (Dayrule's
   overview, found by Chris 2026-09-21), while the gaps inside the row were .875rem: the one
   place on the page with no air was the join between two kit classes. Voks had solved it
   for itself with this exact rule - which is how you know it belonged here.
   Margins, not gap: the page column is a block, so the head's own 1.25rem below it
   collapses with this rather than adding to it. */
.mx-page > * + *,
.mx-page__inner > * + * { margin-top: 0.875rem; }
/* A landing band dropped into an app page (Storefront's plan page shows the kit's pricing band): it stays in
   the column. Its 100vw break-out assumed a page with nothing beside it, and beside a sidebar it made the
   whole admin scroll sideways by half the sidebar (2026-11-02). */
.mx-page .lp-band { width: auto; margin-inline: 0; padding-block: 0.5rem 0; }
.mx-page .lp-band > .lp-container { max-width: none; padding-inline: 0; }
/* Two narrower columns than the page's own. A form is easier to read short, but 30rem of
   fields floating in 1200px of page reads as unfinished: a sign-in box is narrow, a page of
   settings is medium, and only a table or a calendar takes the whole width. */
.mx-narrow { max-width: 34rem; margin-inline: auto; }
.mx-medium { max-width: 48rem; margin-inline: auto; }
/* Beside a sidebar the page starts at the sidebar's edge: a centred narrower column moved the left edge between
   a list and its own edit page (x=264 vs 376 in Storefront, 2026-11-02). The column stays short, flush start. */
.mx-app:has(> .mx-side) :is(.mx-narrow, .mx-medium) { margin-inline: 0; }

.mx-page__head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: 0.75rem;
  margin-bottom: 1.25rem;
}
.mx-eyebrow {
  margin: 0 0 0.375rem;
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--mx-label-size);
  text-transform: uppercase;
  letter-spacing: 0.15em;
  color: var(--mx-label);
}
.mx-h1 {
  margin: 0;
  font-size: clamp(1.375rem, 1.4rem + 1vw, 1.875rem);
  font-weight: 600;
  letter-spacing: -0.02em;
  color: var(--c-ink);
}
/* A section of a page, under its h1 ("People", "Waiting to join"): one step up from body text - the browser's
   own h2 (24px, nearly the h1) stood in for it until 2026-11-02. More air above than below. */
:where(.mx-h2) { margin: 0; }
.mx-h2 { font-size: 1.0625rem; font-weight: 600; letter-spacing: -0.01em; line-height: 1.35; color: var(--c-ink); }
.mx-page__inner > .mx-h2:not(:first-child),
.mx-page > .mx-h2:not(:first-child) { margin-top: 1.75rem; }
.mx-page__inner > .mx-h2 + *,
.mx-page > .mx-h2 + * { margin-top: 0.625rem; }
.mx-lead {
  margin: 0.5rem 0 0;
  max-width: 62ch;
  font-size: 0.875rem;
  line-height: 1.6;
  color: var(--c-ink-muted);
}
.mx-crumb { margin: 0 0 0.75rem; font-size: 0.8125rem; }
.mx-crumb a { color: var(--c-ink-muted); text-decoration: none; }
/* A crumb that is one link back ("Back to the coupons") wears an arrow, so it reads as the way back. */
.mx-crumb > a:only-child::before { content: '\2190'; display: inline-block; margin-inline-end: 0.375rem; }
:root[dir='rtl'] .mx-crumb > a:only-child::before { content: '\2192'; }
.mx-crumb a:hover { color: var(--c-ink); }
/* An icon in a button or a crumb is text-sized. :where() keeps it at element weight, so a product's own size
   class on the svg still wins; without any size an inline <svg> drew as wide as its button (FleetFault, 2026-10-03). */
:where(.mx-btn, .mx-crumb) svg { width: 1rem; height: 1rem; flex-shrink: 0; }

/* ── Surfaces ────────────────────────────────────────────────────────────*/

.mx-card {
  border: var(--mx-stroke, 1px) solid var(--c-line);
  border-radius: var(--radius-md);
  background: var(--c-surface);
  padding: 1.25rem;
}
.mx-card__title { margin: 0 0 0.375rem; font-size: 0.9375rem; font-weight: 600; color: var(--c-ink); }
.mx-card__text { margin: 0; font-size: 0.8438rem; line-height: 1.6; color: var(--c-ink-muted); }
.mx-card__text + .mx-card__text { margin-top: 0.625rem; }
/* A card's head: its title, and one small action at the end of the same line (Hide this card) - never a
   button stranded under the card's last word. The action may be a form holding the button. */
.mx-card__head { display: flex; align-items: flex-start; justify-content: space-between; gap: 0.75rem; margin-bottom: 0.375rem; }
.mx-card__head > .mx-card__title { margin: 0; min-width: 0; padding-top: 0.125rem; }
.mx-card__head > :not(.mx-card__title) { flex: none; margin-block: -0.375rem -0.25rem; margin-inline-end: -0.5rem; }
.mx-card__head form { margin: 0; }
.mx-card__head .mx-btn { padding-block: 0.375rem; font-size: 0.8125rem; }
.mx-panel {
  /* clip, not hidden: hidden makes the panel a scroll container, and a sticky child (a form's "Saved")
     would stick to the panel instead of the window. Browsers without clip keep hidden. */
  overflow: hidden;
  overflow: clip;
  border: var(--mx-stroke, 1px) solid var(--c-line);
  border-radius: var(--radius-md);
  background: var(--c-surface);
}
.mx-panel__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  border-bottom: 1px solid var(--c-line);
  padding: 0.5rem 1rem;
  min-height: 2.5rem;
}
.mx-panel__head > :first-child { min-width: 0; }
.mx-panel__head .mx-btn,
.mx-panel__head .mx-badge,
.mx-panel__head .mx-sub { flex: 0 0 auto; white-space: nowrap; }
.mx-panel__head .mx-btn { margin: -0.25rem -0.5rem -0.25rem 0; }
/* A head holding a set of choices (a segmented control, [role=group]: Today / 7 days / 30 days) is a label and its
   control, not a title and one action: the set drops under the label when the row is too short for both, and on a
   phone it always does - "PERIOD" floated half-way down beside two rows of chips (Storefront's Analytics, 2026-11-03). */
.mx-panel__head:has(> :is(.mx-seg, [role='group'])) { flex-wrap: wrap; row-gap: 0.5rem; padding-block: 0.625rem; }
.mx-panel__head > :is(.mx-seg, [role='group']) { min-width: 0; max-width: 100%; }
.mx-panel__head > [role='group'] .mx-btn { margin: 0; }
@media (max-width: 639.98px) {
  .mx-panel__head > :is(.mx-seg, [role='group']) { flex: 1 1 100%; }
}
/* The segmented control: one of a few choices, the open one lit. Links (aria-current="page") or buttons
   (aria-pressed="true"). When the choices outgrow the row they wrap into full rows - every option stretches, so
   a second row is as wide as the first, never a ragged tail - and every choice stays in sight (a sideways scroll
   hid "All time" past the edge with nothing saying it was there). The page never scrolls sideways for it.
     <div class="mx-seg" role="group" aria-label="Period"><a href="?p=7" aria-current="page">7 days</a>…</div> */
.mx-seg {
  display: flex;
  flex-wrap: wrap;
  gap: 0.125rem;
  max-width: 100%;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  padding: 0.1875rem;
  border-radius: calc(var(--mx-control-radius, var(--radius-sm)) + 0.1875rem);
  background: var(--c-surface-2);
}
.mx-seg::-webkit-scrollbar { display: none; }
.mx-seg > :is(a, button) {
  -webkit-appearance: none;
  appearance: none;
  flex: 1 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 2rem;
  padding: 0.3125rem 0.75rem;
  border: 0;
  border-radius: var(--mx-control-radius, var(--radius-sm));
  background: transparent;
  color: var(--c-ink-muted);
  font: inherit;
  font-size: 0.8125rem;
  font-weight: 500;
  white-space: nowrap;
  text-decoration: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background-color 0.15s ease, color 0.15s ease;
}
@media (hover: hover) { .mx-seg > :is(a, button):hover { color: var(--c-ink); } }
.mx-seg > :is([aria-current]:not([aria-current='false']), [aria-pressed='true']) {
  background: var(--c-surface);
  color: var(--c-ink);
  box-shadow: 0 0 0 1px var(--c-line-strong), 0 1px 2px rgb(0 0 0 / 0.12);
}
.mx-seg > :is(a, button):focus-visible { outline: 2px solid var(--c-accent); outline-offset: -2px; }
@media (hover: none) and (pointer: coarse) { .mx-seg > :is(a, button) { min-height: 44px; font-size: 0.875rem; } }
/* The label belongs inside the box it describes - a heading floating above a border reads
   as a different kind of thing when it is not one. */
.mx-panel__title {
  margin: 0;
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--mx-label-size);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.15em;
  color: var(--mx-label);
}
.mx-panel__body { padding: 1rem; }
.mx-panel__body--flush { padding: 0; }
/* A panel that has been given a height (Panel's `scroll`): the heading stays, the body
   scrolls inside it. With no height set it is an ordinary panel. */
.mx-panel--scroll { display: flex; flex-direction: column; }
.mx-panel--scroll > .mx-panel__head { flex-shrink: 0; }
.mx-panel--scroll > .mx-panel__body { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }

.mx-grid {
  display: grid;
  gap: 0.875rem;
  align-items: start;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
}
/* Panels side by side, and the one that holds a form or a table taking the whole row. */
/* Two panels to a row, not three: a panel holds a form, and 340px of form is the narrow strip
   this grid exists to get away from. One column below ~880px. */
.mx-cols { display: grid; gap: 0.875rem; align-items: start; grid-template-columns: repeat(auto-fit, minmax(min(27rem, 100%), 1fr)); }
/* min(): on a phone the column is narrower than 27rem, and a bare minmax(27rem) made the page scroll sideways (FleetFault, 2026-10-03). */
.mx-cols--wide { grid-column: 1 / -1; }
.mx-stack { display: grid; gap: 0.875rem; }

/* A number with its label under it: the four that must agree, side by side. */
.mx-stat { display: grid; gap: 2px; margin: 0; } /* often a <p>: without this it keeps the browser's paragraph margins */
.mx-stat__label {
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--mx-label-size-sm);
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--mx-label);
}
.mx-stat__value { font-size: 1.375rem; font-weight: 600; color: var(--c-ink); }
.mx-stats { display: flex; flex-wrap: wrap; gap: 1.5rem; }

/* ── Controls ────────────────────────────────────────────────────────────*/

.mx-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  /* A bare .mx-btn (no --primary/--ghost/--quiet) is the ghost button, not the browser's: it had no colour of its
     own, so a plain <button class="mx-btn"> was the platform's button - grey in Chrome, and iOS Safari's system
     blue text (#0a84ff dark, #007aff light) on Storefront's "Show these days" (Chris's iPhone, 2026-11-03). */
  -webkit-appearance: none;
  appearance: none;
  /* The look's corners and line weight (tokens.css, kit/look.ts): unset, the kit's own. */
  border: var(--mx-stroke, 1px) solid var(--c-line-strong);
  border-radius: var(--mx-control-radius, var(--radius-sm));
  background: transparent;
  color: var(--c-ink);
  padding: 0.625rem 1rem;
  font: inherit;
  font-size: 0.875rem;
  font-weight: 500;
  line-height: 1.2;
  text-decoration: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
@media (hover: hover) { .mx-btn:not([class*='mx-btn--']):hover { background: var(--c-surface-2); } }
.mx-btn--primary, .mx-btn--danger, .mx-btn--quiet, .mx-btn--icon { border-color: transparent; }
.mx-btn--primary { background: var(--c-accent); color: var(--c-accent-ink); }
.mx-btn--primary:hover { background: var(--c-accent-strong); }
.mx-btn--ghost { border-color: var(--c-line-strong); background: transparent; color: var(--c-ink); }
.mx-btn--ghost:hover { background: var(--c-surface-2); }
.mx-btn--quiet { background: transparent; color: var(--c-ink-muted); padding-inline: 0.625rem; }
.mx-btn--quiet:hover { color: var(--c-ink); background: var(--c-surface-2); }
/* An icon on its own ("View shop" in a bar, a row's "more"): the cog's size and weight - a 2.25rem round target,
   no border, muted until hovered - with its words in aria-label and title. A button that keeps its words on a
   laptop and drops them on a phone puts them in .mx-btn__text: in the phone bar the kit hides that and draws the
   button as this one (below, "The phone bar"). */
.mx-btn--icon { width: 2.25rem; height: 2.25rem; padding: 0; border-radius: 9999px; background: transparent; color: var(--c-ink-muted); }
.mx-btn--icon svg { width: 1.25rem; height: 1.25rem; }
@media (hover: hover) { .mx-btn--icon:hover { color: var(--c-ink); background: var(--c-surface-2); } }
/* A destructive action - delete, ban, remove for good. White on the kit's danger fill, which is the
   light theme's red in both themes (tokens.css --c-danger: 5.1:1; the dark theme's own red carries
   white at 3.2:1). One per screen, and never the only way out of a confirm. */
.mx-btn--danger { background: var(--c-danger); color: var(--c-danger-ink); }
.mx-btn--danger:hover { background: var(--c-danger-strong); }
.mx-btn--danger:focus-visible { outline-color: var(--c-danger); }
.mx-btn:focus-visible { outline: 2px solid var(--c-accent); outline-offset: 2px; }
.mx-btn[disabled], .mx-btn[aria-disabled='true'] { opacity: 0.5; pointer-events: none; }
.mx-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; }

/* Controls are 2.5rem tall so a row of them lines up without hand-tuning, and labels are
   mono caps - the studio's voice for metadata. */
/* A form is a stack of fields; a field is label, control, then whatever it has to say. */
.mx-form { display: grid; gap: 1rem; margin-top: 1.125rem; min-width: 0; }
.mx-panel__body > .mx-form:first-child { margin-top: 0; } /* nothing above it to keep apart from */
/* Fields side by side once there is room for them.
 *
 * A form reads badly in long lines, but the cure is not a narrow page - a column of controls
 * stranded in the middle of a desktop reads as a bug, and it is the first thing anyone says
 * about it. Keep the page the width of every other page and keep the LINE short: two or three
 * fields to a row here, one to a row on a phone. A field that needs the room (a list of
 * checkboxes, a paste box) takes it with .mx-field--wide. */
.mx-fields {
  display: grid;
  gap: 1rem;
  /* min(): a column narrower than 14rem (a field row in a phone's card) stacks instead of pushing past the edge. */
  grid-template-columns: repeat(auto-fit, minmax(min(14rem, 100%), 1fr));
  align-items: start;
  min-width: 0;
}
/* Two short fields that belong side by side even on a phone - From and To, a date and its time, a min and a max -
   sharing the row in halves (minmax(0, 1fr): a date input's own width can never push the pair past the card's
   edge), one under the other below 360px. */
.mx-fields--two { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.75rem; }
@media (max-width: 359.98px) { .mx-fields--two { grid-template-columns: minmax(0, 1fr); } }
.mx-fields.mx-fields--two .mx-field :is(input, select) { max-width: none; }
.mx-field--wide { grid-column: 1 / -1; }
/* A number, a date or a time is a few characters: an input as wide as the row says otherwise. */
.mx-field input[type='number'],
.mx-field input[type='date'],
.mx-field input[type='time'] { max-width: 15rem; }
/* A date or time input has a width of its own (iOS Safari: the native picker's, ~200px, centred text) that ignored
   width: 100% and pushed two of them side by side past a 390px card (Storefront's From / To, 2026-11-03).
   min-width 0 lets it take its column; the value starts at the start like every other field's. */
.mx-field input:is([type='date'], [type='time'], [type='datetime-local'], [type='month'], [type='week']),
.mx-input:is([type='date'], [type='time'], [type='datetime-local']) { display: block; min-width: 0; }
.mx-field input::-webkit-date-and-time-value, .mx-input::-webkit-date-and-time-value { text-align: start; }
/* iOS only (-webkit-touch-callout exists nowhere else): its native control keeps that width and draws the value in
   the system blue whatever colour the field says; without its appearance it is the kit's field and still opens the
   picker on a tap. */
@supports (-webkit-touch-callout: none) {
  .mx-field input:is([type='date'], [type='time'], [type='datetime-local'], [type='month'], [type='week']),
  .mx-input:is([type='date'], [type='time'], [type='datetime-local']) {
    -webkit-appearance: none; appearance: none; line-height: calc(2.5rem - 2 * var(--mx-stroke, 1px));
  }
  /* Its value is a line box of its own: as tall as the field's inside, or it sits high in the 44px touch field. */
  @media (hover: none) and (pointer: coarse) {
    .mx-field input:is([type='date'], [type='time'], [type='datetime-local'], [type='month'], [type='week']),
    .mx-input:is([type='date'], [type='time'], [type='datetime-local']) { line-height: calc(max(2.5rem, 44px) - 2 * var(--mx-stroke, 1px)); }
  }
}
.mx-form .mx-actions { margin-top: 2px; }
.mx-field { display: grid; gap: 0.375rem; min-width: 0; }
.mx-field > label, .mx-label {
  display: block;
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--mx-label-size);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--mx-label);
}
/* Not checkboxes or radios: a field's box rules made a checkbox 100% wide and 2.5rem tall
   (RentKeeper's "rent out anyway", 2026-10-05). Those are .mx-check below. */
.mx-field input:not([type='checkbox'], [type='radio'], [type='file'], [type='hidden']),
.mx-field select,
.mx-field textarea,
.mx-input {
  width: 100%;
  height: 2.5rem;
  /* --c-line-field: 3:1 against the box and the card round it (WCAG 1.4.11) - an empty field has to be seen. */
  border: var(--mx-stroke, 1px) solid var(--c-line-field);
  border-radius: var(--mx-control-radius, var(--radius-sm));
  background: var(--c-canvas);
  padding: 0 0.75rem;
  font: inherit;
  font-size: 0.875rem;
  color: var(--c-ink);
  transition: border-color 0.15s ease;
}
.mx-field textarea { height: auto; min-height: 6rem; padding: 0.625rem 0.75rem; line-height: 1.5; }
/* A box of several lines takes the look's corners only up to a card's: a pill-shaped paragraph cuts its own words. */
.mx-field textarea { border-radius: min(var(--mx-control-radius, var(--radius-sm)), var(--radius-lg)); }
.mx-field input:not([type='checkbox'], [type='radio']):focus, .mx-field select:focus, .mx-field textarea:focus, .mx-input:focus {
  border-color: var(--c-accent);
  outline: none;
  /* A ring as well as the border: a 1px colour change was the only sign of focus, and with a red accent it
     read as the invalid state (2026-11-02). */
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--c-accent) 22%, transparent);
}
.mx-check input:focus-visible,
.mx-field > label > input:is([type='checkbox'], [type='radio']):focus-visible,
.mx-table td input[type='checkbox']:focus-visible { outline: 2px solid var(--c-accent); outline-offset: 2px; }
/* A link on its own gets the same ring as a button when it is reached by the keyboard. */
:where(.mx-app, .lp) a:focus-visible { outline: 2px solid var(--c-accent); outline-offset: 2px; border-radius: 2px; }
/* A checkbox's own hint sits under its words, not under the field's edge a row away. */
.mx-field:has(> .mx-check) > .mx-check + .mx-hint,
.mx-field > label:has(> input[type='checkbox']) + .mx-hint { margin-top: -0.5rem; padding-inline-start: 1.75rem; }
.mx-field input[aria-invalid='true'], .mx-field select[aria-invalid='true'] { border-color: var(--c-rec); }
.mx-hint { margin: 0; font-size: 0.75rem; color: var(--c-ink-subtle); }
/* The browser's own placeholder grey is 4.3:1 at best (Chromium) and Firefox fades it to 54%: the kit's small-text
   colour instead, at full strength, so the example in an empty box reads at 4.5:1 like every hint. */
.mx-field :is(input, textarea)::placeholder, .mx-input::placeholder { color: var(--c-ink-subtle); opacity: 1; }
.mx-err { margin: 0; font-size: 0.7812rem; color: var(--c-rec); }
/* Notices, by what they mean - never by how loud a product wants them (2026-11-02, Storefront's dashboard had
   three warnings dressed as errors):
     .mx-alert          something went wrong or was refused (red). The kit's autosave reads a form's .mx-alert as
                        the reason a save failed, so it is never a reminder or a tip.
     .mx-note           it worked / it is fine (green).
     .mx-note--warn     needs the person's attention, nothing is broken (amber): "set up shipping".
     .mx-note--info     worth knowing, nothing to fix (blue): "you are looking at a demo".
   A notice with something to do holds its words in an element and its buttons in .mx-actions: the buttons sit at
   the end of the line when there is room and drop under the words when there is not. Its action is a --ghost
   .mx-btn (a page has one primary, and a notice is rarely it) and "Got it" a --quiet one.
   The margin reset is :where()'d: the page column's rhythm (.mx-page > * + *) must win over it, or two notices in
   a row touch (they did). */
:where(.mx-alert, .mx-note) { margin: 0; }
.mx-alert,
.mx-note {
  --mx-tone: var(--c-ok);
  border: 1px solid color-mix(in srgb, var(--mx-tone) 40%, transparent);
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--mx-tone) 10%, transparent);
  padding: 0.625rem 0.75rem;
  font-size: 0.8438rem;
  line-height: 1.55;
  color: var(--c-ink);
}
.mx-alert { --mx-tone: var(--c-rec); }
.mx-note--warn { --mx-tone: var(--c-warn); }
.mx-note--info { --mx-tone: var(--c-info, var(--c-build)); }
:is(.mx-alert, .mx-note):has(> .mx-actions) {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem 1rem;
  padding-block: 0.5rem;
}
:is(.mx-alert, .mx-note):has(> .mx-actions) > :not(.mx-actions) { flex: 1 1 18rem; min-width: 0; margin: 0; }
/* The buttons may shrink to the note's width and wrap under each other: two long Hungarian labels side by side were
   wider than a 360px note and were cut off (Storefront's demo banner, 2026-11-03). */
:is(.mx-alert, .mx-note) > .mx-actions { flex: 0 1 auto; min-width: 0; max-width: 100%; margin: 0; }
:is(.mx-alert, .mx-note) > .mx-actions > .mx-btn { max-width: 100%; }
:is(.mx-alert, .mx-note) > .mx-actions form { margin: 0; }
/* On the tint a secondary button stands on the page's own surface. */
:is(.mx-alert, .mx-note) .mx-btn--ghost { border-color: color-mix(in srgb, var(--mx-tone) 40%, var(--c-line-strong)); background: var(--c-surface); }
:is(.mx-alert, .mx-note) .mx-btn--ghost:hover { background: var(--c-surface-2); }
/* A form that saves itself (kit/forms.ts AUTOSAVE): its Save button gives way to a quiet status, and
   comes back beside "Not saved" when a save fails. A live search's button gives way for good. */
form[data-mx-on]:not(.mx-autosave--failed) [data-mx-saver] { display: none; }
.mx-autosave {
  margin: 0;
  min-height: 1.25rem;
  font-size: 0.7812rem;
  line-height: 1.6;
  color: var(--c-ink-subtle);
}
.mx-autosave[data-state='saved'] { color: color-mix(in srgb, var(--c-ok) 70%, var(--c-ink-subtle)); }
.mx-autosave[data-state='notSaved'] { color: var(--c-rec); }
/* SAID WHERE IT IS SEEN (2026-11-02). The status sits at the end of its form, and a long settings form ends
   a screen or two below the field being typed in - Storefront's "Saved" landed 2,000px under it. So once a
   save starts it is a small pill that sticks to the bottom of the window while its form is on screen, and
   "Saved" fades after a moment; "Not saved" stays. Sticky within its own form, so it never covers another. */
.mx-autosave[data-state] {
  position: sticky;
  z-index: 5;
  bottom: calc(1rem + env(safe-area-inset-bottom, 0px));
  justify-self: start;
  align-self: start;
  width: max-content;
  max-width: 100%;
  box-sizing: border-box;
  min-height: 0;
  padding: 0.3125rem 0.75rem;
  border: 1px solid var(--c-line);
  border-radius: 999px;
  background: var(--c-surface);
  box-shadow: var(--shadow-lift);
  font-weight: 500;
}
.mx-autosave[data-state='saved'] { color: var(--c-ok); animation: mx-saved 2.4s ease forwards; }
.mx-autosave[data-state='notSaved'] { border-color: color-mix(in srgb, var(--c-rec) 45%, var(--c-line)); }
@keyframes mx-saved { 0%, 70% { opacity: 1; } 100% { opacity: 0; } }
/* Above the phone's tab bar. */
@media (max-width: 767.98px) {
  .mx-app:has(> .mx-tabs) .mx-autosave[data-state] { bottom: calc(4.75rem + env(safe-area-inset-bottom, 0px)); }
}
@media (prefers-reduced-motion: reduce) {
  .mx-autosave[data-state='saved'] { animation-duration: 4s; }
}
/* A list being fetched again as its search is typed: dimmed only when the answer is slow. */
[data-mx-loading] { opacity: 0.55; transition: opacity 0.15s ease 0.15s; }
.mx-row { display: flex; flex-wrap: wrap; gap: 0.875rem; }
.mx-row > * { flex: 1 1 200px; }

/* ── Tables ──────────────────────────────────────────────────────────────
   A table is the one thing allowed to be wider than the page, inside its own scroller. */
.mx-scroll { overflow-x: auto; }
.mx-table { width: 100%; border-collapse: collapse; font-size: 0.875rem; }
.mx-table th {
  text-align: start;
  padding: 0.625rem 0.875rem;
  border-bottom: 1px solid var(--c-line);
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--mx-label-size-sm);
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--mx-label);
  white-space: nowrap;
}
.mx-table td { padding: 0.75rem 0.875rem; border-bottom: 1px solid var(--c-line); color: var(--c-ink); vertical-align: top; }
.mx-table tr:last-child td { border-bottom: 0; }
/* A row answers the pointer (Chris, 2026-09-21: "if a table anywhere has rows, i want the
   row's bg to slightly change on hover"): a faint wash of ink, so it reads on either theme and
   on any surface the table sits on. Only where there is a real pointer. */
.mx-table td { transition: background-color 0.12s ease; }

/* A chart's zoom, in its card's corner (<ChartZoom> in @mixtape/ui/chart): ‹ 3M ›. Quiet until
   pointed at, like the rest of a card's header. */
.mx-chart-zoom button,
.mx-chart-zoom select {
  font: inherit;
  font-size: 0.75rem;
  color: var(--c-ink-muted);
  background: transparent;
  border: 0;
  cursor: pointer;
  transition: color 0.12s ease, background-color 0.12s ease;
}
.mx-chart-zoom select {
  appearance: none;
  -webkit-appearance: none;
  padding: 0.1875rem 0.5rem;
  border-radius: 0.375rem;
  font-family: var(--font-mono, ui-monospace, monospace);
  letter-spacing: 0.06em;
  text-align: center;
}
.mx-chart-zoom button:disabled { opacity: 0.35; cursor: default; }
@media (hover: hover) {
  .mx-chart-zoom button:hover:not(:disabled),
  .mx-chart-zoom select:hover { color: var(--c-ink); background: var(--c-surface-2); }
}
@media (hover: hover) {
  .mx-table tbody tr:hover td { background: color-mix(in srgb, var(--c-ink) 4%, transparent); }
}
.mx-table td .sub { display: block; margin-top: 2px; font-size: 0.75rem; color: var(--c-ink-subtle); }
.mx-table .num { text-align: end; font-variant-numeric: tabular-nums; }
.mx-table a { color: var(--c-ink); text-decoration-color: var(--c-line-strong); text-underline-offset: 3px; }
.mx-table a:hover { color: var(--c-accent); }

.mx-badge {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  border: 1px solid var(--c-line);
  border-radius: 999px;
  padding: 3px 0.625rem;
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--mx-label-size-sm);
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--c-ink-muted);
  /* One line while it fits; in a narrow column it wraps rather than running into its neighbour
     (RentKeeper's "SÉRÜLTEN VISSZAHOZVA" in a half-width fact on a phone, 2026-10-05). */
  max-width: 100%;
  overflow-wrap: anywhere;
}
/* A table sizes its columns to their content: there a badge keeps to one line. */
.mx-table .mx-badge { white-space: nowrap; }
.mx-badge--accent { border-color: color-mix(in srgb, var(--c-accent) 30%, transparent); color: var(--c-accent); }
.mx-badge--ok { border-color: color-mix(in srgb, var(--c-ok) 30%, transparent); color: var(--c-ok); }
.mx-badge--warn { border-color: color-mix(in srgb, var(--c-warn) 35%, transparent); color: var(--c-warn); }
.mx-badge--rec { border-color: color-mix(in srgb, var(--c-rec) 35%, transparent); color: var(--c-rec); }
.mx-badge--info { border-color: color-mix(in srgb, var(--c-info, var(--c-build)) 35%, transparent); color: var(--c-info, var(--c-build)); }

/* A checkbox or a radio with its words, on one row, 44px tall: "needs repair", "ban this renter".
   In a .mx-field it is still a row, never the field's uppercase label. */
.mx-check, .mx-field > label.mx-check {
  display: flex; align-items: center; gap: 0.625rem; min-height: 44px;
  font-family: inherit; font-size: 0.9375rem; text-transform: none; letter-spacing: normal;
  color: var(--c-ink); cursor: pointer;
}
.mx-check input { width: 1.125rem; height: 1.125rem; margin: 0; flex: 0 0 auto; accent-color: var(--c-accent); }
.mx-check--danger input { accent-color: var(--c-rec); }
/* The same row when a product forgot the class: a bare <label> around a checkbox or radio in a
   .mx-field read as the field's uppercase label ("WITH VAT INCLUDED", Storefront's VAT page,
   2026-10-06). The class is still right; this keeps a forgotten one from shipping. */
.mx-field > label:has(> input[type='checkbox'], > input[type='radio']) {
  display: flex; align-items: center; gap: 0.625rem; min-height: 44px;
  font-family: inherit; font-size: 0.9375rem; text-transform: none; letter-spacing: normal;
  color: var(--c-ink); cursor: pointer;
}
.mx-field > label > input:is([type='checkbox'], [type='radio']) {
  width: 1.125rem; height: 1.125rem; margin: 0; flex: 0 0 auto; accent-color: var(--c-accent);
}
/* A fieldset used as a field has no browser frame: its legend is the field's label. */
fieldset.mx-field { margin: 0; padding: 0; border: 0; }
fieldset.mx-field > legend {
  padding: 0; margin-bottom: 0.375rem;
  font-family: var(--font-mono, ui-monospace, monospace); font-size: var(--mx-label-size);
  text-transform: uppercase; letter-spacing: 0.08em; color: var(--mx-label);
}
/* One of a few, as cards: a fieldset of .mx-check labels, the chosen one outlined. */
.mx-choice { display: grid; gap: 0.5rem; min-width: 0; margin: 0; padding: 0; border: 0; }
.mx-choice > legend {
  padding: 0; margin-bottom: 0.375rem;
  font-family: var(--font-mono, ui-monospace, monospace); font-size: var(--mx-label-size);
  text-transform: uppercase; letter-spacing: 0.08em; color: var(--mx-label);
}
.mx-choice .mx-check {
  padding: 0.5rem 0.875rem; border: 1px solid var(--c-line); border-radius: var(--radius-md);
  background: var(--c-surface-2);
}
.mx-choice .mx-check:has(input:checked) { border-color: var(--c-accent); box-shadow: inset 0 0 0 1px var(--c-accent); }
.mx-choice .mx-check:has(input:focus-visible) { outline: 2px solid var(--c-accent); outline-offset: 2px; }

/* What an empty list says for itself, rather than a blank box. Its margin reset is :where()'d for the same reason
   as the notices': after a block in the page column it keeps the column's rhythm. */
:where(.mx-empty) { margin: 0; }
.mx-empty {
  padding: 2px 0 0.25rem;
  font-size: 0.8438rem;
  line-height: 1.55;
  color: var(--c-ink-subtle);
}
/* A whole page with nothing on it is the one place a centred, roomier empty state belongs. */
.mx-panel__body--flush .mx-empty,
.mx-page > .mx-empty { padding: 1.75rem 1rem; text-align: center; }
.mx-sub { margin: 0; font-size: 0.7812rem; line-height: 1.55; color: var(--c-ink-subtle); }
.mx-stats + .mx-sub { margin-top: 0.875rem; }
.mx-sub + .mx-sub { margin-top: 0.375rem; }
/* A panel body is a stack: whatever is in it, it breathes the same. */
.mx-panel__body > * + * { margin-top: 0.75rem; }

/* ── Photos, a photo's big view, overlays, sortable tables, filters ─────────
   The app layer's working parts (Chris, 2026-10-05, on RentKeeper). Each is plain HTML that
   mixtape-ui.js enhances - DESIGN.md "Photos, overlays, sorting and filters" has the markup.
   Products use these, never their own. */

/* <mixtape-photos>: a drop area whose plus is the biggest thing in it until photos are chosen,
   then a grid of thumbnails with the plus as the last tile. The real input stays in the form,
   out of sight but focusable, so a `required` one can still say so. */
.mx-photos { display: block; position: relative; }
.mx-photos__native {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; border: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; opacity: 0;
}
.mx-photos__area {
  border: 2px dashed var(--c-line-strong);
  border-radius: var(--radius-md);
  padding: 0.75rem;
  background: color-mix(in srgb, var(--c-surface-2) 55%, transparent);
  transition: border-color 0.15s ease, background-color 0.15s ease;
}
.mx-photos__area.is-over { border-color: var(--c-accent); background: var(--c-accent-soft); }
.mx-photos__grid {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-template-columns: repeat(auto-fill, minmax(5.75rem, 1fr)); gap: 0.5rem;
}
.mx-photos__tile { position: relative; aspect-ratio: 1; border-radius: var(--radius-sm); overflow: hidden; background: var(--c-surface-2); }
.mx-photos__tile img { display: block; width: 100%; height: 100%; object-fit: cover; }
.mx-photos__remove {
  position: absolute; top: 0.3rem; inset-inline-end: 0.3rem;
  display: grid; place-items: center; width: 1.875rem; height: 1.875rem;
  border: 0; border-radius: 9999px; background: color-mix(in srgb, var(--c-scrim) 62%, transparent); color: var(--c-scrim-ink); cursor: pointer;
}
.mx-photos__remove svg { width: 0.875rem; height: 0.875rem; }
.mx-photos__remove:focus-visible { outline: 2px solid var(--c-accent); outline-offset: 1px; }
.mx-photos__tile--add { background: transparent; overflow: visible; }
.mx-photos__add {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 0.3rem;
  width: 100%; height: 100%; padding: 0.5rem;
  border: 1px solid var(--c-line-strong); border-radius: var(--radius-sm);
  background: var(--c-surface); color: var(--c-ink); font: inherit; text-align: center; cursor: pointer;
  transition: border-color 0.15s ease, transform 0.12s ease;
}
.mx-photos__actions { display: flex; flex-wrap: wrap; align-items: center; align-content: center; gap: 0.5rem; list-style: none; grid-column: span 2; }
.mx-btn--danger-text { color: var(--c-rec); }
.mx-photos__add:hover { border-color: var(--c-accent); }
.mx-photos__add:active { transform: scale(0.98); }
.mx-photos__add:focus-visible { outline: 2px solid var(--c-accent); outline-offset: 2px; }
.mx-photos__add svg {
  width: 2.25rem; height: 2.25rem; padding: 0.45rem; border-radius: 9999px;
  background: var(--c-accent); color: var(--c-accent-ink);
}
.mx-photos__add-label { font-size: 0.8125rem; font-weight: 600; line-height: 1.25; }
.mx-photos__hint { font-size: 0.6875rem; line-height: 1.3; color: var(--c-ink-subtle); }
.mx-photos:not(.has-files) .mx-photos__grid { grid-template-columns: 1fr; }
.mx-photos:not(.has-files) .mx-photos__tile--add { aspect-ratio: auto; min-height: 9.5rem; }
.mx-photos:not(.has-files) .mx-photos__add { border-style: none; background: transparent; gap: 0.5rem; }
.mx-photos:not(.has-files) .mx-photos__add svg { width: 3.5rem; height: 3.5rem; padding: 0.75rem; }
.mx-photos:not(.has-files) .mx-photos__add-label { font-size: 0.9375rem; }
.mx-photos:not(.has-files) .mx-photos__hint { font-size: 0.75rem; }
.mx-photos__bar { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 0.75rem; margin-top: 0.625rem; }
.mx-photos:not(.has-files) .mx-photos__bar:not(:has(.mx-photos__camera)) { display: none; }
.mx-photos__count { font-size: 0.8125rem; color: var(--c-ink-muted); }
.mx-photos__msg { margin: 0.5rem 0 0; font-size: 0.8125rem; color: var(--c-rec); }

/* ── A table that stacks on a phone: .mx-table--stack (2026-11-02) ─────────────────────────────
   Under 640px a table of five columns is either crushed (dates in four lines, e-mails broken letter by
   letter) or scrolls its last columns out of sight. With --stack each row becomes a block: the cell holding
   the row's link (a[data-mx-row] - the name) leads, the numbers (.num) and the status badge sit at the end
   of that line, a row's checkbox before it, and every other cell follows on the line under it, quieter.
   The header row goes (a sortable table then shows the kit's "Sort by" select). `data-label` on a cell
   names it in the stacked line when its value would not say what it is ("Stock 3"). */
@media (max-width: 639.98px) {
  .mx-table--stack thead { display: none; }
  .mx-table--stack,
  .mx-table--stack tbody { display: block; width: 100%; }
  .mx-table--stack tr {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.125rem 0.75rem;
    padding: 0.75rem 0.875rem;
    border-bottom: 1px solid var(--c-line);
  }
  .mx-table--stack tbody tr:last-child { border-bottom: 0; }
  .mx-table--stack td {
    display: block;
    order: 3;
    min-width: 0;
    padding: 0;
    border: 0;
    font-size: 0.8125rem;
    color: var(--c-ink-muted);
    overflow-wrap: break-word;
  }
  .mx-table--stack td:empty { display: none; }
  .mx-table--stack td:has(a[data-mx-row]) { order: 0; flex: 1 1 11rem; font-size: 0.9375rem; font-weight: 500; color: var(--c-ink); }
  .mx-table--stack td:is(.num, :has(> .mx-badge)) { order: 1; flex: none; font-size: 0.875rem; color: var(--c-ink); }
  .mx-table--stack td:has(> label > input[type='checkbox'], > input[type='checkbox']) { order: -1; flex: none; }
  /* The break between the leading line and the rest. */
  .mx-table--stack tr::after { content: ''; order: 2; flex-basis: 100%; height: 0; }
  .mx-table--stack td[data-label]::before { content: attr(data-label) ' '; color: var(--c-ink-subtle); }
  .mx-table.mx-table--stack a[data-mx-row] { white-space: normal; }
  .mx-table--stack td .sub { display: inline; margin: 0 0 0 0.5rem; }
  .mx-table--stack td a:has(> .sub) .sub { display: block; margin: 2px 0 0; }
  .mx-table--stack tbody tr:hover td { background: none; }
}
/* A row with an a[data-mx-row] opens it from anywhere on the row (kit/tables.ts). */
.mx-table tr:has(a[data-mx-row]) { cursor: pointer; }

/* .mx-gallery: photos already kept, square, each with its date (and a label) under it. A link
   with data-mx-lightbox opens the big view. */
/* One row, however many: it scrolls sideways, and the edge with more beyond it fades
   (data-mx-more, set by the kit script) - the cut-off photo says "swipe for more". */
.mx-gallery {
  display: grid; grid-auto-flow: column; grid-auto-columns: 6.5rem; gap: 0.625rem;
  overflow-x: auto; overscroll-behavior-x: contain; scroll-snap-type: x proximity;
  scrollbar-width: none; padding-bottom: 0.125rem;
}
.mx-gallery::-webkit-scrollbar { display: none; }
.mx-gallery[data-mx-more='end'] { -webkit-mask-image: linear-gradient(to right, black calc(100% - 4.5rem), transparent); mask-image: linear-gradient(to right, black calc(100% - 4.5rem), transparent); }
.mx-gallery[data-mx-more='start'] { -webkit-mask-image: linear-gradient(to left, black calc(100% - 3rem), transparent); mask-image: linear-gradient(to left, black calc(100% - 3rem), transparent); }
.mx-gallery[data-mx-more='both'] { -webkit-mask-image: linear-gradient(to right, transparent, black 3rem, black calc(100% - 4.5rem), transparent); mask-image: linear-gradient(to right, transparent, black 3rem, black calc(100% - 4.5rem), transparent); }
.mx-gallery figure { margin: 0; min-width: 0; scroll-snap-align: start; }
.mx-gallery a { display: block; aspect-ratio: 1; border-radius: var(--radius-sm); overflow: hidden; background: var(--c-surface-2); }
.mx-gallery a:focus-visible { outline: 2px solid var(--c-accent); outline-offset: 2px; }
.mx-gallery img { display: block; width: 100%; height: 100%; object-fit: cover; transition: transform 0.25s var(--ease-out-soft, ease); }
@media (hover: hover) { .mx-gallery a:hover img { transform: scale(1.04); } }
.mx-gallery figcaption { margin-top: 0.375rem; font-size: 0.75rem; line-height: 1.4; color: var(--c-ink-subtle); overflow-wrap: break-word; }
.mx-gallery time { display: block; color: var(--c-ink-muted); font-variant-numeric: tabular-nums; }

/* The big view: dark in both themes - a photo reads best on black. */
.mx-lightbox {
  width: 100vw; height: 100dvh; max-width: none; max-height: none; margin: 0; padding: 0;
  border: 0; background: var(--c-scrim); color: var(--c-scrim-ink); overscroll-behavior: contain;
}
.mx-lightbox::backdrop { background: color-mix(in srgb, var(--c-scrim) 60%, transparent); }
.mx-lightbox[open] { display: block; animation: mx-fade-in 0.18s ease; }
.mx-lightbox__figure {
  box-sizing: border-box; display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 0.75rem; width: 100%; height: 100%; margin: 0; padding: 3.75rem 1rem 3rem; touch-action: pan-y;
}
.mx-lightbox__img { max-width: 100%; max-height: calc(100dvh - 9rem); object-fit: contain; border-radius: var(--radius-sm); user-select: none; -webkit-user-drag: none; }
.mx-lightbox__caption { max-width: 40rem; font-size: 0.875rem; line-height: 1.45; color: color-mix(in srgb, var(--c-scrim-ink) 82%, transparent); text-align: center; }
.mx-lightbox__btn {
  position: absolute; display: grid; place-items: center; width: 2.75rem; height: 2.75rem;
  border: 0; border-radius: 9999px; background: color-mix(in srgb, var(--c-scrim-ink) 14%, transparent); color: var(--c-scrim-ink); cursor: pointer;
}
.mx-lightbox__btn svg { width: 1.25rem; height: 1.25rem; }
.mx-lightbox__btn:hover { background: color-mix(in srgb, var(--c-scrim-ink) 24%, transparent); }
.mx-lightbox__btn:focus-visible { outline: 2px solid var(--c-scrim-ink); outline-offset: 2px; }
.mx-lightbox__close { top: max(0.75rem, env(safe-area-inset-top)); inset-inline-end: 0.75rem; }
.mx-lightbox__prev { top: 50%; inset-inline-start: 0.75rem; translate: 0 -50%; }
.mx-lightbox__next { top: 50%; inset-inline-end: 0.75rem; translate: 0 -50%; }
/* Previous is on the right in Arabic, and points right. */
[dir='rtl'] :is(.mx-lightbox__prev, .mx-lightbox__next) svg { transform: scaleX(-1); }
.mx-lightbox.is-single .mx-lightbox__prev,
.mx-lightbox.is-single .mx-lightbox__next { display: none; }
.mx-lightbox__count {
  position: absolute; top: max(1.4rem, calc(env(safe-area-inset-top) + 0.65rem)); left: 50%; translate: -50% 0; margin: 0;
  font-size: 0.8125rem; color: color-mix(in srgb, var(--c-scrim-ink) 75%, transparent); font-variant-numeric: tabular-nums;
}
@media (max-width: 575.98px) {
  .mx-lightbox__prev, .mx-lightbox__next { top: auto; bottom: max(0.75rem, env(safe-area-inset-bottom)); translate: none; }
}
/* The question before something that cannot be undone (kit/confirm.ts, data-mx-confirm): the kit's
   own small window, never window.confirm. Cancel first, the action last; a danger action stays red. */
.mx-confirm {
  width: min(28rem, calc(100% - 2rem));
  border: 1px solid var(--c-line-strong);
  border-radius: var(--radius-lg);
  background: var(--c-surface);
  color: var(--c-ink);
  padding: 1.5rem;
  box-shadow: var(--shadow-lift);
}
.mx-confirm::backdrop { background: color-mix(in srgb, var(--c-scrim) 50%, transparent); }
.mx-confirm__text { margin: 0 0 1.25rem; font-size: 0.9375rem; line-height: 1.55; }
.mx-confirm__actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 0.5rem; }
html.mx-confirm-open { overflow: hidden; }
html.mx-lightbox-open, html.mx-overlay-open { overflow: hidden; }

/* An overlay: a centred panel on a laptop, a sheet that fills the screen on a phone. */
.mx-overlay {
  box-sizing: border-box; width: min(40rem, calc(100vw - 2rem)); max-width: none; max-height: min(90dvh, 56rem);
  margin: auto; padding: 0; border: 1px solid var(--c-line); border-radius: var(--radius-lg);
  background: var(--c-surface); color: var(--c-ink); box-shadow: 0 24px 48px -12px color-mix(in srgb, var(--c-scrim) 38%, transparent); overflow: hidden;
}
.mx-overlay[open] { display: flex; flex-direction: column; animation: mx-overlay-in 0.22s var(--ease-out-soft, ease); }
.mx-overlay::backdrop { background: color-mix(in srgb, var(--c-scrim) 50%, transparent); -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px); }
.mx-overlay__head {
  display: flex; flex: none; align-items: center; justify-content: space-between; gap: 0.75rem;
  padding: 0.75rem 0.75rem 0.75rem 1.25rem; border-bottom: 1px solid var(--c-line);
}
.mx-overlay__title { margin: 0; font-size: 1.0625rem; font-weight: 600; line-height: 1.3; }
.mx-overlay__close {
  display: grid; flex: none; place-items: center; width: 2.75rem; height: 2.75rem;
  border: 0; border-radius: 9999px; background: transparent; color: var(--c-ink-muted); cursor: pointer;
}
.mx-overlay__close:hover { background: var(--c-surface-2); color: var(--c-ink); }
.mx-overlay__close:focus-visible { outline: 2px solid var(--c-accent); outline-offset: 2px; }
.mx-overlay__close svg { width: 1.125rem; height: 1.125rem; }
.mx-overlay__body { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 1.25rem; overscroll-behavior: contain; }
.mx-overlay__body > * + * { margin-top: 1rem; }
.mx-overlay__body .mx-page,
.mx-overlay__body .mx-page__inner { max-width: none; padding: 0; margin: 0; }
.mx-overlay__loading { margin: 0; font-size: 0.875rem; color: var(--c-ink-muted); }
@keyframes mx-overlay-in { from { opacity: 0; transform: translateY(0.75rem) scale(0.98); } }
@keyframes mx-sheet-in { from { transform: translateY(100%); } }
@keyframes mx-fade-in { from { opacity: 0; } }
@media (max-width: 575.98px) {
  .mx-overlay { width: 100vw; height: 100dvh; max-height: none; margin: 0; border: 0; border-radius: 0; }
  .mx-overlay[open] { animation-name: mx-sheet-in; }
  .mx-overlay__head { padding-top: max(0.75rem, env(safe-area-inset-top)); }
  .mx-overlay__body { padding: 1rem 1rem max(1.25rem, env(safe-area-inset-bottom)); }
}
@media (prefers-reduced-motion: reduce) {
  .mx-overlay[open], .mx-lightbox[open] { animation: none; }
}

/* The bottom sheet (kit/sheet.ts, data-mx-sheet): on a phone, docked to the bottom and always there,
   a handle on top that opens it to the full screen; on a wider screen, the block it always was. The
   handle and the scrim are the script's, so without it nothing here applies. */
.mx-sheet__handle, .mx-sheet__scrim { display: none; }
@media (max-width: 720px) {
  .mx-sheet[data-mx-sheet-on] {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 45;
    box-sizing: border-box; display: flex; flex-direction: column;
    margin: 0; max-height: min(var(--mx-sheet-peek, 45dvh), calc(100dvh - var(--kb, 0px) - 3rem));
    padding: 0 1rem calc(max(env(safe-area-inset-bottom) - 10px, 4px) + 0.75rem);
    /* Colours: the sheet's own, below. */
    border: 1px solid; border-bottom: 0; border-radius: 1.25rem 1.25rem 0 0;
    overscroll-behavior: contain;
    transition: max-height 0.28s var(--ease-out-soft, ease), height 0.28s var(--ease-out-soft, ease);
    animation: mx-sheet-in 0.28s var(--ease-out-soft, ease);
  }
  .mx-sheet[data-mx-sheet-on][data-state='full'] {
    max-height: none;
    height: calc(100dvh - var(--kb, 0px) - max(env(safe-area-inset-top), 0.75rem) - 1.5rem);
  }
  .mx-sheet[data-mx-sheet-on][data-dragging] { transition: none; user-select: none; }
  /* Everything in it keeps its height except the part that scrolls - the heading and the form stay in view. */
  .mx-sheet[data-mx-sheet-on] > * { flex: none; }
  .mx-sheet[data-mx-sheet-on] [data-mx-sheet-scroll] { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
  /* Open, the scrolling part takes all the room there is - a product's own cap on it (a chat list's
     max-height for the page) would leave a band of nothing under the form (Hotel QR, 2026-10-09). */
  .mx-sheet[data-mx-sheet-on][data-state='full'] [data-mx-sheet-scroll] { max-height: none; }
  .mx-sheet[data-mx-sheet-on] > :has([data-mx-sheet-scroll]) { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
  .mx-sheet__handle {
    display: flex; flex: none; align-items: center; justify-content: center; align-self: stretch;
    min-height: 2.25rem; margin: 0 -1rem; padding: 0; border: 0; background: transparent;
    cursor: grab; touch-action: none; -webkit-tap-highlight-color: transparent;
  }
  /* From the sheet's own ink: a line colour vanished into the dark-page sheet, which is about as light as one. */
  .mx-sheet__handle::before { content: ''; width: 2.5rem; height: 0.3125rem; border-radius: 9999px; background: color-mix(in srgb, var(--c-ink) 32%, transparent); }
  .mx-sheet__handle:focus-visible { outline: none; }
  .mx-sheet__handle:focus-visible::before { background: var(--c-accent); }
  .mx-sheet__scrim {
    /* margin: 0 - it sits among the page's own blocks, whose spacing rule (.mx-page > * + *) would push it down. */
    position: fixed; inset: 0; z-index: 44; margin: 0; background: color-mix(in srgb, var(--c-scrim) 45%, transparent);
    animation: mx-fade-in 0.2s ease;
  }
  :root[data-mx-sheet-full] .mx-sheet__scrim { display: block; }
  :root[data-mx-sheet-full] { overflow: hidden; }
  /* Room at the bottom of the page for the settled sheet, so nothing hides behind it. */
  :root[data-mx-sheet-shown] body { padding-bottom: calc(var(--mx-sheet-h, 0px) + 1rem); }
  /* Typing: above the keyboard on iOS, the editor bar's room inside the sheet on Android (src/lib/keyboard.ts). */
  :root[data-kb] .mx-sheet[data-mx-sheet-on] { bottom: var(--kb, 0px); padding-bottom: calc(var(--kb-bar, 0px) + 0.5rem); }
}
@media (max-width: 720px) and (prefers-reduced-motion: reduce) {
  .mx-sheet[data-mx-sheet-on] { transition: none; animation: none; }
  .mx-sheet__scrim { animation: none; }
}

/* The sheet's own colours, small or open, worked out on <html> so the sheet can re-point the tokens its
   contents read without a loop. A LIGHT page: the whole sheet is dark like a phone's own "now playing",
   pill and open sheet alike ("on light mode the extended mode colors should follow the closed mode"), its
   insides - list, fields, chips - a step lighter than it. A DARK page: the page's card colour 15% brighter
   ("like we have on dark for the card bgs like the wifi section's bg color, only 15% brighter"), its
   insides the page's own darker surfaces, set into it. Chris, 2026-10-09, on Hotel QR's room chat. */
:root {
  /* Toward the ink, not toward #fff: the dark ink is a few units off white, so it looks the same, and a
     product's gate reads this file as its own sheet, where a bare colour is a finding (kit task #3111). */
  --mx-sheet-bg: color-mix(in srgb, var(--c-surface) 85%, var(--c-ink)); --mx-sheet-line: color-mix(in srgb, var(--c-surface) 76%, var(--c-ink));
  /* Small text and field edges a step towards the ink: the sheet is lighter than the page, and the page's own
     subtle read 3.4:1 on it (4.75:1 now; fields 3.3:1 on the sheet, 4.9:1 on their box). */
  --mx-sheet-ink: var(--c-ink); --mx-sheet-muted: var(--c-ink-muted); --mx-sheet-subtle: color-mix(in srgb, var(--c-ink-subtle) 75%, var(--c-ink));
  --mx-sheet-surface: var(--c-surface); --mx-sheet-surface-2: var(--c-surface-2);
  --mx-sheet-in-line: var(--c-line); --mx-sheet-in-line-strong: var(--c-line-strong);
  --mx-sheet-in-line-field: color-mix(in srgb, var(--c-line-field) 80%, var(--c-ink));
  /* A big, soft shadow around it - dark in both themes: a light glow on a dark page looked wrong ("make
     that black too"). The page's own deepest colour, never #000. */
  --mx-sheet-glow: 0 18px 56px -6px color-mix(in srgb, var(--c-canvas) 88%, transparent), 0 4px 16px color-mix(in srgb, var(--c-canvas) 60%, transparent);
}
:root[data-theme='light'] {
  --mx-sheet-bg: var(--c-ink); --mx-sheet-line: transparent;
  --mx-sheet-ink: var(--c-canvas); --mx-sheet-muted: color-mix(in srgb, var(--c-canvas) 68%, var(--c-ink)); --mx-sheet-subtle: color-mix(in srgb, var(--c-canvas) 62%, var(--c-ink));
  --mx-sheet-surface: color-mix(in srgb, var(--c-canvas) 8%, var(--c-ink)); --mx-sheet-surface-2: color-mix(in srgb, var(--c-canvas) 14%, var(--c-ink));
  --mx-sheet-in-line: color-mix(in srgb, var(--c-canvas) 16%, var(--c-ink)); --mx-sheet-in-line-strong: color-mix(in srgb, var(--c-canvas) 26%, var(--c-ink));
  --mx-sheet-in-line-field: color-mix(in srgb, var(--c-canvas) 44%, var(--c-ink));
  --mx-sheet-glow: 0 18px 56px -6px color-mix(in srgb, var(--c-ink) 42%, transparent), 0 4px 16px color-mix(in srgb, var(--c-ink) 14%, transparent);
}
@media (prefers-color-scheme: light) {
  :root:not([data-theme]) {
    --mx-sheet-bg: var(--c-ink); --mx-sheet-line: transparent;
    --mx-sheet-ink: var(--c-canvas); --mx-sheet-muted: color-mix(in srgb, var(--c-canvas) 68%, var(--c-ink)); --mx-sheet-subtle: color-mix(in srgb, var(--c-canvas) 62%, var(--c-ink));
    --mx-sheet-surface: color-mix(in srgb, var(--c-canvas) 8%, var(--c-ink)); --mx-sheet-surface-2: color-mix(in srgb, var(--c-canvas) 14%, var(--c-ink));
    --mx-sheet-in-line: color-mix(in srgb, var(--c-canvas) 16%, var(--c-ink)); --mx-sheet-in-line-strong: color-mix(in srgb, var(--c-canvas) 26%, var(--c-ink));
    --mx-sheet-in-line-field: color-mix(in srgb, var(--c-canvas) 44%, var(--c-ink));
    --mx-sheet-glow: 0 18px 56px -6px color-mix(in srgb, var(--c-ink) 42%, transparent), 0 4px 16px color-mix(in srgb, var(--c-ink) 14%, transparent);
  }
}
.mx-sheet__open { display: none; }
@media (max-width: 720px) {
  .mx-sheet[data-mx-sheet-on] {
    background: var(--mx-sheet-bg); color: var(--mx-sheet-ink); border-color: var(--mx-sheet-line);
    box-shadow: var(--mx-sheet-glow); color-scheme: dark;
    --c-ink: var(--mx-sheet-ink); --c-ink-muted: var(--mx-sheet-muted); --c-ink-subtle: var(--mx-sheet-subtle);
    --c-surface: var(--mx-sheet-surface); --c-surface-2: var(--mx-sheet-surface-2);
    --c-line: var(--mx-sheet-in-line); --c-line-strong: var(--mx-sheet-in-line-strong); --c-line-field: var(--mx-sheet-in-line-field);
    /* The label colour is a mix resolved where it is declared (:root, the page's colours), so it is made again
       here from the sheet's own: on a light page the labels were the page's dark grey on the dark sheet (3:1). */
    --mx-label: color-mix(in srgb, var(--c-ink-subtle) 90%, var(--c-ink));
    /* Fields sit on the canvas colour (.mx-field input/select): inside the sheet that is the sheet's
       own surface, or a select on a light page drew pale words on a white box (Storefront's editor,
       2026-11-02). The --mx-sheet-* values are resolved on :root, so this is no var() loop. */
    --c-canvas: var(--mx-sheet-surface);
  }
  /* Made small (data-mx-sheet-bar): a pill over the bottom of the page - its bar alone and the button
     that brings it back. */
  .mx-sheet[data-mx-sheet-on][data-state='min'] {
    left: 0.75rem; right: 0.75rem; bottom: calc(env(safe-area-inset-bottom) + 0.75rem);
    max-height: none; padding: 0.5rem;
    border: 1px solid var(--mx-sheet-line); border-radius: 9999px;
    cursor: pointer; touch-action: none; -webkit-tap-highlight-color: transparent;
  }
  .mx-sheet[data-mx-sheet-on][data-state='min'] > :not([data-mx-sheet-bar]) { display: none; }
  .mx-sheet[data-mx-sheet-on][data-state='min'] > [data-mx-sheet-bar] {
    display: flex; align-items: center; gap: 0.625rem; min-width: 0; margin: 0;
  }
  /* One line each, cut short rather than wrapped: the pill keeps its height whatever the hotel is called. */
  .mx-sheet[data-mx-sheet-on][data-state='min'] > [data-mx-sheet-bar] :is(span, p, strong, b, small):not(.mx-sheet__open) {
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .mx-sheet[data-mx-sheet-on][data-state='min'] .mx-sheet__open {
    display: inline-flex; align-items: center; justify-content: center; flex: none;
    margin-inline-start: auto; min-height: 2.75rem; padding: 0 1.25rem;
    border: 0; border-radius: 9999px; background: var(--mx-sheet-ink); color: var(--mx-sheet-bg);
    /* Its own size: the bar is usually a heading, and a button at heading size shouts. */
    font: inherit; font-size: 0.9375rem; font-weight: 700; line-height: 1; letter-spacing: 0; cursor: pointer;
  }
  .mx-sheet[data-mx-sheet-on][data-state='min'] .mx-sheet__open:focus-visible { outline: 2px solid var(--c-accent); outline-offset: 2px; }
}

/* Opening hours as a state (kit/hours.ts, data-mx-hours): a dot before "Open until 20:00", green while open. */
[data-mx-hours-on]::before {
  content: ''; display: inline-block; width: 0.5em; height: 0.5em; margin-inline-end: 0.45em;
  border-radius: 50%; vertical-align: 0.08em; background: var(--c-ink-subtle);
}
[data-mx-hours-on][data-mx-open]::before { background: var(--c-ok); }

/* Sortable columns: the header is a button, its arrow shows which way. */
.mx-sort {
  display: inline-flex; align-items: center; gap: 0.4rem; padding: 0; margin: 0; border: 0; background: none;
  font: inherit; letter-spacing: inherit; text-transform: inherit; color: inherit; cursor: pointer;
}
.mx-sort:focus-visible { outline: 2px solid var(--c-accent); outline-offset: 3px; border-radius: 2px; }
.mx-sort__arrow {
  flex: none; width: 0.4rem; height: 0.4rem; border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor;
  opacity: 0; transform: translateY(-0.1rem) rotate(45deg); transition: transform 0.15s ease, opacity 0.15s ease;
}
@media (hover: hover) { .mx-sort:hover { color: var(--c-ink); } .mx-sort:hover .mx-sort__arrow { opacity: 0.45; } }
.mx-table th[aria-sort='ascending'],
.mx-table th[aria-sort='descending'] { color: var(--c-ink); }
.mx-table th[aria-sort='ascending'] .mx-sort__arrow { opacity: 1; transform: translateY(0.12rem) rotate(-135deg); }
.mx-table th[aria-sort='descending'] .mx-sort__arrow { opacity: 1; transform: translateY(-0.1rem) rotate(45deg); }
/* The phone's "Sort by": a kit label and a kit select, like any field (Chris, 2026-10-05: "Rendezés
   looks strange on mobile" - it sat flush to the panel's edge with a bare dash for a choice). In a
   panel it lines up with the rows' text, which sit .875rem in. */
/* Label above, the list across the whole row - as a form field is. Beside each other, the label took
   a third of a phone and "Rückgabe bis · aufsteigend" was cut to "aufstei" (RentKeeper in German,
   2026-10-05); a translated label and option are often a third longer than the English. */
.mx-sortbar { display: flex; flex-direction: column; align-items: stretch; gap: 0.375rem; margin: 0 0 0.75rem; }
.mx-sortbar[hidden] { display: none; }
.mx-sortbar .mx-label { margin: 0; white-space: nowrap; }
/* Inset like the rows wherever the bar sits in a container with no padding of its own: straight in
   a panel, or in a flush panel body (RentKeeper's equipment list, 2026-10-05). */
.mx-panel > .mx-sortbar,
.mx-panel__body--flush > .mx-sortbar { padding: 0.875rem 0.875rem 0; margin-bottom: 0.25rem; }
.mx-sortbar select {
  flex: 1 1 auto; min-width: 0; height: 2.5rem; padding: 0 0.75rem;
  border: 1px solid var(--c-line-field); border-radius: var(--radius-sm);
  background: var(--c-canvas); color: var(--c-ink); font: inherit; font-size: 0.875rem;
}
.mx-sortbar select:focus-visible { outline: 2px solid var(--c-accent); outline-offset: 1px; }

/* Filter chips: one row of choices above a list, each with its count. */
.mx-filter { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 0.875rem; }
.mx-filter button {
  display: inline-flex; align-items: center; gap: 0.4rem; padding: 0.375rem 0.75rem;
  border: 1px solid var(--c-line-strong); border-radius: 9999px; background: transparent;
  color: var(--c-ink-muted); font: inherit; font-size: 0.8125rem; font-weight: 500; line-height: 1.3; cursor: pointer;
  transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
@media (hover: hover) { .mx-filter button:hover { color: var(--c-ink); border-color: var(--c-ink-subtle); } }
.mx-filter button:focus-visible { outline: 2px solid var(--c-accent); outline-offset: 2px; }
.mx-filter button[aria-pressed='true'] { background: var(--c-ink); border-color: var(--c-ink); color: var(--c-canvas); }
.mx-filter__count { font-size: 0.75rem; font-variant-numeric: tabular-nums; opacity: 0.7; }
.mx-filter__empty { margin: 1rem 0 0; font-size: 0.875rem; color: var(--c-ink-muted); }
.mx-filter__empty[hidden] { display: none; }

/* ── Touch ───────────────────────────────────────────────────────────────
   THIS LAYER WAS MEASURED OFF A LAPTOP, and it shows: a button comes out about 39px tall,
   a field is 40px, and both carry 14px text. On a mouse that is right. On a phone it is
   two separate bugs, and Voks found them - a product entered by scanning a sticker on an
   office wall, where every control is touched and nothing is clicked.

   44px is the touch minimum (Apple's number; Google says 48). 16px is not a taste: iOS
   Safari zooms the whole page in when a field with smaller text takes focus, and the way
   back out is a pinch - which is why the control font is one of the few px values the kit
   is allowed to keep.

   Gated on a coarse pointer so a desktop keeps the tighter scale it was designed at.
   `hover: none` as well, because a laptop with a touchscreen reports a coarse pointer for
   its finger and still wants the mouse-sized chrome. */
@media (hover: none) and (pointer: coarse) {
  .mx-btn { min-height: 44px; }
  .mx-btn--icon { min-width: 44px; }
  .mx-field input,
  .mx-field select,
  .mx-input { min-height: 44px; }
  .mx-field input,
  .mx-field select,
  .mx-field textarea,
  .mx-input { font-size: 16px; }
  .mx-nav a,
  .mx-tabs a,
  .mx-brand,
  .mx-side__list .mx-side__item,
  .mx-side__toggle { min-height: 44px; }
  .mx-filter button,
  .mx-sortbar select { min-height: 44px; }
  .mx-sortbar select { font-size: 16px; }
  .mx-photos__remove { width: 2.25rem; height: 2.25rem; }
  /* Links that are something to press, not a word in a sentence (FleetFault #996, 2026-10-03):
     the "Sign in" hint under a form was 38 x 17 px, "Open in OpenStreetMap" 179 x 21. Small-text
     links and a link standing on its own (.mx-link) become a 44px box; links inside running
     text stay text - a 44px box mid-sentence jolts the line. */
  .mx-sub a,
  .mx-hint a,
  .mx-empty a,
  .mx-crumb a,
  a.mx-link { display: inline-flex; align-items: center; min-height: 44px; min-width: 44px; }
  /* A crumb is a way back, not a paragraph: its 44px link does not push the page head further down. */
  .mx-crumb { margin-block: -0.75rem 0; }
  /* A table's row link is the row's own target, 44px tall to a finger (plates wrapped to 18px) - but the 44px is its
     REACH, not its box: an invisible layer centred on the words, so they keep the row's line and take only the room
     they need. A 44px box pulled back by -.75rem each way (2026-11-02) worked for one line only: a name that wrapped
     to two lines in a phone's stacked row ran 10px into the line under it (Storefront's products, 2026-11-03). */
  .mx-table td a:not(.mx-btn) { display: inline-flex; align-items: center; position: relative; }
  .mx-table td a:not(.mx-btn)::after { content: ''; position: absolute; inset-inline: 0; top: 50%; height: max(100%, 44px); transform: translateY(-50%); }
  .mx-table td .mx-btn { margin-block: -0.375rem; }
  .mx-table a[data-mx-row] { white-space: nowrap; }
  /* A row link holding a second line (.sub: an e-mail under a name) keeps it UNDER the name - as a flex row
     the two ran together ("Northlightdemo@northlight…", 2026-11-02). */
  .mx-table td a:has(> .sub) { flex-direction: column; align-items: flex-start; justify-content: center; }
  /* A checkbox in a cell is pressed through its label: 44 x 44, the box in the middle. */
  .mx-table td > label:has(> input[type='checkbox']) {
    display: inline-flex; align-items: center; justify-content: center; min-width: 44px; min-height: 44px; cursor: pointer;
  }
  .mx-table td input[type='checkbox'] { width: 1.25rem; height: 1.25rem; accent-color: var(--c-accent); }
}

/* ── The one button a screen is for ─────────────────────────────────────────
   `.mx-cta`: the landing pages' call to action (landing.css `.lp-btn--primary`), brought to the
   app layer for the single most important button on an app screen - Studio's Continue (Chris,
   2026-10-01: "glow and animate like we do the cta btns on the landing pages"). The same
   gradient flowing A → B → shine → B → A, the same breathing glow, the same light sweeping
   across, the same lift on hover; change one and change the other. One per screen at most:
   a second one glowing beside it and neither is the thing to press.
   Everything stops for anybody who asked for less motion (tokens.css). */
.mx-cta {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  background: linear-gradient(
    120deg,
    var(--c-accent-strong) 0%,
    var(--c-accent) 25%,
    var(--c-accent-shine, color-mix(in srgb, var(--c-accent) 62%, var(--c-rec))) 50%,
    var(--c-accent) 75%,
    var(--c-accent-strong) 100%
  );
  background-size: 200% 100%;
  animation: mx-cta-flow 9s linear infinite, mx-cta-breathe 3.6s ease-in-out infinite;
  box-shadow: 0 0 24px color-mix(in srgb, var(--c-accent) 35%, transparent);
  transition: transform 0.2s ease, box-shadow 0.3s ease;
}
/* The sweep, under the content: every child gets a layer above it, so the light passes behind
   the words rather than washing over them. */
.mx-cta::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  background: linear-gradient(115deg, transparent 38%, rgba(255, 255, 255, 0.18) 50%, transparent 62%);
  transform: translateX(-130%);
  animation: mx-cta-sheen 5.5s ease-in-out infinite;
  pointer-events: none;
}
.mx-cta > * { position: relative; z-index: 1; }
.mx-cta:hover:not(:disabled),
.mx-cta:focus-visible {
  transform: translateY(-2px);
  /* Without the breathing, or the running animation would outrank this stronger glow. */
  animation: mx-cta-flow 9s linear infinite;
  box-shadow:
    0 0 44px color-mix(in srgb, var(--c-accent) 60%, transparent),
    0 0 0 0.375rem color-mix(in srgb, var(--c-accent) 18%, transparent);
}
.mx-cta:active:not(:disabled) { transform: translateY(0); }
@keyframes mx-cta-flow { 0% { background-position: 200% 50%; } 100% { background-position: 0% 50%; } }
@keyframes mx-cta-breathe {
  0%, 100% { box-shadow: 0 0 1.5rem color-mix(in srgb, var(--c-accent) 35%, transparent); }
  50% { box-shadow: 0 0 2.625rem 0.25rem color-mix(in srgb, var(--c-accent) 55%, transparent); }
}
@keyframes mx-cta-sheen {
  0%, 62% { transform: translateX(-130%); }
  78%, 100% { transform: translateX(130%); }
}

/* ── Look settings: data-mx-* on <html> (kit/look.ts, DESIGN.md "Look settings", 2026-11-02) ─────────────────
   What a product's owner chose for its pages - a shop owner's, in Storefront Ninja first. Default = no attribute =
   none of this applies, so every page that chose nothing is what it was. Corners and line weight are tokens
   (tokens.css --mx-control-radius, --mx-stroke, --mx-icon-stroke) the controls above already read; the rest are
   the attributes themselves, styled here for the kit's own parts and by each product for its own. Like the tokens
   they work on any element for its part of a page, not only on <html>. */
/* Line weight reaches the kit's line icons too (a lucide icon's stroke-width is an attribute, which CSS outranks). */
[data-mx-stroke] :is(.mx-nav, .mx-tabs, .mx-side, .mx-btn, .mx-crumb, .mx-switch) svg[stroke] { stroke-width: var(--mx-icon-stroke, 2); }
/* Buttons: outline - the primary keeps its accent as a border and its words, the fill goes. */
[data-mx-buttons='outline'] .mx-btn--primary { background: transparent; border-color: var(--c-accent); color: var(--c-accent); }
[data-mx-buttons='outline'] .mx-btn--primary:hover { background: color-mix(in srgb, var(--c-accent) 12%, transparent); }
/* Accent on hover only: the primary is ink until it is pointed at or reached by the keyboard, then the accent. */
[data-mx-accent='hover'] .mx-btn--primary:not(:hover, :focus-visible) { background: var(--c-ink); border-color: var(--c-ink); color: var(--c-surface); }
[data-mx-accent='hover'][data-mx-buttons='outline'] .mx-btn--primary:not(:hover, :focus-visible),
[data-mx-accent='hover'] [data-mx-buttons='outline'] .mx-btn--primary:not(:hover, :focus-visible) { background: transparent; border-color: var(--c-ink); color: var(--c-ink); }
/* Nav labels off: a nav item with an icon shows only the icon. The words stay for a screen reader (hidden from
   sight, not display: none) and the item's own title, if it has one, is the tooltip. An item with no icon keeps
   its words - an empty link is worse than a label. */
[data-mx-navlabels='off'] :is(.mx-nav, .mx-tabs) a:has(> svg) > span {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; border: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}
/* Counts off: the count beside each filter chip (kit/tables.ts). The kit's nav, tabs and sidebar draw no counts;
   a product's own counts follow the same attribute in its own sheet. */
[data-mx-counts='off'] .mx-filter__count { display: none; }

/* ── Right to left ────────────────────────────────────────────────────────────────────────── */
/* A photo strip scrolls the other way: its fading edge is the other edge. */
[dir='rtl'] .mx-gallery[data-mx-more='end'] { -webkit-mask-image: linear-gradient(to left, black calc(100% - 4.5rem), transparent); mask-image: linear-gradient(to left, black calc(100% - 4.5rem), transparent); }
[dir='rtl'] .mx-gallery[data-mx-more='start'] { -webkit-mask-image: linear-gradient(to right, black calc(100% - 3rem), transparent); mask-image: linear-gradient(to right, black calc(100% - 3rem), transparent); }
[dir='rtl'] .mx-gallery[data-mx-more='both'] { -webkit-mask-image: linear-gradient(to left, transparent, black 3rem, black calc(100% - 4.5rem), transparent); mask-image: linear-gradient(to left, transparent, black 3rem, black calc(100% - 4.5rem), transparent); }
/* Back links and anything a product marks data-mx-flip point the way the page reads. */
[dir='rtl'] :is(.mx-crumb svg, [data-mx-flip]) { transform: scaleX(-1); }

/* Every kit dropdown draws its own chevron at a fixed inset: the native arrow sits on the border
   and padding cannot move it (tokens.css select-field; Chris, 2026-10-05). --mx-end is the side
   a line ends on - left in Arabic. */
.mx-field select, select.mx-input, .mx-sortbar select,
select[data-mx-select]:not([multiple], [size]), [data-mx-select] select:not([multiple], [size]) {
  -webkit-appearance: none;
  appearance: none;
  padding-inline-end: 2.25rem;
  background-image: var(--select-chevron);
  background-repeat: no-repeat;
  background-position: var(--mx-end, right) 0.75rem center;
  background-size: 12px 12px;
}


/* ── One field for a date and its time: data-mx-datetime (kit/datetime.ts, 2026-10-13) ───────────────────────
   The input stays the form's field, out of sight; the button reads like a field and opens the panel in flow
   under it - the month and that day's times side by side, taking turns when the field is narrow. */
.mx-dt { position: relative; display: grid; gap: 0.375rem; min-width: 0; container-type: inline-size; }
.mx-field .mx-dt input.mx-dt__native {
  position: absolute; inset-inline-start: 0; top: 0;
  width: 1px; height: 1px; padding: 0; margin: 0; border: 0;
  opacity: 0; overflow: hidden; clip-path: inset(50%); pointer-events: none;
}
.mx-dt__field {
  display: flex; align-items: center; gap: 0.5rem;
  max-width: 22rem; text-align: start; cursor: pointer;
  padding-inline-end: 2.25rem;
  background-image: var(--select-chevron); background-repeat: no-repeat;
  background-position: var(--mx-end, right) 0.75rem center; background-size: 12px 12px;
}
.mx-dt__field.is-empty { color: var(--c-ink-subtle); }
.mx-dt__field[aria-invalid='true'] { border-color: var(--c-rec); }
.mx-dt__field[aria-expanded='true'] { border-color: var(--c-accent); }
.mx-dt__shown { min-width: 0; overflow-wrap: anywhere; font-variant-numeric: tabular-nums; }
.mx-dt__panel {
  display: grid; grid-template-columns: minmax(0, 18rem) minmax(6.5rem, 9rem); gap: 1rem;
  width: fit-content; max-width: 100%;
  padding: 0.75rem; border: 1px solid var(--c-line-strong); border-radius: var(--radius-md);
  background: var(--c-surface);
}
.mx-dt__panel[hidden] { display: none; }
/* A day without a time (data-mx-datetime="date"): the month alone. */
.mx-dt--date .mx-dt__panel { grid-template-columns: minmax(0, 18rem); }
.mx-dt__days, .mx-dt__times { min-width: 0; }
.mx-dt__head { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; min-height: 2.25rem; margin: 0 0 0.375rem; }
.mx-dt__month, .mx-dt__label { font-size: 0.875rem; font-weight: 600; color: var(--c-ink); }
.mx-dt__label { font-family: var(--font-mono, ui-monospace, monospace); font-size: var(--mx-label-size); font-weight: 400; text-transform: uppercase; letter-spacing: 0.08em; color: var(--mx-label); }
.mx-dt__nav, .mx-dt__back, .mx-dt__day, .mx-dt__time {
  margin: 0; border: 0; background: none; font: inherit; color: var(--c-ink); cursor: pointer;
  border-radius: var(--radius-sm); -webkit-tap-highlight-color: transparent;
}
.mx-dt__nav { width: 2.25rem; height: 2.25rem; font-size: 1.25rem; line-height: 1; }
.mx-dt__back { display: none; padding: 0 0.5rem; height: 2.25rem; font-size: 0.875rem; font-weight: 600; text-align: start; }
.mx-dt__grid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 2px; }
.mx-dt__wd { padding: 0.25rem 0; text-align: center; font-size: 0.6875rem; color: var(--c-ink-subtle); }
.mx-dt__day { aspect-ratio: 1; min-height: 2.25rem; font-size: 0.875rem; font-variant-numeric: tabular-nums; }
.mx-dt__day.is-out { color: var(--c-ink-subtle); }
.mx-dt__day.is-today { box-shadow: inset 0 0 0 1px var(--c-accent); }
.mx-dt__list { display: grid; gap: 2px; align-content: start; max-height: 17rem; overflow-y: auto; overscroll-behavior: contain; }
.mx-dt__time { height: 2.25rem; padding: 0 0.5rem; font-size: 0.875rem; font-variant-numeric: tabular-nums; }
.mx-dt__day.is-on, .mx-dt__time.is-on { background: var(--c-accent); color: var(--c-accent-ink); font-weight: 600; }
.mx-dt__nav:disabled, .mx-dt__day:disabled, .mx-dt__time:disabled { opacity: 0.3; cursor: default; }
@media (hover: hover) {
  .mx-dt__nav:not(:disabled):hover, .mx-dt__back:hover, .mx-dt__day:not(:disabled, .is-on):hover, .mx-dt__time:not(:disabled, .is-on):hover { background: var(--c-surface-sunk); }
}
.mx-dt__nav:focus-visible, .mx-dt__back:focus-visible, .mx-dt__day:focus-visible, .mx-dt__time:focus-visible { outline: 2px solid var(--c-accent); outline-offset: 1px; }
.mx-dt__msg[hidden] { display: none; }
/* A narrow field: one at a time - the month, then that day's times with the date above them to go back. */
@container (max-width: 29rem) {
  .mx-dt__panel { grid-template-columns: minmax(0, 1fr); width: auto; }
  .mx-dt__panel[data-showing='days'] .mx-dt__times, .mx-dt__panel[data-showing='times'] .mx-dt__days { display: none; }
  .mx-dt__back { display: inline-flex; align-items: center; gap: 0.375rem; }
  .mx-dt__list { grid-template-columns: repeat(3, minmax(0, 1fr)); max-height: 18rem; }
}

/* "Made with mixtape.works" - the same line as landing.css, for the public pages built on the app layer. */
.mx-made-with {
  display: block;
  margin: 1.5rem auto 0.75rem;
  text-align: center;
  font-size: 0.75rem;
  color: var(--c-ink-subtle);
}
.mx-made-with a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
.mx-made-with a:hover { color: var(--c-ink); }


/* ── Soft navigation (kit/softnav.ts, 2026-11-03) ───────────────────────────────────────────────
   A shell with data-mx-softnav swaps its main region instead of loading the whole page. While the next
   page is on its way (after 150 ms, so a fast one never flashes it) a thin bar in the accent runs along
   the top of the window, under the notch; the main region fades in from 40% when the reader allows
   motion. margin 0: anything the kit injects picks up the page rhythm otherwise. */
.mx-softnav-bar {
  position: fixed;
  inset-inline: 0;
  top: env(safe-area-inset-top, 0px);
  z-index: 70;
  height: 3px;
  margin: 0;
  pointer-events: none;
  transform-origin: 0 50%;
  transform: scaleX(0);
  opacity: 0;
  background: var(--c-accent);
  box-shadow: 0 0 8px color-mix(in srgb, var(--c-accent) 55%, transparent);
}
:root[dir='rtl'] .mx-softnav-bar { transform-origin: 100% 50%; }
.mx-softnav-bar[data-state='loading'] {
  opacity: 1;
  transform: scaleX(0.85);
  transition: transform 8s cubic-bezier(0.08, 0.7, 0.2, 1), opacity 0.12s ease;
}
.mx-softnav-bar[data-state='done'] {
  opacity: 0;
  transform: scaleX(1);
  transition: transform 0.2s ease, opacity 0.25s ease 0.15s;
}
.mx-softnav-live {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
/* The new page's h1 takes the focus (so a screen reader starts there); it is not a control, so no ring. */
[data-mx-softnav] h1[tabindex='-1']:focus { outline: none; }
@keyframes mx-softnav-in { from { opacity: 0.4; } }
@media (prefers-reduced-motion: no-preference) {
  .mx-softnav-in { animation: mx-softnav-in 0.18s ease-out; }
}
@media print {
  .mx-softnav-bar { display: none; }
}

/* ── Sliders, the font picker and scene tiles (2026-11-04, Storefront's Appearance > Style; Studio's design
   page draws the same things in React and can switch to these) ─────────────────────────────────────────── */

/* A slider with what its value means beside it (kit/range.ts writes the words as it moves). The track is a
   solid bar in a field's edge colour (3:1 like any control), the thumb the accent; the browser's own keys move it. */
.mx-range { display: flex; align-items: center; gap: 0.75rem; min-width: 0; }
.mx-range > input[type='range'] {
  -webkit-appearance: none; appearance: none;
  flex: 1 1 auto; min-width: 0; height: 1.75rem; margin: 0; padding: 0;
  border: 0; border-radius: 0; box-shadow: none; background: transparent; cursor: pointer; accent-color: var(--c-accent);
}
.mx-range > input[type='range']::-webkit-slider-runnable-track {
  height: 0.25rem; border-radius: 999px; background: var(--c-line-field);
}
.mx-range > input[type='range']::-moz-range-track {
  height: 0.25rem; border-radius: 999px; background: var(--c-line-field);
}
.mx-range > input[type='range']::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 1.125rem; height: 1.125rem; margin-top: -0.4375rem; border: 2px solid var(--c-surface); border-radius: 50%;
  background: var(--c-accent); box-shadow: 0 0 0 1px var(--c-accent);
}
.mx-range > input[type='range']::-moz-range-thumb {
  width: 0.875rem; height: 0.875rem; border: 2px solid var(--c-surface); border-radius: 50%;
  background: var(--c-accent); box-shadow: 0 0 0 1px var(--c-accent);
}
.mx-range > input[type='range']:focus-visible { outline: 2px solid var(--c-accent); outline-offset: 2px; border-radius: 999px; }
.mx-range > output {
  flex: 0 0 auto; min-width: 3.5rem; text-align: end;
  color: var(--c-ink); font-size: 0.875rem; font-weight: 500; font-variant-numeric: tabular-nums;
}
@media (hover: none) and (pointer: coarse) {
  .mx-range > input[type='range'] { height: 44px; }
  .mx-range > input[type='range']::-webkit-slider-thumb { width: 1.5rem; height: 1.5rem; margin-top: -0.625rem; }
  .mx-range > input[type='range']::-moz-range-thumb { width: 1.25rem; height: 1.25rem; }
}

/* Two thumbs on one track (kit/range.ts, .mx-range--dual): a low and a high end over the page's two fields, which
   the kit hides and keeps writing. The values sit over the track's two ends; the filled part between the thumbs is
   the accent. The whole track is the press target - 44px tall to a finger, and a vertical swipe still scrolls. */
.mx-range.mx-range--dual { display: block; }
/* The fields the slider is drawn over: hidden whatever display a product's own rule gives its row. */
.mx-range--dual [hidden] { display: none !important; }
.mx-range__values {
  display: flex; justify-content: space-between; gap: 0.75rem; margin-bottom: 0.125rem;
  color: var(--c-ink); font-size: 0.875rem; font-weight: 500; font-variant-numeric: tabular-nums;
}
.mx-range__values > output { min-width: 0; white-space: nowrap; }
.mx-range__slider {
  --mx-thumb: 1.125rem;
  position: relative; height: 2rem; touch-action: pan-y; cursor: pointer;
  -webkit-tap-highlight-color: transparent; user-select: none; -webkit-user-select: none;
}
.mx-range__rail {
  position: absolute; top: 50%; inset-inline: calc(var(--mx-thumb) / 2); height: 0.25rem; margin-top: -0.125rem;
  border-radius: 999px; background: var(--c-line-field);
}
.mx-range__fill {
  position: absolute; top: 0; bottom: 0; border-radius: inherit; background: var(--c-accent);
  inset-inline-start: calc(var(--lo, 0) * 100%); inset-inline-end: calc((1 - var(--hi, 1)) * 100%);
}
.mx-range__thumb {
  position: absolute; top: 50%; inset-inline-start: calc(var(--at) * 100%); z-index: 1;
  width: var(--mx-thumb); height: var(--mx-thumb); margin-top: calc(var(--mx-thumb) / -2); margin-inline-start: calc(var(--mx-thumb) / -2);
  box-sizing: border-box; border: 2px solid var(--c-surface); border-radius: 50%;
  background: var(--c-accent); box-shadow: 0 0 0 1px var(--c-accent);
  transition: transform 0.12s ease;
}
.mx-range__thumb[data-end='lo'] { --at: var(--lo, 0); }
.mx-range__thumb[data-end='hi'] { --at: var(--hi, 1); }
.mx-range__slider.is-lo-up .mx-range__thumb[data-end='lo'] { z-index: 2; }
.mx-range__slider.is-dragging .mx-range__thumb { transition: none; }
/* The real ranges: the keyboard's and a screen reader's, never seen - their thumbs are drawn above. */
.mx-range__slider > .mx-range__input {
  position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; padding: 0;
  opacity: 0; pointer-events: none; -webkit-appearance: none; appearance: none;
}
.mx-range__slider:has(> .mx-range__input[data-end='lo']:focus-visible) .mx-range__thumb[data-end='lo'],
.mx-range__slider:has(> .mx-range__input[data-end='hi']:focus-visible) .mx-range__thumb[data-end='hi'] {
  outline: 2px solid var(--c-accent); outline-offset: 2px;
}
.mx-range__slider:has(> .mx-range__input:disabled) { cursor: default; opacity: 0.5; }
@media (hover: hover) { .mx-range__slider:hover .mx-range__thumb { transform: scale(1.08); } }
@media (hover: none) and (pointer: coarse) {
  .mx-range__slider { --mx-thumb: 1.5rem; height: 2.75rem; }
}

/* The kit's dropdown list (kit/select.ts): what a select opens instead of the browser's list, to a mouse or the
   keys (a finger gets the phone's own picker). It sits in the top layer beside the select, as wide as the select
   at least; the row the pointer or the keys are on takes the product's accent, the chosen one says so in it with a
   tick. Its edge is the kit's hairline, the one the icon buttons and menus draw. */
.mx-select-pop {
  position: fixed; inset: auto; z-index: 2147483000; box-sizing: border-box;
  width: max-content; height: auto; margin: 0; padding: 0.25rem;
  overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain;
  border: 1px solid var(--c-line); border-radius: min(var(--mx-control-radius, var(--radius-sm)), var(--radius-lg));
  background: var(--c-surface); color: var(--c-ink); box-shadow: var(--shadow-lift);
  line-height: 1.35; text-align: start; outline: none;
  animation: mx-select-in 0.12s ease-out;
}
.mx-select-pop[data-side='above'] { animation-name: mx-select-up; }
@keyframes mx-select-in { from { opacity: 0; transform: translateY(-0.25rem); } }
@keyframes mx-select-up { from { opacity: 0; transform: translateY(0.25rem); } }
.mx-select-pop__opt {
  display: flex; align-items: center; gap: 0.625rem; min-height: 2.25rem; padding: 0.375rem 0.625rem;
  border-radius: max(0px, calc(min(var(--mx-control-radius, var(--radius-sm)), var(--radius-lg)) - 0.25rem));
  cursor: pointer; outline: none;
}
.mx-select-pop__text { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
.mx-select-pop__tick { display: inline-flex; flex: none; width: 1rem; height: 1rem; color: var(--c-accent); visibility: hidden; }
.mx-select-pop__tick svg { width: 100%; height: 100%; }
.mx-select-pop__opt.is-active { background: var(--c-accent-soft); }
.mx-select-pop__opt[aria-selected='true'] { color: var(--c-accent); font-weight: 600; }
.mx-select-pop__opt[aria-selected='true'] .mx-select-pop__tick { visibility: visible; }
.mx-select-pop__opt[aria-disabled='true'] { color: var(--c-ink-subtle); cursor: default; }
.mx-select-pop__opt[aria-disabled='true'].is-active { background: transparent; }
.mx-select-pop__group {
  padding: 0.5rem 0.625rem 0.25rem; color: var(--mx-label, var(--c-ink-subtle));
  font-size: 0.6875rem; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase;
}
.mx-select-pop__sep { height: 1px; margin: 0.25rem 0.375rem; background: var(--c-line); }
/* The select whose list is open reads as the field in use. */
select[aria-expanded='true'] { border-color: var(--c-accent); }

/* The font picker (kit/fontpick.ts around select[data-mx-fontpick]): the closed button and every row show
   the font's name and a sample in its own face; the list opens IN FLOW (a rail or a sheet has no room for a
   popover) with a search field on top. A font that cannot write the page's language is greyed with why. */
.mx-fontpick { position: relative; min-width: 0; }
.mx-fontpick__btn {
  display: flex; align-items: center; gap: 0.5rem; width: 100%; min-height: 3.25rem; margin: 0;
  padding: 0.375rem 0.75rem; border: var(--mx-stroke, 1px) solid var(--c-line-field);
  border-radius: var(--mx-control-radius, var(--radius-sm)); background: var(--c-surface); color: var(--c-ink);
  font: inherit; text-align: start; cursor: pointer;
}
.mx-fontpick__btn > svg { width: 1rem; height: 1rem; flex: none; color: var(--c-ink-muted); transition: transform 0.15s ease; }
.mx-fontpick.is-open .mx-fontpick__btn > svg { transform: rotate(180deg); }
.mx-fontpick__now { display: flex; flex-direction: column; flex: 1 1 auto; min-width: 0; }
.mx-fontpick__name { color: var(--c-ink-muted); font-size: 0.75rem; line-height: 1.3; }
.mx-fontpick__sample {
  overflow: hidden; color: var(--c-ink); font-size: 1.125rem; line-height: 1.35; text-overflow: ellipsis; white-space: nowrap;
}
.mx-fontpick__pop {
  margin-top: 0.375rem; overflow: hidden; border: 1px solid var(--c-line); border-radius: var(--radius-md); background: var(--c-surface);
}
.mx-fontpick__q {
  display: block; width: 100%; height: 2.5rem; margin: 0; padding: 0 0.75rem; border: 0; border-bottom: 1px solid var(--c-line);
  border-radius: 0; background: transparent; color: var(--c-ink); font: inherit; font-size: 16px; -webkit-appearance: none; appearance: none;
}
.mx-fontpick__q:focus-visible { outline: 0; background: var(--c-surface-2); }
.mx-fontpick__list { max-height: min(22rem, 55vh); margin: 0; padding: 0.25rem; overflow-y: auto; overscroll-behavior: contain; list-style: none; }
.mx-fontpick__group {
  margin: 0; padding: 0.625rem 0.5rem 0.25rem; color: var(--mx-label, var(--c-ink-subtle));
  font-size: 0.6875rem; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase;
}
.mx-fontpick__opt {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; column-gap: 0.5rem;
  margin: 0; padding: 0.375rem 0.5rem; border-radius: 0.4375rem; cursor: pointer;
}
.mx-fontpick__opt > :not(.mx-fontpick__tick) { grid-column: 1; }
.mx-fontpick__tick { grid-column: 2; grid-row: 1 / span 3; display: none; color: var(--c-accent); }
.mx-fontpick__tick svg { width: 1rem; height: 1rem; }
.mx-fontpick__opt[aria-selected='true'] .mx-fontpick__tick { display: inline-flex; }
.mx-fontpick__opt[aria-selected='true'] .mx-fontpick__name { color: var(--c-accent); font-weight: 600; }
.mx-fontpick__opt.is-active { background: var(--c-surface-2); }
.mx-fontpick__opt[aria-disabled='true'] { cursor: not-allowed; }
.mx-fontpick__opt[aria-disabled='true'] .mx-fontpick__sample { color: var(--c-ink-subtle); }
.mx-fontpick__note { color: var(--c-ink-subtle); font-size: 0.75rem; }
.mx-fontpick__none { margin: 0; padding: 0.75rem; color: var(--c-ink-muted); font-size: 0.875rem; }
@media (hover: none) and (pointer: coarse) {
  .mx-fontpick__opt { min-height: 44px; }
}

/* Background animations as tiles (Studio's design page draws them the same way): each scene's page colour
   under its own edge shading and a dot per colour it shows, in the theme the PICKER's page is in - the
   product writes both (`--mx-tile-light` / `--mx-tile-dark`, from mixtape-design.mjs sceneSwatch) as inline
   custom properties and the kit picks. A radio inside each, so it posts with no script. */
.mx-scene-pick { display: grid; grid-template-columns: repeat(auto-fill, minmax(7.5rem, 1fr)); gap: 0.5rem; margin: 0; padding: 0; border: 0; }
.mx-scene-tile { position: relative; display: flex; flex-direction: column; gap: 0.25rem; min-width: 0; cursor: pointer; }
.mx-scene-tile > input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; }
.mx-scene-tile__art {
  display: flex; align-items: flex-end; gap: 0.1875rem; height: 3rem; padding: 0.375rem;
  border: 1px solid var(--c-line); border-radius: var(--radius-sm); background: var(--mx-tile-light, var(--c-surface-2));
}
:root[data-theme='dark'] .mx-scene-tile__art { background: var(--mx-tile-dark, var(--c-surface-2)); }
@media not (prefers-color-scheme: light) { :root:not([data-theme]) .mx-scene-tile__art { background: var(--mx-tile-dark, var(--c-surface-2)); } }
.mx-scene-tile__art > i { width: 0.5rem; height: 0.5rem; border-radius: 50%; background: var(--c, transparent); box-shadow: 0 0 0 1px color-mix(in srgb, var(--c-ink) 18%, transparent); }
.mx-scene-tile__art > i[data-dark] { display: none; }
:root[data-theme='dark'] .mx-scene-tile__art > i[data-light] { display: none; }
:root[data-theme='dark'] .mx-scene-tile__art > i[data-dark] { display: block; }
@media not (prefers-color-scheme: light) {
  :root:not([data-theme]) .mx-scene-tile__art > i[data-light] { display: none; }
  :root:not([data-theme]) .mx-scene-tile__art > i[data-dark] { display: block; }
}
.mx-scene-tile__name { overflow: hidden; color: var(--c-ink-muted); font-size: 0.8125rem; text-overflow: ellipsis; white-space: nowrap; }
.mx-scene-tile:hover .mx-scene-tile__art { border-color: var(--c-line-strong); }
.mx-scene-tile:has(> input:checked) .mx-scene-tile__art { border-color: var(--c-accent); box-shadow: 0 0 0 1px var(--c-accent); }
.mx-scene-tile:has(> input:checked) .mx-scene-tile__name { color: var(--c-ink); font-weight: 600; }
.mx-scene-tile:has(> input:focus-visible) .mx-scene-tile__art { outline: 2px solid var(--c-accent); outline-offset: 2px; }

/* app.css — the few rules the kit does not already carry. Prefix sn- so nothing collides with
 * the kit's lp- and mx- layers. No colour, radius, font or easing is invented here: every value
 * is a token from static/kit/tokens.css or static/accent.css.
 *
 * Long strings: Hungarian runs 20-40% longer than English and the all-languages pass will bring
 * worse. Nothing below is sized to fit a word - the brand and the language links wrap instead of
 * overflowing, and every flex child that holds text gets min-width: 0. Same reason `dir="rtl"`
 * works: margins are logical properties, not left/right.
 */

/* ── border-box, and it has to be first ──────────────────────────────────────────────────────
 * THE KIT ASSUMES IT. In the monorepo Tailwind's preflight sets border-box on everything, so
 * static/kit/app.css writes `width: 100%` plus `padding: 0 0.75rem` plus a 1px border on every input
 * and means "as wide as the field". A product that vendors the kit without Tailwind gets the
 * browser default instead - content-box - and that same input comes out 26px WIDER than the
 * field holding it. Task #201: the new-product sheet's panes and every .mx-field in them
 * overflowed by exactly that, so the sheet grew a horizontal scrollbar and a phone got the
 * whole form shifted sideways under a finger. .mx-page__inner did it too, 48px for its two
 * 24px gutters, on every page of the admin.
 *
 * The same bug hit Dayrule (#44) and the studio wrote it down; this file never carried the
 * cure. It is a rule about the box model, not a patch on a sheet, so it goes at the top and
 * applies to everything - nothing here wants content-box.
 *
 * To catch it coming back: in a browser at 360px, documentElement.scrollWidth - innerWidth
 * must be 0, and no element may be wider than the one holding it. static/css.test.mjs asserts
 * the rule is present and first.
 */
*,
*::before,
*::after {
  box-sizing: border-box;
}

/* ── The admin bar: the brand may not paint over the page's own button ───────────────────────
 * THIS IS THE KIT'S BUG, NOT OURS. static/kit/app.css here is byte-identical to
 * https://mixtape.works/kit/app.css (checked 2026-09-23), so every product with an admin shell
 * has it. It is cured here rather than there because static/kit/ is vendored "exactly as downloaded"
 * and this sheet is concatenated after it - keeping that copy pristine is what let the bug be
 * traced upstream at all. DELETE THIS BLOCK once the same two rules land in
 * packages/ui/src/app.css (Studio task #344); nothing below is specific to this product.
 *
 * WHAT GOES WRONG. .mx-bar__inner is a flex row of two groups. The kit gives .mx-bar__right
 * `flex-shrink: 0` and .mx-bar__left `min-width: 0`, so the left group is the only one that can
 * give - and .mx-brand inside it is `flex-shrink: 0` too, so it refuses. The group gets squeezed
 * to 98px with 157px of brand in it, and overflow is visible, so the wordmark is simply painted
 * on top of its neighbour. Measured signed in in Chromium on /admin, /admin/settings,
 * /admin/products and a product page, in both languages:
 *   360px - "Storefront Ninja" ran 59px through "Bolt megnyitása", 21px through "View shop".
 *   768px - WORSE, and #216 did not know about it: the top nav appears, the brand still refuses
 *           to shrink, and it is the nav's LAST item that lands on the button - 122px of
 *           "Beállítások" painted through "Bolt megnyitása". Measuring .mx-brand against
 *           .mx-bar__right reports a comfortable 325px gap here and sees nothing; the check has
 *           to be every child of the left group against the right one.
 * Nothing scrolls sideways at any width (scrollWidth - innerWidth is 0) - the groups collide in
 * place, which is why the #201 sideways-scroll check could never have caught either of them.
 *
 * The two rules below are the cure, and both are about a layout surviving a longer string - the
 * Hungarian label is what made it visible, and the all-languages pass will bring worse.
 * Verified at 320/360/375/414/600/767/768/820/1024/1280px, hu and en, all four pages: no
 * overlap, no spill out of the left group, and the shop link still 142x44 under a thumb.
 */

/* 1. THE BRAND GIVES WAY INSTEAD OF OVERFLOWING. min-width: 0 is what lets a flex child shrink
 *    below its content at all; without it text-overflow never fires. So a product name, a
 *    translation 40% longer, or a narrow desktop window now ends in an ellipsis - the right
 *    failure - rather than two strings on the same pixels. */
.mx-brand {
  min-width: 0;
  flex-shrink: 1;
}
/* nowrap here is the ONE kind this product allows, and it is the kit's own idiom for a bar
   label (.mx-who is written exactly this way): it is paired with text-overflow, so the string
   is free to be any length and degrades to an ellipsis instead of being cut or wrapped into a
   56px-tall bar. views/layout.test.mjs enforces that pairing. */
.mx-brand > span:not([class]) {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* 2. AND BELOW 768px THE WORDMARK STOPS TAKING ROOM AT ALL. That is the kit's own breakpoint:
 *    it already hides .mx-nav there and hands the destinations to the bottom tab bar, so a
 *    phone's bar becomes the mark, the page's button and the two menus. The mark alone is what
 *    identifies a product on a phone anyway.
 *
 *    It is CLIPPED, not `display: none`: the brand link's only other content is an SVG, so
 *    hiding the text outright would leave the home link with no accessible name. `:not([class])`
 *    is the bare wordmark - it deliberately spares both the kit's .mx-badge-app and a mark that
 *    a product wrapped in a span of its own, as the landing header does. */
@media (max-width: 767.98px) {
  .mx-brand > span:not([class]) {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    margin: -1px;
    padding: 0;
    clip-path: inset(50%);
  }
  /* A 22px mark is not a thumb. The kit's coarse-pointer block already gives .mx-brand the 44px
     minimum in height; once it is icon-only it needs the same across. Logical, like every other
     size in this file, so the tap area lands on the right side of a right-to-left bar. */
  .mx-brand {
    min-inline-size: 44px;
  }
}

/* ── Elevation, from tokens only ─────────────────────────────────────────────────────────────
 * A drop shadow is a colour, and this product may not name one (DESIGN.md: "if you find
 * yourself typing a hex code outside tokens.css, stop"). The kit's app layer carries no
 * elevation token yet - it lifts nothing off the page - so the one shadow this product needs is
 * mixed from the tokens that already mean "the darkness behind a window" in each theme:
 *   dark  - --c-canvas, the near-black page itself (and true black on a phone, where tokens.css
 *           takes the canvas to black for OLED); the sheet floats on it.
 *   light - --c-ink, the text colour; paper cannot cast a paper-coloured shadow, and over the
 *           light scrim this is the only thing separating a white sheet from a white page.
 * Both are within three units of the plain black they replaced, so nothing moved on screen.
 *
 * Keyed exactly the way tokens.css keys its palettes - the explicit choice first, the OS
 * fallback only for :root:not([data-theme]). Keyed on the media query alone, picking Light in
 * the settings menu on a dark-mode laptop would leave the modal casting a dark-mode shadow.
 * When @mixtape/ui grows an elevation token (task #197 promotes this modal to the kit), this
 * block goes and the token takes over.
 */
:root,
:root[data-theme='dark'] {
  --sn-lift: 0 24px 60px color-mix(in srgb, var(--c-canvas) 45%, transparent);
}

:root[data-theme='light'] {
  --sn-lift: 0 24px 60px color-mix(in srgb, var(--c-ink) 45%, transparent);
}

@media (prefers-color-scheme: light) {
  :root:not([data-theme]) {
    --sn-lift: 0 24px 60px color-mix(in srgb, var(--c-ink) 45%, transparent);
  }
}

body {
  margin: 0;
  background: var(--c-canvas);
  color: var(--c-ink);
  font-family: var(--font-sans);
  -webkit-font-smoothing: antialiased;
}

/* Keyboard users land here first; it is off-screen until it has focus. */
.sn-skip {
  position: absolute;
  inset-inline-start: -9999px;
  top: 0;
  z-index: 100;
  padding: 12px 18px;
  border-radius: var(--radius-sm);
  background: var(--c-accent);
  color: var(--c-accent-ink);
  font-weight: 700;
  text-decoration: none;
}
.sn-skip:focus {
  inset-inline-start: 12px;
  top: 12px;
}

.sn-brand {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
  color: var(--c-ink);
  font-weight: 700;
  text-decoration: none;
}
.sn-brand__mark {
  display: flex;
  flex: none;
  color: var(--c-accent);
}
.sn-mark {
  display: block;
  /* In rem, not its 26px attributes, so it follows the interface size like the name beside it (Chris, 2026-10-26). */
  width: 1.625rem;
  height: 1.625rem;
}
.sn-brand__name {
  font-weight: 700;
}

/* An inline link inside a sentence: the footer's address, and the links in the privacy text. The
   kit styles buttons and card links, not these, and the browser's own blue is neither readable on
   the dark canvas nor one of our colours. Underlined, so it is a link without relying on colour. */
.link {
  color: var(--c-accent);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
  overflow-wrap: anywhere;
}
.link:hover {
  color: var(--c-ink);
}
/* In the admin a link is ink with the accent under it (2026-11-02): Storefront's accent is a rose, and a
   customer's name, an address and "Change it" all in it read as a page full of errors. */
.mx-app .link {
  color: var(--c-ink);
  text-decoration-color: color-mix(in srgb, var(--c-accent) 70%, transparent);
}
.mx-app .link:hover {
  color: var(--c-accent);
}

/* The landing's three points. auto-fit means one column on a phone and three on a desktop with
   no breakpoint of our own, and minmax(0, ...) keeps a long Hungarian heading from widening the
   whole grid. */
.sn-points {
  display: grid;
  gap: 18px;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
  margin-top: 40px;
}
.sn-point {
  min-width: 0;
  padding: 24px;
  border: 1px solid var(--c-line);
  border-radius: var(--radius-lg);
  background: var(--c-surface);
}
.sn-point h2 {
  margin: 0 0 8px;
  font-size: 1.0625rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  text-wrap: balance;
}
.sn-point p {
  margin: 0;
  color: var(--c-ink-muted);
  line-height: 1.55;
  text-wrap: pretty;
}

/* The shop address on the landing page, and any other identifier a reader may retype. */
.sn-address {
  font-family: var(--font-mono);
  color: var(--c-accent);
  overflow-wrap: anywhere;
}

.sn-note {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  margin-top: 28px;
  padding: 12px 16px;
  border: 1px solid var(--c-line);
  border-radius: var(--radius-md);
  background: var(--c-surface-sunk);
  color: var(--c-ink-muted);
  line-height: 1.45;
}

/* The no-JavaScript language switcher in the footer. The cog in the header is the kit's own
   menu; this is two links, not a second settings menu. */
.sn-lang {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 14px 0 6px;
}
.sn-lang__item {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 36px;
  padding: 6px 12px;
  border: 1px solid var(--c-line);
  border-radius: 999px;
  color: var(--c-ink-muted);
  font-size: 0.875rem;
  text-decoration: none;
}
.sn-lang__item svg {
  flex: none;
  border-radius: 2px;
}
a.sn-lang__item:hover {
  border-color: var(--c-line-strong);
  color: var(--c-ink);
}
.sn-lang__item.is-current {
  border-color: var(--c-accent);
  color: var(--c-ink);
}

/* The free plan's line under the hero's buttons: the offer's small print, and it is small print. */
.sn-free {
  margin: 16px 0 0;
  color: var(--c-ink-subtle);
  font-size: 0.875rem;
}

/* The demo admin's own door under the hero (task #3304), the same small-print shape as .sn-free
   right above it - its own class since the two can appear together and a test of one must never
   be confused by the other. */
.sn-demo-admin {
  margin: 4px 0 0;
  font-size: 0.875rem;
}

/* A field whose value is only part of what it builds: the shop's address, with .storefront.ninja
   after it. The suffix is a flex item that keeps its size and drops onto its own line when the
   field gets narrow, so nothing is ever clipped and nothing needs a fixed width. */
/* The address and its fixed ending on one line at every width: wrapped, ".storefront.ninja" sat alone on the
   phone's edge under the box (2026-11-02). The box gives way, not the ending. */
.sn-affix {
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: 0.375rem;
  min-width: 0;
}
.sn-affix input {
  flex: 1 1 0;
  min-width: 4rem;
}
.sn-affix__suffix {
  flex: none;
  color: var(--c-ink-muted);
  font-family: var(--font-mono);
  font-size: 0.875rem;
}

/* A card that ends in something to press. The kit's .mx-card is text; this is the gap before the
   button, and nothing else. */
.sn-card__actions {
  margin-top: 16px;
}

/* ── The product editor ──────────────────────────────────────────────────────────────────────
 * The kit has no language strip and no photo grid (DESIGN.md: "if you need something that is
 * not there, add it"), so they are here, sn- prefixed, built only from kit tokens. The sheet
 * itself opens over the list with the kit's own overlay now (task #2943) - .sn-sheet is just the
 * card it renders as, on its own page or lifted into the overlay's dialog.
 */
.sn-sheet {
  display: flex;
  flex-direction: column;
  min-width: 0;
  border: 1px solid var(--c-line);
  border-radius: var(--radius-md);
  background: var(--c-surface);
}
.sn-sheet__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  border-bottom: 1px solid var(--c-line);
  padding: 0.875rem 1.25rem;
}
.sn-sheet__title {
  margin: 0;
  min-width: 0;
  font-size: 1.125rem;
}
.sn-sheet__body {
  display: grid;
  gap: 1rem;
  min-width: 0;
  padding: 1.25rem;
}
/* A part of the sheet ("Price and stock", "Delivery") is a heading, not one more field label: sentence
   case in the text font, a hairline above it (2026-11-02 - two mono caps labels in a row read as fields). */
.sn-sheet__section {
  margin: 0.75rem 0 -0.25rem;
  padding-top: 1.125rem;
  border-top: 1px solid var(--c-line);
  font-family: inherit;
  font-size: 0.9375rem;
  font-weight: 600;
  letter-spacing: normal;
  text-transform: none;
  color: var(--c-ink);
}
.sn-sheet__body > .sn-sheet__section:first-child {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
}
.sn-sheet__note {
  margin-top: -0.5rem;
}
/* Cancel and Save on the end, Delete on the start and apart from them: the one control that
   cannot be undone does not sit next to the one people press without looking. */
.sn-sheet__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  border-top: 1px solid var(--c-line);
  padding: 0.875rem 1.25rem;
  background: var(--c-surface-sunk);
}
.sn-sheet__foot-start {
  min-width: 0;
}
/* Save is always at hand: the foot sticks to the bottom of the window (of the overlay, inside one) while the
   sheet is on screen - a product sheet is 3,800px tall, and its Save was only at the very end. Above the
   phone's tab bar. Its actions stay on the end on a phone too. */
.sn-sheet__foot {
  position: sticky;
  bottom: 0;
  z-index: 4;
  border-radius: 0 0 var(--radius-md) var(--radius-md);
}
.sn-sheet__foot > .mx-actions:last-child {
  margin-inline-start: auto;
}
@media (max-width: 767.98px) {
  .mx-app:has(> .mx-tabs) .sn-sheet__foot {
    bottom: calc(3.75rem + max(env(safe-area-inset-bottom) - 10px, 4px));
  }
  .mx-overlay .sn-sheet__foot {
    bottom: 0;
  }
}
/* Lifted into the kit's overlay the dialog is the frame: no second card inside it, and no empty head
   strip once the kit has taken the h1 up into the dialog's own title. */
.mx-overlay__body .sn-sheet {
  border: 0;
  border-radius: 0;
  background: transparent;
}
.mx-overlay__body .sn-sheet__head:not(:has(> :not(h1)))  {
  display: none;
}
.mx-overlay__body .sn-sheet__body {
  padding-inline: 0;
}
.mx-overlay__body .sn-sheet__foot {
  border: 1px solid var(--c-line);
  border-radius: var(--radius-md);
}

/* The language strip above a translated field. Hidden until the script is running, because
   without it every language's pane is already on the page and there is nothing to switch. */
.sn-strip {
  display: flex;
  flex-wrap: wrap;
  gap: 0.375rem;
}
.sn-strip__btn {
  border: 1px solid var(--c-line);
  border-radius: 999px;
  background: transparent;
  padding: 0.25rem 0.75rem;
  min-height: 2rem;
  font: inherit;
  font-size: 0.8125rem;
  color: var(--c-ink-muted);
  cursor: pointer;
  transition: border-color 0.12s ease, color 0.12s ease, background-color 0.12s ease;
}
.sn-strip__btn:hover {
  border-color: var(--c-line-strong);
  color: var(--c-ink);
}
.sn-strip__btn.is-current {
  border-color: var(--c-accent);
  background: var(--c-accent-soft);
  color: var(--c-ink);
}
.sn-strip__btn:focus-visible {
  outline: 2px solid var(--c-accent);
  outline-offset: 2px;
}
.sn-panes,
.sn-pane {
  display: grid;
  gap: 1rem;
  min-width: 0;
}
.sn-pane[hidden] {
  display: none;
}
/* Each label names its language while the panes are all on the page at once. Once the strip is
   running it says which language is open, and the repetition on every label is noise. */
.sn-pane__lang {
  color: var(--c-ink-subtle);
}
/* Only inside a pane the strip switches: a translated field outside one (an attribute's options, a text
   attribute on the product) shows every language at once and needs its name ("Option English"). */
.sn-sheet[data-js='on'] .sn-pane .sn-pane__lang,
.sn-appearance[data-js='on'] .sn-pane .sn-pane__lang,
#order-message-form[data-js='on'] .sn-pane .sn-pane__lang {
  display: none;
}

/* The photos: a grid of squares, the first one carrying the "main photo" flag. */
.sn-photos__list {
  display: grid;
  gap: 0.625rem;
  grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr));
  margin: 0;
  padding: 0;
  list-style: none;
}
.sn-photos__list:empty {
  display: none;
}
.sn-photo {
  position: relative;
  overflow: hidden;
  border: 1px solid var(--c-line-strong);
  border-radius: var(--radius-sm);
  background: var(--c-surface-sunk);
  aspect-ratio: 1;
}
.sn-photo.is-dragging {
  opacity: 0.4;
}
.sn-photo.is-over {
  border-color: var(--c-accent);
}
.sn-photo__img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.sn-photo__main {
  position: absolute;
  inset-block-start: 0.375rem;
  inset-inline-start: 0.375rem;
  border-radius: 999px;
  background: var(--c-accent);
  padding: 2px 0.5rem;
  font-size: 0.6875rem;
  font-weight: 600;
  color: var(--c-accent-ink);
}
.sn-photo:not(:first-child) .sn-photo__main {
  display: none;
}
.sn-photo__tools {
  position: absolute;
  inset-block-end: 0;
  inset-inline: 0;
  display: flex;
  justify-content: center;
  gap: 0.25rem;
  padding: 0.375rem;
  background: color-mix(in srgb, var(--c-canvas) 80%, transparent);
}
.sn-photo__btn,
.sn-iconbtn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  border: 1px solid var(--c-line-strong);
  border-radius: var(--radius-sm);
  background: var(--c-surface);
  color: var(--c-ink-muted);
  cursor: pointer;
}
/* A finger needs 44px: move, remove and the other small icon buttons (photos, related products, pages). */
@media (hover: none) and (pointer: coarse) {
  .sn-photo__btn,
  .sn-iconbtn {
    width: 2.75rem;
    height: 2.75rem;
  }
}
.sn-photo__btn:hover,
.sn-iconbtn:hover:not(:disabled) {
  border-color: var(--c-accent);
  color: var(--c-ink);
}
.sn-photo__btn:focus-visible,
.sn-iconbtn:focus-visible {
  outline: 2px solid var(--c-accent);
  outline-offset: 1px;
}
/* A category's arrows at the top or bottom of the list: still there, so the rows line up, but
   plainly not something to press. */
.sn-iconbtn:disabled {
  opacity: 0.35;
  cursor: default;
}
/* A finger is not a mouse: on a touch screen the arrows (categories, pages) are 44px, the same
   floor the kit gives .mx-btn, and the row scrolls sideways rather than shrinking them. */
@media (hover: none) and (pointer: coarse) {
  .sn-iconbtn {
    width: 2.75rem;
    height: 2.75rem;
  }
  /* The language strip above a translated field (Appearance, the product form) is 44px tall
     too; it already wraps, so the taller pills cost a row, not a sideways scroll. */
  .sn-strip__btn {
    min-height: 2.75rem;
    padding-inline: 1rem;
  }
}
.sn-moves--tight {
  flex-wrap: nowrap;
  gap: 0.375rem;
}
.sn-photos__input {
  font-size: 0.8125rem;
  color: var(--c-ink-muted);
}
.sn-photos__status:empty {
  display: none;
}

/* The download delivery's one file (task #3311): a name, its size, and a Remove button - one row,
   never a grid of squares the way photos are, because there is only ever one. */
.sn-filebox__current {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.625rem;
  border: 1px solid var(--c-line-strong);
  border-radius: var(--radius-sm);
  background: var(--c-surface-sunk);
  padding: 0.625rem 0.875rem;
}
.sn-filebox__name {
  flex: 1 1 auto;
  min-width: 0;
  margin: 0;
  overflow-wrap: anywhere;
}
.sn-filebox__size {
  color: var(--c-ink-muted);
  font-size: 0.8125rem;
}
.sn-filebox__picker {
  font-size: 0.8125rem;
  color: var(--c-ink-muted);
}
.sn-filebox__status:empty {
  display: none;
}

/* The shop's logo as settings previews it: the real file, on a surface rather than on the page's
   own background, because a logo drawn in white on nothing is invisible on a white page and a
   preview an owner cannot see is worse than none. Capped by height, like the storefront's bar. */
.sn-logo {
  display: block;
  max-height: 4.5rem;
  max-width: min(100%, 20rem);
  width: auto;
  margin-block-end: 0.5rem;
  padding: 0.5rem;
  border: 1px solid var(--c-line);
  border-radius: var(--radius-sm);
  background: var(--c-surface);
  object-fit: contain;
}

/* The banner as the Appearance page previews it: the same 3 : 1 frame the shop crops it to, so
   what is cut off is visible before Save. */
.sn-banner-preview {
  display: block;
  width: 100%;
  max-width: 32rem;
  aspect-ratio: 3 / 1;
  margin-block-end: 0.5rem;
  border: 1px solid var(--c-line);
  border-radius: var(--radius-sm);
  background: var(--c-surface);
  object-fit: cover;
}

/* The shop's tab icon as the Appearance page previews it: the real file at tab size on a light
   ground and on a dark one. Each chip sets --tone-ground and --tone-ink on itself (the kit's own
   surface and ink for that theme), so this file still names no colour. */
.sn-fav {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin-block-end: 0.5rem;
}
.sn-fav__chip {
  display: inline-flex;
  align-items: center;
  gap: 0.625rem;
  min-width: 0;
  padding-block: 0.5rem;
  padding-inline: 0.625rem 0.875rem;
  border: 1px solid var(--c-line-strong);
  border-radius: var(--radius-sm);
  background: var(--tone-ground);
  color: var(--tone-ink);
  font-size: 0.8125rem;
}
.sn-fav__chip img {
  flex: none;
  width: 2rem;
  height: 2rem;
  object-fit: contain;
}

/* The accent colour's box. The kit gives every input the full width of its field; a colour well
   is a swatch and wants to stay one. */
.sn-colour {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
}
input.sn-color {
  flex: none;
  width: 4rem;
  height: 2.5rem;
  padding: 0.125rem;
  cursor: pointer;
}

/* The colour box on /admin/appearance: a swatch and its hex in an ordinary field, with the real
   colour picker laid over the whole of it, so a tap anywhere on the box opens the picker. */
.sn-swatch {
  position: relative;
  display: flex;
  align-items: center;
  gap: 0.625rem;
  width: min(100%, 14rem);
  min-height: 2.75rem;
  padding: 0 0.75rem;
  border: 1px solid var(--c-line-strong);
  border-radius: var(--radius-sm);
  background: var(--c-canvas);
  color: var(--c-ink);
}
.sn-swatch:focus-within {
  outline: 2px solid var(--c-accent);
  outline-offset: 2px;
}
.sn-swatch__chip {
  flex: none;
  width: 1.75rem;
  height: 1.75rem;
  border: 1px solid var(--c-line-strong);
  border-radius: 50%;
}
.sn-swatch__hex {
  font-size: 0.875rem;
  font-variant-numeric: tabular-nums;
}
/* Beats the kit's .mx-field input rules (its width, height and focus ring), which would otherwise
   size the picker to the field and not to the swatch it sits over. */
.sn-swatch > input.sn-swatch__input[type='color'] {
  position: absolute;
  inset: -1px;
  width: calc(100% + 2px);
  height: calc(100% + 2px);
  margin: 0;
  padding: 0;
  border: 0;
  opacity: 0;
  cursor: pointer;
}

/* A badge (task #3141): the owner's own colour as a fill, never the kit's tones - its text
   colour is set alongside it inline (lib/accent.mjs badgeInk), so this rule never guesses one. */
.sn-swatch__chip.sn-badge-chip {
  width: auto;
  height: auto;
  min-width: 1.75rem;
  padding: 0.1875rem 0.625rem;
  border-radius: 999px;
  font-size: 0.8125rem;
  font-weight: 600;
}

/* How the chosen colour reads on a light page and on a dark one. Each sample sets --tone-* on
   itself (the numbers lib/accent.mjs computed) and these rules read them, so this file still names
   no colour. The ground and ink are the storefront's own for that theme, whatever theme the admin
   is showing in. */
.sn-tones {
  display: grid;
  gap: 0.75rem;
  grid-template-columns: repeat(auto-fit, minmax(min(12rem, 100%), 1fr));
  margin-block-start: 0.75rem;
}
.sn-tone {
  display: grid;
  gap: 0.75rem;
  justify-items: start;
  padding: 1rem;
  border: 1px solid var(--c-line-strong);
  border-radius: var(--radius-md);
  background: var(--tone-ground);
  color: var(--tone-ink);
}
.sn-tone__name {
  margin: 0;
  font-size: 0.8125rem;
  font-weight: 600;
}
.sn-tone__btn {
  display: inline-flex;
  align-items: center;
  min-height: 2.5rem;
  padding: 0 1rem;
  border-radius: var(--radius-sm);
  background: var(--tone-fill);
  color: var(--tone-label);
  font-size: 0.875rem;
  font-weight: 600;
}
.sn-tone__link {
  color: var(--tone-fill);
  font-size: 0.875rem;
  text-decoration: underline;
  text-underline-offset: 0.2em;
}

/* A checkbox with its label, which .mx-field's label-above-control shape does not fit. */
.sn-check {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  min-height: 2.75rem;
  font-size: 0.875rem;
  color: var(--c-ink);
  cursor: pointer;
}
.sn-check input {
  width: 1.125rem;
  height: 1.125rem;
  accent-color: var(--c-accent);
  flex: none;
}

/* A row's own checkbox, or the list's header one that picks every row on the page (task #3062):
   centred in its narrow column, with no label text beside it. */
.sn-check--select {
  justify-content: center;
  min-height: 0;
}
@media (hover: none) and (pointer: coarse) {
  /* The kit already gives a checkbox in a td this treatment (kit/app.css); the header's own
     select-all is a th, which that rule does not reach. */
  .sn-list th > label:has(> input[type='checkbox']) {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 2.75rem;
    min-height: 2.75rem;
    cursor: pointer;
  }
  .sn-list th input[type='checkbox'] {
    width: 1.25rem;
    height: 1.25rem;
    accent-color: var(--c-accent);
  }
}

/* The bar above the product list once a row is ticked (task #3062): how many, and the five
   things a batch of them can be put through in one post. Its groups wrap and stack rather than
   scroll - a phone gets a taller bar, never a wider page. From 721px up (task #3078) it is one
   row instead, below. */
.sn-bulkbar {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  padding: 0.875rem 1rem;
  margin-bottom: 0.875rem;
  border: 1px solid var(--c-line-strong);
  border-radius: var(--radius-md);
  background: var(--c-surface-sunk);
}
.sn-bulkbar[hidden] {
  display: none;
}
.sn-bulkbar__count {
  margin: 0;
  font-weight: 600;
  font-size: 0.875rem;
  color: var(--c-ink);
}
.sn-bulkbar__group {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
}
.sn-bulkbar__num {
  width: 6rem;
  flex: 0 1 6rem;
}
.sn-bulkbar__date {
  width: 9rem;
  flex: 0 1 9rem;
}
.sn-bulkbar__percent {
  color: var(--c-ink-muted);
}
.sn-bulkbar__pct {
  display: flex;
  align-items: center;
  gap: 0.375rem;
}

/* From 721px up (a laptop) the bar is one row that wraps rather than a tall stack: the count
   first, Delete pushed to the far end. A phone keeps the stacked bar above. */
@media (min-width: 45.0625rem) {
  .sn-bulkbar {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
  }
  .sn-bulkbar__group:last-child {
    margin-inline-start: auto;
  }
}

/* The "Mit csináljunk velük?" picker (task #3211): a script-only narrowing, so it stays out of
   the way entirely until static/products.js both finds something to switch between AND is able
   to keep it in sync - below 45rem (just under the row breakpoint above) it replaces the seven
   stacked groups with the one the owner picked, each already carrying its own visible label. */
.sn-bulkbar__picker {
  display: none;
}
/* At every width since 2026-11-02: eleven controls in a wrapping row were a 290px wall over the list. One
   "What to do with them?" choice, then only the controls it needs - and the bar stays in view under the top
   bar while the list scrolls. */
@media all {
  #product-bulk-form[data-js='on'] .sn-bulkbar {
    position: sticky;
    top: calc(3.5rem + env(safe-area-inset-top, 0px) + 0.5rem);
    z-index: 6;
    box-shadow: var(--sn-lift);
  }
  #product-bulk-form[data-js='on'] .sn-bulkbar__picker {
    display: grid;
  }
  #product-bulk-form[data-js='on'] .sn-bulkbar__group[data-bulk-action] {
    display: none;
  }
  #product-bulk-form[data-js='on'][data-bulk-picked='category'] .sn-bulkbar__group[data-bulk-action='category'],
  #product-bulk-form[data-js='on'][data-bulk-picked='badge'] .sn-bulkbar__group[data-bulk-action='badge'],
  #product-bulk-form[data-js='on'][data-bulk-picked='collection'] .sn-bulkbar__group[data-bulk-action='collection'],
  #product-bulk-form[data-js='on'][data-bulk-picked='price'] .sn-bulkbar__group[data-bulk-action='price'],
  #product-bulk-form[data-js='on'][data-bulk-picked='sale'] .sn-bulkbar__group[data-bulk-action='sale'],
  #product-bulk-form[data-js='on'][data-bulk-picked='endsale'] .sn-bulkbar__group[data-bulk-action='endsale'],
  #product-bulk-form[data-js='on'][data-bulk-picked='stock'] .sn-bulkbar__group[data-bulk-action='stock'],
  #product-bulk-form[data-js='on'][data-bulk-picked='export'] .sn-bulkbar__group[data-bulk-action='export'],
  #product-bulk-form[data-js='on'][data-bulk-picked='delete'] .sn-bulkbar__group[data-bulk-action='delete'] {
    display: flex;
  }
}

/* "Export these" (import phase 3): a filtered list's own CSV / Excel, at the end of its panel head. */
.sn-export-these {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.25rem;
  margin-inline-start: auto;
}
.sn-export-these__label {
  font-size: 0.8125rem;
  color: var(--c-ink-muted);
  margin-inline-end: 0.25rem;
}

/* A child category's checkbox, indented under its parent (task #486). */
.sn-check--child {
  padding-inline-start: 1.75rem;
}

/* A product's several categories: a checkbox list in a sunk box, the same shape .sn-zone gives
   the delivery zones and .sn-option gives an option's row - it repeats, and reads as its own
   thing. It scrolls rather than growing a modal without end once a shop has fifty categories. */
.sn-categories {
  margin: 0;
  padding: 0.625rem 0.875rem;
  border: 1px solid var(--c-line);
  border-radius: var(--radius-md);
  background: var(--c-surface-sunk);
}
.sn-categories__list {
  display: flex;
  flex-direction: column;
  max-height: 14rem;
  overflow-y: auto;
}

/* The related-products picker (task #3142): the picked list and the search results are the same
   row shape, a name that clips rather than widening the row and an optional SKU beside it - the
   same sunk box .sn-categories already is. .sn-related__list has no horizontal scroll of its own:
   it is a column of rows, so a phone simply gets more, narrower rows, never a sideways scroll. */
.sn-related {
  min-width: 0;
  padding: 0.625rem 0.875rem;
  border: 1px solid var(--c-line);
  border-radius: var(--radius-md);
  background: var(--c-surface-sunk);
}
.sn-related__list,
.sn-related__results {
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
  margin: 0.5rem 0 0;
  padding: 0;
  list-style: none;
}
.sn-related__results {
  max-height: 14rem;
  overflow-y: auto;
}
.sn-related__item,
.sn-related__result {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-width: 0;
  padding: 0.375rem 0.5rem;
  border: 1px solid var(--c-line);
  border-radius: var(--radius-sm);
  background: var(--c-surface);
}
.sn-related__name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sn-related__sku {
  flex: 0 0 auto;
  font-size: 0.8125rem;
  color: var(--c-ink-muted);
}
.sn-related__tools {
  display: flex;
  flex: 0 0 auto;
  gap: 0.25rem;
}
.sn-related__search {
  margin-block-start: 0.625rem;
}
.sn-related__searchrow {
  display: flex;
  gap: 0.5rem;
}
.sn-related__searchrow input {
  flex: 1 1 auto;
  min-width: 0;
}
.sn-related__pick {
  flex: 0 0 auto;
}

/* A rule row (task #3201) packs a label, a value control and sometimes a trailing hint span into
   the same .sn-related__item - too many flex children for a narrow phone to fit beside the fixed
   ellipsising .sn-related__name without clipping the label (task #3213). Below 640px the label
   gets its own line, full width and no ellipsis, and the value control grows to fill the one it
   now shares only with the trailing hint (if any) and the remove button. */
@media (max-width: 39.9375rem) {
  .sn-rule-row {
    flex-wrap: wrap;
  }
  .sn-rule-row .sn-related__name {
    flex: 1 1 100%;
    overflow: visible;
    text-overflow: initial;
    white-space: normal;
    font-size: 0.8125rem;
    color: var(--c-ink-muted);
  }
  .sn-rule-row input,
  .sn-rule-row select {
    flex: 1 1 auto;
    min-width: 0;
  }
}

/* The attribute form's "+ Another option" (task #3188): same spacing as the related-products
   search box above its own list. */
.sn-related__add {
  margin-block-start: 0.625rem;
}

/* A category's own picture, in the admin form (task #486) - the same treatment the shop logo and
   the front page banner get: a wide, capped preview above the file input. */
.sn-category-pic {
  display: block;
  width: 100%;
  max-width: 24rem;
  max-height: 12rem;
  object-fit: cover;
  border: 1px solid var(--c-line);
  border-radius: var(--radius-md);
  background: var(--c-surface-sunk);
}

/* The header's three layouts (task #783) and the product page's three (task #785), as tiles with
   a drawing each - never a dropdown, so an owner sees the arrangement before picking it. The two
   sections share one widget (sn-hdr-tile* and sn-pdp-tile* are the same rules, so a change to how
   a tile looks cannot fix one section and miss the other) - only the drawing inside .*__art
   differs, per axis. The input sits first and is sized like any other radio, not hidden: a screen
   reader announces it as one of three, the drawing is aria-hidden. */
.sn-hdr-tiles,
.sn-pdp-tiles {
  display: grid;
  gap: 0.75rem;
  grid-template-columns: repeat(auto-fit, minmax(min(16rem, 100%), 1fr));
  margin-block-start: 0.75rem;
}
.sn-hdr-tile,
.sn-pdp-tile {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: start;
  gap: 0.25rem 0.625rem;
  padding: 0.875rem;
  border: 1px solid var(--c-line-strong);
  border-radius: var(--radius-md);
  cursor: pointer;
}
.sn-hdr-tile:has(input:checked),
.sn-pdp-tile:has(input:checked) {
  border-color: var(--c-accent);
  background: color-mix(in srgb, var(--c-accent) 6%, transparent);
}
.sn-hdr-tile input,
.sn-pdp-tile input {
  width: 1.125rem;
  height: 1.125rem;
  accent-color: var(--c-accent);
}
.sn-hdr-tile__art,
.sn-pdp-tile__art {
  grid-column: 1 / -1;
  width: 100%;
  height: auto;
  color: var(--c-ink-muted);
}
.sn-hdr-tile:has(input:checked) .sn-hdr-tile__art,
.sn-pdp-tile:has(input:checked) .sn-pdp-tile__art {
  color: var(--c-accent);
}
.sn-hdr-tile__name,
.sn-pdp-tile__name {
  grid-column: 2;
  font-size: 0.9375rem;
  font-weight: 600;
  color: var(--c-ink);
}
.sn-hdr-tile__hint,
.sn-pdp-tile__hint {
  grid-column: 1 / -1;
  font-size: 0.8125rem;
  color: var(--c-ink-muted);
}

/* A sub-option only does something once its parent holds the value it depends on (Shopiza's
   showWhen): CSS alone reveals it, from the very radio the owner just pressed, no script. */
[data-subopt="header_bg"],
[data-subopt="header_search_w"] {
  display: none;
}
#appearance-form:has(input[name="header_sticky"][value="sticky"]:checked) [data-subopt="header_bg"] {
  display: block;
}
#appearance-form:has(input[name="header_layout"][value="search_forward"]:checked) [data-subopt="header_search_w"] {
  display: block;
}

/* A name the owner typed that may be one long word (a shipping method's, up to 80 characters): it
   wraps inside its box instead of widening the row or the page. */
.sn-longname {
  min-width: 0;
  overflow-wrap: anywhere;
}

/* A coupon's code is a thing typed and read out, so it is set like one. */
.sn-code {
  font-family: var(--font-mono);
  font-weight: 600;
  letter-spacing: 0.02em;
}

/* The rules and the private note under a coupon's code in the list: a second line, kept narrow so
   a 200-character note wraps in its cell instead of pushing the table wider than the screen. */
.sn-code-note {
  display: block;
  max-width: 22rem;
  font-size: 0.8125rem;
  font-weight: 400;
  overflow-wrap: anywhere;
}

/* "Takes off": two choices, each with its own box, so the form needs no script to be read. */
.sn-kind {
  display: grid;
  gap: 0.375rem;
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}
.sn-kind legend {
  padding: 0;
  margin-bottom: 0.375rem;
}
.sn-kind__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem 1rem;
}
.sn-kind__input {
  flex: 0 1 11rem;
}
.sn-kind__box {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.sn-kind + .mx-fields {
  margin-top: 0.25rem;
}

/* The confirmation before a delete: our own words, naming the thing, never window.confirm. */
.sn-confirm {
  position: fixed;
  inset: 0;
  z-index: 70;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  background: color-mix(in srgb, var(--c-canvas) 78%, transparent);
}
.sn-confirm[hidden] {
  display: none;
}
.sn-confirm__box {
  width: min(28rem, 100%);
  border: 1px solid var(--c-line-strong);
  border-radius: var(--radius-lg);
  background: var(--c-surface);
  padding: 1.5rem;
  box-shadow: var(--sn-lift);
}
.sn-confirm__title {
  margin: 0 0 0.5rem;
  font-size: 1.0625rem;
}
.sn-confirm__text {
  margin: 0 0 1.25rem;
  color: var(--c-ink-muted);
  line-height: 1.55;
}
.sn-confirm__warn {
  color: var(--c-warn);
}
.sn-confirm__actions {
  justify-content: flex-end;
}
/* On its own page - the one a browser with no script reaches - the box is a card in the flow
   rather than an overlay: there is nothing behind it to dim. */
.sn-confirm--inline {
  position: static;
  justify-content: flex-start;
  padding: 0;
  background: none;
}

/* A product row: the thumbnail and the name are one thing, and the row is clickable. */
.sn-rowname {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  min-width: 0;
}
.sn-rowname__text {
  min-width: 0;
}
.sn-thumb {
  flex: none;
  display: block;
  width: 2.75rem;
  height: 2.75rem;
  border: 1px solid var(--c-line);
  border-radius: var(--radius-sm);
  object-fit: cover;
  background: var(--c-surface-sunk);
}
.sn-thumb--none {
  background: repeating-linear-gradient(
    45deg,
    var(--c-surface-sunk),
    var(--c-surface-sunk) 6px,
    var(--c-surface-2) 6px,
    var(--c-surface-2) 12px
  );
}
.sn-quiet {
  color: var(--c-ink-subtle);
}

/* A child category, indented under its parent in the admin's tree (task #486). One level only -
   the database itself will not let a category nest any deeper. */
.sn-list__child td:first-child {
  padding-inline-start: 2rem;
}

/* A page that is only a message: 404, a subdomain with no shop, an error. */
.sn-message {
  padding: 96px 0 72px;
}
.sn-message .lp-container {
  max-width: 44rem;
}

/* A page that is only text: /privacy, and whatever else has to be read rather than used. The
   kit's own .lp-doc (landing.css) now gives the section its header clearance and reading
   measure (task #3008) and centres its title and lead the same way as .lp-band's (task #3066)
   - this is only the colour, list-marker and footer touches it doesn't ship, kept on .sn-doc
   beside .lp-doc on the same <section>. */
.sn-doc__h {
  margin: 36px 0 10px;
  font-size: 1.0625rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  text-wrap: balance;
}
.sn-doc p,
.sn-doc li {
  margin: 0 0 12px;
  color: var(--c-ink-muted);
  line-height: 1.6;
  text-wrap: pretty;
}
.sn-doc .lp-lead {
  color: var(--c-ink);
}
.sn-doc__list {
  margin: 0;
  padding-inline-start: 1.25rem;
}
.sn-doc__list li::marker {
  color: var(--c-accent);
}
.sn-doc__updated {
  margin-top: 32px;
  padding-top: 16px;
  border-top: 1px solid var(--c-line);
  font-size: 0.875rem;
}

/* ── The storefront: <shop>.storefront.ninja ─────────────────────────────────────────────────
 * The kit has a landing layer (lp-) and an admin layer (mx-) and no public content layer, so
 * this is the one. Every value below is a token from static/kit/tokens.css or static/accent.css - the
 * rose a sale badge and a buy button read is this product's accent, set in accent.css, and
 * nothing here names a colour (static/css.test.mjs fails the build if it ever does).
 *
 * WHOSE PAGE IT IS. This layer never draws our brand: the header is the shop's name, its
 * languages and its cart, and Storefront Ninja appears once, quietly, in the footer.
 *
 * THE BUY BUTTON CANNOT BE HIDDEN. On a phone the buy row is copied into a bar fixed to the
 * bottom of the screen (.sn-sf-sticky) and the page is padded so nothing ends up underneath it.
 * The bar is outside every scroller, because a sticky child of a sideways scroller sticks to the
 * scroller rather than to the page (DESIGN.md).
 *
 * Long strings: every flex child that holds text has min-width: 0, no width is in pixels, and
 * every inset is logical, so Hungarian (20-40% longer) and a right-to-left script both survive.
 */

.sn-sf {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  min-height: 100svh;
}

.sn-sf-wrap {
  width: 100%;
  max-width: min(var(--page-max), 72rem);
  margin-inline: auto;
  padding-inline: clamp(1rem, 4vw, 2rem);
}

.sn-sf-narrow {
  max-width: 40rem;
}

.sn-sf-main {
  flex: 1 1 auto;
}

/* The header is the shop's: its name, its search, its languages, its cart (task #783). ONE row of
   markup, for every header_layout - lib/header-layout.mjs stamps data-hdr-* on <html> and
   everything below matches on those, never on a class this file would have to add per layout.
   `sticky` is the axis's own default and needs no attribute to select it; `static` (header_sticky)
   overrides it below, same for every other axis's default value. */

/* The owner's own announcement bar (task #3283), BEFORE .sn-sf-bar on purpose: it is plain flow
   content, so it scrolls away with the page while the sticky header above it stays pinned. Its
   background and text colour are the owner's pick and lib/accent.mjs badgeInk's contrast choice,
   set inline per request (views/announcements.mjs) - this file names no colour (css.test.mjs),
   so neither could live here anyway. */
.sn-announce {
  padding: 0.6rem 1rem;
}
.sn-announce__inner {
  max-width: 72rem;
  margin: 0 auto;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.5rem 1rem;
  text-align: center;
  font-size: 0.9rem;
  line-height: 1.4;
}
.sn-announce__text {
  overflow-wrap: anywhere;
}
.sn-announce__link {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.5rem 1rem;
  min-height: 2.75rem;
  padding: 0.25rem 0;
  color: inherit;
  text-decoration: none;
}
.sn-announce__cta {
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* The shopper-facing popup (task #3284): a native <dialog>, empty until static/popup.js clones
   its own <template> in and calls showModal() - this file names no colour (css.test.mjs), so the
   backdrop reads the same token sn-confirm's overlay already does. */
.sn-popup {
  position: fixed;
  inset: 0;
  margin: auto;
  width: min(26rem, calc(100% - 2rem));
  max-height: calc(100% - 2rem);
  padding: 0;
  border: 1px solid var(--c-line-strong);
  border-radius: var(--radius-lg);
  background: var(--c-surface);
  box-shadow: var(--sn-lift);
  overflow: hidden;
}
.sn-popup::backdrop {
  background: color-mix(in srgb, var(--c-canvas) 78%, transparent);
}
.sn-popup__inner {
  position: relative;
  max-height: inherit;
  padding: 2rem 1.5rem 1.5rem;
  overflow-y: auto;
}
.sn-popup__close {
  position: absolute;
  inset-block-start: 0.5rem;
  inset-inline-end: 0.5rem;
  width: 2.75rem;
  height: 2.75rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  background: none;
  border-radius: var(--radius-sm);
  font-size: 1.5rem;
  line-height: 1;
  color: var(--c-ink-muted);
  cursor: pointer;
}
.sn-popup__close:hover {
  background: var(--c-surface-sunk);
}
.sn-popup__heading {
  margin: 0 0 0.5rem;
  padding-inline-end: 2.5rem;
  font-size: 1.25rem;
  overflow-wrap: anywhere;
}
.sn-popup__text {
  margin: 0;
  color: var(--c-ink-muted);
  line-height: 1.55;
  overflow-wrap: anywhere;
}
.sn-popup__text p {
  margin: 0;
}
.sn-popup__text p + p {
  margin-top: 0.75rem;
}
.sn-popup__btn {
  margin-top: 1.25rem;
}
/* The optional picture (task #3285): full width, above the heading, outside the padded inner box
   so it reaches the dialog's own rounded corners. Capped on height rather than let the owner's
   own photo decide how much of the dialog it eats. */
.sn-popup__picture {
  display: block;
  width: 100%;
  max-height: 14rem;
  object-fit: cover;
}
/* Below this width (DESIGN.md's own phone breakpoint) the popup is a bottom sheet, never taller
   than 60% of the screen, so it never hides the whole page on arrival - and the picture shrinks
   with it, so the heading and text are never pushed out of what fits. */
@media (max-width: 48rem) {
  .sn-popup {
    inset-block-start: auto;
    inset-block-end: 0;
    inset-inline: 0;
    margin: 0;
    width: 100%;
    max-height: 60%;
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  }
  .sn-popup__picture {
    max-height: 6rem;
  }
}

/* The sheet's own live preview (task #3285): built and redrawn entirely by
   static/popup-preview.js, which also removes the `hidden` both this and its frame switch carry -
   with that file blocked, or failing to load, neither ever appears (the sheet still saves). The
   frame switch toggles data-frame on the wrapper; [data-frame="phone"] mirrors .sn-popup's own
   48rem rules above WITHOUT a media query, because shrinking the PREVIEW must not wait for the
   browser window itself to narrow. */
.sn-popup-preview {
  margin-top: 1.5rem;
}
.sn-popup-preview__tabs {
  display: flex;
  gap: 0.5rem;
  margin-bottom: 0.75rem;
}
.sn-popup-preview__tabs button[aria-pressed='true'] {
  background: var(--c-surface-sunk);
}
.sn-popup-preview__frame {
  position: relative;
  padding: 2rem 1.5rem;
  border-radius: var(--radius-lg);
  background: color-mix(in srgb, var(--c-canvas) 78%, transparent);
}
.sn-popup-preview__frame .sn-popup {
  position: static;
  inset: auto;
  margin: 0 auto;
  box-shadow: var(--sn-lift);
}
.sn-popup-preview[data-frame='phone'] .sn-popup-preview__frame {
  max-width: 22rem;
}
.sn-popup-preview[data-frame='phone'] .sn-popup {
  width: 100%;
  border-radius: var(--radius-lg) var(--radius-lg) 0 0;
}
.sn-popup-preview[data-frame='phone'] .sn-popup__picture {
  max-height: 6rem;
}
.sn-popup-preview .sn-popup__close {
  visibility: hidden;
}

.sn-sf-bar {
  position: sticky;
  top: 0;
  z-index: 20;
  border-bottom: 1px solid var(--c-line);
  background: var(--c-canvas);
}
html[data-hdr-sticky="static"] .sn-sf-bar {
  position: static;
}
/* Translucent only reads as intentional while the bar stays put over the page scrolling under
   it, and only where a mouse can hover it: backdrop-filter breaks position:fixed during momentum
   scroll on a touch screen, so `(hover: hover) and (pointer: fine)` is what keeps a phone always
   solid, whatever header_bg holds - the same guard line 594 already uses for a touch target. */
@media (min-width: 48rem) and (hover: hover) and (pointer: fine) {
  html[data-hdr-sticky="sticky"][data-hdr-bg="translucent"] .sn-sf-bar {
    background: color-mix(in srgb, var(--c-canvas) 82%, transparent);
    backdrop-filter: blur(10px);
  }
}
/* ONE ROW, ONE SET OF MARKUP, EVERY LAYOUT A REARRANGEMENT OF THE SAME THREE ITEMS (task #783):
   the shop (logo or name), the search box, the language-and-cart cluster - in that DOM order,
   which is what "classic" already looks like with no override at all. FLEX, NOT GRID: a CSS Grid
   `auto` track sizes to its full max-content (a name on ONE unbroken line) whenever the row has
   room, ahead of a `fr` track getting anything - which starved the name's column to 0 and wrapped
   a 40-character name one letter per line even with plenty of space free. Flexbox's shrink/grow
   is proportional to what is actually short, so the shop's `flex: 1 1 auto` gives it the room
   between the fixed-size items and only gives up what it must; `min-width: 0` with
   `overflow-wrap: anywhere` is what then lets a long name wrap AT ALL, inside the box it has,
   instead of forcing the row wider than the phone. `flex-wrap: wrap` is the last-resort safety
   net a plain shop always had (the row itself wraps if even a shrunk name is not enough room) and
   what search_forward's own full-width row on a phone (below) is built from. */
.sn-sf-bar__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
  width: 100%;
  max-width: min(var(--page-max), 72rem);
  margin-inline: auto;
  padding: 0.75rem clamp(1rem, 4vw, 2rem);
}
.sn-sf-shop {
  flex: 1 1 auto;
  min-width: 0;
  color: var(--c-ink);
  font-size: 1.125rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  text-decoration: none;
  overflow-wrap: anywhere;
}
/* A shop's logo is capped by HEIGHT and never by width: a wide wordmark and a square badge then
   sit the same height in the bar and each keeps its own shape, whatever pixels the owner sent.
   max-width is the phone's guard - a very wide banner shrinks rather than pushing the rest of the
   row off, inside a box that can itself shrink to nothing before that (task #783: measured with a
   900x60 logo at 360/390/768/1280, sticky and static, on all three layouts). */
.sn-sf-shop__logo {
  display: block;
  height: var(--sn-logo-h, 2.25rem);
  width: auto;
  max-width: 100%;
  object-fit: contain;
}
/* CENTERED (Chris, 2026-11-04: "in centred mode the logo is not in the center" - the flex row centred
   it between a lone search icon and the whole cluster, so it sat left of the middle): a grid of three
   named areas, `1fr auto 1fr`. The two flanks are EQUAL tracks whatever is in them, so the shop sits on
   the bar's centre line exactly; each flank's minimum is still its own content (`1fr` is minmax(auto,
   1fr)), so on a phone too narrow for equal flanks the cluster keeps its size and the shop gives way -
   the cart is never pushed off or covered. The shop's own track is `auto`: it takes its whole name when
   there is room for it and wraps inside what is left when there is not. */
html[data-hdr-logo="center"] .sn-sf-bar__inner {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  grid-template-areas: "search shop end";
  align-items: center;
}
html[data-hdr-logo="center"] .sn-sf-search {
  grid-area: search;
  justify-self: start;
}
html[data-hdr-logo="center"] .sn-sf-shop {
  grid-area: shop;
  justify-self: center;
  text-align: center;
}
html[data-hdr-logo="center"] .sn-sf-bar__right {
  grid-area: end;
  justify-self: end;
}
/* An opened search fills its own flank on a wide screen, and takes a row of its own on a phone. */
html[data-hdr-logo="center"] .sn-sf-search.is-open {
  justify-self: stretch;
}
@media (max-width: 47.99rem) {
  html[data-hdr-logo="center"] .sn-sf-bar__inner:has(.sn-sf-search.is-open) {
    grid-template-areas: "shop shop end" "search search search";
  }
  html[data-hdr-logo="center"] .sn-sf-bar__inner:has(.sn-sf-search.is-open) .sn-sf-shop {
    justify-self: start;
  }
}
/* SEARCH-FORWARD: a field beside the logo, not a button on the right. The FIELD takes all the room
   between the shop and the cluster (Chris, 2026-11-04: "the search field should be 100% width, fill
   the space between the logo and the buttons" - the shop used to grow too and kept a third of it);
   the shop keeps its own width and only gives way when it must, and the cluster never does.
   header_search_w `normal` caps the field below. */
@media (min-width: 48rem) {
  html[data-hdr-search="inline"] .sn-sf-shop {
    flex: 0 1 auto;
  }
}
html[data-hdr-search="inline"] .sn-sf-search {
  flex: 1 1 0%;
  min-width: 0;
}
/* A phone has no room to widen the bar for a field: order pushes it after the shop and the
   cluster, and flex-basis: 100% is what makes flex-wrap actually start a new row for it, rather
   than squeezing it into the first one - which is also what "also on phones" means for
   search_forward: the field is still there, never a button, just on its own row. */
@media (max-width: 47.99rem) {
  html[data-hdr-search="inline"] .sn-sf-search {
    order: 1;
    flex-basis: 100%;
  }
}
.sn-sf-bar__right {
  flex: none;
  display: flex;
  align-items: center;
  gap: 0.75rem;
}
/* The settings cog (views/storefront.mjs shopSettings): the kit's own menu, drawn as one more of the
   bar's icon boxes - the same size, hairline, corners and line weight as the cart beside it - through
   the kit's `::part(trigger)`. Its menu opens under it, towards the page. */
.sn-sf-cog {
  --mx-cog-edge: 0;
  --mx-cog-color: var(--c-ink);
  --mx-cog-hover: var(--c-accent);
  flex: none;
}
.sn-sf-cog::part(trigger) {
  width: calc(1rem + 1.25rem + 2 * var(--mx-stroke, 1px));
  height: calc(1rem + 1.25rem + 2 * var(--mx-stroke, 1px));
  min-width: 0;
  min-height: 0;
  border: var(--mx-stroke, 1px) solid var(--c-line);
  border-radius: var(--mx-control-radius, var(--radius-sm));
  transition: border-color 0.15s ease, background-color 0.15s ease, color 0.15s ease;
}
.sn-sf-cog::part(trigger):hover {
  border-color: var(--c-accent);
  background: var(--c-surface-2);
}
@media (hover: none) and (pointer: coarse) {
  .sn-sf-cog::part(trigger) {
    width: 2.75rem;
    height: 2.75rem;
  }
}
.sn-sf-cart {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.375rem;
  padding: 0.5rem;
  border: var(--mx-stroke, 1px) solid var(--c-line-strong);
  border-radius: var(--mx-control-radius, var(--radius-sm));
  color: var(--c-ink);
  text-decoration: none;
}
.sn-sf-cart:hover {
  border-color: var(--c-accent);
  color: var(--c-accent);
}
/* A finger is not a mouse: the same floor line 594 gives every other icon button in this product. */
@media (hover: none) and (pointer: coarse) {
  .sn-sf-cart {
    min-width: 2.75rem;
    min-height: 2.75rem;
  }
}
.sn-sf-cart__count {
  min-width: 1.25rem;
  padding: 0 0.25rem;
  border-radius: var(--radius-sm);
  background: var(--c-accent);
  color: var(--c-accent-ink);
  font-size: 0.75rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  text-align: center;
}
.sn-sf-cart__count[hidden] {
  display: none;
}
/* A shopper's own sign-in (task #3157), the same icon-box shape .sn-sf-cart already is - so the
   two sit as a matched pair, whichever one draws first in a given header_layout. */
.sn-sf-account {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.5rem;
  border: var(--mx-stroke, 1px) solid var(--c-line-strong);
  border-radius: var(--mx-control-radius, var(--radius-sm));
  color: var(--c-ink);
  text-decoration: none;
}
.sn-sf-account:hover {
  border-color: var(--c-accent);
  color: var(--c-accent);
}
@media (hover: none) and (pointer: coarse) {
  .sn-sf-account {
    min-width: 2.75rem;
    min-height: 2.75rem;
  }
}
.sn-sf-account__initial {
  min-width: 1.25rem;
  font-size: 0.875rem;
  font-weight: 700;
  line-height: 1;
}

/* The wishlist link beside it (task #3159), the same icon-box shape and the same count badge
   .sn-sf-cart__count already is. */
.sn-sf-wish-link {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.375rem;
  padding: 0.5rem;
  border: var(--mx-stroke, 1px) solid var(--c-line-strong);
  border-radius: var(--mx-control-radius, var(--radius-sm));
  color: var(--c-ink);
  text-decoration: none;
}
.sn-sf-wish-link:hover {
  border-color: var(--c-accent);
  color: var(--c-accent);
}
@media (hover: none) and (pointer: coarse) {
  .sn-sf-wish-link {
    min-width: 2.75rem;
    min-height: 2.75rem;
  }
}
.sn-sf-wish-link__count {
  min-width: 1.25rem;
  padding: 0 0.25rem;
  border-radius: var(--radius-sm);
  background: var(--c-accent);
  color: var(--c-accent-ink);
  font-size: 0.75rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  text-align: center;
}
.sn-sf-wish-link__count[hidden] {
  display: none;
}

/* "My orders" (task #3164): a plain list, not the browser's own bulleted one - each <li> holds
   ONE link, and that link IS the row, so a tap anywhere on it (not only the number) opens the
   order. The number-and-date column grows to fill the row, which is what pushes the pill and the
   tabular-numeral total to the right without a separate alignment rule for either. */
.sn-sf-account-orders {
  margin: 0;
  padding: 0;
  list-style: none;
  border-top: 1px solid var(--c-line);
}
.sn-sf-account-order {
  border-bottom: 1px solid var(--c-line);
}
.sn-sf-account-order__link {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.375rem 0.75rem;
  margin: 0 -0.5rem;
  padding: 0.875rem 0.5rem;
  border-radius: var(--radius-sm);
  color: inherit;
  text-decoration: none;
}
.sn-sf-account-order__link:hover {
  background: var(--c-surface-2);
}
.sn-sf-account-order__link:hover .sn-sf-account-order__number {
  color: var(--c-accent);
}
.sn-sf-account-order__link:focus-visible {
  outline: 2px solid var(--c-accent);
  outline-offset: -2px;
}
.sn-sf-account-order__text {
  display: flex;
  flex: 1 1 10rem;
  flex-direction: column;
  gap: 0.125rem;
  min-width: 0;
}
.sn-sf-account-order__number {
  font-weight: 600;
}
.sn-sf-account-order__date {
  color: var(--c-ink-muted);
  font-size: 0.8125rem;
}
.sn-sf-account-order__status {
  flex: 0 0 auto;
}
.sn-sf-account-order__total {
  flex: 0 0 auto;
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}
.sn-sf-account-signout {
  margin-top: 1.5rem;
}

/* The four places under /account (task #3161 added Privacy to the three #3173 had), as the
   shop's own tabs, so none is reachable only by typing its address. */
/* Four tabs no longer fit a 360-390px phone without a word wrapping - the same
   horizontally-scrolling, mouse-wraps row .sn-sf-cats__list already is, rather than letting a
   tab's own text break. */
/* Unlike .sn-sf-cats__list (an owner's own, possibly long, list of categories - wrapping it on a
   fine pointer reveals real content a hidden scrollbar would otherwise trap), this is always the
   same four short tabs: wrapping them never reveals anything a scroll does not, so there is no
   fine-pointer exception here - a plain desktop window resized down to 390px keeps them on ONE
   row (task #3173's own browser check #3176 caught a copy of the categories' own wrap rule
   putting "Kívánságlista" on its own line at that width with a mouse, not a finger, attached).
   Still reachable with no touch: shift+wheel, a trackpad, or Tab (a focused link auto-scrolls
   into view) - the task's own words allow "a scrolling tab row". */
/* Task #3186: account.*.tab (views/account.mjs) shortened the visible text itself (the page's own
   h1 keeps account.*.title, the long form) so the row fits a 360px phone in both languages
   measured, but the fallback the task also asked for stays in for whichever translated word ends
   up longer - the inline script after the nav (accountTabs()) scrolls the open tab into view on
   load, and toggles .is-scrolled/.has-more on the wrap below from the nav's own scroll position so
   a reader can tell there is more to the row. */
.sn-sf-account-tabs-wrap {
  position: relative;
  margin: 1rem 0 1.5rem;
}
.sn-sf-account-tabs-wrap::before,
.sn-sf-account-tabs-wrap::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 3px;
  width: 1.5rem;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.15s;
}
.sn-sf-account-tabs-wrap::before {
  inset-inline-start: 0;
  background: linear-gradient(to right, var(--c-canvas), transparent);
}
.sn-sf-account-tabs-wrap::after {
  inset-inline-end: 0;
  background: linear-gradient(to left, var(--c-canvas), transparent);
}
.sn-sf-account-tabs-wrap.is-scrolled::before {
  opacity: 1;
}
.sn-sf-account-tabs-wrap.has-more::after {
  opacity: 1;
}
/* Right to left (the kit's own .mx-gallery fade follows the same shape): the edge each pseudo
   sits on flips with inset-inline-*, but a gradient keyword never does, so the two swap here. */
[dir='rtl'] .sn-sf-account-tabs-wrap::before {
  background: linear-gradient(to left, var(--c-canvas), transparent);
}
[dir='rtl'] .sn-sf-account-tabs-wrap::after {
  background: linear-gradient(to right, var(--c-canvas), transparent);
}
.sn-sf-account-tabs {
  display: flex;
  gap: 1.25rem;
  border-bottom: 1px solid var(--c-line);
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.sn-sf-account-tabs::-webkit-scrollbar {
  display: none;
}
.sn-sf-account-tabs a {
  display: inline-block;
  /* flex: none alone keeps a tab on one line - it never shrinks below its own text's width, the
     same reason .sn-sf-cats__list li needs no forced single-line rule either (layout.test.mjs
     bans one with nowhere to go, task #3173's own second pass). */
  flex: none;
  padding: 0.625rem 0.125rem;
  margin-bottom: -1px;
  border-bottom: 2px solid transparent;
  color: var(--c-ink-muted);
  font-weight: 600;
  text-decoration: none;
}
.sn-sf-account-tabs a[aria-current='page'] {
  color: var(--c-ink);
  border-bottom-color: var(--c-accent);
}
@media (hover: hover) {
  .sn-sf-account-tabs a:hover {
    color: var(--c-ink);
  }
}

/* The saved-addresses table (task #3166): the kit's own table, laptop keeps it, but below 640px
   the four columns (name, address, default, actions) squeeze too narrow to read - so a phone gets
   one row per address instead, the name and address stacked, the default pill/button and the
   delete button side by side underneath. The rows and their a[data-mx-row] are untouched: this is
   a reflow of the SAME <tr>/<td>s, not a second markup, so the kit's own row-click delegation
   (table.mx-table tr) still opens the edit page from anywhere on the row that is not a button. */
@media (max-width: 39.9375rem) {
  .sn-addr-table thead {
    display: none;
  }
  .sn-addr-table,
  .sn-addr-table tbody {
    display: block;
    width: 100%;
  }
  .sn-addr-table tr {
    display: grid;
    grid-template-columns: 1fr auto;
    grid-template-areas: 'name name' 'addr addr' 'default actions';
    column-gap: 0.75rem;
    row-gap: 0.375rem;
    padding: 0.875rem 1.25rem;
    border-bottom: 1px solid var(--c-line);
  }
  .sn-addr-table tr:last-child {
    border-bottom: 0;
  }
  .sn-addr-table td {
    display: block;
    padding: 0;
    border-bottom: 0;
  }
  .sn-addr-table td:nth-child(1) {
    grid-area: name;
    font-weight: 600;
  }
  .sn-addr-table td:nth-child(2) {
    grid-area: addr;
    color: var(--c-ink-muted);
    font-size: 0.875rem;
  }
  .sn-addr-table td:nth-child(3) {
    grid-area: default;
    align-self: center;
  }
  .sn-addr-table td:nth-child(4) {
    grid-area: actions;
    align-self: center;
    justify-self: end;
  }
}

/* The activity log table (task #3171): same reflow idiom as .sn-addr-table above - the SAME
   <tr>/<td>s, just repositioned by grid-area, so a[data-mx-row] on the subject cell still opens
   the target through the kit's own row-click delegation. Below 640px the four columns (when, who,
   action, subject) become two lines: what happened and to what (action · subject) first, who and
   when second, small and muted - grid-area placement reorders the cells regardless of DOM order,
   so the markup itself (views/team-activity.mjs row()) stays untouched. */
@media (max-width: 39.9375rem) {
  .sn-activity-table thead {
    display: none;
  }
  .sn-activity-table,
  .sn-activity-table tbody {
    display: block;
    width: 100%;
  }
  .sn-activity-table tr {
    display: grid;
    grid-template-columns: auto 1fr;
    grid-template-areas: 'action subject' 'when who';
    column-gap: 0.375rem;
    row-gap: 0.25rem;
    padding: 0.875rem 1.25rem;
    border-bottom: 1px solid var(--c-line);
  }
  .sn-activity-table tr:last-child {
    border-bottom: 0;
  }
  .sn-activity-table td {
    display: block;
    padding: 0;
    border-bottom: 0;
  }
  .sn-activity-table td:nth-child(1) {
    grid-area: when;
    font-size: 0.8125rem;
    color: var(--c-ink-muted);
  }
  .sn-activity-table td:nth-child(2) {
    grid-area: who;
    font-size: 0.8125rem;
    color: var(--c-ink-muted);
    min-width: 0;
    overflow-wrap: anywhere;
  }
  .sn-activity-table td:nth-child(2)::before {
    content: '· ';
  }
  .sn-activity-table td:nth-child(3) {
    grid-area: action;
    font-weight: 600;
  }
  .sn-activity-table td:nth-child(4) {
    grid-area: subject;
    font-weight: 600;
    min-width: 0;
    overflow-wrap: anywhere;
  }
  .sn-activity-table td:nth-child(4):not(:empty)::before {
    content: '· ';
    font-weight: 400;
    color: var(--c-ink-muted);
  }
}

/* The search box (task #783): ONE piece of markup (views/storefront.mjs searchBox) - a real link
   AND a real form, always both in the DOM - and every layout is which one static/app.css shows.
   `button` (classic, centered: the default, so it needs no attribute) shows the link; a script
   toggles `.is-open` to swap to the form in place, and with no script the link still opens
   /search on its own. `inline` (search_forward) shows the form outright, no press needed. */
.sn-sf-search {
  flex: none;
  display: flex;
  align-items: center;
  min-width: 0;
}
.sn-sf-search__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.5rem;
  height: 2.5rem;
  border: var(--mx-stroke, 1px) solid var(--c-line-strong);
  border-radius: var(--mx-control-radius, var(--radius-sm));
  color: var(--c-ink);
}
.sn-sf-search__btn:hover {
  border-color: var(--c-accent);
  color: var(--c-accent);
}
.sn-sf-search__form {
  display: none;
  align-items: center;
  width: 100%;
  min-width: 0;
  border: var(--mx-stroke, 1px) solid var(--c-line-strong);
  border-radius: var(--mx-control-radius, var(--radius-sm));
  background: var(--c-surface);
}
.sn-sf-search__input {
  flex: 1 1 auto;
  min-width: 0;
  height: 2.5rem;
  padding-inline: 0.75rem;
  border: 0;
  border-radius: inherit;
  background: transparent;
  color: var(--c-ink);
  font-size: 0.9375rem;
}
.sn-sf-search__input::placeholder {
  color: var(--c-ink-subtle);
}
.sn-sf-search__go {
  display: inline-flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: 2.5rem;
  height: 2.5rem;
  border: 0;
  background: transparent;
  color: var(--c-ink);
}
.sn-sf-search__go:hover {
  color: var(--c-accent);
}
/* ONE focus ring, round the whole pill (Chris, 2026-11-04: "the search field when clicked has a weird
   looking focus border issue" - the kit's ring was drawn on the field INSIDE the pill, with the kit's
   own corner, so it sat misaligned within the pill's edge). The pill carries the kit's ring (2px accent,
   2px off the edge, its own corners); the field and the button inside draw none, and the button says it
   has the focus by its colour. Both themes: the ring is the accent, which is made readable on each. */
.sn-sf-search__input:focus-visible,
.sn-sf-search__go:focus-visible {
  outline: none;
}
.sn-sf-search__go:focus-visible {
  color: var(--c-accent);
}
.sn-sf-search__form:focus-within {
  outline: 2px solid var(--c-accent);
  outline-offset: 2px;
}
/* A finger is not a mouse: the same floor line 594 gives every other icon button in this product. */
@media (hover: none) and (pointer: coarse) {
  .sn-sf-search__btn,
  .sn-sf-search__go {
    width: 2.75rem;
    height: 2.75rem;
  }
}
.sn-sf-search.is-open .sn-sf-search__btn {
  display: none;
}
.sn-sf-search.is-open .sn-sf-search__form {
  display: flex;
}
html[data-hdr-search="inline"] .sn-sf-search__btn {
  display: none;
}
html[data-hdr-search="inline"] .sn-sf-search__form {
  display: flex;
}
/* header_search_w, desktop only (mobile:false - a phone's field is already full width, whatever
   this holds): `normal` caps how far the field grows, next to the logo; `wide` is uncapped and fills
   the whole gap up to the cluster (the search box's flex-grow, above). */
@media (min-width: 48rem) {
  html[data-hdr-search="inline"][data-hdr-searchw="normal"] .sn-sf-search__form {
    max-width: 26rem;
  }
}

/* Bands, headings and the grid. */
.sn-sf-hero {
  padding: clamp(2rem, 6vw, 3.5rem) 0 0;
}
.sn-sf-band {
  padding: clamp(1.5rem, 5vw, 2.75rem) 0;
}
.sn-sf-h1,
.sn-sf-slide__title {
  margin: 0;
  font-size: clamp(1.75rem, 5vw, 2.5rem);
  font-weight: 800;
  letter-spacing: -0.02em;
  line-height: 1.1;
  text-wrap: balance;
  overflow-wrap: anywhere;
}
.sn-sf-h1--product,
.sn-sf-h1--cart {
  font-size: clamp(1.375rem, 3.5vw, 1.875rem);
}
.sn-sf-h2 {
  margin: 0 0 1rem;
  font-size: 1.0625rem;
  font-weight: 700;
  letter-spacing: -0.01em;
}
.sn-sf-lead {
  margin: 0.75rem 0 0;
  max-width: 60ch;
  color: var(--c-ink-muted);
  line-height: 1.6;
  text-wrap: pretty;
}
/* A category's own picture, above its name (task #486). Capped, never full-bleed like the shop's
   banner: a category page is words and a grid, not a landing page. */
.sn-sf-cat-pic {
  display: block;
  width: 100%;
  max-width: 40rem;
  max-height: 16rem;
  margin-bottom: 1rem;
  object-fit: cover;
  border-radius: var(--radius-md);
}
/* The shop's banner (task #479). A fixed-shape frame, cropped from its centre, so the page does not
   jump when the picture arrives and a portrait photograph cannot push the products off a phone.
   The words are NOT on it: they sit under it on the page's own canvas, where the tokens already
   guarantee they can be read whatever colour the seller's photograph is. */
.sn-sf-banner {
  display: block;
  width: 100%;
  aspect-ratio: 3 / 1;
  margin: 0 0 clamp(1rem, 3vw, 1.5rem);
  border-radius: var(--radius-md);
  background: var(--c-surface-2);
  object-fit: cover;
}
@media (max-width: 40rem) {
  .sn-sf-banner {
    aspect-ratio: 2 / 1;
  }
}

/* The home page's blocks (task #782). The hero's height is the picture frame's shape; medium is what the
   banner always was (3:1, 2:1 on a phone). */
.sn-sf-hero--short .sn-sf-banner {
  aspect-ratio: 4 / 1;
}
.sn-sf-hero--tall .sn-sf-banner {
  aspect-ratio: 2 / 1;
}
@media (max-width: 40rem) {
  .sn-sf-hero--short .sn-sf-banner {
    aspect-ratio: 5 / 2;
  }
  .sn-sf-hero--tall .sn-sf-banner {
    aspect-ratio: 4 / 3;
  }
}
/* A shop's own blog (task #3319). The list is a card grid, the same minmax rule .sn-sf-grid
   already uses so a phone gets one column and a wide screen gets several with no media query of
   its own; the single post reuses .sn-sf-banner outright for its cover. */
.sn-sf-blog-grid {
  display: grid;
  gap: clamp(1rem, 2.5vw, 1.5rem);
  grid-template-columns: repeat(auto-fill, minmax(min(18rem, 100%), 1fr));
  margin: 1.5rem 0 0;
  padding: 0;
  list-style: none;
}
.sn-sf-blog-card {
  display: block;
  color: inherit;
  text-decoration: none;
}
.sn-sf-blog-card__pic {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  margin-bottom: 0.75rem;
  border-radius: var(--radius-md);
  background: var(--c-surface-2);
  object-fit: cover;
}
.sn-sf-blog-card__date {
  display: block;
  margin: 0 0 0.25rem;
  font-size: 0.8125rem;
  color: var(--c-ink-muted);
}
.sn-sf-blog-card__title {
  margin: 0 0 0.375rem;
  font-size: 1.125rem;
  font-weight: 700;
  letter-spacing: -0.01em;
}
.sn-sf-blog-card__excerpt {
  margin: 0;
  color: var(--c-ink-muted);
  line-height: 1.5;
  overflow-wrap: anywhere;
}
.sn-sf-blog-post__date {
  display: block;
  margin: 0 0 1rem;
  color: var(--c-ink-muted);
}
/* Where the words sit under the picture, and how their lines are set. Left is what the banner did:
   the whole width. Start and end, not left and right, so a right-to-left shop mirrors. */
.sn-sf-words {
  min-width: 0;
}
.sn-sf-words--at-center,
.sn-sf-words--at-right {
  max-width: 48rem;
}
.sn-sf-words--at-center {
  margin-inline: auto;
}
.sn-sf-words--at-right {
  margin-inline-start: auto;
}
.sn-sf-words--text-center {
  text-align: center;
}
.sn-sf-words--text-right {
  text-align: end;
}
.sn-sf-words--text-center .sn-sf-lead {
  margin-inline: auto;
}
.sn-sf-words--text-right .sn-sf-lead {
  margin-inline-start: auto;
}
.sn-sf-slide__title {
  margin-top: 0;
}
.sn-sf-slide__act {
  margin: 1rem 0 0;
}
/* Two or more slides: a strip the browser scrolls and snaps, so it works with no script, and one
   dot per slide. The picture keeps its rounded frame; the strip only clips sideways. */
.sn-sf-slides {
  display: flex;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  border-radius: var(--radius-md);
}
.sn-sf-slides::-webkit-scrollbar {
  display: none;
}
.sn-sf-slides:focus-visible {
  outline: 2px solid var(--c-accent);
  outline-offset: 2px;
}
@media (prefers-reduced-motion: no-preference) {
  .sn-sf-slides {
    scroll-behavior: smooth;
  }
}
.sn-sf-slide {
  flex: 0 0 100%;
  min-width: 0;
  scroll-snap-align: start;
}
.sn-sf-dots {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  margin: 0;
  padding: 0;
  list-style: none;
}
.sn-sf-dot {
  position: relative;
  display: block;
  width: 2.75rem;
  height: 2.75rem;
}
.sn-sf-dot::after {
  content: '';
  position: absolute;
  inset: 0;
  width: 0.625rem;
  height: 0.625rem;
  margin: auto;
  border-radius: 50%;
  background: var(--c-line-strong);
}
.sn-sf-dot[aria-current='true']::after {
  background: var(--c-accent);
}
.sn-sf-dot:focus-visible {
  outline: 2px solid var(--c-accent);
  outline-offset: -4px;
  border-radius: 50%;
}

/* A block of the owner's words. It is the page editor's plain text, so the prose rules apply. */
.sn-sf-text.sn-sf-prose__body {
  margin-top: 0;
}
.sn-sf-text > :first-child {
  margin-top: 0;
}
.sn-sf-text--center {
  text-align: center;
}
.sn-sf-text--right {
  text-align: end;
}
.sn-sf-text--center ul,
.sn-sf-text--right ul {
  display: inline-block;
  text-align: start;
}

/* Blocks side by side: six columns on a wide screen, each block spanning its fraction of them; one
   column on a phone, where every block is the full width. */
.sn-sf-row {
  display: grid;
  gap: clamp(1rem, 3vw, 2rem);
  grid-template-columns: minmax(0, 1fr);
}
.sn-sf-col {
  min-width: 0;
}
@media (min-width: 48rem) {
  .sn-sf-row {
    grid-template-columns: repeat(6, minmax(0, 1fr));
  }
  .sn-sf-col--2 {
    grid-column: span 2;
  }
  .sn-sf-col--3 {
    grid-column: span 3;
  }
  .sn-sf-col--4 {
    grid-column: span 4;
  }
}

/* The categories grid: a tile is the first photo of a product in the category, or just its name. */
.sn-sf-tiles {
  display: grid;
  gap: clamp(0.75rem, 2vw, 1.25rem);
  grid-template-columns: repeat(auto-fill, minmax(min(var(--tile), 100%), 1fr));
  margin: 0;
  padding: 0;
  list-style: none;
}
.sn-sf-tiles--small {
  --tile: 8rem;
}
.sn-sf-tiles--medium {
  --tile: 12rem;
}
.sn-sf-tiles--large {
  --tile: 16rem;
}
.sn-sf-tile {
  min-width: 0;
}
.sn-sf-tile__link {
  display: block;
  min-width: 0;
  color: var(--c-ink);
  text-decoration: none;
}
.sn-sf-tile__shot {
  display: block;
  width: 100%;
  overflow: hidden;
  border: 1px solid var(--c-line);
  border-radius: var(--radius-lg);
  background: var(--c-surface-2);
}
.sn-sf-tile__shot--square {
  aspect-ratio: 1 / 1;
}
.sn-sf-tile__shot--portrait {
  aspect-ratio: 3 / 4;
}
.sn-sf-tile__shot--landscape {
  aspect-ratio: 4 / 3;
}
.sn-sf-tile__shot img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.sn-sf-tile__name {
  display: block;
  margin-top: 0.5rem;
  font-weight: 600;
  overflow-wrap: anywhere;
}
.sn-sf-tile__link:hover .sn-sf-tile__name {
  color: var(--c-accent);
}
.sn-sf-tile__link:focus-visible {
  outline: 2px solid var(--c-accent);
  outline-offset: 2px;
  border-radius: var(--radius-lg);
}

/* A separator: space above and below, and a line if the owner wants one. The two spaces arrive as
   custom properties from the server (integers, 0 to 200), like the colour samples do. */
.sn-sf-sep {
  padding-block: var(--sep-above, 24px) var(--sep-below, 24px);
}
.sn-sf-sep__line {
  margin: 0;
  border: 0;
  border-top: 1px solid var(--c-line);
}

/* What the owner's preview says where a block has nothing to show. Shoppers never get this. */
.sn-sf-band--note {
  padding-block: 0.5rem;
}
.sn-sf-note {
  margin: 0;
  padding: 0.75rem 1rem;
  border: 1px dashed var(--c-line-strong);
  border-radius: var(--radius-md);
  color: var(--c-ink-muted);
  font-size: 0.9375rem;
  overflow-wrap: anywhere;
}
.sn-sf-note__tag {
  margin-inline-end: 0.375rem;
  color: var(--c-ink);
  font-weight: 700;
}

/* Selling points (task #784): the owner never picks a column count, the number of items does -
   1 to 4, one per row on a phone. The icon takes the shop's accent, already text-safe. */
.sn-sf-features {
  display: grid;
  gap: clamp(1.25rem, 3vw, 2rem);
  grid-template-columns: minmax(0, 1fr);
  margin: 0;
  padding: 0;
  list-style: none;
}
@media (min-width: 40rem) {
  .sn-sf-features {
    grid-template-columns: repeat(var(--cols, 3), minmax(0, 1fr));
  }
}
.sn-sf-feature {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  min-width: 0;
}
.sn-sf-feature__icon {
  flex: none;
  color: var(--c-accent);
}
.sn-sf-feature__text {
  padding-top: 0.25rem;
  font-weight: 600;
  overflow-wrap: anywhere;
}

/* A call-to-action band (task #784): a fixed fill the owner picked, edge to edge, with text a
   derived black or white so it always reads - never a token, because the fill itself is not one. */
.sn-sf-cta {
  padding-block: clamp(1.5rem, 4vw, 2.5rem);
  background: var(--cta-bg);
  color: var(--cta-ink);
}
.sn-sf-cta__text,
.sn-sf-cta__link {
  margin: 0;
  font-size: 1.125rem;
  font-weight: 700;
  overflow-wrap: anywhere;
}
.sn-sf-cta__link {
  display: inline-block;
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 0.2em;
}
.sn-sf-cta--center {
  text-align: center;
}
.sn-sf-cta--right {
  text-align: end;
}

/* A coupon ticket (task #784): a dashed box, the code as selectable text, and a Copy button that
   works with no script at all selected - the script only saves the reader a manual select-and-copy. */
.sn-sf-ticket {
  display: flex;
  justify-content: center;
}
.sn-sf-ticket__body {
  max-width: 32rem;
  width: 100%;
  padding: clamp(1.25rem, 3vw, 1.75rem);
  border: 2px dashed var(--c-line-strong);
  border-radius: var(--radius-lg);
  background: var(--c-surface);
  text-align: center;
}
.sn-sf-ticket__heading {
  margin: 0 0 0.75rem;
  font-weight: 700;
  font-size: 1.0625rem;
  overflow-wrap: anywhere;
}
.sn-sf-ticket__code {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
}
.sn-sf-ticket__value {
  padding: 0.5rem 0.875rem;
  border: 1px solid var(--c-line);
  border-radius: var(--radius-md);
  background: var(--c-surface-2);
  font-family: var(--font-mono, monospace);
  font-size: 1.125rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  user-select: all;
  overflow-wrap: anywhere;
}
.sn-sf-ticket__fine {
  margin-top: 0.75rem;
  color: var(--c-ink-muted);
  font-size: 0.875rem;
}
.sn-sf-ticket__fine :first-child {
  margin-top: 0;
}

/* One or more pictures (task #784): a still picture at its natural height, or - two or more - the
   hero's own strip-and-dots, so nothing here duplicates static/hero.js or its CSS. */
.sn-sf-imgset__pic {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius-md);
}
.sn-sf-imgset .sn-sf-slides .sn-sf-imgset__pic {
  aspect-ratio: 16 / 9;
  height: 100%;
  object-fit: cover;
}
@media (max-width: 40rem) {
  .sn-sf-imgset .sn-sf-slides .sn-sf-imgset__pic {
    aspect-ratio: 4 / 3;
  }
}
.sn-sf-imgset--center {
  max-width: 48rem;
  margin-inline: auto;
}
.sn-sf-imgset--right {
  max-width: 48rem;
  margin-inline-start: auto;
}

/* A video block (task #784): a cover and a play button until pressed, at the shape the owner
   picked. The button fills the frame so the whole picture is the target, not just the glyph. */
.sn-sf-video__frame {
  position: relative;
  aspect-ratio: var(--video-ratio, 16 / 9);
  overflow: hidden;
  border-radius: var(--radius-lg);
  background: var(--c-accent);
}
.sn-sf-video__play {
  display: block;
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: pointer;
}
.sn-sf-video__cover {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.sn-sf-video__icon {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 4rem;
  height: 4rem;
  margin: auto;
  border-radius: 50%;
  background: var(--c-accent);
  color: var(--c-accent-ink);
}
.sn-sf-video__play:hover .sn-sf-video__icon,
.sn-sf-video__play:focus-visible .sn-sf-video__icon {
  background: var(--c-accent-strong);
}
.sn-sf-video__play:focus-visible {
  outline: 2px solid var(--c-accent);
  outline-offset: 2px;
}
.sn-sf-video__embed {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
}
/* Pressing play is the shopper's consent to load YouTube (task #796), so the cover says so before
   the click - quiet, but readable in both themes, and outside the button so it is never read as
   part of its label. */
.sn-sf-video__note {
  margin: 0.5rem 0 0;
  color: var(--c-ink-muted);
  font-size: 0.8125rem;
}

/* A label only a screen reader gets: clipped to nothing rather than display: none, which would
   take it out of the accessibility tree with everything else. Logical sizes, like the rest. */
.sn-sf-hide {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
}

.sn-sf-grid {
  display: grid;
  gap: clamp(0.75rem, 2vw, 1.25rem);
  grid-template-columns: repeat(auto-fill, minmax(min(15rem, 100%), 1fr));
  margin: 0;
  padding: 0;
  list-style: none;
}
/* Task #3278: the quick-add row sits below the card as a normal sibling, the exact reason
   .sn-sf-wishcard moved its own height:100% off the link and onto its own wrapper (same CSS
   comment, further down) - this flexes/stretches the CELL itself, so a card's own height:100%
   below (now flex:1 1 auto) only ever fills what is left after the actions row, and the grid's
   row-height still comes from the WHOLE cell's natural content, actions row included. */
.sn-sf-cell {
  position: relative;
  display: flex;
  flex-direction: column;
  height: 100%;
  min-width: 0;
}
/* The shared wishlist's own grid (task #3177): nothing else sits between the h1 and the list on
   that page, so the grid needs its own top margin - the same 1rem the tabs/share box give every
   other account page's heading. */
.sn-sf-wish-shared-grid {
  margin-top: 1rem;
}
.sn-sf-card {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  border: 1px solid var(--c-line);
  border-radius: var(--radius-lg);
  background: var(--c-surface);
  color: var(--c-ink);
  text-decoration: none;
  transition: border-color 0.15s var(--ease-out-soft), transform 0.15s var(--ease-out-soft);
}
.sn-sf-card:hover {
  border-color: var(--c-line-strong);
  transform: translateY(-2px);
}
.sn-sf-card__shot {
  position: relative;
  display: block;
  aspect-ratio: 1 / 1;
  padding: var(--sn-card-pad, 0);
  background: var(--c-surface-2);
}
.sn-sf-card__body {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  gap: 0.375rem;
  min-width: 0;
  padding: 0.875rem;
}
.sn-sf-card__name {
  margin: 0;
  font-size: 1rem;
  font-weight: 600;
  line-height: 1.35;
  overflow-wrap: anywhere;
}
.sn-sf-card__short {
  margin: 0;
  color: var(--c-ink-muted);
  font-size: 0.875rem;
  line-height: 1.45;
}
/* The grid card's own quick-add (task #3278): a plain sibling of the link below the card's body,
   the same reason the heart/compare above are - full width, unlike the row's own inline button,
   because there is no room beside wish/compare for a third control at two columns. */
.sn-sf-card__actions {
  padding: 0 0.875rem 0.875rem;
}
.sn-sf-card__actions .sn-sf-quickadd {
  display: flex;
}
.sn-sf-card__actions .sn-sf-quickadd-btn,
.sn-sf-card__actions .sn-sf-quickadd--pick {
  width: 100%;
}
/* The wishlist's own combination label (task #3159), beside a product's name when it was wished
   against one. */
.sn-sf-card__variant {
  color: var(--c-ink-muted);
  font-weight: 400;
}
/* Two to a row below 640px (task #3190): one shop.ninja front page scroll per 24 products was the
   whole complaint, and every listing - home blocks, /products, a category, /search, "You may also
   like" and both wishlists - draws this same `.sn-sf-grid`/`.sn-sf-card`/`.sn-sf-wishcard` shape,
   so the fix is here once rather than per page. `minmax(0, 1fr)` (not the auto-fill rule above)
   fixes each column's width to exactly half the row regardless of content - the trap the colours
   table hit (studio note: a shared table-cell class with overflow-wrap:anywhere breaks words when
   a column is sized from content). Placed AFTER the unconditional card rules above: a media query
   earlier in the file loses a tie on specificity to a later unconditional rule for the same
   property (font-size, padding), which is why this block must come last. Smaller padding, a
   smaller name size and a tighter name line-height all exist for one reason - fitting this
   shop's longest real word ("Mosogatógépben") on its own line without a mid-word break at 360px,
   the narrowest width this is checked at. */
@media (max-width: 40rem) {
  .sn-sf-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.625rem;
  }
  .sn-sf-card__body,
  .sn-sf-wishcard .sn-sf-card__body {
    padding: 0.5rem;
    gap: 0.25rem;
  }
  .sn-sf-card__name {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    font-size: 0.8125rem;
    line-height: 1.3;
  }
  .sn-sf-card__short {
    display: none;
  }
  .sn-sf-card__actions {
    padding: 0 0.5rem 0.5rem;
  }
}
/* The list view (task #3274): one column at every width, unlike .sn-sf-grid's own 2-up below
   40rem - a row's own content (name, brand, badges, rating, price, stock) needs the full line's
   width to stay readable beside a fixed 96px picture, which two side-by-side columns would not
   leave room for. */
.sn-sf-list {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  margin: 0;
  padding: 0;
  list-style: none;
}
/* The border and background sit here, not on .sn-sf-row itself - the exact reason .sn-sf-wishcard
   below moved them off its own link: the quick-add/heart/compare row is a normal sibling of the
   link, not absolutely positioned over it, so this <li> is what has to wrap both. */
.sn-sf-row-cell {
  display: flex;
  flex-direction: column;
  min-width: 0;
  overflow: hidden;
  border: var(--mx-stroke, 1px) solid var(--c-line);
  border-radius: var(--radius-lg);
  background: var(--c-surface);
  transition: border-color 0.15s var(--ease-out-soft);
}
.sn-sf-row-cell:hover {
  border-color: var(--c-line-strong);
}
.sn-sf-row {
  display: flex;
  gap: 1rem;
  min-width: 0;
  padding: 0.875rem;
  color: var(--c-ink);
  text-decoration: none;
}
.sn-sf-row__shot {
  flex: 0 0 6rem;
  width: 6rem;
  height: 6rem;
  overflow: hidden;
  border-radius: var(--radius-md);
  background: var(--c-surface-2);
}
.sn-sf-row__body {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  gap: 0.25rem;
  min-width: 0;
}
.sn-sf-row__name {
  margin: 0;
  font-size: 1rem;
  font-weight: 600;
  line-height: 1.35;
  overflow-wrap: anywhere;
}
.sn-sf-row__brand {
  margin: 0;
  color: var(--c-ink-muted);
  font-size: 0.8125rem;
}
/* Cut to two lines whatever the width, not just below 40rem the way .sn-sf-card__short is: a row
   stays this narrow at every width, never the grid's own wide desktop column. */
.sn-sf-row__short {
  margin: 0;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  color: var(--c-ink-muted);
  font-size: 0.875rem;
  line-height: 1.45;
}
.sn-sf-row__actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  padding: 0 0.875rem 0.875rem;
}

/* The wishlist's own card (task #3173): the border and background .sn-sf-card gives itself move
   to THIS wrapper, and the link inside carries none of its own - because .sn-sf-card's
   `height: 100%` sizes it against the GRID ROW, and on a row with only one column (a phone) that
   row was being auto-sized from .sn-sf-card's own box alone, with .sn-sf-wish-remove (a normal
   sibling below it, not absolutely positioned like a card's own heart) read as outside that box
   entirely - so it overflowed into the next cell's card rather than growing the row. Moving
   `height: 100%` up to wrap BOTH the link and the remove form fixes the row's own measurement:
   now there is nothing left outside the one box whose height the grid is sizing against. */
.sn-sf-wishcard {
  display: flex;
  flex-direction: column;
  height: 100%;
  min-width: 0;
  overflow: hidden;
  border: var(--mx-stroke, 1px) solid var(--c-line);
  border-radius: var(--radius-lg);
  background: var(--c-surface);
  transition: border-color 0.15s var(--ease-out-soft);
}
.sn-sf-wishcard:hover {
  border-color: var(--c-line-strong);
}
.sn-sf-wishcard__link {
  display: flex;
  flex-direction: column;
  min-width: 0;
  color: var(--c-ink);
  text-decoration: none;
}
/* A plain form under the card, not the heart again (task #3159): the task names it as its own
   action, so it reads as one even though either would work. .sn-sf-wish-action is the same shape
   for a wishlist card's OTHER own forms (task #3160's own "Tell me when it is back" and the shared
   page's own "Add to cart") - never .sn-sf-wish-remove's own name for an action that is not a
   remove. */
.sn-sf-wish-remove,
.sn-sf-wish-action {
  margin: 0.5rem 0.875rem 0.875rem;
}
/* "Share my wishlist" (task #3160): the button alone, or the lead sentence, the link field and
   "Stop sharing" once a link exists. */
.sn-sf-wish-share {
  margin: 1rem 0 1.5rem;
  padding: 1rem 1.25rem;
  border: 1px solid var(--c-line);
  border-radius: var(--radius-lg);
  background: var(--c-surface);
}
.sn-sf-wish-share__actions {
  margin-bottom: 0.75rem;
}
.sn-sf-wish-share input[type='text'] {
  width: 100%;
  margin: 0.5rem 0 0.75rem;
  padding: 0.5rem 0.75rem;
  border: var(--mx-stroke, 1px) solid var(--c-line);
  border-radius: var(--mx-control-radius, var(--radius-md));
  background: var(--c-surface-2);
  color: var(--c-ink);
  font: inherit;
}
.sn-sf-shot {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.sn-sf-shot--none {
  display: block;
  width: 100%;
  height: 100%;
  background: var(--c-surface-2);
}
.sn-sf-out {
  align-self: flex-start;
  margin: 0;
  padding: 0.125rem 0.5rem;
  border: 1px solid var(--c-line-strong);
  border-radius: var(--radius-sm);
  color: var(--c-ink-muted);
  font-size: 0.75rem;
}

/* A price: the badge, the struck figure, then what is charged. One shape everywhere. */
.sn-sf-price {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.375rem 0.5rem;
  margin: auto 0 0;
  min-width: 0;
}
.sn-sf-price__now {
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.sn-sf-price__was {
  color: var(--c-ink-subtle);
  font-size: 0.875rem;
  font-variant-numeric: tabular-nums;
}
.sn-sf-price--big .sn-sf-price__now {
  font-size: 1.5rem;
}
.sn-sf-price--sale .sn-sf-price__now {
  color: var(--c-accent);
}
.sn-sf-badge {
  padding: 0.125rem 0.375rem;
  border-radius: var(--radius-sm);
  background: var(--c-accent);
  color: var(--c-accent-ink);
  font-size: 0.75rem;
  font-weight: 700;
}

/* "€8.40 / kg" (task #3272): a price-per-measure disclosure, smaller and quieter than the price
   it sits under - a sibling <p>, not part of the .sn-sf-price flex row, so it always wraps to its
   own line under a struck "was" price rather than fighting it for space. */
.sn-sf-price__unit {
  margin: 0.125rem 0 0;
  color: var(--c-ink-subtle);
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
}

/* The owner's own badges (task #3141), after the sale badge above: each one a flat colour the
   owner picked, its text black or white (lib/accent.mjs badgeInk), set inline alongside it. */
.sn-sf-badges {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
  margin: 0.375rem 0 0;
}
.sn-sf-badge-pill {
  padding: 0.125rem 0.5rem;
  border-radius: 999px;
  font-size: 0.75rem;
  font-weight: 600;
}

/* The product page. */
.sn-sf-crumb {
  margin: 0 0 1rem;
  font-size: 0.875rem;
}
.sn-sf-crumb a {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  color: var(--c-ink-muted);
  text-decoration: none;
}
.sn-sf-crumb a:hover {
  color: var(--c-ink);
}
/* With a category, the crumb is a list: All products / Mugs. The separator is drawn, not
   written, so a screen reader reads two links and no slash. */
.sn-sf-crumb__list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.25rem 0.5rem;
  margin: 0;
  padding: 0;
  list-style: none;
}
.sn-sf-crumb__list li {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  min-width: 0;
}
.sn-sf-crumb__list li + li::before {
  content: '/';
  color: var(--c-ink-subtle);
}
.sn-sf-crumb [data-category] {
  color: var(--c-ink);
  font-weight: 600;
}

/* The shop's categories, under its bar (task #477). One row that scrolls sideways on a phone
   rather than wrapping into a wall of links; a long Hungarian name stays on its line. Not sticky:
   the bar above it already is, and two pinned rows eat a phone screen. */
.sn-sf-cats {
  border-bottom: 1px solid var(--c-line);
  background: var(--c-canvas);
}
.sn-sf-cats__list {
  display: flex;
  gap: 0.375rem;
  width: 100%;
  max-width: min(var(--page-max), 72rem);
  margin: 0 auto;
  padding: 0.5rem clamp(1rem, 4vw, 2rem);
  list-style: none;
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.sn-sf-cats__list::-webkit-scrollbar {
  display: none;
}
/* A mouse cannot reach a row whose scrollbar is hidden: the wheel scrolls the page, not the row,
   so on a desktop the later categories were simply not there. Where there is a fine pointer and
   hover the row wraps and every category is on screen; touch keeps the swipe. */
@media (hover: hover) and (pointer: fine) {
  .sn-sf-cats__list {
    flex-wrap: wrap;
    overflow: visible;
  }
}
/* flex: none sizes each item to its own text, so a name stays on one line with no nowrap; the
   max-width is what lets an 80-character name wrap inside its pill instead of running on. */
.sn-sf-cats__list li {
  flex: none;
  max-width: 20rem;
}
.sn-sf-cats__link {
  display: inline-flex;
  align-items: center;
  min-height: 2.25rem;
  padding: 0.375rem 0.875rem;
  border: var(--mx-stroke, 1px) solid transparent;
  border-radius: var(--mx-control-radius, 999px);
  color: var(--c-ink-muted);
  font-size: 0.875rem;
  text-decoration: none;
}
.sn-sf-cats__link:hover {
  color: var(--c-ink);
  background: var(--c-surface-2);
}
.sn-sf-cats__link:focus-visible {
  outline: 2px solid var(--c-accent);
  outline-offset: 1px;
}
.sn-sf-cats__link[aria-current] {
  border-color: var(--c-accent);
  color: var(--c-ink);
}
/* A parent with children (Chris, 2026-11-04): its link and its chevron are one pill - one hover, one
   "current" edge - and the chevron opens the children. On a phone (the default here) the row scrolls
   sideways, so the open list is a second row INSIDE the menu: positioned on .sn-sf-cats, which no
   scroller sits between (the row's own overflow does not clip it), at the menu's foot, and
   static/header.js gives the menu that much more room below the row so nothing is covered. With a
   mouse (the media query below) it is the kit's dropdown under its parent instead. */
.sn-sf-cats {
  position: relative;
}
.sn-sf-cats__pair {
  display: inline-flex;
  align-items: stretch;
  border: var(--mx-stroke, 1px) solid transparent;
  border-radius: var(--mx-control-radius, 999px);
}
.sn-sf-cats__pair:hover {
  background: var(--c-surface-2);
}
.sn-sf-cats__pair > .sn-sf-cats__link {
  padding-inline-end: 0.25rem;
  border: 0;
  border-start-end-radius: 0;
  border-end-end-radius: 0;
}
.sn-sf-cats__pair > .sn-sf-cats__link:hover {
  background: none;
}
.sn-sf-cats__pair:has(> .sn-sf-cats__link[aria-current]) {
  border-color: var(--sn-line);
}
.sn-sf-cats__more {
  display: inline-flex;
  flex: none;
  align-items: center;
  justify-content: center;
  padding: 0 0.625rem 0 0.25rem;
  border: 0;
  border-start-end-radius: inherit;
  border-end-end-radius: inherit;
  background: none;
  color: var(--c-ink-muted);
  cursor: pointer;
}
.sn-sf-cats__more:hover,
.sn-sf-cats__more[aria-expanded='true'] {
  color: var(--c-ink);
}
.sn-sf-cats__more:focus-visible {
  outline: 2px solid var(--c-accent);
  outline-offset: 1px;
}
.sn-sf-cats__more .sn-ico {
  transition: transform 0.15s ease;
}
.sn-sf-cats__more[aria-expanded='true'] .sn-ico {
  transform: rotate(180deg);
}
.sn-sf-cats__sub[hidden] {
  display: none;
}
.sn-sf-cats__sub {
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  z-index: 15;
  display: flex;
  flex-wrap: wrap;
  gap: 0.375rem;
  margin: 0;
  padding: 0.5rem clamp(1rem, 4vw, 2rem);
  border-top: 1px solid var(--c-line);
  background: var(--c-canvas);
  list-style: none;
}
.sn-sf-cats__sub li {
  flex: none;
  max-width: 20rem;
}
.sn-sf-cats__sublink {
  display: inline-flex;
  align-items: center;
  min-height: 2.25rem;
  padding: 0.375rem 0.875rem;
  border: var(--mx-stroke, 1px) solid var(--c-line);
  border-radius: var(--mx-control-radius, 999px);
  color: var(--c-ink);
  font-size: 0.875rem;
  text-decoration: none;
}
.sn-sf-cats__sublink:hover {
  background: var(--c-surface-2);
}
.sn-sf-cats__sublink:focus-visible {
  outline: 2px solid var(--c-accent);
  outline-offset: 1px;
}
.sn-sf-cats__sublink[aria-current] {
  border-color: var(--sn-line);
  font-weight: 600;
}
@media (prefers-reduced-motion: reduce) {
  .sn-sf-cats__more .sn-ico {
    transition: none;
  }
}
/* A mouse: the kit's dropdown (app.css .mx-select-pop's look) under its parent, over the page. */
@media (hover: hover) and (pointer: fine) {
  .sn-sf-cats__item {
    position: relative;
  }
  .sn-sf-cats__sub {
    inset-inline: 0 auto;
    top: calc(100% + 0.25rem);
    bottom: auto;
    z-index: 15;
    display: block;
    width: max-content;
    min-width: 100%;
    max-width: 20rem;
    padding: 0.25rem;
    border: 1px solid var(--c-line);
    border-radius: min(var(--mx-control-radius, var(--radius-sm)), var(--radius-lg));
    background: var(--c-surface);
    box-shadow: var(--sn-lift);
    animation: sn-cats-in 0.12s ease-out;
  }
  .sn-sf-cats__sub li {
    max-width: none;
  }
  .sn-sf-cats__sublink {
    display: flex;
    padding: 0.375rem 0.625rem;
    border: 0;
    border-radius: max(0px, calc(min(var(--mx-control-radius, var(--radius-sm)), var(--radius-lg)) - 0.25rem));
    overflow-wrap: anywhere;
  }
  .sn-sf-cats__sublink:hover,
  .sn-sf-cats__sublink:focus-visible {
    outline: none;
    background: var(--c-accent-soft);
  }
  .sn-sf-cats__sublink[aria-current] {
    color: var(--c-ink);
  }
}
@keyframes sn-cats-in {
  from {
    opacity: 0;
    transform: translateY(-0.25rem);
  }
}
@media (prefers-reduced-motion: reduce) {
  .sn-sf-cats__sub {
    animation: none;
  }
}

/* A parent category's own children, as chips under its page's title. They wrap: a parent rarely has
   more than a handful, and every one should be in sight. */
.sn-sf-subcats__list {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 0.75rem 0 0;
  padding: 0;
  list-style: none;
}
.sn-sf-subcats__list li {
  max-width: 100%;
}
.sn-sf-subcats__link {
  display: inline-flex;
  align-items: center;
  max-width: 100%;
  min-height: 2.25rem;
  padding: 0.375rem 0.875rem;
  border: var(--mx-stroke, 1px) solid var(--c-line-strong);
  border-radius: var(--mx-control-radius, 999px);
  background: var(--c-surface);
  color: var(--c-ink);
  font-size: 0.875rem;
  font-weight: 500;
  text-decoration: none;
  overflow-wrap: anywhere;
}
.sn-sf-subcats__link:hover {
  border-color: var(--c-ink-subtle);
  background: var(--c-surface-2);
}
.sn-sf-subcats__link:focus-visible {
  outline: 2px solid var(--c-accent);
  outline-offset: 1px;
}

/* The owner's own pages placed in the header (task #507): the SAME row shape as .sn-sf-cats just
   above it, under the bar rather than squeezed into .sn-sf-bar__inner - the 360px Hungarian bar
   is already full with the shop's name, its search and its languages-and-cart cluster (task #783).
   Most shops place nothing here, so this row is absent far more often than categoryNav's is. */
.sn-sf-pages {
  border-bottom: 1px solid var(--c-line);
  background: var(--c-canvas);
}
.sn-sf-pages__list {
  display: flex;
  gap: 0.375rem;
  width: 100%;
  max-width: min(var(--page-max), 72rem);
  margin: 0 auto;
  padding: 0.5rem clamp(1rem, 4vw, 2rem);
  list-style: none;
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.sn-sf-pages__list::-webkit-scrollbar {
  display: none;
}
@media (hover: hover) and (pointer: fine) {
  .sn-sf-pages__list {
    flex-wrap: wrap;
    overflow: visible;
  }
}
.sn-sf-pages__list li {
  flex: none;
  max-width: 20rem;
}
.sn-sf-pages__link {
  display: inline-flex;
  align-items: center;
  min-height: 2.25rem;
  padding: 0.375rem 0.875rem;
  border: var(--mx-stroke, 1px) solid transparent;
  border-radius: var(--mx-control-radius, 999px);
  color: var(--c-ink-muted);
  font-size: 0.875rem;
  text-decoration: none;
}
.sn-sf-pages__link:hover {
  color: var(--c-ink);
  background: var(--c-surface-2);
}
.sn-sf-pages__link:focus-visible {
  outline: 2px solid var(--c-accent);
  outline-offset: 1px;
}

/* A page's own contact form (task #507): the kit's .mx-field stacked in one column, narrower than
   the page's prose so a long line of text does not make an over-wide input beside it. */
.sn-sf-contact {
  margin-top: 2rem;
  padding-top: 2rem;
  border-top: 1px solid var(--c-line);
}
.sn-sf-form {
  display: grid;
  gap: 1rem;
  max-width: 32rem;
  margin-top: 1rem;
}
/* The honeypot (views/contact-form.mjs): off-screen, never display:none or visibility:hidden,
   which a spam script is more likely to skip filling in on purpose. Pushed off the canvas with
   inset-inline-start rather than clipped to a pixel box, so it stays out of the way of both the
   fixed-width and the physical-direction rules this file is held to. */
.sn-sf-contact__hp {
  position: absolute;
  inset-inline-start: -999em;
  margin: 0;
  padding: 0;
  overflow: hidden;
}

/* The shop's own terms, linked next to Place order (task #507). */
.sn-sf-sum__terms {
  margin: 0.75rem 0 0;
  font-size: 0.8125rem;
  text-align: center;
}

/* ONE FLAT GRID FOR THE PRODUCT PAGE (task #785): three named areas - the gallery, the product's
   own name, and the rest of the buying decision (.sn-sf-panel: price, options, stock line,
   quantity and Add to cart) - and every layout below is one grid-template-areas swap on them.
   views/storefront.mjs never branches on the layout; only these three area names exist to match. */
.sn-sf-product {
  display: grid;
  gap: clamp(1rem, 4vw, 2.5rem);
  align-items: start;
}
.sn-sf-gallery {
  min-width: 0;
}
/* No photos: gallery() renders the placeholder span as .sn-sf-gallery's only
   child, with no .sn-sf-gallery__main wrapper and no data-gallery attribute -
   give it the same real box gallery__main gives a photo, so the span's
   height:100% has something to resolve against (task #787). */
.sn-sf-gallery:not([data-gallery]) {
  aspect-ratio: 1 / 1;
  overflow: hidden;
  border: 1px solid var(--c-line);
  border-radius: var(--radius-lg);
}
.sn-sf-h1--product {
  min-width: 0;
}
/* Badges sit with the name, just above it (task #3147) - .sn-sf-badges keeps its own top margin
   for when it follows something else, so this one block undoes it between the two. */
.sn-sf-nameblock {
  min-width: 0;
}
.sn-sf-nameblock .sn-sf-badges {
  margin: 0 0 0.375rem;
}
.sn-sf-brand {
  margin: 0 0 0.25rem;
  color: var(--c-ink-subtle);
  font-size: 0.9rem;
}
.sn-sf-gallery__main {
  aspect-ratio: 1 / 1;
  overflow: hidden;
  border: 1px solid var(--c-line);
  border-radius: var(--radius-lg);
  background: var(--c-surface);
}
.sn-sf-thumbs {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 0.5rem 0 0;
  padding: 0;
  list-style: none;
}
.sn-sf-thumb {
  display: block;
  width: 4rem;
  overflow: hidden;
  border: 1px solid var(--c-line);
  border-radius: var(--radius-sm);
}
.sn-sf-thumb img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 1 / 1;
  object-fit: cover;
}
.sn-sf-thumb.is-current {
  border-color: var(--c-accent);
}
.sn-sf-panel {
  display: flex;
  flex-direction: column;
  gap: 0.875rem;
  min-width: 0;
}
.sn-sf-panel .sn-sf-price {
  margin: 0;
}
.sn-sf-stock {
  margin: 0;
  color: var(--c-ink-muted);
  font-size: 0.875rem;
}
.sn-sf-stock--out {
  color: var(--c-rec);
}
.sn-sf-desc {
  margin-top: clamp(1.5rem, 5vw, 2.5rem);
  max-width: 62ch;
}
.sn-sf-desc p {
  margin: 0 0 0.75rem;
  color: var(--c-ink-muted);
  line-height: 1.65;
  text-wrap: pretty;
}

/* The "Specifications" table (task #3181, phone fix #3188). table-layout: fixed plus word-wrap on
   the cells is what keeps a long written value from forcing the table past 360px - a plain table
   would rather widen a column than wrap a word. The label column gets about half the table (not
   40%) so a long Hungarian compound label has room to break between words; overflow-wrap:
   break-word only steps in as the LAST resort, and hyphens: auto (the <html lang> already carries
   the page's own language, see views/layout.mjs) finds a real syllable break first, so "Mosogató-
   gépben" reads as a word split, never "Mosogatógépb/en" cut mid-letter. */
.sn-sf-specs {
  width: 100%;
  max-width: 100%;
  table-layout: fixed;
  border-collapse: collapse;
}
.sn-sf-specs th,
.sn-sf-specs td {
  padding: 0.6rem 0;
  border-bottom: 1px solid var(--c-border);
  text-align: start;
  font-weight: 400;
  color: var(--c-ink-muted);
  overflow-wrap: break-word;
  hyphens: auto;
}
.sn-sf-specs th {
  width: 50%;
  color: var(--c-ink);
  padding-inline-end: 1rem;
}
.sn-sf-specs tr:last-child th,
.sn-sf-specs tr:last-child td {
  border-bottom: 0;
}

/* Volume pricing under the price (task #3270): a small table, never wider than the panel it sits
   in - the same fixed-layout/word-wrap rule .sn-sf-specs already needs on a 360px phone, but
   compact: no border between rows, and the figures sit close to the price they qualify. */
.sn-sf-volume {
  width: 100%;
  max-width: 100%;
  table-layout: fixed;
  border-collapse: collapse;
  margin: 0.5rem 0 0;
  font-size: 0.9rem;
}
.sn-sf-volume th,
.sn-sf-volume td {
  padding: 0.15rem 0;
  text-align: start;
  font-weight: 400;
  color: var(--c-ink-muted);
  overflow-wrap: break-word;
}
.sn-sf-volume th {
  width: 40%;
  color: var(--c-ink);
  padding-inline-end: 1rem;
}

/* The choice of size, colour and so on (task #478). Each value is a real radio, clipped to
   nothing so the label around it is what is seen and pressed - 44px tall, like every other
   control a thumb has to hit - and the ring a keyboard user needs is drawn on that label. A value
   that would make a sold-out combination is struck through but still readable: it says what the
   shop sells, just not today. */
.sn-sf-pick {
  display: grid;
  gap: 0.875rem;
}
.sn-sf-opt {
  display: grid;
  gap: 0.5rem;
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}
.sn-sf-opt__name {
  padding: 0;
  color: var(--c-ink-muted);
  font-size: 0.875rem;
  font-weight: 600;
}
.sn-sf-opt__values {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}
.sn-sf-chip {
  position: relative;
  display: inline-flex;
}
.sn-sf-chip__input {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}
.sn-sf-chip__text {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 2.75rem;
  min-height: 2.75rem;
  padding: 0.5rem 0.875rem;
  border: var(--mx-stroke, 1px) solid var(--c-line-strong);
  border-radius: var(--mx-control-radius, var(--radius-sm));
  background: var(--c-surface);
  color: var(--c-ink);
  font-weight: 600;
  overflow-wrap: anywhere;
  transition: border-color 0.12s ease, background-color 0.12s ease;
}
.sn-sf-chip__input:checked + .sn-sf-chip__text {
  border-color: var(--c-accent);
  background: var(--c-accent-soft);
}
.sn-sf-chip__input:focus-visible + .sn-sf-chip__text {
  outline: 2px solid var(--c-accent);
  outline-offset: 2px;
}
.sn-sf-chip.is-out .sn-sf-chip__text {
  color: var(--c-ink-subtle);
  text-decoration: line-through;
}
.sn-sf-chip__input:disabled {
  cursor: default;
}
/* A colour option (task #504): the value is a round swatch, 44px to tap. The circle is the seller's
   own colour (--swatch, set inline from a validated #rrggbb); everything around it is the tokens.
   The ring is the accent when chosen, and a swatch that cannot be bought is faded and struck
   through instead of crossed out with text-decoration, which a circle has no text for. */
.sn-sf-chip--swatch .sn-sf-chip__text {
  min-width: 2.75rem;
  min-height: 2.75rem;
  padding: 0;
  border: 2px solid transparent;
  border-radius: 50%;
  background: transparent;
}
.sn-sf-chip--swatch .sn-sf-chip__input:checked + .sn-sf-chip__text {
  border-color: var(--c-accent);
  background: transparent;
}
.sn-sf-swatch {
  display: block;
  inline-size: 2rem;
  block-size: 2rem;
  border: 1px solid var(--c-line-strong);
  border-radius: 50%;
  background: var(--swatch);
}
.sn-sf-chip.is-out .sn-sf-swatch {
  /* A pale --swatch (Fehér) faded at 0.4 with the old muted line was a grey disc barely apart from
     an actual grey value's own swatch beside it. The line stays --c-ink (the strongest token, so
     it reads over both a light and a dark --swatch) and the fade is only a touch less aggressive. */
  opacity: 0.5;
  background:
    linear-gradient(to top right, transparent calc(50% - 1px), var(--c-ink) calc(50% - 1px), var(--c-ink) calc(50% + 1px), transparent calc(50% + 1px)),
    var(--swatch);
}
.sn-sf-vprice .sn-sf-price,
.sn-sf-vstock .sn-sf-stock {
  margin: 0;
}
.sn-sf-vstock:empty {
  display: none;
}
.sn-sf-line__variant {
  margin: 0.125rem 0 0;
  color: var(--c-ink-muted);
  font-size: 0.8125rem;
}

/* Buttons and the quantity box. 44px tall on a coarse pointer, like the kit's own controls. */
.sn-sf-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  min-height: 2.75rem;
  padding: 0.625rem 1.125rem;
  border: var(--mx-stroke, 1px) solid var(--c-line-strong);
  border-radius: var(--mx-control-radius, var(--radius-sm));
  background: var(--c-surface-2);
  color: var(--c-ink);
  font: inherit;
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
}
.sn-sf-btn--primary {
  border-color: var(--c-accent);
  background: var(--c-accent);
  color: var(--c-accent-ink);
}
.sn-sf-btn--ghost {
  background: none;
}
.sn-sf-btn--quiet {
  min-height: 2.25rem;
  padding: 0.375rem 0.75rem;
  border-color: var(--c-line);
  background: none;
  color: var(--c-ink-muted);
  font-size: 0.8125rem;
  font-weight: 500;
}
.sn-sf-btn:disabled,
.sn-sf-btn[aria-disabled='true'] {
  opacity: 0.55;
  cursor: default;
}
.sn-sf-btn.is-added {
  border-color: var(--c-ok);
  background: var(--c-ok);
  color: var(--c-accent-ink);
}
.sn-sf-buy {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.25rem;
}
/* The heart beside Add to cart (task #3159): the buy form keeps the room it needs, the heart is
   a fixed icon box beside it - on a narrow phone .sn-sf-buy's own flex-wrap already lets the
   button fall to its own row before the heart could ever crowd it off screen. */
.sn-sf-buycluster {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
}
.sn-sf-buycluster .sn-sf-buy {
  flex: 1 1 auto;
  min-width: 0;
}
/* The heart itself (task #3159): a card's own sits over its photo's top-right corner (never
   inside the card's own <a> - wishButton's own doc comment in views/cards.mjs says why), the
   product page's sits beside Add to cart as a plain sibling. Signed out it is a link, signed in a
   one-button form - both share this shape, so a shopper sees the same heart whichever it is. */
.sn-sf-wish {
  display: inline-flex;
}
.sn-sf-wish--card {
  position: absolute;
  inset-block-start: 0.5rem;
  inset-inline-end: 0.5rem;
  z-index: 1;
}
.sn-sf-wish-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  padding: 0;
  border: var(--mx-stroke, 1px) solid var(--c-line-strong);
  border-radius: var(--mx-control-radius, var(--radius-sm));
  background: var(--c-surface);
  color: var(--c-ink);
  cursor: pointer;
}
.sn-sf-wish--card .sn-sf-wish-btn,
.sn-sf-wish--card.sn-sf-wish-btn {
  border-color: transparent;
  background: color-mix(in srgb, var(--c-surface) 85%, transparent);
}
.sn-sf-wish-btn:hover {
  border-color: var(--c-accent);
  color: var(--c-accent);
}
.sn-sf-wish[data-wished] .sn-sf-wish-btn {
  color: var(--c-accent);
}
@media (hover: none) and (pointer: coarse) {
  .sn-sf-wish-btn {
    width: 2.75rem;
    height: 2.75rem;
  }
}
/* The "Compare" toggle (task #3184): a card's own sits over its photo's top-LEFT corner - the
   heart already owns top-right - the product page's sits beside Add to cart and the heart as a
   plain sibling (.sn-sf-buycluster already gives a third icon box room). Unlike the heart there is
   no signed-in/signed-out split: compare has no account behind it, so it is always the one-button
   form, same shape whoever presses it. */
.sn-sf-compare {
  display: inline-flex;
}
.sn-sf-compare--card {
  position: absolute;
  inset-block-start: 0.5rem;
  inset-inline-start: 0.5rem;
  z-index: 1;
}
.sn-sf-compare-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  padding: 0;
  border: var(--mx-stroke, 1px) solid var(--c-line-strong);
  border-radius: var(--mx-control-radius, var(--radius-sm));
  background: var(--c-surface);
  color: var(--c-ink);
  cursor: pointer;
}
.sn-sf-compare--card .sn-sf-compare-btn {
  border-color: transparent;
  background: color-mix(in srgb, var(--c-surface) 85%, transparent);
}
.sn-sf-compare-btn:hover {
  border-color: var(--c-accent);
  color: var(--c-accent);
}
.sn-sf-compare[data-compared] .sn-sf-compare-btn {
  color: var(--c-accent);
}
@media (hover: none) and (pointer: coarse) {
  .sn-sf-compare-btn {
    width: 2.75rem;
    height: 2.75rem;
  }
}
/* The tray (task #3184): fixed to the bottom of every shop page, whatever the width, while the
   list holds two or more - unlike the buy bar above it is not phone-only, since there is no
   in-page equivalent to fall back on at any width. */
.sn-sf-compare-tray {
  position: fixed;
  inset-inline: 0;
  bottom: 0;
  z-index: 25;
  border-top: 1px solid var(--c-line);
  background: var(--c-canvas);
  padding-bottom: env(safe-area-inset-bottom);
}
.sn-sf-compare-tray[hidden] {
  display: none;
}
.sn-sf-compare-tray__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  max-width: min(var(--page-max), 72rem);
  margin-inline: auto;
  padding: 0.625rem clamp(1rem, 4vw, 2rem);
}
/* Nothing of the page ends up underneath the tray, on any width. */
body:has(.sn-sf-compare-tray:not([hidden])) .sn-sf-main {
  padding-bottom: 4.5rem;
}
body:has(.sn-sf-compare-tray:not([hidden])) .sn-sf-toast {
  bottom: 4.5rem;
}
/* /compare itself (task #3184): a pinned label column, the rest scrolling sideways - the
   scroller is its own element (DESIGN.md: a sticky child of a sideways scroller sticks to the
   scroller, never to the page), never the table's own overflow nested inside something else that
   scrolls too. */
.sn-sf-compare-scroll {
  overflow-x: auto;
  margin-top: 1rem;
}
.sn-sf-compare-table {
  border-collapse: collapse;
  width: 100%;
}
.sn-sf-compare-table__label {
  position: sticky;
  inset-inline-start: 0;
  z-index: 1;
  min-width: 8rem;
  padding: 0.75rem 0.75rem 0.75rem 0;
  background: var(--c-canvas);
  color: var(--c-ink-muted);
  font-weight: 500;
  text-align: start;
  vertical-align: top;
  overflow-wrap: anywhere;
}
.sn-sf-compare-table__col {
  min-width: 14rem;
  padding: 0.75rem;
  vertical-align: top;
  text-align: start;
  border-inline-start: 1px solid var(--c-line);
}
.sn-sf-compare-table td {
  min-width: 14rem;
  padding: 0.75rem;
  border-top: 1px solid var(--c-line);
  border-inline-start: 1px solid var(--c-line);
  vertical-align: top;
  overflow-wrap: anywhere;
}
.sn-sf-compare-table__link {
  display: block;
  color: var(--c-ink);
  text-decoration: none;
}
.sn-sf-compare-table__name {
  margin: 0.5rem 0;
  font-size: 1rem;
  font-weight: 600;
  line-height: 1.35;
}
.sn-sf-compare-table__acts {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.5rem;
  margin-top: 0.625rem;
}
.sn-sf-compare-diff {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
  margin-top: 1rem;
}
.sn-sf-compare-diff__label {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.sn-sf-qty input,
.sn-sf-line__qty input[type='number'] {
  width: 4.5rem;
  min-height: 2.75rem;
  padding: 0.5rem;
  border: var(--mx-stroke, 1px) solid var(--c-line-strong);
  border-radius: var(--mx-control-radius, var(--radius-sm));
  background: var(--c-surface);
  color: var(--c-ink);
  font: inherit;
  font-variant-numeric: tabular-nums;
  text-align: center;
}
.sn-sf-buy .sn-sf-btn {
  flex: 1 1 10rem;
}

/* The bar a phone keeps on screen, so no layout can hide the buy button. */
.sn-sf-sticky {
  display: none;
}
.sn-sf-toast {
  position: fixed;
  inset-inline: 0.75rem;
  bottom: 0.75rem;
  z-index: 40;
  max-width: 28rem;
  margin-inline: auto;
  padding: 0.75rem 1rem;
  border: 1px solid var(--c-line-strong);
  border-radius: var(--radius-md);
  background: var(--c-surface-2);
  box-shadow: var(--sn-lift);
  color: var(--c-ink);
  font-size: 0.9375rem;
  animation: var(--animate-rise);
}
.sn-sf-toast[hidden] {
  display: none;
}
.sn-sf-toast__text {
  margin: 0;
}
.sn-sf-toast__acts {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 0.625rem;
}
.sn-sf-toast__acts[hidden] {
  display: none;
}
.sn-sf-toast.is-bad {
  border-color: var(--c-rec);
}

/* The cart. The basket is the page's own column: the lines, the code, the totals, the way on.
   It is not .sn-sf-cart, which is the header's cart link and brings a border and a row with it. */
.sn-sf-basket {
  display: flex;
  flex-direction: column;
  min-width: 0;
}
.sn-sf-notice {
  margin: 1rem 0 0;
  padding: 0.75rem 1rem;
  border: 1px solid var(--c-line-strong);
  border-radius: var(--radius-md);
  background: var(--c-surface);
  color: var(--c-ink-muted);
}
.sn-sf-lines {
  margin: 1.25rem 0 0;
  padding: 0;
  list-style: none;
  border-top: 1px solid var(--c-line);
}
.sn-sf-line {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
  padding: 0.875rem 0;
  border-bottom: 1px solid var(--c-line);
}
.sn-sf-line.is-busy {
  opacity: 0.6;
}
.sn-sf-line__shot {
  flex: none;
  width: 4rem;
  height: 4rem;
  overflow: hidden;
  border-radius: var(--radius-sm);
  background: var(--c-surface-2);
}
.sn-sf-line__text {
  flex: 1 1 12rem;
  min-width: 0;
}
.sn-sf-line__name {
  color: var(--c-ink);
  font-weight: 600;
  text-decoration: none;
  overflow-wrap: anywhere;
}
.sn-sf-line__name:hover {
  color: var(--c-accent);
}
.sn-sf-line .sn-sf-price {
  margin: 0.25rem 0 0;
  color: var(--c-ink-muted);
  font-size: 0.875rem;
}
.sn-sf-line__qty {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.25rem;
}
/* The line form has no Update button: a changed number is saved when the box changes (shop.js), and
   Enter in the box saves it too. A browser presses the form's FIRST submit button on Enter, so an
   empty, unfocusable one comes first in the markup; a step button there would take the typed value
   as a step. Without JavaScript that button posts what was typed. */
.sn-sf-line__total {
  flex: none;
  min-width: 6rem;
  margin: 0;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  text-align: end;
}
.sn-sf-step {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  border: var(--mx-stroke, 1px) solid var(--c-line-strong);
  border-radius: var(--mx-control-radius, var(--radius-sm));
  background: var(--c-surface-2);
  color: var(--c-ink);
  cursor: pointer;
}
.sn-sf-step:disabled {
  opacity: 0.45;
  cursor: default;
}
.sn-sf-step--drop {
  border-color: var(--c-line);
  background: none;
  color: var(--c-ink-subtle);
}
.sn-sf-step--drop:hover {
  color: var(--c-rec);
}
/* A finger is not a mouse: the same floor line 594 gives every other icon button in this
   product. Frissítés is the quiet 36px button everywhere else on the page, but on a line it is
   pressed with a thumb next to − and +, so it takes the same 44px floor here. */
@media (hover: none) and (pointer: coarse) {
  .sn-sf-step {
    width: 2.75rem;
    height: 2.75rem;
  }
}
.sn-sf-total {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 0.5rem;
  margin: 1.25rem 0 0;
  font-size: 1.125rem;
}
.sn-sf-total strong {
  font-variant-numeric: tabular-nums;
}
.sn-sf-keep {
  margin: 1.5rem 0 0;
}

/* The toolbar above a listing's grid or list (task #3274): the sort form and the grid/list switch
   share one row, each wrapping independently onto its own line below 480px or so rather than
   squeezing. The bottom margin lives here, not on .sn-sf-sort, since both of this row's children
   sit beside each other now rather than one owning the whole row's own spacing. */
.sn-sf-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin: 0 0 1rem;
}

/* The sort picker shared by /products, /c/<slug> and /search (task #3182): a real GET form - the
   select, the no-JS "Apply" button for Enter to miss, wrapping onto its own line rather than
   squeezing at 360px. */
.sn-sf-sort {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
}

/* The grid/list switch (task #3274): two icon buttons sharing one bordered box, beside the sort
   picker in the toolbar above. `flex: 0 0 auto` keeps it its own width when the sort form wraps
   onto a line of its own at a narrow width, rather than stretching to match it. */
.sn-sf-viewswitch {
  display: inline-flex;
  flex: 0 0 auto;
  border: var(--mx-stroke, 1px) solid var(--c-line);
  border-radius: var(--mx-control-radius, var(--radius-sm));
  overflow: hidden;
}
.sn-sf-viewswitch__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.5rem;
  height: 2.5rem;
  color: var(--c-ink-muted);
  background: var(--c-surface);
  text-decoration: none;
}
.sn-sf-viewswitch__btn + .sn-sf-viewswitch__btn {
  border-inline-start: 1px solid var(--c-line);
}
.sn-sf-viewswitch__btn.is-current {
  color: var(--c-accent);
  background: var(--c-surface-2);
}
.sn-sf-viewswitch__btn:hover {
  color: var(--c-accent);
}
.sn-sf-sort__label {
  font-weight: 600;
  color: var(--c-ink-muted);
}
.sn-sf-sort__select {
  min-height: 2.25rem;
  padding: 0.25rem 0.5rem;
  border: var(--mx-stroke, 1px) solid var(--c-line);
  border-radius: var(--mx-control-radius, var(--radius-md));
  background: var(--c-surface);
  color: var(--c-ink);
  font: inherit;
}
/* static/shop.js hides this once the select's own 'change' listener is submitting the form
   itself (task #3190) - .sn-sf-btn's own `display: inline-flex` is an author rule and would
   otherwise outrank the `hidden` attribute's UA-stylesheet `display: none`, the same reason
   every other hide-by-script target here (.sn-sf-toast[hidden] etc.) repeats this line. */
.sn-sf-sort__go[hidden] {
  display: none;
}

/* The catalogue filter panel (task #3183): a fixed-width sidebar beside the grid on a laptop,
   hidden below 40rem where the "Filters (n)" button opens it instead (the kit's own overlay -
   static/kit/mixtape-ui.js - lifts [data-mx-overlay-body] out, no CSS of ours draws the sheet
   itself). `--plain` is the no-filterable-dimension case: one column, same as before this task. */
.sn-sf-catalog {
  display: grid;
  grid-template-columns: 16rem 1fr;
  align-items: start;
  gap: clamp(1.5rem, 4vw, 2.5rem);
}
.sn-sf-catalog--plain {
  grid-template-columns: 1fr;
}
.sn-sf-catalog__main,
.sn-sf-filters {
  min-width: 0;
}
.sn-sf-filters-btn {
  display: none;
  margin: 0 0 1rem;
}
.sn-sf-filters__title {
  margin: 0 0 1rem;
  color: var(--c-ink);
  font-weight: 700;
  font-size: 1.125rem;
}
.sn-sf-filters__form {
  display: flex;
  flex-direction: column;
  gap: 1.25rem;
}
.sn-sf-filters__group {
  margin: 0;
  padding: 0;
  border: 0;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.sn-sf-filters__heading {
  padding: 0;
  color: var(--c-ink);
  font-weight: 700;
  font-size: 0.9375rem;
}
.sn-sf-filters__list {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  max-height: 16rem;
  overflow-y: auto;
}
.sn-sf-filters__check-label {
  flex: 1 1 auto;
  min-width: 0;
  overflow-wrap: anywhere;
}
.sn-sf-filters__count {
  flex: none;
  color: var(--c-ink-muted);
  font-size: 0.8125rem;
  font-variant-numeric: tabular-nums;
}
.sn-sf-filters__range {
  display: flex;
  gap: 0.5rem;
}
.sn-sf-filters__field {
  display: flex;
  flex: 1 1 0;
  flex-direction: column;
  gap: 0.25rem;
  min-width: 0;
  color: var(--c-ink-muted);
  font-size: 0.8125rem;
}
.sn-sf-filters__field input {
  width: 100%;
  min-height: 2.25rem;
  padding: 0.375rem 0.5rem;
  border: var(--mx-stroke, 1px) solid var(--c-line);
  border-radius: var(--mx-control-radius, var(--radius-sm));
  background: var(--c-surface);
  color: var(--c-ink);
  font: inherit;
}
.sn-sf-filters__actions .sn-sf-btn {
  width: 100%;
}
@media (max-width: 40rem) {
  .sn-sf-catalog {
    grid-template-columns: 1fr;
  }
  .sn-sf-filters {
    display: none;
  }
  .sn-sf-filters-btn {
    display: inline-flex;
  }
}

/* The applied-filter chips (task #3183): one per choice, a plain link that removes just that one
   and resubmits - "Clear all" is the same link with every filter gone. Scrolls sideways rather
   than wraps onto a third line at 360px, the same posture the pager's own numbers take. */
.sn-sf-chips {
  display: flex;
  flex-wrap: nowrap;
  gap: 0.5rem;
  margin: 0 0 1.25rem;
  overflow-x: auto;
  scrollbar-width: none;
}
.sn-sf-chips::-webkit-scrollbar {
  display: none;
}
.sn-sf-chip {
  display: inline-flex;
  flex: none;
  align-items: center;
  gap: 0.375rem;
  padding: 0.375rem 0.75rem;
  border: var(--mx-stroke, 1px) solid var(--c-line-strong);
  border-radius: var(--mx-control-radius, 9999px);
  background: var(--c-surface-2);
  color: var(--c-ink);
  font-size: 0.8125rem;
  font-weight: 500;
  text-decoration: none;
}
.sn-sf-chip:hover {
  border-color: var(--c-accent);
  color: var(--c-accent);
}
.sn-sf-chip--clear {
  background: none;
  color: var(--c-ink-muted);
}

/* The pager shared by /products, /c/<slug> and /search (task #3182): previous/next either side of
   a windowed row of page numbers. The numbers scroll sideways rather than wrap, the same posture
   .sn-sf-cats__list already takes at 360px - a dozen pages never force the row onto three lines. */
.sn-sf-pager {
  display: flex;
  align-items: center;
  gap: 0.375rem;
  margin: 1.5rem 0 0;
  min-width: 0;
}
.sn-sf-pager__nums {
  display: flex;
  gap: 0.25rem;
  min-width: 0;
  overflow-x: auto;
  scrollbar-width: none;
}
.sn-sf-pager__nums::-webkit-scrollbar {
  display: none;
}
.sn-sf-page {
  display: inline-flex;
  flex: none;
  align-items: center;
  justify-content: center;
  min-width: 2.25rem;
  min-height: 2.25rem;
  padding: 0 0.5rem;
  border-radius: var(--mx-control-radius, var(--radius-md));
  color: var(--c-ink);
  text-decoration: none;
}
.sn-sf-page:hover {
  background: var(--c-surface-2);
}
.sn-sf-page.is-current {
  background: var(--c-accent);
  color: var(--c-accent-ink);
  font-weight: 600;
}
.sn-sf-page__gap {
  display: inline-flex;
  flex: none;
  align-items: center;
  padding: 0 0.25rem;
  color: var(--c-ink-muted);
}

/* The shop's footer is the kit's landing footer (views/storefront.mjs shopFooter, Chris 2026-11-04): its four
   columns, its type and its quiet links, on the shop's own tokens. Only the shop's parts are drawn here: its
   name or logo (the header's own, at the header's size), the cookie-settings button as one more quiet link,
   the language links, and the shop's own content width so the columns line up with the page above. */
.sn-sf-foot {
  border-top: 1px solid var(--sn-line);
}
.sn-sf-foot .lp-container {
  max-width: min(var(--page-max), 72rem);
  padding-inline: clamp(1rem, 4vw, 2rem);
}
.sn-sf-foot__brand {
  display: inline-flex;
  align-items: center;
  min-width: 0;
  max-width: 100%;
}
.sn-sf-foot__shop {
  color: var(--c-ink);
  font-size: 1.125rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  overflow-wrap: anywhere;
}
.sn-sf-foot__logo {
  display: block;
  height: var(--sn-logo-h, 2.25rem);
  width: auto;
  max-width: 100%;
  object-fit: contain;
}
.sn-sf-foot .sn-sf-foot__cookies {
  padding: 0;
  border: 0;
  background: none;
  color: var(--c-ink-muted);
  font: inherit;
  text-align: start;
  text-decoration: none;
  cursor: pointer;
}
.sn-sf-foot .sn-sf-foot__cookies:hover {
  color: var(--c-ink);
}
.sn-sf-foot__lang .sn-lang {
  margin: 0;
}

/* One of the shop's own pages: a reading column, the owner's paragraphs, headings and lists. The
   words are theirs and can be any length, so nothing here fixes a height and long words break. */
.sn-sf-prose__body {
  margin-top: 1.25rem;
  line-height: 1.7;
  overflow-wrap: anywhere;
  text-wrap: pretty;
}
.sn-sf-prose__body > * {
  margin: 0;
}
.sn-sf-prose__body > * + * {
  margin-top: 1rem;
}
.sn-sf-prose__body h2 {
  margin-top: 2rem;
  font-size: 1.25rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: 1.25;
}
.sn-sf-prose__body h3 {
  margin-top: 1.5rem;
  font-size: 1.0625rem;
  font-weight: 700;
  line-height: 1.3;
}
.sn-sf-prose__body ul {
  padding-inline-start: 1.25rem;
}
.sn-sf-prose__body li + li {
  margin-top: 0.375rem;
}

/* The owner's read of a draft, above the shop's own chrome: what this is, and the way back. */
.sn-sf-preview {
  padding: 0.75rem clamp(1rem, 4vw, 2rem);
  border-bottom: 1px solid var(--c-line-strong);
  background: var(--c-surface-sunk);
  font-size: 0.875rem;
  line-height: 1.5;
}
.sn-sf-preview p {
  max-width: 72rem;
  margin: 0 auto;
}
.sn-sf-preview__links {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 1.25rem;
  margin-top: 0.375rem !important;
  color: var(--c-ink-muted);
}
.sn-sf-preview__lang {
  display: inline-flex;
  flex-wrap: wrap;
  gap: 0.25rem 0.75rem;
}
.sn-sf-preview [aria-current='true'] {
  color: var(--c-ink);
  font-weight: 700;
  text-decoration: none;
}

/* Three product-page layouts to choose from (task #785, lib/product-layout.mjs), at Shopiza's own
   992px rather than the 48rem every other desktop/phone split in this file uses - the brief names
   992px explicitly, so this is a deliberate departure, not the oversight header_search_w's own
   comment above flags the opposite way. Below 992px none of these three selectors match anything
   (data-pdp-layout is only ever one of the three values, all handled here), so gallery/name/panel
   get no grid-template-areas at all and fall back to auto-placement in document order: the
   picture, then the name, then the rest of the buying decision - one phone layout whatever the
   shop chose, because there is nothing here for a narrower screen to choose between. */
@media (min-width: 62rem) {
  /* The three named areas exist only here: a phone never names them, so an item with a grid-area
     name and no area of that name all lands in one cell (measured at 390: name over the photo). */
  .sn-sf-gallery {
    grid-area: gallery;
  }
  .sn-sf-nameblock {
    grid-area: name;
  }
  .sn-sf-panel {
    grid-area: panel;
  }
  /* The gallery spans both rows of the standard and gallery_wide layouts, so its height is shared
     between the name row and the panel row. The name row stays its own height and the panel row takes the
     rest, so the price sits under the name instead of at the photo's middle. */
  [data-pdp-layout='standard'] .sn-sf-product {
    grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
    grid-template-rows: auto 1fr;
    row-gap: 0.75rem;
    grid-template-areas:
      'gallery name'
      'gallery panel';
  }
  [data-pdp-layout='title_top'] .sn-sf-product {
    grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
    grid-template-areas:
      'name name'
      'gallery panel';
  }
  [data-pdp-layout='gallery_wide'] .sn-sf-product {
    grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
    grid-template-rows: auto 1fr;
    row-gap: 0.75rem;
    grid-template-areas:
      'gallery name'
      'gallery panel';
  }
}

/* One fixed phone layout: the buy row is always on screen, whatever else is. */
@media (max-width: 47.99rem) {
  .sn-sf-sticky {
    display: block;
    position: fixed;
    inset-inline: 0;
    bottom: 0;
    z-index: 30;
    border-top: 1px solid var(--c-line);
    background: var(--c-canvas);
    padding-bottom: env(safe-area-inset-bottom);
  }
  .sn-sf-sticky__inner {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.625rem clamp(1rem, 4vw, 2rem);
  }
  .sn-sf-sticky__price {
    flex: 0 1 auto;
    min-width: 0;
  }
  .sn-sf-sticky .sn-sf-buy {
    flex: 1 1 auto;
    justify-content: flex-end;
    margin: 0;
  }
  .sn-sf-sticky .sn-sf-btn {
    flex: 0 1 auto;
  }
  /* Nothing ends up underneath the bar, on the page or under the toast. */
  body:has(.sn-sf-sticky) .sn-sf-main {
    padding-bottom: 5.5rem;
  }
  body:has(.sn-sf-sticky) .sn-sf-toast {
    bottom: 5rem;
  }
  /* The compare tray must not cover this bar either (the task's own words): on a phone, while
     both are up, it rises to sit above it rather than under it. */
  body:has(.sn-sf-sticky):has(.sn-sf-compare-tray:not([hidden])) .sn-sf-compare-tray {
    bottom: 4.25rem;
  }
  body:has(.sn-sf-sticky):has(.sn-sf-compare-tray:not([hidden])) .sn-sf-main {
    padding-bottom: 10rem;
  }
  body:has(.sn-sf-sticky):has(.sn-sf-compare-tray:not([hidden])) .sn-sf-toast {
    bottom: 9.5rem;
  }
  /* /compare (task #3194): a narrow label column and narrow product columns, so two products sit
     beside the labels at 390px (and 360px) instead of one - the photo shrinks with its column
     (aspect-ratio 1/1, no width of its own), the name wraps up to three lines, and the buy/remove
     buttons shrink to fit a thumb's reach without forcing the column wider. */
  .sn-sf-compare-table__label {
    min-width: 5rem;
    font-size: 0.8125rem;
  }
  .sn-sf-compare-table__col,
  .sn-sf-compare-table td {
    min-width: 7.75rem;
    padding: 0.5rem;
  }
  .sn-sf-compare-table__name {
    display: -webkit-box;
    overflow: hidden;
    font-size: 0.875rem;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
  }
  .sn-sf-compare-table__acts .sn-sf-btn,
  .sn-sf-compare-table__acts .mx-btn {
    min-height: 2.25rem;
    padding: 0.375rem 0.625rem;
    font-size: 0.8125rem;
  }
}

/* ── The checkout, the order page, and the payment settings ──────────────────────────────────
 * Task #189. Two columns on a wide screen (the form, then the summary that follows it down the
 * page), one column on a phone with the summary and its button at the end, which is where a
 * thumb is. Every value is a token; the fields themselves are the kit's .mx-field, so this layer
 * only holds what the kit has no public-facing equivalent of: the payment radios, the summary
 * and the bank details block.
 *
 * Long strings and RTL: no width is in pixels, every inset is logical, every flex child that
 * holds text has min-width: 0, and the money column is tabular-nums so the figures line up
 * whatever the script.
 */

/* A section heading inside an admin form. The kit has .mx-h1 for a page and nothing for this. */
.sn-formhead {
  margin: 1.75rem 0 0;
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--c-ink);
}
/* A part inside one section's fields (Bank transfer under How you get paid): two "Fee or discount" boxes in one
   grid with nothing between them read as one thing twice (2026-11-02). */
.sn-formsub {
  margin: 0.5rem 0 -0.25rem;
  padding-top: 0.875rem;
  border-top: 1px solid var(--c-line);
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--c-ink);
}

/* A shopper's delivery address on an owner's own order or customer page (task #2947) - an
   address, not an identifier a reader retypes, so never .sn-address: plain text in the page's
   own type, like the e-mail and phone next to it, only without <address>'s own italic. */
.sn-order-addr {
  margin: 0;
  font-style: normal;
  line-height: 1.6;
  overflow-wrap: anywhere;
}

.sn-sf-notice--bad {
  border-color: var(--c-rec);
  color: var(--c-ink);
}

.sn-sf-basket__go {
  display: flex;
  justify-content: flex-end;
  margin: 1rem 0 0;
}
@media (max-width: 40rem) {
  .sn-sf-basket__go .sn-sf-btn {
    flex: 1 1 auto;
  }
}

.sn-sf-checkout {
  display: grid;
  gap: clamp(1.25rem, 3vw, 2rem);
  margin-top: 1.5rem;
  min-width: 0;
}
.sn-sf-checkout__main,
.sn-sf-checkout__side,
.sn-sf-order__main,
.sn-sf-order__side {
  min-width: 0;
}

/* A fieldset with no browser furniture on it: the legend is the heading. */
/* The billing box at checkout (views/checkout.mjs billingBlock, Chris 2026-11-04): its fields show while the box is
   ticked - CSS alone, so it works with no script and the server reads exactly what the shopper saw. Unticked they
   are only hidden: every typed word stays in them. */
.sn-sf-billing__fields {
  display: none;
  margin-top: 1rem;
}
.sn-sf-billing:has(> .sn-sf-billing__toggle input:checked) > .sn-sf-billing__fields {
  display: block;
}
.sn-sf-billing__toggle {
  font-weight: 600;
}
.sn-sf-billing__toggle + .mx-hint {
  margin-top: 0.25rem;
}
.sn-sf-billing__fields > .mx-hint {
  margin: 0 0 0.75rem;
}
.sn-sf-billing__fields > .mx-fields + .sn-sf-billing__lead {
  margin-top: 1.25rem;
}

.sn-sf-set {
  margin: 0 0 1.75rem;
  padding: 0;
  border: 0;
  min-width: 0;
}
.sn-sf-set > legend {
  padding: 0;
}
.sn-sf-set:last-child {
  margin-bottom: 0;
}
/* A sentence under a fieldset's heading, before its fields (the address of a shop that delivers nothing). */
.sn-sf-set > legend + .mx-hint {
  margin: 0.375rem 0 1rem;
}

/* Payment: one row per method, the whole row is the label. */
.sn-sf-pay__opt {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  margin-bottom: 0.75rem;
  padding: 0.875rem 1rem;
  border: var(--mx-stroke, 1px) solid var(--c-line-strong);
  border-radius: var(--mx-control-radius, var(--radius-md));
  background: var(--c-surface);
  cursor: pointer;
}
.sn-sf-pay__opt:hover {
  border-color: var(--c-accent);
}
.sn-sf-pay__opt:has(input:checked) {
  border-color: var(--c-accent);
  background: var(--c-accent-soft);
}
.sn-sf-pay__opt:has(input:focus-visible) {
  outline: 2px solid var(--c-accent);
  outline-offset: 2px;
}
.sn-sf-pay__opt input {
  flex: 0 0 auto;
  width: 1.125rem;
  height: 1.125rem;
  margin: 0.125rem 0 0;
  accent-color: var(--c-accent);
}
.sn-sf-pay__body {
  display: block;
  min-width: 0;
}
.sn-sf-pay__name {
  display: block;
  font-weight: 600;
  /* A delivery method's name is the owner's, up to 80 characters, and may be one long word. */
  overflow-wrap: anywhere;
}
.sn-sf-pay__text {
  display: block;
  margin-top: 0.25rem;
  color: var(--c-ink-muted);
  font-size: 0.875rem;
  line-height: 1.5;
}
/* A delivery method's price sits beside its name, lighter than the name it belongs to. */
.sn-sf-pay__price {
  margin-inline-start: 0.5rem;
  color: var(--c-ink-muted);
  font-weight: 400;
}
/* A shop with no delivery method: the sentence stands where the radios would. */
.sn-sf-collect {
  margin: 0;
}

/* The seller's bank details: read once, typed into a banking app, so the IBAN is monospaced. */
.sn-sf-bank {
  display: grid;
  gap: 0.375rem;
  margin: 0.75rem 0 0;
  padding: 0.75rem 0.875rem;
  border-radius: var(--radius-sm);
  background: var(--c-surface-sunk);
  font-size: 0.875rem;
}
.sn-sf-bank__row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 0.75rem;
  min-width: 0;
}
.sn-sf-bank dt {
  flex: 0 0 auto;
  color: var(--c-ink-subtle);
}
.sn-sf-bank dd {
  flex: 1 1 auto;
  min-width: 0;
  margin: 0;
  overflow-wrap: anywhere;
}
.sn-sf-bank__iban {
  font-family: var(--font-mono);
  letter-spacing: 0.02em;
}

/* The EPC "scan to pay" code (task #3220), euro shops only: a fixed-size square beside its own
   one-line caption, so it reads as part of the bank panel rather than a stray image. */
.sn-sf-bank__qr {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-top: 0.625rem;
  padding-top: 0.625rem;
  border-top: 1px solid var(--c-border);
}
.sn-sf-bank__qr-svg {
  width: 5.5rem;
  height: 5.5rem;
  flex: 0 0 auto;
}
.sn-sf-bank__qr-text {
  margin: 0;
  color: var(--c-ink-subtle);
}

/* The summary, on the checkout and on the order. */
.sn-sf-sum {
  padding: 1.25rem;
  border: 1px solid var(--c-line);
  border-radius: var(--radius-lg);
  background: var(--c-surface);
}
.sn-sf-sum__lines {
  margin: 0;
  padding: 0;
  list-style: none;
}
.sn-sf-sum__line {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  padding: 0.625rem 0;
  border-bottom: 1px solid var(--c-line);
  min-width: 0;
}
.sn-sf-sum__shot {
  flex: 0 0 auto;
  width: 3rem;
  height: 3rem;
  border-radius: var(--radius-sm);
  object-fit: cover;
  background: var(--c-surface-sunk);
}
.sn-sf-sum__shot--none {
  display: block;
}
.sn-sf-sum__text {
  flex: 1 1 auto;
  min-width: 0;
}
.sn-sf-sum__name {
  display: block;
  overflow-wrap: anywhere;
}
.sn-sf-sum__name a {
  color: var(--c-ink);
  text-decoration: none;
}
.sn-sf-sum__name a:hover {
  color: var(--c-accent);
}
/* The combination bought, under the product's name: "Size: M, Colour: Red". */
.sn-sf-sum__variant {
  display: block;
  margin-top: 0.125rem;
  color: var(--c-ink-muted);
  font-size: 0.8125rem;
  overflow-wrap: anywhere;
}
.sn-sf-sum__qty {
  display: block;
  margin-top: 0.125rem;
  color: var(--c-ink-subtle);
  font-size: 0.8125rem;
  font-variant-numeric: tabular-nums;
}
.sn-sf-sum__amount {
  flex: 0 0 auto;
  font-variant-numeric: tabular-nums;
}
.sn-sf-sum__totals {
  margin: 1rem 0 0;
}
/* "Arrives in 2-4 business days", under the delivery row it belongs to. */
.sn-sf-sum__estimate {
  margin: -0.25rem 0 0.5rem;
  color: var(--c-ink-muted);
  font-size: 0.8125rem;
}
.sn-sf-sum__row {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 0.25rem 1rem;
  margin: 0 0 0.5rem;
  min-width: 0;
}
.sn-sf-sum__row dt {
  color: var(--c-ink-muted);
  /* "Delivery: <the owner's method name>" - one long word must wrap inside the card, not widen the page. */
  min-width: 0;
  overflow-wrap: anywhere;
}
.sn-sf-sum__row dd {
  margin: 0;
  font-variant-numeric: tabular-nums;
}
.sn-sf-sum__row--total {
  margin-top: 0.75rem;
  padding-top: 0.75rem;
  border-top: 1px solid var(--c-line);
  font-size: 1.125rem;
  font-weight: 700;
}
.sn-sf-sum__row--total dt {
  color: var(--c-ink);
}
/* The discount code: a field and its button on one line, or the code held and a way to drop it. */
.sn-sf-coupon {
  margin: 1rem 0 0;
  padding: 1rem 0 0;
  border-top: 1px solid var(--c-line);
}
.sn-sf-coupon__row {
  display: flex;
  gap: 0.5rem;
  min-width: 0;
}
.sn-sf-coupon__row input {
  flex: 1 1 auto;
  min-width: 0;
  text-transform: uppercase;
}
.sn-sf-coupon__row .sn-sf-btn {
  flex: none;
}
.sn-sf-coupon__on {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem 1rem;
  margin: 0;
}
.sn-sf-coupon__code {
  min-width: 0;
  overflow-wrap: anywhere;
  font-weight: 600;
}
/* The lines already end in a rule; a second one 1rem under it reads as an empty row, and stacking on the last line's own border-bottom would double it to 2px. */
.sn-sf-basket .sn-sf-coupon {
  margin-top: 0;
  border-top: 0;
}
/* Taking a code off is a tap on a phone, so it is 44px like the other buttons here, not the quiet 36. */
.sn-sf-coupon__on .sn-sf-btn--quiet {
  min-height: 2.75rem;
}
.sn-sf-coupon__note {
  margin: 0.5rem 0 0;
}
/* "E-mail me when it is back" (task #3143, field wrap #3149): the same field-and-button-on-one-line
   shape as the coupon box just above, minus its uppercase code styling - an e-mail address is
   never shouted. The field itself is the kit's own .mx-field (same wrapping as the coupon box),
   so it wears the theme's border, radius and focus ring in both themes rather than a browser's
   bare input box. */
.sn-sf-notify {
  margin: 1rem 0 0;
  padding: 1rem 0 0;
  border-top: 1px solid var(--c-line);
}
.sn-sf-notify__lead {
  margin: 0 0 0.5rem;
}
.sn-sf-notify__row {
  display: flex;
  gap: 0.5rem;
  min-width: 0;
}
.sn-sf-notify__row input {
  flex: 1 1 auto;
  min-width: 0;
}
.sn-sf-notify__row .sn-sf-btn {
  flex: none;
}
.sn-sf-total--discount {
  flex-wrap: nowrap;
  align-items: baseline;
  margin-top: 0.75rem;
  font-size: 1rem;
}
/* The code is the shop's to name and can be 30 characters: the label gives way, the sum does not. */
.sn-sf-total--discount span {
  min-width: 0;
  overflow-wrap: anywhere;
}
.sn-sf-total--discount strong {
  flex: none;
}
.sn-sf-total--discount[hidden] {
  display: none;
}
.sn-sf-total--discount strong {
  color: var(--c-ok);
}
/* What the code took off is money coming off, so it reads as a saving, not as another cost. */
.sn-sf-sum__row--discount dd {
  color: var(--c-ok);
}
/* VAT is inside the total, not on top of it: a quiet line under the figure, never a cost. */
.sn-sf-sum__row--vat {
  margin-top: 0.5rem;
  font-size: 0.875rem;
}
.sn-sf-sum__row--vat dt,
.sn-sf-sum__row--vat dd {
  color: var(--c-ink-muted);
}
.sn-sf-sum__go {
  width: 100%;
  margin-top: 1.25rem;
  justify-content: center;
}

/* The order page. */
.sn-sf-order {
  display: grid;
  gap: clamp(1.25rem, 3vw, 2rem);
  margin-top: 1.75rem;
  min-width: 0;
}
.sn-sf-order__number {
  margin: 0.5rem 0 0;
  font-family: var(--font-mono);
  font-size: 1rem;
  color: var(--c-ink-muted);
}
.sn-sf-order__when {
  margin: 0.5rem 0 0;
  color: var(--c-ink-subtle);
  font-size: 0.875rem;
}
.sn-sf-order__line {
  margin: 0 0 0.25rem;
  overflow-wrap: anywhere;
}
.sn-sf-addr {
  margin: 0;
  font-style: normal;
  line-height: 1.6;
  overflow-wrap: anywhere;
}

/* ── Reviews (task #3139): the shopper's "Review this", and the stars everywhere else reads ──── */
.sn-sf-review {
  margin: 0 0 1rem;
}
.sn-sf-review:last-child {
  margin-bottom: 0;
}
.sn-sf-review__name {
  margin: 0 0 0.25rem;
  font-weight: 600;
}
.sn-sf-review__body {
  margin: 0.375rem 0 0;
  overflow-wrap: anywhere;
}
.sn-sf-review__form {
  margin-top: 0.375rem;
}
.sn-sf-review__form > summary {
  cursor: pointer;
  color: var(--c-accent);
  font-weight: 600;
}
.sn-sf-review__form .mx-field {
  margin-top: 0.625rem;
}
.sn-sf-review__form button {
  margin-top: 0.75rem;
}
/* Five radio buttons, drawn 5→1 so the general sibling combinator can light up every star up to
   and including the one picked, with row-reverse putting 1★ back on the left where a reader
   expects it - the classic no-JS star-rating shape. */
.sn-stars {
  display: inline-flex;
  flex-direction: row-reverse;
  justify-content: flex-end;
  gap: 0.125rem;
  border: 0;
  padding: 0;
  margin: 0;
  font-size: 1.375rem;
  line-height: 1;
  color: var(--c-line-strong);
}
.sn-stars--static {
  /* The base rule's row-reverse exists for the input's radio trick (5→1 in markup, 1★ visually
     on the left); a static rating has no such trick and must draw its two spans (filled, then
     empty) in document order, or they come out backwards. */
  flex-direction: row;
  letter-spacing: 0.0625rem;
}
.sn-stars__on {
  color: var(--c-accent);
}
.sn-stars__opt {
  position: relative;
  cursor: pointer;
}
.sn-stars__opt input {
  position: absolute;
  inset: 0;
  opacity: 0;
  margin: 0;
  cursor: pointer;
}
.sn-stars__opt:has(input:checked) span[aria-hidden],
.sn-stars__opt:has(input:checked) ~ .sn-stars__opt span[aria-hidden],
.sn-stars__opt:hover span[aria-hidden],
.sn-stars__opt:hover ~ .sn-stars__opt span[aria-hidden] {
  color: var(--c-accent);
}
.sn-stars__opt input:focus-visible ~ span[aria-hidden] {
  outline: 2px solid var(--c-accent);
  outline-offset: 2px;
}
.sn-sf-rating {
  margin: 0 0 0.75rem;
}
.sn-sf-rating a {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  color: inherit;
  text-decoration: none;
}
.sn-sf-rating__text {
  color: var(--c-ink-subtle);
  font-size: 0.875rem;
  text-decoration: underline;
}

/* Two columns once there is room, with the summary following the form down the page. */
@media (min-width: 52rem) {
  .sn-sf-checkout,
  .sn-sf-order {
    grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
    align-items: start;
  }
  .sn-sf-checkout__side,
  .sn-sf-order__side {
    position: sticky;
    top: 5rem;
  }
}

/* ── Orders: the list's filter, the order page's pairs, its history and its moves ─────────────
 * Every one of these sits INSIDE a kit element - .mx-form, .mx-panel, .mx-actions, .mx-table -
 * and only says how this product's own content is laid out in it. No colour, radius or font is
 * named that tokens.css does not already carry.
 */

/* The search-and-status bar above the list. It is a form, so the kit already spaces the fields;
 * this only stops it growing a page-tall gap under the button on a wide screen. */
.sn-filter {
  margin-bottom: 1.25rem;
}
.sn-filter .mx-actions {
  margin-top: 0.75rem;
}

/* Label and value, one pair per line: the customer, the payment method, the totals. A definition
 * list, because that is what it is - and the label wraps above the value on a narrow screen
 * rather than squeezing a Hungarian word into four characters. */
.sn-pairs {
  margin: 0;
  display: grid;
  gap: 0.5rem 1rem;
  min-width: 0;
}
.sn-pair {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 0.125rem 1rem;
  min-width: 0;
}
.sn-pair dt {
  color: var(--c-ink-muted);
}
.sn-pair dd {
  margin: 0;
  min-width: 0;
  overflow-wrap: anywhere;
  font-variant-numeric: tabular-nums;
}
.sn-pair--total {
  padding-top: 0.5rem;
  border-top: 1px solid var(--c-line);
  font-weight: 700;
}
/* A list where a row has nothing to open (the kit's rule: a table with nothing to open per row is a
   list, not a table). Each item is its label, then its figures as label/value pairs, so a 390px
   phone shows every figure without a sideways scroll. */
.sn-rows {
  margin: 0;
  padding: 0;
  list-style: none;
}
.sn-row {
  display: grid;
  gap: 0.5rem;
  min-width: 0;
  padding: 0.75rem 1rem;
  border-top: 1px solid var(--c-line);
}
.sn-rows > .sn-row:first-child {
  border-top: 0;
}
.sn-row__label {
  min-width: 0;
  font-weight: 600;
  overflow-wrap: break-word;
}
.sn-row__label .sub {
  display: block;
  font-weight: 400;
  overflow-wrap: anywhere;
}
/* A name in a list links to its page. The kit styles links inside a table only, so this is the table's own
   link look: ink, a quiet underline, the accent on hover - not the browser's blue. */
.sn-row__label a {
  color: var(--c-ink);
  text-decoration-color: var(--c-line-strong);
  text-underline-offset: 3px;
}
.sn-row__label a:hover {
  color: var(--c-accent);
}
.sn-row__qty {
  margin: 0;
  color: var(--c-ink-muted);
}
.sn-row--total {
  font-weight: 700;
}
.sn-row__figs {
  margin-top: 0.25rem;
}
/* The fields of one item (a combination, a colour): a label above each control, as many across as fit. */
.sn-row__fields {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr));
  gap: 0.5rem 0.75rem;
  min-width: 0;
}
.sn-row__fields .mx-field {
  min-width: 0;
}
.sn-row .mx-err {
  margin: 0;
}
/* The totals under an order's lines: the same pairs, set off from the lines above them. */
.sn-totals {
  padding: 0.75rem 1rem;
  border-top: 1px solid var(--c-line);
}

/* The status pill and where the order came from, side by side under the heading. */
.sn-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 0.625rem 0 0;
}

/* Where the order has been: a pill, when it happened, and who moved it. Three columns while
 * there is room for them, stacked when there is not. */
.sn-history {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 0.625rem;
}
.sn-history__row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.375rem 0.75rem;
  min-width: 0;
}
.sn-history__when {
  color: var(--c-ink);
  font-variant-numeric: tabular-nums;
}
.sn-history__who {
  color: var(--c-ink-subtle);
  font-size: 0.875rem;
  min-width: 0;
  overflow-wrap: anywhere;
}
/* What to do about a message that did not arrive. It is a whole sentence, so it takes the row
 * under the badge rather than trailing off the end of it - the flex line above wraps, and a
 * basis of 100% is what makes this child start its own. */
.sn-history__row .mx-hint {
  flex-basis: 100%;
  margin: 0;
}
/* "Send it again", under the sentence explaining why it has to be pressed. Its own line for the
 * same reason the hint has one, and its own form because it is a POST: one press, no script. */
.sn-history__act {
  flex-basis: 100%;
  margin: 0;
}

/* The buttons that move an order along. Each is its own form - one press, one POST, no script -
 * so the forms are the flex children here and shrink to their button rather than taking a row
 * each. Not `display: contents`: it drops the form's own box, and with it the form semantics
 * some screen readers announce. */
.sn-moves {
  flex-wrap: wrap;
  gap: 0.625rem;
}
.sn-moves form {
  margin: 0;
  flex: none;
}

/* The shopper's own link, to be copied out of. Monospace so a token can be read character by
 * character, and the whole field selects on focus in the browser without a script. */
.sn-copy {
  font-family: var(--font-mono);
  font-size: 0.8125rem;
}

/* One line of a phone order: a product and a quantity, the quantity kept narrow. */
.sn-orderline {
  margin-bottom: 0.25rem;
}
@media (min-width: 40rem) {
  .sn-orderline {
    grid-template-columns: minmax(0, 1fr) 8rem;
  }
}

/* ── The dashboard's period chooser ──────────────────────────────────────────────────────────
 * Quiet buttons in a row; the one the page is showing is marked with aria-current, and the
 * style follows the attribute rather than a second class - the accessible fact IS the state.
 */
/* The page editor's text box (task #480): a policy is long, so it starts tall and the owner can drag
   it taller; the kit's .mx-field already gives it the field look. */
.sn-pagebody {
  width: 100%;
  min-height: 16rem;
  resize: vertical;
  line-height: 1.55;
}

/* The pages an owner usually wants first, as a row of quiet buttons under the list. */
.sn-suggest {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.375rem;
  margin: 1rem 0 0;
}
.sn-suggest__label {
  margin-inline-end: 0.25rem;
  color: var(--c-ink-muted);
  font-size: 0.875rem;
}

/* ── The shipping methods editor ───────────────────────────────────────────────────────────────
 * One block per method: its fields, and the tick that removes it. The block is sunk into the page
 * so the rows of an editor that repeats read as separate things rather than one long form.
 */
.sn-zone {
  margin-top: 0.875rem;
  padding: 0.875rem;
  border: 1px solid var(--c-line);
  border-radius: var(--radius-md);
  background: var(--c-surface-sunk);
}
.sn-zone .mx-field:last-child {
  margin-bottom: 0;
}
.sn-zone .sn-check,
.sn-zone .mx-hint:last-child {
  margin-top: 0.5rem;
}

/* A method's own pricing tiers (task #800): each one is a row of the same .mx-fields, set off from
 * the one before it by a dashed rule rather than another sunk box - a method is already boxed, and
 * boxing its tiers too would read as a box inside a box. */
.sn-bands {
  margin-top: 0.875rem;
}
.sn-band {
  padding-top: 0.75rem;
  margin-top: 0.75rem;
  border-top: 1px dashed var(--c-line);
}
.sn-band:first-child {
  padding-top: 0;
  margin-top: 0;
  border-top: none;
}
/* The line under a row ("A new tier", the remove tick) and the tiers' own lead keep clear of the fields
   next to them (they touched the labels below, 2026-11-02). */
.sn-bands > .mx-hint {
  margin: 0 0 0.75rem;
}
.sn-band > .mx-hint,
.sn-band > .sn-check {
  margin-top: 0.5rem;
}
.sn-zone > .mx-hint:last-child {
  margin-top: 0.75rem;
}

/* ── Options and the combinations table (task #478) ──────────────────────────────────────────
 * One sunk block per option, like the delivery zones: they repeat, and each must read as its own
 * thing. The table scrolls sideways inside .mx-scroll on a phone rather than squeezing its boxes
 * to nothing; the boxes are sized to what goes in them.
 */
.sn-options {
  display: grid;
  gap: 1rem;
  min-width: 0;
}
.sn-option {
  display: grid;
  gap: 0.75rem;
  min-width: 0;
  margin: 0;
  padding: 0.875rem;
  border: 1px solid var(--c-line);
  border-radius: var(--radius-md);
  background: var(--c-surface-sunk);
}
.sn-option__title {
  padding: 0;
}
.sn-option .mx-field textarea {
  min-height: 5.5rem;
}
.sn-options__actions {
  flex-wrap: wrap;
  align-items: center;
}
.sn-options__status:empty {
  display: none;
}
.sn-grid {
  display: grid;
  gap: 0.5rem;
  min-width: 0;
}
.sn-grid__input--sku {
  min-width: 8rem;
}
.sn-grid__input--delta,
.sn-grid__input--stock {
  min-width: 5.5rem;
  max-width: 8rem;
  text-align: end;
  font-variant-numeric: tabular-nums;
}
/* Colours and photos of one colour option (task #504), and the photo choice in the combinations
   list: the photo is a select with its picture beside it, so nobody has to count "Photo 3". */
.sn-looks {
  display: grid;
  gap: 0.5rem;
  min-width: 0;
}
.sn-photopick {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
}
.sn-photopick__img {
  flex: none;
  inline-size: 2rem;
  block-size: 2rem;
  border: 1px solid var(--c-line);
  border-radius: var(--radius-sm);
  background: var(--c-surface-sunk);
  object-fit: cover;
}
.sn-photopick__select {
  min-width: 7rem;
}
/* "Automatikus" is the longest thing a combination's select says; the table scrolls inside itself,
   so the select takes the room it needs rather than being squeezed to "Automati". */
.sn-grid .sn-photopick__select {
  min-width: 8.75rem;
}
.sn-looks input.sn-color {
  width: 3rem;
}
/* The product's own stock box and the line that replaces it take turns; the kit's .mx-field is a
   grid, which would otherwise show a [hidden] one anyway. */
[data-stock-own][hidden],
[data-stock-variants][hidden] {
  display: none;
}

/* The seller's own sentence about the transfer reference, under the reference itself. */
.sn-sf-bank__note dd {
  color: var(--c-ink-muted);
}

/* ── The founders' page for Resend's delivery news (task #346) ──────────────────────────────
 * An endpoint address and four event names, printed as code because they are typed or ticked in
 * Resend's dashboard character for character. A URL has no space to break at, so at 360px it has
 * to be allowed to break anywhere rather than push its card wider than the phone.
 */
.sn-founder code {
  overflow-wrap: anywhere;
}
.sn-founder ol {
  margin: 0 0 0.625rem;
  padding-inline-start: 1.25rem;
}
.sn-founder li + li {
  margin-top: 0.375rem;
}

/* ── The landing page's own three shapes ─────────────────────────────────────
   Everything else on that page is the kit's: the band, the card, the FAQ, the
   final call. These three are the arrangements the kit has no opinion about -
   a numbered step, a plan's price line, and the tile grid's own rhythm - and
   they read the same tokens, which is what keeps this file colour-free. */
/* Six cards, as many per row as the width allows. The kit's own .lp-grid is a sticky-head
   two-column layout for a section with one big point, not a grid of equals - using it put a
   360px column of narrow cards beside a column of wide ones. */
.sn-tiles {
  display: grid;
  gap: 18px;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
  margin-top: 28px;
}
.sn-steps {
  display: grid;
  gap: 18px;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
  margin: 28px 0 0;
  padding: 0;
  list-style: none;
  counter-reset: none;
}
.sn-step {
  min-width: 0;
}
/* The number is a marker, not a heading: it sits above the title in its own
   weight so the eye reads 1-2-3 down the row before it reads a word. */
.sn-step__n {
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  color: var(--c-accent);
}
.sn-plans {
  margin-top: 28px;
}
/* A plan card in Shopiza's shape (Chris, 2026-09-29): the kit's comparison card with a price that
   leads, what is billed under it, a checked list, and the way in at the foot of every card - the
   kit's .lp-card__cta margin-top: auto lines the three buttons up whatever the words above. */
.sn-plan {
  display: flex;
  flex-direction: column;
  min-width: 0;
}
.sn-plan__price {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 2px 6px;
  margin: 10px 0 0;
  color: var(--c-ink);
  line-height: 1.1;
}
.sn-plan__price strong {
  font-size: 2.125rem;
  font-weight: 800;
  letter-spacing: -0.03em;
}
.sn-plan__price .lp-was {
  margin-inline-end: 0;
  font-size: 1.25rem;
  font-weight: 600;
}
.sn-plan__per {
  font-size: 0.9375rem;
  font-weight: 500;
  color: var(--c-ink-muted);
}
.sn-plan__billed {
  margin: 6px 0 0;
  color: var(--c-ink-muted);
  font-size: 0.875rem;
}
.sn-checks {
  display: grid;
  gap: 8px;
  margin: 0 0 20px;
  padding: 0;
  list-style: none;
}
/* The button (or badge) sits at the foot of every card, whatever the words above it: the flex
   column pushes it down, so two cards of different heights still line their buttons up. */
.sn-plan__foot {
  margin-top: auto;
}
/* Usage across the page's width: each count sits in its own column beside the others rather than
   three full-width lines under one heading. */
.sn-usage {
  display: grid;
  gap: 1rem 1.5rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
}
.sn-usage > .sn-usage__item {
  min-width: 0;
  margin-top: 0;
}
.sn-usage__item .mx-hint {
  margin-top: 0;
}
.sn-checks li {
  position: relative;
  padding-inline-start: 22px;
  color: var(--c-ink);
  font-size: 0.9375rem;
}
.sn-checks li::before {
  content: "✓";
  position: absolute;
  inset-inline-start: 0;
  color: var(--c-accent);
  font-weight: 700;
}
.sn-seg-row {
  display: flex;
  justify-content: center;
}
.sn-fine {
  text-align: center;
}
/* The vignettes this page adds to the kit's: a shop's address in its chip, and three blank lines
   standing for a list or a search result - lines, never made-up words. */
.sn-viz-address {
  padding: 6px 12px;
  border: 1px solid var(--lp-hair);
  border-radius: 999px;
  background: var(--c-surface);
  color: var(--c-accent);
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  overflow-wrap: anywhere;
}
.sn-viz-lines > span {
  height: 10px;
  padding: 0;
  background: color-mix(in srgb, var(--c-ink) 12%, transparent);
  border: 0;
}
.sn-viz-lines > span:nth-child(1) { width: 70%; background: color-mix(in srgb, var(--c-accent) 55%, transparent); }
.sn-viz-lines > span:nth-child(2) { width: 92%; }
.sn-viz-lines > span:nth-child(3) { width: 58%; }

/* ── Analytics (task #484) ─────────────────────────────────────────────────────────────────────
 * The kit has no chart, so these are ours - and they are only tokens. The SVG is stretched to the
 * panel (preserveAspectRatio="none") and carries no text, so nothing is distorted; the axis
 * labels are HTML underneath it. A bar takes the accent, the baseline and the funnel's track take
 * the line colour, and nothing here is a colour of its own.
 */
.sn-chart {
  display: block;
  width: 100%;
  height: 9rem;
  overflow: visible;
}
.sn-chart__bar {
  fill: var(--c-accent);
}
.sn-chart__hit {
  fill: transparent;
}
.sn-chart__base {
  stroke: var(--c-line-strong);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}
@media (hover: hover) {
  .sn-chart g:hover .sn-chart__bar {
    fill: var(--c-accent-strong);
  }
  .sn-chart g:hover .sn-chart__hit {
    fill: color-mix(in srgb, var(--c-ink) 5%, transparent);
  }
}
.sn-chart__axis {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  margin-top: 0.375rem;
  font-size: 0.75rem;
  color: var(--c-ink-subtle);
}
/* Right to left: the axis is a flex row, so its labels turn round by themselves, and the drawing has
   to with them - otherwise the oldest bar stays at the left over the NEWEST label. The SVGs hold
   no text, so mirroring them mirrors nothing that has to be read. The funnel's bars grow from the
   reader's start the same way. */
[dir='rtl'] .sn-chart,
[dir='rtl'] .sn-bar {
  transform: scaleX(-1);
}

/* The figures behind the charts: a native <details> whose summary is the panel's own head. The kit
   has no disclosure in the app layer, so the chevron is ours: a border turned 45 degrees, in the
   line colour, that follows the reader's direction because it has no left or right in it.
   .sn-seo (the product sheet's "Search engines" section, task #3243) is the same disclosure. */
.sn-figures > summary,
.sn-seo > summary {
  cursor: pointer;
  list-style: none;
  min-height: 2.75rem;
}
/* A row, so the chevron is a chevron at the row's end - inline it collapsed to a stray "|" (2026-11-02);
   and in a sheet a section's heading, like the sheet's other sections. */
.sn-seo > summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  font-family: inherit;
  font-size: 0.9375rem;
  font-weight: 600;
  letter-spacing: normal;
  text-transform: none;
  color: var(--c-ink);
}
.sn-figures > summary::-webkit-details-marker,
.sn-seo > summary::-webkit-details-marker {
  display: none;
}
.sn-figures > summary::after,
.sn-seo > summary::after {
  content: '';
  flex: 0 0 auto;
  width: 0.5rem;
  height: 0.5rem;
  margin-top: -0.25rem;
  border-inline-end: 2px solid var(--c-ink-subtle);
  border-block-end: 2px solid var(--c-ink-subtle);
  transform: rotate(45deg);
}
.sn-figures[open] > summary::after,
.sn-seo[open] > summary::after {
  margin-top: 0.25rem;
  transform: rotate(225deg);
}
.sn-figures:not([open]) > summary,
.sn-seo:not([open]) > summary {
  border-bottom: 0;
}
.sn-figures > summary:focus-visible,
.sn-seo > summary:focus-visible {
  outline: 2px solid var(--c-accent);
  outline-offset: -2px;
}

/* The product sheet's Search engines section (task #3243): the fields above, the preview card
   below. Its own card echoes a real search result loosely enough not to be mistaken for one -
   the kit's sunk surface, not white-on-white, and the title in the accent colour a link would be. */
.sn-seopreview {
  margin-top: 0.75rem;
  padding: 0.875rem 1rem;
  border: 1px solid var(--c-line);
  border-radius: var(--radius-md);
  background: var(--c-surface-sunk);
}
.sn-seopreview__eyebrow {
  margin: 0 0 0.375rem;
  font-size: 0.8rem;
  color: var(--c-ink-muted);
}
.sn-seopreview__title {
  margin: 0;
  color: var(--c-accent);
  font-size: 1.05rem;
  line-height: 1.3;
  overflow-wrap: anywhere;
}
.sn-seopreview__url {
  margin: 0.125rem 0 0;
  font-family: var(--font-mono);
  font-size: 0.85rem;
  /* Google's own result shows the address in green - the one place this preview borrows a colour
     that is not the brand's own, because the whole point of the card is to look like that. */
  color: var(--c-ok);
  overflow-wrap: anywhere;
}
.sn-seopreview__desc {
  margin: 0.25rem 0 0;
  color: var(--c-ink-muted);
  line-height: 1.4;
  overflow-wrap: anywhere;
}
.sn-seocount {
  margin-top: 0.25rem;
}
.sn-seocount.is-over {
  color: var(--c-warn);
}

/* Two panels side by side when there is room. The kit's .mx-cols asks for 27rem a column, which
   is wider than a phone (360px): the panels would push the whole page sideways. min(100%, ...)
   lets one column shrink to the screen, and min-width: 0 lets a table inside scroll in its own
   wrapper instead of stretching the column. */
.sn-cols {
  display: grid;
  gap: 0.875rem;
  align-items: start;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 24rem), 1fr));
}
.sn-cols > * {
  min-width: 0;
}
/* The order and customer pages hold panels wide enough (a table with five columns, an address
   block) that 24rem's tablet break comes too early and folds their table's own horizontal
   scrollbar in with it (task #430 review): this modifier keeps the kit's original 27rem break
   for them while still shrinking to the phone below it. Analytics keeps plain .sn-cols at 24rem -
   two narrower product tables that were built and measured at that width. */
.sn-cols--wide {
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 27rem), 1fr));
}
/* A customer's two privacy actions, each with the sentence that says what it does right under it. */
.sn-privacy-acts .mx-hint {
  margin-top: 0.375rem;
}
/* The first column is the page's work (a customer's orders, a table of five), the second its facts: on a
   laptop beside a sidebar two equal halves squeezed the table until its dates wrapped (2026-11-02). */
@media (min-width: 75rem) {
  .sn-cols--lead {
    grid-template-columns: minmax(0, 1.7fr) minmax(0, 1fr);
  }
}

/* A hint and the action it explains, side by side while they fit - the customer page's CSV
   export used to sit in the panel head instead, where a long Hungarian title has no room to
   shrink and printed straight through the button (task #430 review). */
.sn-hint-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem 1rem;
}

/* An order's own delivery note is the shopper's words, not ours - a pasted map link with no
   spaces must break rather than push the panel wider than the phone that renders it (task #430
   review; kit's .mx-panel is overflow: hidden, but text inside it is not, so it prints through). */
.sn-notes-text {
  overflow-wrap: anywhere;
}

/* The two dates by hand, next to the presets: side by side while they fit, stacked when they do not. */
.sn-range {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 0.75rem;
  margin: 0 0 0.75rem;
}
.sn-range .mx-field {
  margin: 0;
  flex: 1 1 9rem;
}

/* The funnel: a bar under each step's name, as long as its count is against the biggest step. */
.sn-bar {
  display: block;
  width: 100%;
  height: 0.375rem;
  margin-top: 0.375rem;
  border-radius: 999px;
  overflow: hidden;
}
.sn-bar__back {
  fill: var(--c-line);
}
.sn-bar__fill {
  fill: var(--c-accent);
}
.sn-cols + .mx-hint {
  margin: 0.625rem 0 0;
}

/* A shop's front page in each language (task #539): a language and its address. An address has no
   space to break at, so at 360px it is allowed to break anywhere instead of widening the form. */
.sn-lang-addr {
  margin: 0 0 0.625rem;
  padding-inline-start: 1.25rem;
  font-size: 0.875rem;
  color: var(--c-ink);
}
.sn-lang-addr li {
  min-width: 0;
  overflow-wrap: anywhere;
}
.sn-lang-addr li + li {
  margin-top: 0.25rem;
}
.sn-lang-addr + .mx-hint {
  margin-top: 0;
}
/* Settings > Languages (views/languages.mjs): one row per language - its name (in itself, and in the reader's
   language under it), the Main badge or the Remove / Add button at the row's end, and the front page's address
   under both. A long address breaks anywhere rather than pushing the row wider than a phone. */
.sn-langrow {
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  column-gap: 0.75rem;
}
.sn-langrow__end {
  justify-self: end;
}
.sn-langrow__end form {
  margin: 0;
}
.sn-langrow__addr,
.sn-langrow > .mx-hint,
.sn-langrow > .mx-alert {
  grid-column: 1 / -1;
  margin: 0;
  min-width: 0;
  overflow-wrap: anywhere;
}
.sn-langrow__addr {
  font-size: 0.8125rem;
  color: var(--c-ink-muted);
}
.sn-langs__summary {
  margin: 0;
}
.sn-langs__summary .mx-link {
  margin-inline-start: 0.25rem;
}
.sn-langs__none {
  margin: 0;
  padding: 0.75rem 1rem;
}
#shop-langs + .mx-hint {
  margin-top: 0.5rem;
}
.sn-langs__saver {
  margin-top: 0;
}

/* The shop's language switch with more than three languages (views/layout.mjs langLinks): one pill, the current
   language, that opens the others - a <details>, so it opens with JavaScript off too. In the header the list drops
   over the page from the pill's end; in the footer it opens in place. */
.sn-lang--menu {
  position: relative;
}
.sn-lang__menu > summary {
  cursor: pointer;
  list-style: none;
}
.sn-lang__menu > summary::-webkit-details-marker {
  display: none;
}
.sn-lang__menu > summary::after {
  content: '';
  inline-size: 0.4rem;
  block-size: 0.4rem;
  border-inline-end: 1.5px solid var(--c-ink-muted);
  border-block-end: 1.5px solid var(--c-ink-muted);
  rotate: 45deg;
  margin-block-start: -0.2rem;
}
.sn-lang__menu[open] > summary::after {
  rotate: 225deg;
  margin-block-start: 0.2rem;
}
/* The end edge is the left one in Arabic: turned the other way, the chevron still points down (and up, open). */
[dir='rtl'] .sn-lang__menu > summary::after {
  rotate: -45deg;
}
[dir='rtl'] .sn-lang__menu[open] > summary::after {
  rotate: -225deg;
}
.sn-lang__list {
  list-style: none;
  margin: 0.375rem 0 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.375rem;
}

/* The taxes page's "check a country" row (task #2276): the select and its Show button share one row,
   the button on the select's baseline. On a phone the row wraps and the button drops under it. */
.sn-taxes-sample__row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 0.75rem;
}
.sn-taxes-sample__row > .mx-field {
  flex: 1 1 14rem;
  min-width: 0;
}

/* The taxes page's rate lists (task #2288): one row per country or class - its name, its rate and
   the Remove button on one line, with no bullet. The name takes the room it needs and is the part
   that wraps on a phone, so the rate and the button stay on the row they belong to. */
.sn-zone__rows {
  margin: 0.75rem 0 0;
  padding: 0;
  list-style: none;
}
.sn-zone__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.25rem 0.75rem;
  padding: 0.5rem 0;
  border-top: 1px solid var(--c-line);
}
.sn-zone__row > span:first-child {
  flex: 1 1 8rem;
  min-width: 0;
  overflow-wrap: anywhere;
}
.sn-zone__row > span:nth-child(2) {
  min-width: 0;
  overflow-wrap: anywhere;
  font-variant-numeric: tabular-nums;
}
.sn-zone__row form {
  margin: 0;
}

/* The "if you switch" sentence (settings.lang.warn) carries two addresses of its own, in <code>,
   inside running text rather than a list - the same 68-character problem as the list above, so it
   needs the same rule (#539 review: 30+ character slugs widened the whole form past 360px). */
.sn-lang-addr + .mx-hint code {
  overflow-wrap: anywhere;
}

/* The customers list (task #481). Four columns while there is room for four; on a phone the same
   three figures are written under the name instead (views/customers.mjs row), because at 360px in
   Hungarian the table was 825px wide inside a 334px panel and the columns the sort menu orders by
   were off the edge with nothing to say so. display:none, not visibility, so only one copy is read
   aloud. */
.sn-facts {
  display: none;
  margin: 0.5rem 0 0;
  font-size: 0.8125rem;
}
@media (max-width: 40rem) {
  .sn-customers .sn-col {
    display: none;
  }
  .sn-customers .sn-facts {
    display: grid;
  }
}
/* A stranger's address has no spaces to break at: without this one long one keeps its column as
   wide as the address, whatever the screen. */
.sn-list .sub {
  overflow-wrap: anywhere;
}
/* The pager under the list: the range says where you are, the buttons go either way. */
.sn-pager .mx-actions {
  margin-top: 0.75rem;
}

/* The photo import's own progress bar (task #3098) - a native <progress>, so a screen reader and
   a browser with no stylesheet both still get a real one. */
.sn-import-photos__bar {
  width: 100%;
  height: 0.5rem;
  accent-color: var(--c-accent);
}

/* The home page's block editor (task #782): a card a block with its four actions, the list of types to
   add, and a hero's slides. Every target is 44px on any pointer - this is read on a phone - and every
   row wraps, so a 40-character shop address and Hungarian's longer words push nothing sideways. */
.sn-blocks {
  display: grid;
  gap: 0.75rem;
  margin: 0;
  padding: 0;
  list-style: none;
}
.sn-blockcard {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem 1rem;
  padding: 1rem;
  border: 1px solid var(--c-line);
  border-radius: var(--radius-lg);
  background: var(--c-surface);
}
.sn-blockcard__main {
  flex: 1 1 14rem;
  min-width: 0;
}
.sn-blockcard__type {
  margin: 0;
  font-size: 1rem;
  font-weight: 700;
}
.sn-blockcard__sum {
  margin: 0.25rem 0 0;
  color: var(--c-ink-muted);
  overflow-wrap: anywhere;
}
.sn-blockcard__acts {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
}
.sn-blockcard__acts form {
  margin: 0;
}
.sn-blockcard .sn-iconbtn,
.sn-slide__acts .sn-iconbtn {
  min-width: 2.75rem;
  min-height: 2.75rem;
}
.sn-blockcard .mx-btn,
.sn-slide__acts .mx-btn,
.sn-slides-editor > p .mx-btn {
  min-height: 2.75rem;
}
.sn-types {
  display: grid;
  gap: 0.75rem;
  margin: 1rem 0 0;
  padding: 0;
  list-style: none;
}
.sn-types__link {
  display: block;
  min-height: 2.75rem;
  padding: 1rem;
  border: 1px solid var(--c-line);
  border-radius: var(--radius-lg);
  background: var(--c-surface);
  color: var(--c-ink);
  text-decoration: none;
}
.sn-types__link:hover {
  border-color: var(--c-accent);
}
.sn-types__name {
  display: block;
  font-weight: 700;
}
.sn-types__desc {
  display: block;
  margin-top: 0.25rem;
  color: var(--c-ink-muted);
  overflow-wrap: anywhere;
}
.sn-wordsgroup {
  margin-top: 0.5rem;
}
/* A fieldset is as wide as its widest content unless it is told otherwise, which is how a slide made a
   360px page scroll sideways. */
.sn-slide {
  min-inline-size: 0;
  margin: 1rem 0 0;
  padding: 1rem;
  border: 1px solid var(--c-line);
  border-radius: var(--radius-lg);
}
.sn-slide__title {
  padding-inline: 0.5rem;
  font-weight: 700;
}
.sn-slide__acts {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 1rem;
}
.sn-homerow td {
  overflow-wrap: anywhere;
}
.sn-homeacts .mx-btn,
.sn-homeedit .mx-btn,
.sn-homeedit .sn-iconbtn,
.sn-homeedit .sn-strip__btn {
  min-height: 2.75rem;
}
.sn-homeedit .sn-iconbtn {
  min-width: 2.75rem;
}
/* The free plan's line (Chris, 2026-10-13), on every admin page of a free shop - views/layout.mjs
   freePlanLine. Quiet, louder from 20 of 25 products; its own class, so a test asserting some
   OTHER notice is absent can never be confused by it. */
.sn-plan-line { margin: 0; font-size: 0.8125rem; color: var(--c-ink-subtle); }
/* The Free card's "Current plan" stands where its button would: a label, so not stretched like one. */
.sn-plan-current.lp-card__cta { align-self: center; justify-self: center; width: auto; }
.sn-plan-off { margin-top: 0.75rem; text-align: center; }
.sn-plan-line a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
.sn-plan-line--near { color: var(--c-warn); }
.sn-plan-line--full { color: var(--c-warn); font-weight: 600; }
/* The admin's banners and the dashboard's notices are the kit's own .mx-alert / .mx-note with .mx-actions
   (static/kit/app.css "Notices"): their layout - words, then the buttons at the end of the line or under it -
   is the kit's, and a stack (.mx-stack) keeps them apart. sn-notice, sn-pastdue-notice and sn-demo-notice only
   name them for the tests. */

/* "Get your shop ready" on the dashboard (task #2763, views/admin.mjs). A kit .mx-card holding one
   line per step: the kit's disabled tick, the step's link, and whether it is done. The line is a
   row so the link sits beside its tick, and wraps under them only at the phone width. */
.sn-setup__steps {
  list-style: none;
  margin: 0.875rem 0 0;
  padding: 0;
  display: grid;
  gap: 2px;
}
/* A step is a row: its tick, its link, and whether it is done - the row a 44px target on a phone. */
.sn-setup__step {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  min-height: 2.5rem;
  padding: 0 0.5rem;
  margin-inline: -0.5rem;
  border-radius: var(--radius-sm);
}
.sn-setup__step .mx-check {
  margin: 0;
  min-height: 0;
}
.sn-setup__step > a,
.sn-setup__step > span:not(.mx-check):not(.mx-hint) {
  flex: 1 1 auto;
  min-width: 0;
}
.sn-setup__step > .mx-hint {
  flex: none;
}
/* A step is a way to the page where it is done, not a warning: ink, underlined on hover - five accent-red
   underlines in a row read as five errors. Done steps go quieter, so what is left stands out. */
.sn-setup__step > a {
  color: var(--c-ink);
  font-weight: 500;
  text-decoration: none;
}
.sn-setup__step > a:hover {
  text-decoration: underline;
  text-underline-offset: 3px;
}
@media (hover: hover) {
  .sn-setup__step:has(> a):hover {
    background: var(--c-surface-2);
  }
}
.sn-setup__step.is-done > a,
.sn-setup__step.is-done > span:not(.mx-check):not(.mx-hint) {
  color: var(--c-ink-muted);
  font-weight: 400;
}
.sn-setup form,
.sn-share form {
  margin: 0;
}

/* "Share your shop" on the dashboard (task #1321, views/admin.mjs). A kit .mx-card: the address, then one
   row of ways to send it (Copy and Share… carry the hidden attribute until static/share.js shows them - the
   kit's .mx-btn sets display itself, so the attribute is given the last word here), and the QR code at the
   side with its two downloads, under the rest once the card is narrow. */
.sn-share__body {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem 1.5rem;
  align-items: flex-start;
  margin-top: 0.875rem;
}
.sn-share__main {
  flex: 1 1 16rem;
  min-width: 0;
  display: grid;
  gap: 0.75rem;
}
.sn-share__address,
.sn-feed__address {
  margin: 0;
  overflow-wrap: anywhere;
  font-weight: 500;
}
/* The address and its Copy button as one control: the address in a sunk box, the button at its end. */
.sn-share__copy {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
}
.sn-share__copy > .sn-share__address,
.sn-share__copy > .sn-feed__address {
  flex: 1 1 14rem;
  min-width: 0;
  padding: 0.5rem 0.75rem;
  border: 1px solid var(--c-line);
  border-radius: var(--radius-sm);
  background: var(--c-surface-2);
  font-size: 0.875rem;
}
.sn-share__copy a {
  color: var(--c-ink);
  text-decoration: none;
}
.sn-share__copy a:hover {
  text-decoration: underline;
}
.sn-share__main > .mx-hint:empty {
  display: none;
}
.sn-share__qr {
  margin: 0;
  flex: 0 0 auto;
  display: grid;
  justify-items: center;
  gap: 0.375rem;
  width: 9rem;
  text-align: center;
}
.sn-share__code {
  display: block;
  width: 7.5rem;
  height: auto;
}
.sn-share__qr .mx-hint {
  margin: 0;
}
.sn-share__downloads {
  display: grid;
  justify-items: center;
  font-size: 0.8125rem;
}
.sn-share [hidden],
.sn-feed [hidden] {
  display: none !important;
}

/* The feed card (task #3099, views/admin.mjs): the address and its copy button as the Share card has them,
   so the two cards read as one family; then the two exact routes, and the small print. */
.sn-feed .sn-share__copy {
  margin: 0.875rem 0;
}
.sn-feed__steps {
  margin: 0 0 0.75rem;
  padding-inline-start: 1.25rem;
  display: grid;
  gap: 0.375rem;
  font-size: 0.8438rem;
  line-height: 1.55;
  color: var(--c-ink-muted);
}
.sn-feed__note {
  margin: 0;
}
.sn-feed__note + .sn-feed__note {
  margin-top: 0.25rem;
}

/* The admin bar's View shop (every page) becomes an icon button below 640px (task #3128): since kit 9ed5409 the
 * kit does it for any bar button whose words sit in .mx-btn__text (views/admin.mjs viewShopButton), and the word
 * stays in the anchor's aria-label and title. */


/* /superadmin/failures (task #3009): .sn-founder code already sets overflow-wrap: anywhere (the
   Resend events page's own fix, where a <code> sits in a flowing card with no scroll affordance at
   all) - inside THIS page's table that shrank a route pattern or an error's own message to one
   character per column instead of letting the table wrap at its normal word/slash breaks. nowrap
   is not the answer either: it is banned bare in this file (layout.test.mjs - task #216's bug, text
   pinned to one line and left to spill) and pairing it with text-overflow: ellipsis would hide the
   very thing a founder opened this page to read. Reset back to the ordinary rule, which already
   wraps a long address or a long message correctly on its own. */
.sn-founder .sn-fail-reason {
  overflow-wrap: normal;
}
/* Crawler traffic is counted, never flagged (lib/failures.mjs): its own section is greyed so a
   founder's eye goes to Open first. */
.sn-fail-crawlers {
  opacity: 0.6;
}

/* ── The packing slip (task #3063) ──────────────────────────────────────────────────────────
   A page made for paper, not for the admin's own frame: its own body class, carrying none of
   the kit's bars, tabs or footer either on screen or printed. documentHead's `theme: 'light'`
   forces the surface and ink tokens to their light values, which is what keeps a slip printed
   from a dark admin readable - black ink on white, whatever the owner's own palette is set to. */
body.sn-slip-page {
  background: var(--c-surface);
  color: var(--c-ink);
}
/* <html> keeps the page's own canvas colour by default, which shows past the body once a short
   slip ends before the window does - paint it to match the "paper" (task #3081). */
html:has(> body.sn-slip-page) {
  background: var(--c-surface);
}
.sn-slip__bar {
  max-width: 44rem;
  margin: 1.5rem auto 0;
  padding: 0 1.5rem;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}
.sn-slip__print {
  margin: 0;
}
.sn-slip {
  max-width: 44rem;
  margin: 2rem auto;
  padding: 0 1.5rem;
  padding-bottom: 2rem;
}
/* Every slip after the first starts its own sheet of paper (task #3063's own batch page); on a
   screen this does nothing, which is exactly right for the one slip a single order's own page shows. */
.sn-slip + .sn-slip {
  page-break-before: always;
}
.sn-slip__head {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  flex-wrap: wrap;
  gap: 1.5rem;
  border-bottom: 2px solid var(--c-line-strong);
  padding-bottom: 1rem;
  margin-bottom: 1.5rem;
}
.sn-slip__brand {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  font-weight: 600;
  min-width: 0;
}
.sn-slip__logo {
  height: 2.5rem;
  width: auto;
  max-width: 10rem;
  object-fit: contain;
}
.sn-slip__meta {
  text-align: end;
}
.sn-slip__meta h1 {
  margin: 0 0 0.25rem;
  font-size: 1.25rem;
}
.sn-slip__meta p {
  margin: 0;
  color: var(--c-ink-muted);
}
.sn-slip__block {
  margin-bottom: 1.25rem;
}
.sn-slip__block h2 {
  margin: 0 0 0.35rem;
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--c-ink-muted);
}
.sn-slip__block p,
.sn-slip__block address {
  margin: 0;
  font-style: normal;
}
.sn-slip__lines {
  width: 100%;
  border-collapse: collapse;
  margin-top: 1.5rem;
}
.sn-slip__lines th,
.sn-slip__lines td {
  text-align: left;
  padding: 0.5rem 0.5rem 0.5rem 0;
  border-bottom: 1px solid var(--c-line);
  vertical-align: top;
}
.sn-slip__lines th:last-child,
.sn-slip__lines td:last-child {
  text-align: end;
  padding-inline-end: 0;
}
.sn-slip__lines .sub {
  display: block;
  color: var(--c-ink-muted);
  font-size: 0.85rem;
}
@media print {
  .sn-slip__bar {
    display: none;
  }
  .sn-slip {
    padding-bottom: 0;
  }
  @page {
    size: A4;
    margin: 15mm;
  }
}

/* ── The import mapping table on a phone (task #3106) ────────────────────────────────────────
 * Sideways scroll inside .mx-scroll is fine for a table nobody has to act on mid-row, but the one
 * control that matters here is the "Goes to" select, and at 390px a third column pushed it past
 * the edge. Below 720px every row becomes a block instead: the column name and its sample stay on
 * one line, the select drops full width under them, so it is the thing a thumb lands on first.
 */
@media (max-width: 44.9375rem) {
  .sn-import-map,
  .sn-import-map thead,
  .sn-import-map tbody,
  .sn-import-map tr,
  .sn-import-map th,
  .sn-import-map td {
    display: block;
    width: 100%;
  }
  .sn-import-map thead {
    display: none;
  }
  .sn-import-map tr {
    padding: 0.625rem 0;
    border-bottom: 1px solid var(--c-line);
  }
  .sn-import-map tr:first-child {
    padding-top: 0;
  }
  .sn-import-map td:first-child,
  .sn-import-map td:nth-child(2) {
    display: inline;
  }
  .sn-import-map td:nth-child(2) {
    margin-inline-start: 0.375rem;
  }
  .sn-import-map td:nth-child(3) {
    margin-top: 0.5rem;
  }
  .sn-import-map td:nth-child(3) select {
    width: 100%;
  }
}

/* ── A reorderable list's table on a phone (task #3224, widened #3230, row shape #3233) ───────
 * Below the breakpoint a multi-column table with move-up/down buttons in its last column ran
 * past the card's edge inside .mx-scroll's sideways scroll: a header read "SOR" instead of
 * "Sorrend" and the move buttons were half off-screen. #3224 fixed this for the own-payment-
 * methods table (name, state, order); #3230 found the same cut-off, live at 390px, on the
 * attributes, pages and collections tables (one or two extra columns between the name and the
 * order buttons) - badges and categories were checked too but, with only name/products/order and
 * no extra column, were never actually cut off, so they are deliberately left alone.
 * Each row becomes a flex line instead, still inside the card: the name link grows to fill the
 * space (no longer forced to one line by the touch nowrap rule above) and every other column
 * sits beside it, wrapping onto its own line if a long name needs the room.
 *
 * #3233: that left two shapes in one list, because flex-wrap decides the break point from each
 * row's own content width - a short name let every column share the name's line, a long one
 * pushed only some of them down. The name cell's flex-basis: 100% forces it to fill the whole
 * line on every row, whatever its length, so the break after it is unconditional: the rest of the
 * columns always land together on the row's second line, and the last column's auto margin pushes
 * the move buttons to that line's end every time. The tr's own padding is the panel's normal
 * inset (the same 0.875rem 1.25rem .sn-addr-table and .sn-activity-table use below), since the
 * flush panel body gives the table none of its own.
 */
@media (max-width: 45rem) {
  .sn-movelist thead {
    display: none;
  }
  .sn-movelist,
  .sn-movelist tbody {
    display: block;
    width: 100%;
  }
  .sn-movelist tr {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    padding: 0.875rem 1.25rem;
    border-bottom: 1px solid var(--c-line);
  }
  .sn-movelist tr:last-child {
    border-bottom: 0;
  }
  .sn-movelist td {
    display: block;
    padding: 0;
    border-bottom: 0;
  }
  .sn-movelist td:first-child {
    flex: 1 1 100%;
    min-width: 0;
  }
  .sn-movelist td:first-child a[data-mx-row] {
    white-space: normal;
    overflow-wrap: anywhere;
  }
  .sn-movelist td:not(:first-child) {
    flex: 0 0 auto;
  }
  .sn-movelist td:last-child {
    margin-inline-start: auto;
  }
}

/* The consent banner (task #3289): a bottom sheet on a phone, the same fixed full-width bar as
   .sn-sf-compare-tray, above it (z-index) since a choice here blocks every third-party script -
   nothing of the page needs to make room for it, because it is [hidden] until static/consent.js
   decides a shopper has not answered yet, and goes back to [hidden] the moment they press either
   button, so it never sits over the sticky buy row once a choice is made. */
.sn-consent {
  position: fixed;
  inset-inline: 0;
  bottom: 0;
  z-index: 60;
  border-top: 1px solid var(--c-line-strong);
  background: var(--c-surface-2);
  box-shadow: var(--sn-lift);
  padding-bottom: env(safe-area-inset-bottom);
}
.sn-consent[hidden] {
  display: none;
}
.sn-consent__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  max-width: min(var(--page-max), 72rem);
  margin-inline: auto;
  padding: 1rem clamp(1rem, 4vw, 2rem);
}
.sn-consent__text {
  margin: 0;
  flex: 1 1 16rem;
  font-size: 0.9375rem;
  color: var(--c-ink);
}
.sn-consent__privacy {
  margin: 0.25rem 0 0;
  font-size: 0.8125rem;
}
.sn-consent__actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}
/* Equal weight, equal size: no primary/ghost pair here, the brief's own "no pre-ticked anything". */
.sn-consent__actions .mx-btn {
  min-width: 7rem;
}

/* ── Shop design pass (2026-11-02) ──────────────────────────────────────────────────────────────
   The 2026-11-02 review of the customer-facing shop, in one block. It sits LAST on purpose: each
   rule here wins its tie with the older rule for the same element above, so the older rules were
   left as they were. Everything is the shop's own - .sn-sf-* classes, or the kit's form classes
   only under body.sn-sf (every shop page, views/storefront.mjs shopPage) - never the admin, never
   the kit. THE SELLER'S ACCENT NEVER MEANS "SELECTED" OR "WRONG" HERE: it can be any colour,
   red included, so selected is the shopper's own ink and an error carries its own icon. */

/* 1. Checkout choices. Selected: a 2px ink border, the plain surface, a check mark - next to a red
   error a red-tinted card read as a second error. A single method is a fact, not a choice. */
.sn-sf-pay__opt {
  position: relative;
  transition: border-color 0.15s var(--ease-out-soft), box-shadow 0.15s var(--ease-out-soft);
}
.sn-sf-pay__opt:hover {
  border-color: var(--c-ink-subtle);
}
.sn-sf-pay__opt input {
  accent-color: var(--c-ink);
}
.sn-sf-pay__opt:has(input:checked) {
  padding-inline-end: 3rem;
  border-color: var(--c-ink);
  outline: 1px solid var(--c-ink);
  outline-offset: -2px;
  background: var(--c-surface);
}
/* The check: an ink disc with the tick cut out of it (evenodd), so it reads on either theme. */
.sn-sf-pay__opt:has(input:checked)::after {
  content: '';
  position: absolute;
  inset-block-start: 0.875rem;
  inset-inline-end: 1rem;
  width: 1.25rem;
  height: 1.25rem;
  background: var(--c-ink);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath fill-rule='evenodd' d='M10 0a10 10 0 1 1 0 20 10 10 0 0 1 0-20zm4.4 5.9-5.6 6.2-3.1-2.9-1.2 1.3 4.4 4.1 6.8-7.5z'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath fill-rule='evenodd' d='M10 0a10 10 0 1 1 0 20 10 10 0 0 1 0-20zm4.4 5.9-5.6 6.2-3.1-2.9-1.2 1.3 4.4 4.1 6.8-7.5z'/%3E%3C/svg%3E") center / contain no-repeat;
}
.sn-sf-pay__opt:has(input:focus-visible) {
  outline: 2px solid var(--c-ink);
  outline-offset: 2px;
}
.sn-sf-pay__opt:only-of-type:has(input:checked) {
  padding-inline-end: 1rem;
  border-color: var(--c-line);
  outline: 0;
  cursor: default;
}
.sn-sf-pay__opt:only-of-type:has(input:checked)::after {
  content: none;
}
.sn-sf-pay__opt:only-of-type:has(input:checked) input {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  margin: 0;
  opacity: 0;
  pointer-events: none;
}
.sn-sf-pay:focus:not(:focus-visible) {
  outline: none;
}
/* Field errors: their own icon and weight, so they never depend on red alone - and never pass for
   the seller's accent. [hidden] repeated: display:flex would otherwise outrank the attribute. */
.sn-sf .mx-err {
  display: flex;
  align-items: flex-start;
  gap: 0.375rem;
  font-size: 0.8125rem;
  font-weight: 600;
  line-height: 1.4;
}
.sn-sf .mx-err[hidden] {
  display: none;
}
.sn-sf .mx-err::before,
.sn-sf-errsum__title::before,
p.sn-sf-notice--bad::before {
  content: '';
  flex: none;
  width: 1rem;
  height: 1rem;
  margin-top: 0.1rem;
  background: var(--c-rec);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0zM7.1 3.5h1.8l-.3 5.7H7.4zM8 10.6a1.1 1.1 0 1 1 0 2.2 1.1 1.1 0 0 1 0-2.2z'/%3E%3C/svg%3E") center / contain no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0zM7.1 3.5h1.8l-.3 5.7H7.4zM8 10.6a1.1 1.1 0 1 1 0 2.2 1.1 1.1 0 0 1 0-2.2z'/%3E%3C/svg%3E") center / contain no-repeat;
}
.sn-sf .mx-field [aria-invalid='true'] {
  border-color: var(--c-rec);
  outline: 1px solid var(--c-rec);
  outline-offset: -2px;
}
.sn-sf-notice--bad {
  border-color: var(--c-rec);
  border-inline-start-width: 0.25rem;
  background: color-mix(in srgb, var(--c-rec) 6%, var(--c-surface));
  color: var(--c-ink);
}
p.sn-sf-notice--bad {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
}
/* The checkout's error summary (views/checkout.mjs errorSummary): checkout.js focuses it on load. */
.sn-sf-errsum {
  scroll-margin-top: 6rem;
}
.sn-sf-errsum:focus {
  outline: none;
}
.sn-sf-errsum:focus-visible {
  outline: 2px solid var(--c-ink);
  outline-offset: 2px;
}
.sn-sf-errsum__title {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  margin: 0;
  font-weight: 600;
}
.sn-sf-errsum__list {
  margin: 0.5rem 0 0;
  padding-inline-start: 1.5rem;
}
.sn-sf-errsum__list li + li {
  margin-top: 0.25rem;
}
.sn-sf-errsum__list a {
  color: var(--c-ink);
  text-decoration: underline;
  text-underline-offset: 0.15em;
}
.sn-sf-errsum__list a:hover {
  text-decoration-thickness: 2px;
}
#co-shipping,
#co-payment {
  scroll-margin-top: 6rem;
}

/* 3. Product cards. The border, radius and surface move to the cell, so Add to cart sits INSIDE
   the card instead of hanging under it; price and badges share a footer pushed to the bottom, so
   prices line up across a row whatever each name and description run to. */
.sn-sf-cell {
  border: var(--mx-stroke, 1px) solid var(--c-line);
  border-radius: var(--radius-lg);
  background: var(--c-surface);
  transition: border-color 0.15s var(--ease-out-soft), transform 0.15s var(--ease-out-soft);
}
.sn-sf-cell:hover {
  border-color: var(--c-line-strong);
}
.sn-sf-cell:has(.sn-sf-card:hover) {
  transform: translateY(-2px);
}
.sn-sf-card {
  border: 0;
  border-radius: calc(var(--radius-lg) - 1px);
  background: none;
}
.sn-sf-card:hover {
  transform: none;
}
.sn-sf-card__foot {
  display: flex;
  flex-direction: column;
  gap: 0.375rem;
  margin-top: auto;
  padding-top: 0.25rem;
}
.sn-sf-card__foot .sn-sf-price,
.sn-sf-card__foot .sn-sf-badges,
.sn-sf-card__foot .sn-sf-price__unit {
  margin: 0;
}
/* The price is the foot's LAST line on screen, so every price in a row sits on the same line
   whether or not its card wears badges or a "Sold out" pill. Only the drawing order moves: the
   markup (and so a screen reader) keeps the brief's own order - the price with its sale badge
   first, then the owner's pills (task #3141). */
.sn-sf-card__foot .sn-sf-badges,
.sn-sf-card__foot .sn-sf-out,
.sn-sf-card__foot .sn-sf-stock {
  order: -1;
}
.sn-sf-card__rating {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.375rem;
  margin: 0;
  color: var(--c-ink-muted);
  font-size: 0.8125rem;
  line-height: 1.2;
}
.sn-sf-card__actions .sn-sf-quickadd-btn,
.sn-sf-card__actions .sn-sf-quickadd--pick {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
@media (max-width: 40rem) {
  .sn-sf-card__actions .sn-sf-quickadd-btn,
  .sn-sf-card__actions .sn-sf-quickadd--pick {
    padding-inline: 0.375rem;
    font-size: 0.875rem;
  }
  /* "Opciók kiválasztása" is the longest label a 360px card has to hold on one line. */
  .sn-sf-card__actions .sn-sf-quickadd--pick {
    font-size: 0.8125rem;
  }
}

/* 4. No photo: a calm parcel line drawing on a softly tinted tile, the same everywhere a photo
   would be (views/cards.mjs noPhotoIcon). */
.sn-sf-shot--none,
.sn-sf-sum__shot--none {
  display: grid;
  place-items: center;
  background: color-mix(in srgb, var(--c-ink) 4%, var(--c-surface-2));
  color: var(--c-ink-subtle);
}
.sn-sf-shot__icon {
  width: 34%;
  min-width: 1.25rem;
  max-width: 5rem;
  height: auto;
}
.sn-sf-sum__shot--none .sn-sf-shot__icon {
  width: 55%;
}

/* 5. Search: a form in the page's own head (search page, 404), and the way on when nothing is
   found. The native clear cross goes: it is drawn differently by every engine and the box is a
   plain form anyway. */
.sn-sf-findform {
  display: flex;
  gap: 0.5rem;
  max-width: 36rem;
  margin: 1.25rem 0 0;
}
.sn-sf-findform__input {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 2.75rem;
  padding: 0 0.875rem;
  border: var(--mx-stroke, 1px) solid var(--sn-line);
  border-radius: var(--mx-control-radius, var(--radius-sm));
  background: var(--c-surface);
  color: var(--c-ink);
  font: inherit;
  font-size: 1rem;
}
.sn-sf-findform__input:focus {
  border-color: var(--c-accent);
  outline: 3px solid color-mix(in srgb, var(--c-accent) 22%, transparent);
  outline-offset: 0;
}
.sn-sf-findform__go {
  flex: none;
}
.sn-sf-findform__input::-webkit-search-cancel-button,
.sn-sf-search__input::-webkit-search-cancel-button {
  -webkit-appearance: none;
  appearance: none;
  display: none;
}
.sn-sf-ways {
  margin-top: 1.5rem;
}
.sn-sf-ways__lead {
  margin: 0 0 0.75rem;
  color: var(--c-ink-muted);
}
.sn-sf-ways__list {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 0;
  padding: 0;
  list-style: none;
}
.sn-sf-way {
  display: inline-flex;
  align-items: center;
  min-height: 2.75rem;
  padding: 0.5rem 1rem;
  border: var(--mx-stroke, 1px) solid var(--c-line-strong);
  border-radius: var(--mx-control-radius, 999px);
  background: var(--c-surface);
  color: var(--c-ink);
  font-size: 0.9375rem;
  font-weight: 500;
  text-decoration: none;
}
.sn-sf-way:hover {
  border-color: var(--c-ink-subtle);
}

/* 6. The product page in two columns from 48rem, not 62rem: on a tablet Add to cart sat ~1,200px
   down under a full-width photo, and the phone's pinned buy bar stops at 48rem. Equal columns up
   to 62rem, where the layouts' own proportions above take over again (re-stated after this rule,
   which would otherwise win their tie). Below 48rem the photo is capped so a large phone held
   sideways does not open on a wall of picture. */
@media (min-width: 48rem) {
  .sn-sf-gallery {
    grid-area: gallery;
  }
  .sn-sf-nameblock {
    grid-area: name;
  }
  .sn-sf-panel {
    grid-area: panel;
  }
  [data-pdp-layout='standard'] .sn-sf-product,
  [data-pdp-layout='gallery_wide'] .sn-sf-product {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    grid-template-rows: auto 1fr;
    row-gap: 0.75rem;
    grid-template-areas:
      'gallery name'
      'gallery panel';
  }
  [data-pdp-layout='title_top'] .sn-sf-product {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    grid-template-areas:
      'name name'
      'gallery panel';
  }
}
@media (min-width: 62rem) {
  [data-pdp-layout='standard'] .sn-sf-product,
  [data-pdp-layout='title_top'] .sn-sf-product {
    grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
  }
  [data-pdp-layout='gallery_wide'] .sn-sf-product {
    grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
  }
}
@media (max-width: 47.99rem) {
  .sn-sf-gallery {
    width: 100%;
    max-width: 34rem;
  }
}

/* 7. A phone's header is one row: the shop | search | settings | wishlist | account | cart. An opened
   search takes a row of its own, as the search-forward layout's field always did. */
@media (max-width: 47.99rem) {
  .sn-sf-bar__inner {
    gap: 0.5rem;
  }
  html:not([data-hdr-search='inline']) .sn-sf-bar__inner {
    flex-wrap: nowrap;
  }
  html:not([data-hdr-search='inline']) .sn-sf-bar__inner:has(.sn-sf-search.is-open) {
    flex-wrap: wrap;
  }
  .sn-sf-search.is-open {
    order: 1;
    flex: 1 0 100%;
  }
  .sn-sf-bar__right {
    gap: 0.375rem;
  }
}
/* The smallest phones (360px): six things in one row - the shop, search, settings, wishlist, account,
   cart - left a one-word name like "Northlight" 74px and it broke mid-word. Tighter gaps and the name a
   step smaller give it ~85px; every icon keeps its 44px. */
@media (max-width: 24rem) {
  .sn-sf-bar__inner {
    gap: 0.375rem;
  }
  .sn-sf-bar__right {
    gap: 0.25rem;
  }
  .sn-sf-shop {
    font-size: 1rem;
  }
}
/* 8. Shop forms (checkout, contact, sign-in, an address): the shop's own text face, sentence case
   and ink - not the admin's small mono capitals - and 1rem text in the boxes, which is also what stops
   iOS zooming the page on focus. The contrast is the kit's own since 2026-11-02 (tokens.css): hints at
   4.5:1 (--c-ink-subtle). The boxes' edges are the shop's one line (--sn-line, at the end of this file), not
   the kit's 3:1 field edge: Chris, 2026-11-04. */
.sn-sf .mx-field > label:not(.sn-check, .mx-check),
.sn-sf .mx-label {
  color: var(--c-ink);
  font-family: inherit;
  font-size: 0.875rem;
  font-weight: 600;
  letter-spacing: normal;
  text-transform: none;
}
.sn-sf .mx-hint {
  font-size: 0.8125rem;
  line-height: 1.45;
}
.sn-sf .mx-field input:not([type='checkbox'], [type='radio'], [type='file'], [type='hidden']),
.sn-sf .mx-field select {
  height: 2.75rem;
  background-color: var(--c-surface);
  font-size: 1rem;
}
.sn-sf .mx-field textarea {
  background-color: var(--c-surface);
  font-size: 1rem;
}
.sn-sf-qty input,
.sn-sf-line__qty input[type='number'],
.sn-sf-sort__select,
.sn-sf-filters__field input,
.sn-sf-search__form {
  border-color: var(--sn-line);
}

/* 9. The shopper's sign-in: lead, label, box and button as one spaced stack, and a notice clear of
   the heading. */
.sn-sf-account-form {
  display: grid;
  gap: 1rem;
  max-width: 28rem;
  margin-top: 1rem;
}
.sn-sf-account-form .sn-sf-lead,
.sn-sf-account-form__wish {
  margin: 0;
}
.sn-sf-account-form__wish {
  color: var(--c-ink);
  font-weight: 600;
}
.sn-sf-account-form .sn-sf-btn {
  justify-self: start;
}
@media (max-width: 30rem) {
  .sn-sf-account-form .sn-sf-btn {
    justify-self: stretch;
  }
}
.sn-sf-account-notice {
  max-width: 36rem;
  margin: 1rem 0 0;
}

/* 10. Section headings that read as headings under a 40px h1 - above a blog card's own title,
   never below it. Form sections and the order summary keep a step down: they are parts of one
   form, not sections of a page. */
.sn-sf-h2 {
  margin: 0 0 1rem;
  font-size: clamp(1.25rem, 1.05rem + 0.9vw, 1.5rem);
  line-height: 1.25;
  letter-spacing: -0.015em;
  text-wrap: balance;
}
.sn-sf-set > legend.sn-sf-h2,
.sn-sf-set > .sn-sf-h2,
.sn-sf-sum .sn-sf-h2 {
  margin-bottom: 0.75rem;
  font-size: 1.125rem;
}

/* 11. "You may also like": its own full-width section, four to a row, two on a phone. */
.sn-sf-related {
  margin-top: clamp(2.5rem, 7vw, 4rem);
}
@media (min-width: 40.0625rem) {
  .sn-sf-related .sn-sf-grid--row {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}
@media (min-width: 48rem) {
  .sn-sf-related .sn-sf-grid--row {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}

/* 12. A finger is not a mouse: the 44px floor for the shop's small links and pills. */
@media (hover: none) and (pointer: coarse) {
  .sn-sf-cats__link,
  .sn-sf-cats__more,
  .sn-sf-cats__sublink,
  .sn-sf-subcats__link,
  .sn-sf-sort__select,
  .sn-sf-crumb a,
  .sn-sf-backlink a,
  .sn-sf-basket__keep a,
  .sn-sf-foot__cookies,
  .sn-sf-errsum__list a,
  .sn-lang__item,
  .sn-sf-chip {
    min-height: 2.75rem;
  }
  .sn-sf-crumb a,
  .sn-sf-backlink a,
  .sn-sf-basket__keep a,
  .sn-sf-errsum__list a {
    display: inline-flex;
    align-items: center;
  }
  .sn-sf-viewswitch__btn {
    width: 2.75rem;
    height: 2.75rem;
  }
}

/* 13. The cart: the lines, then a summary card - code, totals, one full-width Checkout and a quiet
   way back - beside them once there is room. The code box is capped by the card. */
.sn-sf-basket__sum {
  display: flex;
  flex-direction: column;
  gap: 0.875rem;
  margin-top: 1.25rem;
  padding: 1.25rem;
  border: 1px solid var(--c-line);
  border-radius: var(--radius-lg);
  background: var(--c-surface);
}
.sn-sf-basket__sum .sn-sf-coupon {
  margin: 0;
  padding: 0 0 1rem;
  border-top: 0;
  border-bottom: 1px solid var(--c-line);
}
.sn-sf-basket__sum .sn-sf-total,
.sn-sf-basket__sum .sn-sf-notice,
.sn-sf-basket__sum .sn-sf-giftcard {
  margin: 0;
}
.sn-sf-basket__go {
  display: block;
  margin: 0.25rem 0 0;
}
.sn-sf-basket__go .sn-sf-btn {
  width: 100%;
}
.sn-sf-basket__keep {
  margin: 0;
  font-size: 0.9375rem;
  text-align: center;
}
.sn-sf-basket__keep a {
  color: var(--c-ink-muted);
}
.sn-sf-basket__keep a:hover {
  color: var(--c-ink);
}
@media (min-width: 56rem) {
  .sn-sf-basket {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(18rem, 22rem);
    gap: 2rem;
    align-items: start;
  }
  .sn-sf-basket__sum {
    position: sticky;
    top: 5rem;
  }
}

/* 15. The thank-you page: a success mark beside the heading (the shop's "ok" green, not its
   accent), and the line saying where the confirmation went. */
.sn-sf-order__head {
  display: flex;
  align-items: center;
  gap: 0.875rem;
}
.sn-sf-done-mark {
  display: inline-flex;
  flex: none;
  align-items: center;
  justify-content: center;
  width: 3rem;
  height: 3rem;
  border-radius: 50%;
  background: color-mix(in srgb, var(--c-ok) 14%, transparent);
  color: var(--c-ok);
}
.sn-sf-done-mark svg {
  stroke-width: 2.4;
}
.sn-sf-order__emailed {
  margin: 0.5rem 0 0;
  color: var(--c-ink-muted);
  overflow-wrap: anywhere;
}

/* 16. Category tiles two to a row on a phone (five full-width squares were ~2,000px), and blog
   covers shown whole at 4:3 on a tint rather than square art cut to a 16:9 strip. */
@media (max-width: 40rem) {
  .sn-sf-tiles {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.625rem;
  }
}
.sn-sf-blog-card__pic {
  aspect-ratio: 4 / 3;
  object-fit: contain;
  background: var(--c-surface-2);
}

/* 17. The toast's two ways on, the same size. */
.sn-sf-toast__acts .sn-sf-btn {
  flex: 1 1 0;
}

/* The phone's pinned buy bar: the quantity and the button stay on one line beside the price
   (they wrapped into two rows on a sale price), and the page is padded at the BODY's foot, not
   main's, so the footer's language switch and its last line are never left under the bar. */
@media (max-width: 47.99rem) {
  .sn-sf-sticky .sn-sf-buy {
    flex-wrap: nowrap;
  }
  /* The button keeps its words on one line; the price beside it is what gives way. */
  .sn-sf-sticky .sn-sf-buy .sn-sf-btn {
    flex: none;
  }
  body:has(.sn-sf-sticky) .sn-sf-main,
  body:has(.sn-sf-sticky):has(.sn-sf-compare-tray:not([hidden])) .sn-sf-main {
    padding-bottom: 0;
  }
  body:has(.sn-sf-sticky) {
    padding-bottom: 5.5rem;
  }
  body:has(.sn-sf-sticky):has(.sn-sf-compare-tray:not([hidden])) {
    padding-bottom: 10rem;
  }
}

/* "Where you are" in the shop's own menus - the current language and the current category - in
   ink and weight, not the seller's accent alone, which can be any colour (the rule at the top of
   this block). */
.sn-sf .sn-lang__item.is-current,
.sn-sf-cats__link[aria-current] {
  border-color: var(--sn-line);
  color: var(--c-ink);
  font-weight: 600;
}

/* The last thumb-sized gaps the 390px audit found: the shop's name in the bar and the product
   page's rating link (it jumps to the reviews). */
@media (hover: none) and (pointer: coarse) {
  .sn-sf-shop,
  .sn-sf-rating a {
    display: inline-flex;
    align-items: center;
    min-height: 2.75rem;
  }
  html[data-hdr-logo='center'] .sn-sf-shop {
    justify-content: center;
  }
}

/* 18. The shop's own look (lib/shop-design.mjs, the Appearance editor). ONE SET OF MARKUP: every
   css axis is an attribute on <html> and every var axis a custom property there, so the editor's
   frame flips them in place and nothing here needs the server. With no attribute set (a shop that
   never opened the editor) every rule below either does not match or falls back to the value the
   shop always had. The kit's own look tokens (--mx-control-radius, --mx-stroke, --mx-icon-stroke)
   are set only for a non-default choice; the controls above read them with their own value as the
   fallback, so "round" and "regular" leave each control as it was. */

/* Catalog cards: the photo's shape, fit and inset (card_shape, card_fit, card_pad). The inset is
   padding on the tile, so the photo keeps its fit inside it on the tile's own colour. */
html[data-card-shape='portrait'] .sn-sf-card__shot {
  aspect-ratio: 3 / 4;
}
html[data-card-shape='landscape'] .sn-sf-card__shot {
  aspect-ratio: 4 / 3;
}
html[data-card-fit='fit'] .sn-sf-card__shot .sn-sf-shot,
html[data-card-fit='fit'] .sn-sf-row__shot .sn-sf-shot {
  object-fit: contain;
}
/* The second photo on hover (card_hover, a mouse only): display:none everywhere else, which is
   what keeps a lazy image from ever being fetched. Laid exactly over the first, inside the inset. */
.sn-sf-shot--next {
  display: none;
}
@media (hover: hover) {
  html[data-card-hover='on'] .sn-sf-shot--next {
    display: block;
    position: absolute;
    inset-block-start: var(--sn-card-pad, 0);
    inset-inline-start: var(--sn-card-pad, 0);
    width: calc(100% - 2 * var(--sn-card-pad, 0px));
    height: calc(100% - 2 * var(--sn-card-pad, 0px));
    background: var(--c-surface-2);
    opacity: 0;
    transition: opacity 0.2s var(--ease-out-soft);
  }
  html[data-card-hover='on'] .sn-sf-card:hover .sn-sf-shot--next,
  html[data-card-hover='on'] .sn-sf-card:focus-visible .sn-sf-shot--next {
    opacity: 1;
  }
}
/* The SKU under the name (card_sku) and the "Only 3 left" line (card_low): drawn whenever the
   product has them, shown only when the shop asked. */
.sn-sf-sku {
  display: none;
  margin: 0;
  color: var(--c-ink-muted);
  font-size: 0.75rem;
  line-height: 1.3;
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}
html[data-card-sku='on'] .sn-sf-sku {
  display: block;
}
.sn-sf-low {
  display: none;
  margin: 0;
  color: var(--c-ink);
  font-size: 0.8125rem;
  font-weight: 600;
  line-height: 1.3;
}
html[data-card-low='on'] .sn-sf-low {
  display: block;
}
.sn-sf-card__foot .sn-sf-low {
  order: -1;
}
/* No short description (card_short off), on the grid and the list alike. */
html[data-card-short='off'] .sn-sf-card__short,
html[data-card-short='off'] .sn-sf-row__short {
  display: none;
}
/* Up to three "Label: value" lines (card_attrs - drawn by the server only when it is on). */
.sn-sf-specline {
  margin: 0;
  padding: 0;
  list-style: none;
  color: var(--c-ink);
  font-size: 0.8125rem;
  line-height: 1.4;
}
.sn-sf-specline li {
  overflow-wrap: anywhere;
}
.sn-sf-specline__label {
  color: var(--c-ink-muted);
}
@media (max-width: 40rem) {
  .sn-sf-specline {
    font-size: 0.75rem;
  }
}
/* The quick add's quantity box (card_qty): the one qty field the form posts, out of sight unless
   the shop shows it. Beside the button, which gives up the width. */
.sn-sf-quickadd__qty {
  display: none;
}
html[data-card-qty='on'] .sn-sf-quickadd__qty {
  display: inline-flex;
  flex: none;
}
html[data-card-qty='on'] .sn-sf-quickadd__qty input {
  width: 3.75rem;
}
.sn-sf-card__actions .sn-sf-quickadd,
.sn-sf-row__actions .sn-sf-quickadd {
  gap: 0.5rem;
}
.sn-sf-row__actions .sn-sf-quickadd:not(.sn-sf-quickadd--pick) {
  display: flex;
  align-items: center;
}
.sn-sf-card__actions .sn-sf-quickadd-btn {
  min-width: 0;
}
/* A 15rem card holds the box and "Kosárba teszem" side by side once the button's own padding
   gives a little. */
html[data-card-qty='on'] .sn-sf-card__actions .sn-sf-quickadd-btn {
  padding-inline: 0.5rem;
}
/* A phone's two-up card has no room for both on one line ("Kosárba teszem" would be cut to a
   few letters): the box takes the full width above the button. */
@media (max-width: 40rem) {
  html[data-card-qty='on'] .sn-sf-card__actions .sn-sf-quickadd {
    flex-wrap: wrap;
  }
  html[data-card-qty='on'] .sn-sf-card__actions .sn-sf-quickadd__qty {
    flex: 1 1 100%;
  }
  html[data-card-qty='on'] .sn-sf-card__actions .sn-sf-quickadd__qty input {
    width: 100%;
  }
}

/* Header: a short word beside each icon (the kit's nav labels, nav_labels). Only from 64rem:
   below that the bar's one row (shop, search, settings, wishlist, account, cart) has no
   room for "Kívánságlista" and the rest, so a narrower screen keeps the icons alone - every one
   of them still carries its own aria-label. */
.sn-sf-bar__label {
  display: none;
}
@media (min-width: 64rem) {
  html:not([data-mx-navlabels='off']) .sn-sf-bar__label {
    display: inline;
    font-size: 0.875rem;
    font-weight: 600;
    line-height: 1;
  }
  html:not([data-mx-navlabels='off']) .sn-sf-search__btn {
    width: auto;
    gap: 0.375rem;
    padding-inline: 0.625rem;
  }
  html:not([data-mx-navlabels='off']) .sn-sf-cart,
  html:not([data-mx-navlabels='off']) .sn-sf-account,
  html:not([data-mx-navlabels='off']) .sn-sf-wish-link {
    gap: 0.375rem;
    padding-inline: 0.625rem;
  }
}

/* The kit's look axes on the shop's own controls. Corners and line weight: the controls above
   read --mx-control-radius and --mx-stroke in place; the language pills are shared with the
   admin, so the shop's copy of them is set here. */
.sn-sf .sn-lang__item {
  border-width: var(--mx-stroke, 1px);
  border-radius: var(--mx-control-radius, 999px);
}
/* Icon line weight: CSS beats the stroke-width attribute icon() writes. */
.sn-ico {
  stroke-width: var(--mx-icon-stroke, 1.7);
}
/* Outlined primary buttons: the accent as border and text on the page (--c-accent is already made
   readable on the shop's own canvas, lib/accent.mjs), filled on hover and focus. */
html[data-mx-buttons='outline'] .sn-sf-btn--primary:not(.is-added) {
  border-color: var(--c-accent);
  background: none;
  color: var(--c-accent);
}
html[data-mx-buttons='outline'] .sn-sf-btn--primary:not(.is-added, :disabled):hover,
html[data-mx-buttons='outline'] .sn-sf-btn--primary:not(.is-added):focus-visible {
  background: var(--c-accent);
  color: var(--c-accent-ink);
}
/* The accent only when pointed at: neutral ink on the surface until then. */
html[data-mx-accent='hover'] .sn-sf-btn--primary:not(:hover, :focus-visible, .is-added) {
  border-color: var(--c-ink);
  background: var(--c-surface);
  color: var(--c-ink);
}
/* No counts beside the filter choices. */
html[data-mx-counts='off'] .sn-sf-filters__count {
  display: none;
}
/* The fonts (font_body, font_heading): the kit's two custom properties, set on :root only for a
   font other than the kit's own, so a shop that chose none reads exactly as before. The shop only,
   never the admin. */
.sn-sf {
  font-family: var(--mx-font-text, var(--font-sans));
}
.sn-sf :is(h1, h2, h3, h4),
.sn-sf .sn-sf-shop,
.sn-sf .sn-sf-foot__shop {
  font-family: var(--mx-font-display, inherit);
}

/* ── A shop's legal pages (views/legal.mjs, 2026-11-02) ─────────────────────────────────────────
   The seller's facts as labelled rows, the template's lists, the model withdrawal form in a box of its own (it is
   meant to be copied out whole), and the line that says whose template it is, quiet, at the foot. */
/* No margin of its own: inside the prose column its rhythm (.sn-sf-prose__body > * + *) spaces it, as it does a
   paragraph - a reset here would win on order and glue the list to its heading. */
.sn-legal__facts {
  display: grid;
  gap: 0.375rem;
}
.sn-legal__fact {
  display: grid;
  grid-template-columns: minmax(0, 12rem) minmax(0, 1fr);
  gap: 0.25rem 1rem;
}
.sn-legal__fact dt {
  color: var(--c-ink-muted);
}
.sn-legal__fact dd {
  margin: 0;
  overflow-wrap: anywhere;
}
@media (max-width: 36rem) {
  .sn-legal__fact {
    grid-template-columns: minmax(0, 1fr);
    gap: 0;
  }
}
.sn-legal__list {
  padding-inline-start: 1.25rem;
}
.sn-legal__list > li + li {
  margin-top: 0.5rem;
}
.sn-legal__form {
  margin-top: 2rem;
  padding: 1.25rem;
  border: 1px solid var(--c-line-strong);
  border-radius: var(--radius-md);
}
.sn-legal__form > * {
  margin: 0;
}
.sn-legal__form > * + * {
  margin-top: 0.75rem;
}
.sn-sf-prose__body .sn-legal__form > h2 {
  margin-top: 0;
}
.sn-legal__formline {
  padding-bottom: 1.25rem;
  border-bottom: 1px dashed var(--c-line-field);
}
.sn-legal__star {
  font-size: 0.875rem;
  color: var(--c-ink-subtle);
}
.sn-legal__mark {
  margin: 2.5rem 0 0;
  padding-top: 1rem;
  border-top: 1px solid var(--c-line);
  font-size: 0.875rem;
  color: var(--c-ink-subtle);
}
/* The admin's own: the facts that live on another settings page, and the template-or-own choice. */
.sn-legal__facts--admin {
  margin: 1.25rem 0 0;
  padding-top: 1rem;
  border-top: 1px solid var(--c-line);
  font-size: 0.875rem;
}
.sn-legal__facts--admin .sn-legal__fact {
  grid-template-columns: minmax(0, 10rem) minmax(0, 1fr);
}
.sn-legal__none {
  color: var(--c-ink-subtle);
}
.sn-legal__choice {
  margin: 0;
  padding: 0;
  border: 0;
  min-width: 0;
}
.sn-legal__choice > legend {
  margin-bottom: 0.375rem;
  padding: 0;
}
.sn-legal__choice-hint {
  margin: -0.25rem 0 0.375rem;
  padding-inline-start: 1.75rem;
}

/* ── Night pass (2026-11-03): phone, tablet and desktop tidy-up ──────────────────────────────────
 * Chris's iPhone screenshot of Analytics (390px): a period label floating left of chips in two rows,
 * date fields wider than their card, a 4,800px page of stacked label/value lines. */

/* "Other days": the two dates, folded away until asked for. */
.sn-an-custom {
  margin-top: 0.25rem;
}
.sn-an-custom > summary {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  min-height: 2.75rem;
  cursor: pointer;
  list-style: none;
  font-weight: 500;
  color: var(--c-ink-muted);
}
.sn-an-custom > summary::-webkit-details-marker {
  display: none;
}
.sn-an-custom > summary::after {
  content: '';
  inline-size: 0.4375rem;
  block-size: 0.4375rem;
  border-inline-end: 1.5px solid var(--c-ink-subtle);
  border-block-end: 1.5px solid var(--c-ink-subtle);
  transform: translateY(-2px) rotate(45deg);
  transition: transform 0.15s ease;
}
.sn-an-custom[open] > summary::after {
  transform: translateY(1px) rotate(-135deg);
}
.sn-an-custom > summary:hover {
  color: var(--c-ink);
}
.sn-an-custom > summary:focus-visible {
  outline: 2px solid var(--c-accent);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}
.sn-an-custom .sn-range {
  display: grid;
  justify-items: start;
  gap: 0.75rem;
  margin: 0.25rem 0 0;
}
.sn-an-custom .sn-range > .mx-fields {
  inline-size: 100%;
  max-inline-size: 26rem;
}
@media (max-width: 40rem) {
  .sn-an-custom .sn-range > .mx-btn {
    inline-size: 100%;
  }
}

/* The headline figures as tiles: two across on a phone, one row from a tablet. */
.sn-tiles {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 0.625rem;
}
@media (min-width: 48rem) {
  .sn-tiles {
    grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  }
}
.sn-tiles > .mx-stat {
  min-width: 0;
  gap: 0.375rem;
  padding: 0.875rem 1rem;
  border: 1px solid var(--c-line);
  border-radius: var(--radius-md);
  background: var(--c-surface-sunk);
}
.sn-tiles .mx-stat__label {
  order: -1;
}
.sn-tiles .mx-stat__value {
  font-size: clamp(1.25rem, 4.5vw, 1.625rem);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
  overflow-wrap: anywhere;
}
.sn-tiles + .mx-hint {
  margin-top: 0.875rem;
}

/* The two charts side by side once there is room for both. */
.sn-an-charts {
  display: grid;
  gap: 0.875rem;
}
.sn-an-charts > .mx-panel {
  margin: 0;
  min-width: 0;
}
@media (min-width: 64rem) {
  .sn-an-charts {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* A figure list as a table that still fits a phone: the figures side by side under the name, a small
   label over each value; from 40rem the figures sit at the row's end, and only the first row keeps
   its labels on screen (the others keep them for a screen reader), so they read as column heads. */
.sn-rows--grid .sn-row {
  gap: 0.375rem;
  padding: 0.625rem 1rem;
}
.sn-rows--grid .sn-row__figs {
  margin: 0;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  gap: 0.75rem;
}
.sn-rows--grid .sn-pair {
  display: grid;
  justify-content: start;
  gap: 0;
}
.sn-rows--grid .sn-pair dt {
  font-size: 0.75rem;
}
.sn-rows--grid .sn-pair dd {
  font-weight: 600;
}
@media (min-width: 40rem) {
  .sn-rows--grid .sn-row {
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    column-gap: 1.5rem;
  }
  .sn-rows--grid .sn-row__figs {
    grid-auto-columns: 7.5rem;
  }
  /* justify-content too: a row whose label is hidden has only its value in the pair, and a track packed at the
     start put every value after the first row left of its column head. */
  .sn-rows--grid .sn-pair {
    justify-content: end;
    justify-items: end;
    text-align: end;
  }
  .sn-rows--grid .sn-row + .sn-row .sn-pair dt {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    overflow: hidden;
    clip-path: inset(50%);
  }
  .sn-rows--grid .sn-row + .sn-row {
    padding-block: 0.5rem;
  }
}
.mx-panel__body > .sn-tiles {
  margin: 0;
}
/* Three tiles: the first (revenue) takes the row, the other two share the next. */
.sn-tiles > .mx-stat:first-child:nth-last-child(odd) {
  grid-column: 1 / -1;
}
@media (min-width: 48rem) {
  .sn-tiles > .mx-stat:first-child:nth-last-child(odd) {
    grid-column: auto;
  }
}

/* A list's filter on a phone: the search across, the choices two to a row - the four stacked fields
   took the whole first screen of Products before a single product. */
@media (max-width: 47.99rem) {
  .sn-filter .mx-fields {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.75rem;
  }
  .sn-filter .mx-fields > .mx-field:first-child {
    grid-column: 1 / -1;
  }
  .sn-filter .mx-fields > .mx-field:last-child:nth-child(even) {
    grid-column: 1 / -1;
  }
}

/* ── Night pass, the shop (2026-11-03) ───────────────────────────────────────────────────────── */

/* The header's icon boxes: a hairline, not a dark frame round each of four icons - the shop's name and
   its products are what the eye should find first. The owner's stroke width (Appearance > Style) still applies. */
.sn-sf-bar :is(.sn-sf-cart, .sn-sf-account, .sn-sf-wish-link, .sn-sf-search__btn) {
  border-color: var(--sn-line);
  transition: border-color 0.15s ease, background-color 0.15s ease, color 0.15s ease;
}
.sn-sf-bar :is(.sn-sf-cart, .sn-sf-account, .sn-sf-wish-link, .sn-sf-search__btn):hover {
  background: var(--c-surface-2);
}

/* A phone's catalogue: Filters, the sort and the grid/list switch on ONE row above the products, where
   they took two (the sort's own label stays for a screen reader; the select already says what it sorts by). */
@media (max-width: 40rem) {
  .sn-sf-catalog__main:has(> .sn-sf-filters-btn + .sn-sf-toolbar) {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    column-gap: 0.5rem;
    align-items: start;
  }
  .sn-sf-catalog__main:has(> .sn-sf-filters-btn + .sn-sf-toolbar) > * {
    grid-column: 1 / -1;
  }
  .sn-sf-catalog__main:has(> .sn-sf-filters-btn + .sn-sf-toolbar) > .sn-sf-filters-btn {
    grid-column: 1;
  }
  .sn-sf-catalog__main:has(> .sn-sf-filters-btn + .sn-sf-toolbar) > .sn-sf-filters-btn + .sn-sf-toolbar {
    grid-column: 2;
    flex-wrap: nowrap;
    justify-content: flex-end;
    gap: 0.5rem;
    min-width: 0;
  }
  .sn-sf-filters-btn + .sn-sf-toolbar .sn-sf-sort {
    flex: 1 1 auto;
    flex-wrap: nowrap;
    min-width: 0;
  }
  .sn-sf-filters-btn + .sn-sf-toolbar .sn-sf-sort__label {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    overflow: hidden;
    clip-path: inset(50%);
  }
  .sn-sf-filters-btn + .sn-sf-toolbar .sn-sf-sort__select {
    inline-size: 100%;
    min-inline-size: 0;
  }
}

/* Phones: the demo's banner and the editing sheets give the page its width back. */
@media (max-width: 40rem) {
  .sn-demo-notice {
    padding: 0.75rem 0.875rem;
  }
  .sn-demo-notice__text {
    font-size: 0.875rem;
  }
  .sn-sheet__head {
    padding: 0.75rem 1rem;
  }
  .sn-sheet__body {
    padding: 1rem;
  }
  .sn-sheet__foot {
    padding: 0.75rem 1rem;
  }
}

/* The phone's buy bar slides away while the page's own buy row is in view (static/shop.js), and the
   toast it lifted comes back down with it. */
@media (max-width: 47.99rem) {
  .sn-sf-sticky {
    transition: transform 0.2s var(--ease-out-soft, ease), visibility 0.2s;
  }
  .sn-sf-sticky.is-away {
    transform: translateY(100%);
    visibility: hidden;
  }
  body:has(.sn-sf-sticky.is-away) .sn-sf-toast {
    bottom: 0.75rem;
  }
}
@media (prefers-reduced-motion: reduce) {
  .sn-sf-sticky {
    transition: none;
  }
}
/* The product page's rating is a 44px link already: no extra air under it before the price. */
.sn-sf-panel > .sn-sf-rating {
  margin-bottom: 0;
}

/* The catalogue's filter list with a mouse: 32px rows, not the 44px-plus-gap a finger needs (52px a row made
   five materials a screen of air). A touch screen keeps the kit's 44px. */
@media (hover: hover) and (pointer: fine) {
  .sn-sf-filters__list {
    gap: 0;
  }
  .sn-sf-filters .mx-check {
    min-height: 2rem;
  }
}

/* A product row on a phone (the kit's stacked table): the item number goes UNDER the name. The kit sets a cell's
   .sub inline beside its link, which next to a name that wraps ran into the name's second line. */
.sn-rowname__text {
  display: grid;
  justify-items: start;
}
.mx-table .sn-rowname__text .sub {
  display: block;
  margin: 2px 0 0;
}

/* A segmented control of two or three words (Plan's Monthly / Yearly): as wide as its choices, not the page. */
.sn-seg-fit {
  inline-size: fit-content;
}

/* The product page's heart and compare beside Add to cart: the buy row's own 44px, on its line - they stood 36px
   tall and 4px higher than the quantity and the button. */
.sn-sf-buycluster {
  align-items: center;
}
.sn-sf-buycluster :is(.sn-sf-wish-btn, .sn-sf-compare-btn) {
  inline-size: 2.75rem;
  block-size: 2.75rem;
}
.sn-sf-buycluster > :is(form, .sn-sf-wish) {
  display: flex;
}
.sn-sf-buycluster > .sn-sf-buy {
  margin-block: 0;
}

/* A shop with nothing up for sale yet: the parcel drawing over the sentence, centred, with room round it. */
.sn-sf-shelf-empty {
  display: grid;
  justify-items: center;
  gap: 1rem;
  padding: clamp(2rem, 8vw, 4rem) 1rem;
  border: 1px dashed var(--c-line-strong);
  border-radius: var(--radius-lg);
  text-align: center;
}
.sn-sf-shelf-empty__icon {
  display: grid;
  place-items: center;
  inline-size: 4.5rem;
  block-size: 4.5rem;
  border-radius: 999px;
  background: var(--c-surface-sunk);
  color: var(--c-ink-subtle);
}
.sn-sf-shelf-empty__icon svg {
  inline-size: 2.25rem;
  block-size: 2.25rem;
}
.sn-sf-shelf-empty .sn-sf-lead {
  max-inline-size: 28rem;
  margin: 0;
}

/* Team's Members / Activity: the kit's segmented control, an icon beside each word. */
.sn-team-tabs > a {
  gap: 0.375rem;
}
.sn-team-tabs > a > svg {
  inline-size: 1rem;
  block-size: 1rem;
  flex: none;
}
.sn-team-tabs {
  margin-bottom: 1rem;
}

/* Appearance > Catalog > Card button (lib/shop-design.mjs card_btn; night pass 2026-11-03). Filled is today's look
   and every existing shop's; a new shop starts on outline. Icon only draws a 44px button at the card's end, and the
   words stay in it for a screen reader. */
.sn-sf-quickadd__icon {
  display: none;
  inline-size: 1.25rem;
  block-size: 1.25rem;
  flex: none;
}
html[data-card-btn='outline'] .sn-sf-card__actions .sn-sf-quickadd-btn {
  border-color: var(--c-line-strong);
  background: none;
  color: var(--c-ink);
}
html[data-card-btn='outline'] .sn-sf-card__actions .sn-sf-quickadd-btn:hover {
  border-color: var(--c-accent);
  color: var(--c-accent);
}
html[data-card-btn='icon'] .sn-sf-card__actions,
html[data-card-btn='icon'] .sn-sf-card__actions .sn-sf-quickadd {
  display: flex;
  justify-content: flex-end;
}
html[data-card-btn='icon'] .sn-sf-card__actions :is(.sn-sf-quickadd-btn, .sn-sf-quickadd--pick) {
  inline-size: 2.75rem;
  min-inline-size: 2.75rem;
  block-size: 2.75rem;
  padding: 0;
  flex: none;
}
html[data-card-btn='icon'] .sn-sf-card__actions .sn-sf-quickadd__icon {
  display: block;
}
html[data-card-btn='icon'] .sn-sf-card__actions .sn-sf-quickadd__word {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  overflow: hidden;
  clip-path: inset(50%);
}
/* Icon only: the button sits on the price's line at the card's corner, so the card keeps no empty row under it. */
html[data-card-btn='icon'] .sn-sf-cell > .sn-sf-card__actions {
  position: absolute;
  inset-inline-end: calc(0.875rem + var(--sn-card-pad, 0px));
  inset-block-end: 0.625rem;
  z-index: 1;
  padding: 0;
}
html[data-card-btn='icon'] .sn-sf-cell:has(> .sn-sf-card__actions) .sn-sf-card__foot {
  min-height: 2.25rem;
  justify-content: center;
  padding-inline-end: 3.25rem;
}

/* The shopper's sign-in from a tablet up: a card of its own, its one button across it, not a form floating in a wide
   page. A phone keeps it flush with the page. */
@media (min-width: 40rem) {
  .sn-sf-account-form {
    padding: 1.5rem;
    border: 1px solid var(--c-line);
    border-radius: var(--radius-lg);
    background: var(--c-surface);
  }
  .sn-sf-account-form .sn-sf-btn {
    justify-self: stretch;
  }
}

/* ── ONE EDGE COLOUR IN THE SHOP (Chris, 2026-11-04) ─────────────────────────────────────────────────────────
   "the border colors for the buttons, search field and active menu item (and everything else) should be the same
   (the one we use for the buttons on top right)". That is the header's hairline, --c-line, in both themes. --sn-line
   is the shop's one edge, and the kit's two darker edges are pointed at it here, so every kit control (fields,
   selects, ghost buttons, chips, tables, the cog in its shadow root) and every sn-sf rule draws the same line without
   each one naming it. Declared again on the phone sheet: the kit re-points the lines there for its own surface, and
   the alias has to follow. Hover, the accent and the focus ring are states and keep their own colours. The shop's
   field edges are under the kit's 3:1 (tokens.css, 2026-11-02) by this - Chris's call. The admin keeps the kit's. */
.sn-sf,
.sn-sf .mx-sheet[data-mx-sheet-on] {
  --sn-line: var(--c-line);
  --c-line-strong: var(--sn-line);
  --c-line-field: var(--sn-line);
}

/* ── EVERY DROPDOWN'S CHEVRON SITS IN FROM THE EDGE (Chris, 2026-11-04) ──────────────────────────────────────
   "the little arrow in the dropdown(s) they are too close to the right edge". The kit draws its chevron only on
   .mx-field select, select.mx-input and .mx-sortbar select; any other select (the shop's sort) kept the browser's
   arrow, which sits on the border and which padding cannot move. So every single-line select, shop and admin, draws
   the kit's chevron 0.75rem in from the end (--mx-end: the left in Arabic) and keeps 2.25rem at that end, so its
   words stop before the arrow. :root lifts it over a product rule's `background` or `padding` shorthand. */
:root select:not([multiple], [size]) {
  -webkit-appearance: none;
  appearance: none;
  padding-inline-end: 2.25rem;
  background-image: var(--select-chevron);
  background-repeat: no-repeat;
  background-position: var(--mx-end, right) 0.75rem center;
  background-size: 12px 12px;
}

/* Import phase 3b: a customer's notes from a customers file keep their own line breaks. */
.sn-notes {
  white-space: pre-line;
}

/* Import and export's tabs on a phone: an even two-column grid, not rows of 2, 3 and 1. */
@media (max-width: 40rem) {
  .sn-transfer-tabs {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    inline-size: auto;
  }
}

/* editor.css — the Appearance editor's own frame (views/appearance.mjs). Full bleed: a bar on top,
   the rail of sections on the left, the shop in a frame filling the rest. Everything in it is the
   kit's furniture (.mx-btn, .mx-choice, .mx-check, .mx-field, .mx-hint, .mx-badge); this sheet
   only lays the three parts out and draws the few pieces the kit has no class for (the section
   list, the layout tiles, the Desktop | Mobile switch). No colour is named here - only tokens. */

.sn-ed-page {
  margin: 0;
  background: var(--c-canvas);
  color: var(--c-ink);
}
.sn-ed {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  height: 100dvh;
  overflow: hidden;
}

/* ── The bar ───────────────────────────────────────────────────────────────── */
.sn-ed-bar {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  min-width: 0;
  padding: 0.5rem 0.75rem;
  border-bottom: 1px solid var(--c-line);
  background: var(--c-surface);
}
.sn-ed-close {
  flex: 0 0 auto;
  width: 2.5rem;
  padding: 0;
}
.sn-ed-close svg,
.sn-ed-title__icon svg,
.sn-ed-device svg,
.sn-ed-back svg,
.sn-ed-open__icon svg {
  width: 1.125rem;
  height: 1.125rem;
}
.sn-ed-title {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  min-width: 0;
  flex: 0 1 auto;
}
.sn-ed-title__icon {
  display: inline-flex;
  color: var(--c-accent);
}
.sn-ed-title__name {
  margin: 0;
  font-size: 1rem;
  font-weight: 600;
  line-height: 1.2;
}
.sn-ed-title__shop {
  min-width: 0;
  overflow: hidden;
  color: var(--c-ink-muted);
  font-size: 0.875rem;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sn-ed-devices {
  display: inline-flex;
  flex: 0 0 auto;
  gap: 0.125rem;
  margin-inline: auto;
  padding: 0.1875rem;
  border: 1px solid var(--c-line);
  border-radius: var(--radius-md);
  background: var(--c-surface-2);
}
.sn-ed-device {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  min-height: 2.125rem;
  padding: 0.25rem 0.75rem;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--c-ink-muted);
  font: inherit;
  font-size: 0.875rem;
  cursor: pointer;
}
.sn-ed-device[aria-pressed='true'] {
  background: var(--c-surface);
  color: var(--c-ink);
  outline: 1px solid var(--c-line-strong);
}
.sn-ed-actions {
  display: flex;
  flex: 0 0 auto;
  align-items: center;
  gap: 0.5rem;
}
.sn-ed-status {
  color: var(--c-ink-muted);
  font-size: 0.8125rem;
}
.sn-ed-status[data-kind='error'] {
  color: var(--c-rec);
}

/* ── The rail ──────────────────────────────────────────────────────────────── */
.sn-ed-body {
  display: grid;
  grid-template-columns: minmax(18rem, 23rem) minmax(0, 1fr);
  min-height: 0;
}
.sn-ed-rail {
  min-height: 0;
  overflow-y: auto;
  border-inline-end: 1px solid var(--c-line);
  background: var(--c-surface);
}
.sn-ed-rail__bar {
  display: none;
}
.sn-ed-rail__scroll {
  display: grid;
  align-content: start;
  gap: 1rem;
  padding: 1rem;
}
.sn-ed-list ul {
  display: grid;
  gap: 0.25rem;
  margin: 0;
  padding: 0;
  list-style: none;
}
.sn-ed-open {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  width: 100%;
  min-height: 3.25rem;
  padding: 0.625rem 0.75rem;
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  background: none;
  color: var(--c-ink);
  font: inherit;
  text-align: start;
  cursor: pointer;
}
.sn-ed-open:hover {
  border-color: var(--c-line);
  background: var(--c-surface-2);
}
.sn-ed-open__icon {
  display: inline-flex;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: var(--radius-sm);
  background: var(--c-accent-soft);
  color: var(--c-accent);
}
.sn-ed-open__text {
  display: grid;
  flex: 1 1 auto;
  gap: 0.125rem;
  min-width: 0;
}
.sn-ed-open__name {
  font-weight: 600;
}
.sn-ed-open__hint {
  color: var(--c-ink-subtle);
  font-size: 0.8125rem;
  line-height: 1.35;
}
.sn-ed-chev {
  flex: 0 0 auto;
  width: 1rem;
  height: 1rem;
  color: var(--c-ink-subtle);
}
.sn-ed-section {
  display: grid;
  gap: 1rem;
  min-width: 0;
}
.sn-ed-back {
  display: inline-flex;
  align-items: center;
  justify-self: start;
  gap: 0.25rem;
  min-height: 2.25rem;
  margin: 0 0 -0.5rem -0.375rem;
  padding: 0.25rem 0.5rem 0.25rem 0.25rem;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--c-ink-muted);
  font: inherit;
  font-size: 0.875rem;
  cursor: pointer;
}
.sn-ed-back:hover {
  background: var(--c-surface-2);
  color: var(--c-ink);
}
.sn-ed-h {
  margin: 0;
  font-size: 1.25rem;
  font-weight: 650;
  line-height: 1.2;
}
.sn-ed-h:focus {
  outline: none;
}
.sn-ed-sub {
  margin: 0.25rem 0 -0.25rem;
  padding-top: 1rem;
  border-top: 1px solid var(--c-line);
  font-size: 0.9375rem;
  font-weight: 600;
}
.sn-ed-section > .sn-ed-sub:first-of-type {
  padding-top: 0;
  border-top: 0;
}
.sn-ed-field {
  margin: 0;
}
.sn-ed-field[hidden] {
  display: none;
}
.sn-ed-field > .mx-choice,
.sn-ed-field .mx-field {
  margin: 0;
}
.sn-ed-desk {
  margin-inline-start: 0.375rem;
  vertical-align: middle;
  text-transform: none;
  letter-spacing: normal;
}
/* What a phone never shows is quieter while the Mobile preview is on - it still saves. */
.sn-ed[data-device='mobile'] [data-desktop-only] {
  opacity: 0.55;
}

/* A row of short choices, side by side, wrapping when a word is long. */
.sn-ed-seg {
  grid-template-columns: repeat(auto-fit, minmax(5.5rem, 1fr));
}
.sn-ed-seg > .mx-check {
  min-height: 2.5rem;
  padding: 0.375rem 0.625rem;
  font-size: 0.875rem;
}
.sn-ed-seg > .mx-hint {
  grid-column: 1 / -1;
}

/* The layout presets: a drawing, a name and a line each. */
.sn-ed-tiles {
  grid-template-columns: repeat(auto-fit, minmax(6.25rem, 1fr));
}
.sn-ed-tile {
  position: relative;
  display: grid;
  align-content: start;
  gap: 0.375rem;
  padding: 0.625rem;
  border: 1px solid var(--c-line);
  border-radius: var(--radius-md);
  background: var(--c-surface-2);
  color: var(--c-ink);
  cursor: pointer;
}
.sn-ed-tile input {
  position: absolute;
  inset: 0;
  margin: 0;
  opacity: 0;
  cursor: pointer;
}
.sn-ed-tile:has(input:checked) {
  border-color: var(--c-accent);
  outline: 1px solid var(--c-accent);
}
.sn-ed-tile:has(input:focus-visible) {
  outline: 2px solid var(--c-accent);
  outline-offset: 2px;
}
.sn-ed-tile svg {
  display: block;
  width: 100%;
  height: auto;
  color: var(--c-ink-muted);
}
.sn-ed-tile:has(input:checked) svg {
  color: var(--c-accent);
}
.sn-ed-tile__name {
  font-size: 0.875rem;
  font-weight: 600;
  line-height: 1.25;
}
.sn-ed-tile__hint {
  color: var(--c-ink-subtle);
  font-size: 0.75rem;
  line-height: 1.35;
}

/* The sliders are the kit's .mx-range (kit/range.ts says what each value means beside it). */

/* A font's type style, under its picker: weight, size, then italic and capitals side by side - one
   quiet box, so it reads as belonging to the font above it. */
.sn-ed-type {
  display: grid;
  gap: 0.75rem;
  margin-top: -0.375rem;
  padding: 0.75rem;
  border: 1px solid var(--c-line);
  border-radius: var(--radius-md);
  background: var(--c-surface-2);
}
.sn-ed-type__flags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 1.25rem;
}
.sn-ed-type__flags > .sn-ed-field[hidden] {
  display: none;
}

/* One theme's colours and strength for the animation, spaced like the rail's other fields. */
.sn-ed-theme-row {
  display: grid;
  gap: inherit;
}
.sn-ed-theme-row[hidden] {
  display: none;
}
/* An animation's colours: the page colour, then each colour it shows, as small colour boxes. */
.sn-ed-colours {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 0.375rem;
}
.sn-ed-colours input[type='color'] {
  width: 2.5rem;
  height: 2.5rem;
  padding: 0.125rem;
  border: 1px solid var(--c-line-field);
  border-radius: var(--radius-sm);
  background: var(--c-surface);
  cursor: pointer;
}
.sn-ed-colours input[type='color'][hidden] {
  display: none;
}

/* ── The stage ─────────────────────────────────────────────────────────────── */
.sn-ed-stage {
  display: grid;
  min-width: 0;
  min-height: 0;
  overflow: auto;
  background: var(--c-surface-2);
}
.sn-ed-frame {
  width: 100%;
  height: 100%;
  min-height: 0;
}
/* A real phone-wide frame - the shop's own media queries answer, nothing is zoomed. 390px is the
   phone's own CSS width, so it stays px whatever the interface size. */
.sn-ed-frame[data-device='mobile'] {
  justify-self: center;
  width: 390px;
  max-width: 100%;
  height: calc(100% - 2rem);
  margin-block: 1rem;
  overflow: hidden;
  border: 1px solid var(--c-line-strong);
  border-radius: var(--radius-xl);
}
.sn-ed-frame iframe {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
  background: var(--c-canvas);
}

/* ── On a phone: the shop fills the screen and the rail is the kit's bottom sheet over it ──── */
@media (max-width: 720px) {
  .sn-ed-bar {
    gap: 0.5rem;
    padding: 0.375rem 0.5rem;
  }
  .sn-ed-devices,
  .sn-ed-title__shop,
  .sn-ed-title__icon,
  .sn-ed-status {
    display: none;
  }
  .sn-ed-actions {
    margin-inline-start: auto;
  }
  .sn-ed-body {
    grid-template-columns: minmax(0, 1fr);
  }
  .sn-ed-rail {
    border-inline-end: 0;
  }
  .sn-ed-rail__bar {
    display: block;
    padding: 0.25rem 1rem 0.5rem;
    font-weight: 600;
  }
  .sn-ed-rail__scroll {
    padding-top: 0.25rem;
  }
  /* The frame ends where the sheet begins; drag the sheet down to see the whole shop. */
  .sn-ed-stage {
    padding-bottom: var(--mx-sheet-h, 45dvh);
  }
  /* The kit pads the body for its sheet; this page is exactly one screen and never scrolls. */
  :root[data-mx-sheet-shown] body.sn-ed-page {
    padding-bottom: 0;
  }
}
