/* Public-site design tokens.
 *
 * WHY THIS FILE EXISTS, AND WHY IT IS NOT modules/portal-shell/tokens.css.
 * The repo rule is that a new CSS custom property lands in tokens.css in the
 * same commit as its first var() usage, and that a var() without a fallback
 * fails silently at render time. Both halves of that rule aim at one thing:
 * the property must be DEFINED wherever it is USED.
 *
 * The only tokens.css that existed was modules/portal-shell/tokens.css, and no
 * public page loads it - it belongs to the portal shell. Defining hub tokens
 * there would have satisfied the filename and broken the purpose: every var()
 * on the public site would resolve to nothing, silently, which is the exact
 * failure the rule was written to prevent. A dead token is a second truth that
 * never even gets read.
 *
 * The other candidate was assets/css/<slug>-hub.css, which the site-router
 * does serve. That file is per tenant - one copy per customer, named after the
 * customer - so a shared material defined there would exist for operakliniken
 * and be absent for everyone else. Also a second truth, and a silent failure
 * for the second site that used the hero.
 *
 * So the tokens live here, in a file that is shared by construction (no slug
 * in its name), and they reach the page the way the site-router already moves
 * CSS: imported as text into the Worker bundle by a wrangler rule that treats
 * every CSS file as Text, registered in BUNDLED_ASSETS under a slug-less key,
 * and linked by the renderer on pages that read them.
 *
 * (That wrangler glob is written out in words rather than as a pattern on
 * purpose: the pattern contains a star-slash, which ends a CSS comment. An
 * earlier revision of this header pasted it verbatim, the comment closed
 * eleven lines early, the rest of the header parsed as garbage and took the
 * :root block with it. Every token silently resolved to nothing, and the hero
 * rendered with dark text on a dark panel.)
 *
 * ONE TRUTH. These values exist here and nowhere else: fourteen for the dark
 * glass, eleven for the light cover panel. The renderer
 * carries no copies and no equal-value var() fallbacks; the contrast audit in
 * scripts/dev/hero-v2-contrast-check.mjs parses this file rather than
 * restating it. A test asserts that every --hero- property the rendered hero
 * references is defined either here or on the section element itself, so a
 * token can never go missing quietly.
 *
 * WHAT IS AND IS NOT A TOKEN HERE. These describe the material, which is the
 * same on every site. Anything a customer chooses - which image, which overlay
 * colour, how strong, how blurred - is row data, resolved by
 * templates/sections/body-nav/hero-background.js and emitted as custom
 * properties on the section element. Putting either kind in the other's place
 * is the mistake this split exists to prevent.
 */

:root {
  /* ------------------------------------------------------------------ *
   * Hero v2 glass (docs/plans/hero-design-evolution.md)
   *
   * THE GLASS IS DARK, AND THAT IS THE WHOLE POINT. The obvious build of
   * this design is a white translucent panel, and that is what was written
   * first. Measured against the worst pixel an image can contain, SEVEN of
   * eleven text surfaces fell below 4.5:1 - with the backdrop-filter path
   * scoring worse than its own no-blur fallback, because a white veil raises
   * the very field the text has to beat, and the brighter the photograph the
   * worse it gets.
   *
   * A dark veil moves in the safe direction, and it makes readability a
   * property of the panel rather than of whichever image the customer
   * uploads next. That is what lets the image stay editable data.
   * hero-intro.js reached the same conclusion for the same reason: darkening
   * only, because darkening is the safe direction for contrast.
   * ------------------------------------------------------------------ */

  /* The copy block has no panel. It gets a soft radial niche instead, so it
     reads against a protected field without a hard-edged card appearing
     around it - hero-intro.js's pattern, for its documented reason: a linear
     gradient paints its whole rectangle and leaves a visible edge.

     0.34, not the 0.50 this started at. Measured against Session A's render
     on staging: overlay 0.62 plus scrim plus a 0.50 niche left only 12.4
     percent of the photograph showing over the copy, and the marble gallery
     stopped being a place and became a blue-grey field. The blur was not the
     cause - with the overlay switched off the scene reads clearly at 18px,
     retaining 37 percent of its unblurred edge energy. The veil was. */
  --hero-glass-copy-niche: rgba(8,16,24,0.34);

  /* The step panel. It carries body copy, so it needs the steadier field. */
  --hero-glass-rail-bg: rgba(18,26,36,0.55);
  /* Without backdrop-filter nothing softens the boundary, so the solid
     variant leans more opaque to still read as a deliberate surface. */
  --hero-glass-rail-bg-fallback: rgba(18,26,36,0.72);
  --hero-glass-rail-border: rgba(255,255,255,0.22);
  --hero-glass-rail-blur: 20px;
  --hero-glass-rail-shadow: 0 24px 70px -34px rgba(8,16,24,0.72);

  /* Controls sit ON the panel, so they lift rather than darken: a small lift
     is what makes a control read as raised, and the panel underneath has
     already bought the contrast headroom to afford it. */
  --hero-glass-bg: rgba(255,255,255,0.08);
  --hero-glass-bg-fallback: rgba(255,255,255,0.08);
  --hero-glass-bg-hover: rgba(255,255,255,0.16);
  --hero-glass-border: rgba(255,255,255,0.28);
  --hero-glass-blur: 12px;

  /* Type on glass. Full white for headings, measured steps down for body and
     for the faintest labels, so the hierarchy survives without any of them
     dropping below the contrast the panel guarantees. */
  --hero-glass-ink: #ffffff;
  --hero-glass-ink-muted: rgba(255,255,255,0.82);
  --hero-glass-ink-faint: rgba(255,255,255,0.68);

  /* ------------------------------------------------------------------ *
   * THE COVER PANEL. The second material in this file, and the only one
   * that is light.
   *
   * The four dark shapes put the copy on a dark veil over a photograph.
   * The cover shape puts it on ONE warm, light pane instead, so the room
   * around the pane stays a room instead of becoming a field. That makes
   * it a different material, not a variant of the one above: every ink on
   * it is dark, every control lifts with white rather than darkening, and
   * the alpha is chosen against a different worst case.
   *
   * THE WORST CASE IS A BLACK PHOTOGRAPH PIXEL, not a white one. A dark
   * ink on a translucent light pane loses contrast when the pixel behind
   * the pane is dark, and gains it when the pixel is light, which is the
   * opposite of the dark glass above. Composited source-over at alpha
   * 0.78 over a pure black pixel the panel's relative luminance is
   * 0.5423: 10.61:1 for --hero-cover-ink and 6.11:1 for
   * --hero-cover-ink-muted. Over a pure white pixel the same two are
   * 18.11:1 and 10.43:1. Both worst cases clear the 4.5:1 target, and the
   * binding one clears it with room, which is what lets the alpha stay
   * low enough for the photograph to arrive through the pane.
   *
   * WHY THE BASE IS A FIXED TRIPLE AND THE INKS ARE NOT. The numbers above
   * are a guarantee only while the surface they are measured on is the
   * same on every site, so the base is material here rather than row or
   * tenant data, which is the same argument the cover sheet already makes
   * for keeping the contrast out of the customer's overlay colour. The
   * inks and the accent ARE the site's own tokens, because a warm site and
   * a cool site should write in their own dark, and every value a tenant
   * can put there is a dark text colour by construction. The fallbacks are
   * the hub's own approved values, which is what a page without a tenant
   * sheet renders.
   *
   * The base triple is stored as channels rather than as a colour because
   * an alpha has to be applied to it in two places at two strengths, and
   * rgba() over a channel list is the one form that needs no color-mix():
   * a browser without color-mix() resolves the whole declaration to
   * nothing and paints a transparent panel, which is the exact failure the
   * contrast work above exists to prevent.
   * ------------------------------------------------------------------ */
  --hero-cover-base-rgb: 252,249,244;
  --hero-cover-bg: rgba(var(--hero-cover-base-rgb),0.78);
  /* Without backdrop-filter nothing softens the pane, so the solid variant
     leans denser: 14.30:1 and 8.23:1 against the same black pixel. */
  --hero-cover-bg-fallback: rgba(var(--hero-cover-base-rgb),0.9);
  --hero-cover-border: rgba(255,255,255,0.58);
  /* Controls sit ON the light pane, so they lift with white and outline
     with the ink's own hue at low alpha. */
  --hero-cover-control-bg: rgba(255,255,255,0.62);
  --hero-cover-control-border: rgba(74,58,48,0.22);
  --hero-cover-ink: var(--op-text-primary,#1a0f0a);
  --hero-cover-ink-muted: var(--op-text-secondary,#4a3a30);
  --hero-cover-accent: var(--op-accent,#6e5440);
  --hero-cover-accent-strong: var(--op-accent-strong,#4a3a30);
  --hero-cover-accent-ink: var(--op-text-on-accent,#ffffff);
  /* MOBILE VARIANT C (Stanley 2026-09-16, replacing variant B). Below desktop
     the photograph is a band across the top of the hero and the first screen's
     copy is written on that band, so that copy needs a light pair of inks and
     the photograph needs a scrim under them.

     THESE THREE ARE FIXED VALUES, not the site's own tokens, for the reason
     --hero-cover-base-rgb is: the contrast numbers below are a guarantee only
     while the surface they are measured on is the same on every site, and a
     tenant's idea of a light ink is not a surface. The pane's inks above stay
     tenant tokens because every value a tenant can put there is a dark text
     colour by construction; there is no such construction for this pair.

     THE SCRIM IS A GRADIENT, not a flat veil, because it is painted over the
     WHOLE backdrop layer. It has to be dense where the copy is - the top
     third of the hero - and gone by the time the guide's own pane takes over,
     so the photograph is still a room around that pane rather than a dimmed
     sheet. Where it does reach the pane it only moves the pixels behind it
     TOWARDS the black worst case the pane's own contrast is measured against
     above, never away from it, so the pane keeps the guarantee it has.

     THE PLATEAU IS NOT DECORATION. Variant B painted this over the whole hero,
     where the copy sat in the top fifth and the gradient's own falloff was
     never reached. Variant C paints it over a 300px BAND, and the copy sits
     in the middle of that band: with B's stops the same white headline
     measured 3.50:1 over the marble room, because a band-tall crop of that
     picture is a far brighter surface than a hero-tall one and the falloff
     now lands exactly where the type is. So the .66 is held to 78% and the
     fade is the last fifth, under the copy and above the pane.

     MEASURED at 390x844 on the three live pictures, against the LIGHTEST
     pixel in the text's own box (the worst case for a light ink, and a
     stricter reading than the average behind it):

       marble room at rest   headline 6.28:1   subline 4.94:1
       dermapen card         headline 6.71:1   subline 5.75:1
       hifu card             headline 7.85:1   subline 6.12:1

     Against the average pixel instead the same six are 10.84, 8.31, 7.94,
     7.34, 9.36 and 7.32. Target 4.5:1; the binding number is the marble room
     at rest, which is the only one of the three the receding blur does not
     also soften. */
  --hero-cover-ink-on-photo: #ffffff;
  --hero-cover-ink-on-photo-muted: rgba(255,255,255,.82);
  --hero-cover-photo-scrim: linear-gradient(180deg,rgba(20,14,10,.66) 0%,rgba(20,14,10,.66) 78%,rgba(20,14,10,.38) 92%,transparent 100%);
  /* HOW TALL THE BAND IS. Variant A's token, back with variant A's band, and
     it is a property rather than a number in the sheet because three rules
     read it, in four places: the section's own top padding, the band's
     height, and the box that carries the fold copy on it, which is both
     offset by the band and as tall as it. One value, and they cannot drift.

     MEASURED at 390: the fold copy is 157px tall (eyebrow, two headline lines
     and the subline, with their own margins), so the band needs 157 + 24 + 24
     = 205px to carry it with breathing room. 77vw is 300px there, which
     already clears that with 95px of photograph to spare, so the clamp is
     variant A's unchanged - it was raised by nothing because nothing was
     short. The fold box takes the band's full height and centres the copy in
     it, so the 24px is a floor the copy can never lose rather than an offset
     that has to be kept in step with the type. */
  --hero-cover-mobile-photo-band: clamp(260px,77vw,360px);
}

/* THE PLATE (docs/design/2026-10-11-dermashoppen-luxury-pass.md section 3,
   candidate B; refined by the design direction 2026-10-13 section 3.4): the
   porcelain surface a product stands on, drawn from the site's own tokens,
   no image and no colour value. Three layers: a white highlight at the upper
   left, a sandstone light from the upper left (the accent mixed into the
   surface at 15%), over a porcelain-to-floor gradient (the floor the accent
   at 7%). The contact shadow is a separate layer, because a plate with
   nothing on it (an empty state) must not show the shadow of an object that
   is not there: an ellipse of ink at 22% under the product's foot (a picture
   scaled by its fill stands on 87% of the plate height). The picture
   shares are the box a product takes on any plate (a card's, the product
   page's stage): 74% of its height, at most 82% of its width. Every
   reference falls back to a system colour, so a page without a tenant sheet
   still paints a light surface. */
:root {
  --op-plate-picture-height: 74%;
  --op-plate-picture-width: 82%;
  /* A picture's fill fact (product-card.js THE PICTURE'S FILL), printed per
     picture on the img itself; the defaults are the whole picture, so a
     fill rule without its four numbers lays the picture out as the plain
     contain box (round 3, D2-6). */
  --op-fill-x: 0;
  --op-fill-y: 0;
  --op-fill-w: 1;
  --op-fill-h: 1;
  --op-plate-base: var(--op-bg-surface, Canvas);
  --op-plate-light: color-mix(in oklab, var(--op-accent, CanvasText) 15%, var(--op-plate-base));
  --op-plate-floor: color-mix(in oklab, var(--op-accent, CanvasText) 7%, var(--op-plate-base));
  --op-plate-surface: radial-gradient(120% 90% at 26% 18%, var(--op-plate-base) 0%, transparent 55%), radial-gradient(140% 120% at 30% 10%, var(--op-plate-light) 0%, transparent 70%), linear-gradient(180deg, var(--op-porcelain, var(--op-plate-base)) 0%, var(--op-plate-floor) 100%);
  --op-plate-contact: radial-gradient(ellipse 24% 2.6% at 50% 88%, color-mix(in oklab, var(--op-text-primary, CanvasText) 22%, transparent), transparent 100%);
  /* A site's own texture on the plate, between the contact shadow and the
     surface (product-card.js .op-pcard__media). None by default; a site
     sets one image, drawn in alpha only so the palette keeps the colour. */
  --op-plate-texture: none;
}

/* THE SERIES INK (series pages C1-C3 and S5-6; workers/shared/site-series.js
   renderSeriesName). A series name's label is drawn in its series' ink:
   renderSeriesName sets this property on the name itself from the site's
   series list (text_color, refused below 4.5:1 on white). The default is the
   colour the name stands in, so a series without an ink keeps the text's
   own colour. */
:root {
  --op-series-ink: currentColor;
}

/* THE SERIES DOT (design pass 2 merge round, M2; sections/nav.js). A series
   row in the menu marks its series with its colour (site_data.series color,
   by the identity the row carries): the menu sets this property on the row
   itself. The default is the colour the row's text stands in. */
:root {
  --op-series-dot: currentColor;
}

/* THE PRODUCT CARD (luxury pass section 4; templates/product-card.js THE CARD
   ANATOMY; design direction 2026-10-13 section 3.4: a 4:5 plate). The plate's
   ratio, the share of its height a product takes (and the widest share, for
   a wide product): a picture with its fill fact (meta.image_fill) is scaled
   so its product's own box takes exactly these shares, one without it keeps
   them as its picture box. And the card's radius. */
:root {
  --op-card-plate-ratio: 4 / 5;
  --op-card-picture-height: var(--op-plate-picture-height, 74%);
  --op-card-picture-width: var(--op-plate-picture-width, 82%);
  --op-card-radius: var(--op-radius-xl, 18px);
}

/* THE COMPACT HERO'S PICTURE (templates/sections/hero-intro.js THE COMPACT
   SIZE; design direction 2026-10-13 block 6). Its ratio in a narrow hero (a
   wide band, so the first screen holds what follows) and beside the copy from
   the hero's 40rem container step (the photograph's own landscape ratio). */
:root {
  --op-hero-compact-ratio: 21 / 9;
  --op-hero-compact-ratio-wide: 16 / 9;
}

/* THE RELAXED START HERO'S PICTURE (hero-intro.js THE RELAXED LAYOUT, design
   pass 2 addition H2): 4:3 at the content's width on a phone, 3:2 in the
   larger column from the hero's 40rem step, and the focal point of its crop,
   the centre unless the row names one (content.image_focus, set on the
   picture itself). */
:root {
  --op-hero-relaxed-ratio: 4 / 3;
  --op-hero-relaxed-ratio-wide: 3 / 2;
  --op-hero-focus: 50% 50%;
}

/* THE SECTION RHYTHM (luxury pass section 2; design pass 2). The gap is the
   space a section keeps above and below its content: the tenant scale's step
   10 in a narrow section and 6rem from the panel step of the container scale
   (a section reads the wide value in its own @container rule). The hub sheets
   compress step 10 on a phone, so the rhythm tightens with it. The width a
   section's content stops at is the page grid below (--op-page-max), which
   replaced the section measure of design pass 1. */
:root {
  --op-section-gap: var(--op-space-10, 4rem);
  --op-section-gap-wide: 6rem;
}

/* THE DESIGN DIRECTION 2026-10-13 (docs/coordinator/second-opinions/
   2026-10-13-design-direction/README.md section 3), design pass 2. Platform
   names; every colour mixed from the palette roles the palette engine
   injects (--op-accent, --op-accent-strong, --op-bg-surface, --op-text-*),
   so no colour value is written here and each tenant's palette gives its
   own values. */

/* Colour: porcelain and sandstone, and the hairline as the only separator. */
:root {
  --op-porcelain: color-mix(in oklab, var(--op-accent, CanvasText) 4%, var(--op-bg-surface, Canvas));
  --op-porcelain-deep: color-mix(in oklab, var(--op-accent, CanvasText) 9%, var(--op-bg-surface, Canvas));
  --op-hairline: color-mix(in oklab, var(--op-text-primary, CanvasText) 11%, transparent);
  --op-hairline-strong: color-mix(in oklab, var(--op-text-primary, CanvasText) 22%, transparent);
  /* A form field's edge. An empty field is found by its boundary alone, so
     the boundary reaches 3:1 against the surface around it (WCAG 1.4.11,
     audit C8: the hairline border measured 1.46:1). The muted text role
     clears AA on every canvas, so as a line it clears 3:1; the lock screen's
     field (--op-lock-input-border) is the precedent. */
  --op-field-border: var(--op-text-muted, GrayText);
  --op-on-deep: var(--op-text-on-accent, Canvas);
  --op-on-deep-muted: color-mix(in oklab, var(--op-text-on-accent, Canvas) 74%, var(--op-accent-strong, CanvasText));
}

/* Type: one sans for the shop, the type set's voice (Newsreader italic) for
   the clinic. Sizes in the reading container's own units where they grow. */
:root {
  --op-type-display: clamp(2.125rem, 1.45rem + 2.6cqi, 3.25rem);
  --op-type-display-lh: 1.06;
  --op-type-title: clamp(1.75rem, 1.4rem + 1.2cqi, 2.25rem);
  --op-type-title-lh: 1.12;
  --op-type-heading: clamp(1.5rem, 1.3rem + 0.8cqi, 1.875rem);
  --op-type-heading-lh: 1.18;
  --op-type-subheading: 1.125rem;
  --op-type-row: 1.375rem;
  --op-type-lead: 1.125rem;
  --op-type-body: 1rem;
  --op-type-small: 0.875rem;
  --op-type-micro: 0.75rem;
  --op-type-voice: 1.1875rem;
  --op-type-voice-display: clamp(1.75rem, 1.3rem + 2cqi, 2.5rem);
  --op-type-voice-display-lh: 1.1;
  --op-type-price-large: 1.5rem;
  --op-tracking-display: -0.025em;
  --op-tracking-title: -0.018em;
  --op-tracking-micro: 0.14em;
  --op-leading-body: 1.6;
}

/* The page grid: one container for every section's content (and the
   footer's), a 16 px gutter in a narrow container and 40 px from the 40rem
   container step. */
:root {
  --op-page-max: 75rem;
  --op-gutter: 1rem;
  --op-gutter-wide: 2.5rem;
  --op-card-gap: 0.75rem;
  --op-card-gap-wide: 1.5rem;
  --op-card-pad: 0.875rem;
  --op-card-pad-wide: 1.25rem;
  /* The narrowest card in a narrow list: its insets, the buy door's word and
     the 44 px round routine door on one line at the reader's text size (rem,
     so a doubled text size doubles it and the list drops to one column). */
  --op-card-min-narrow: 10.75rem;
  /* The narrowest card in a wide list (four to a row at 1440), and in the
     guide's wide result, where a step's cards share a column with the step's
     name. */
  --op-card-min-wide: 15rem;
  --op-card-min-guide: 12rem;
  /* A card door's padding (block, inline), in rem so it grows with the text. */
  --op-card-door-pad: 0.5rem 0.625rem;
  /* A list row's height: a category row, a brand row (a door of its own). */
  --op-row-min: 64px;
  /* A product page fold row's height (design pass 2, product-folds.js). */
  --op-fold-row-min: 56px;
  --op-heading-gap: 2rem;
}

/* Shape and depth: one large radius for cards, plates and panels; fields and
   thumbs below it; one control height for page buttons (52 px on the product
   page); two elevations, at rest and lifted. */
:root {
  --op-radius-xl: 18px;
  --op-radius-field: 12px;
  --op-radius-thumb: 10px;
  --op-control-h: 48px;
  --op-control-h-large: 52px;
  --op-shadow-elev-1: 0 1px 2px color-mix(in oklab, var(--op-text-primary, CanvasText) 5%, transparent), 0 10px 24px -14px color-mix(in oklab, var(--op-text-primary, CanvasText) 18%, transparent);
  --op-shadow-elev-2: 0 2px 6px color-mix(in oklab, var(--op-text-primary, CanvasText) 6%, transparent), 0 22px 44px -18px color-mix(in oklab, var(--op-text-primary, CanvasText) 26%, transparent);
}

/* Shared routine material. Item count and measured geometry are row/layout data. */
:root {
  --layout-touch-min: 44px;
  --op-card-inset: 16px;
  --op-card-track-gap: 8px;
  --op-card-grid-gap: 16px;
  --op-card-min-inline: 220px;
  --op-card-media-size: 180px;
  --op-card-title-size: .9375rem;
  --op-retail-space-small: 4px;
  --op-retail-space-medium: 12px;
  --op-retail-radius-card: 12px;
  --op-retail-radius-control: 8px;
  --op-retail-text-small: .875rem;
  --op-shelf-host-left: 0px;
  --op-shelf-host-width: 100%;
  --op-shelf-pad: var(--op-space-4, 16px);
  --op-shelf-gap: var(--op-space-2, 8px);
  --op-shelf-line-h: 20px;
  --op-shelf-place-w: 38px;
  --op-shelf-place-h: 48px;
  --op-shelf-panel-min: 400px;
  --op-shelf-bar: calc(var(--op-shelf-gap) + var(--layout-touch-min));
  --op-shelf-edge: max(var(--op-space-3, 12px), env(safe-area-inset-bottom));
  --op-shelf-peek: 0px;
  --op-shelf-cols: auto-fill;
  --op-shell-bar-top: 0px;
  --op-shell-bar-bottom: 0px;
  --op-shell-flow-header-height: 0px;
  --op-search-width: 24rem;
  --op-search-panel-height: 60svh;
  --op-nav-edge: bottom;
  --op-nav-border: 1px;
  --op-nav-focus-width: 2px;
  --op-nav-font-size: .875rem;
  --op-nav-icon-size: 1.25rem;
  --op-nav-icon-selected-fill: var(--accent-strong);
  --op-nav-selected-bg: var(--bg);
  --op-shelf-radius: 12px;
  --op-shelf-motion-duration: .22s;
}

/* THE ROUTINE AS THE CART (site-router sections/shelf.js, the cart view,
   Stanley 2026-10-22). One white surface with hairlines like the product
   cards; the brand colour with the site's own marble (the search bar's
   texture) only on the primary action, buy all. */
:root {
  --op-shelf-buy-surface: var(--op-appbar-brand);
  --op-shelf-buy-texture: var(--op-searchbar-texture, none);
  --op-shelf-buy-ink: var(--op-appbar-on-brand);
  --op-shelf-title-size: 1.375rem;
  --op-shelf-close-size: 1.375rem;
  /* On a phone the open sheet stops short of the top edge by the search
     bar's height, so the bar never covers the sheet's head. */
  --op-shelf-top-clear: calc(var(--op-searchbar-height) + env(safe-area-inset-top, 0px));
  --op-shelf-line-media: 4.5rem;
  --op-shelf-line-name-size: 1.0625rem;
  /* An empty sheet's series card: each product's place in its row, the
     product's face there, and the face grown in the product's own panel. */
  --op-shelf-tile-width: 5rem;
  --op-shelf-face-size: 3.25rem;
  --op-shelf-product-face: 6.5rem;
  /* The share of a face each product's own measured box takes, both ways,
     and the series picture's ratio on the card's stage. */
  --op-shelf-face-fill: 66%;
  --op-shelf-series-plate-ratio: 16 / 9;
  /* A panel coming onto a series card's stage. */
  --op-shelf-series-slide: 320ms;
  --op-shelf-when-size: 1.125rem;
}

/* THE ROUTINE MARK (site-router templates/routine-mark.js). Three pastel
   balls, each a light top, a mid and a deeper rim; the rim keeps an empty
   ball visible on white. The routine script reads the durations from these
   same tokens. */
:root {
  --op-routine-ball-1-light: #fbd3d6;
  --op-routine-ball-1: #f2a7ae;
  --op-routine-ball-1-rim: #d9737f;
  --op-routine-ball-2-light: #fde9b4;
  --op-routine-ball-2: #f8c97d;
  --op-routine-ball-2-rim: #e39a3f;
  --op-routine-ball-3-light: #cdeee5;
  --op-routine-ball-3: #9fd6c9;
  --op-routine-ball-3-rim: #5fae9f;
  --op-routine-ball-gloss: rgb(255 255 255 / .85);
  --op-routine-ball-shadow: rgb(35 42 49 / .2);
  --op-routine-mark-size: 1.75rem;
  --op-routine-mark-size-bar: 1.875rem;
  /* An empty ball's outline, as a share of the mark's size; the wide bar's mark draws it heavier (Stanley 2026-11-10: the rings were barely visible). */
  --op-routine-mark-stroke: .05;
  --op-routine-mark-stroke-bar: .085;
  /* The phone bar's round button draws its mark small, so its outlines are heavier still to show their colours. */
  --op-routine-mark-stroke-hub: .12;
  --op-routine-ring-width: 3px;
  --op-routine-flyer-layer: 1000;
  --op-routine-fill: 600ms;
  --op-routine-spin: 1000ms;
  --op-routine-pop: 760ms;
  --op-routine-sweep: 860ms;
  /* Product card B (Stanley 2026-10-21): the add mark's balls fill one
     stagger apart, its plus and check swap in the swap time. */
  --op-routine-stagger: 120ms;
  --op-routine-swap: 320ms;
  --op-radd-size: 3rem;
  --op-heart-disc: 2rem;
  --op-card-series-bar: 3px;
  --op-card-voice-size: 1.125rem;
  /* The card match fold (storefront-profile.js): its window on a desktop
     pointer stands above the card's name link (layer 1) and heart (layer 2),
     and its words keep this width beside the faces before they wrap below.
     A face is drawn at the size of the icon it is rendered from (32). */
  --op-profile-pop-layer: 3;
  --op-profile-face-size: 32px;
  --op-profile-match-words-min: 7rem;
  --op-routine-ease-soft: cubic-bezier(.22, 1, .36, 1);
  --op-routine-ease-inout: cubic-bezier(.65, 0, .35, 1);
  --op-routine-ease-arc: cubic-bezier(.45, 0, .25, 1);
}

/* Platform fallbacks preserve unconfigured profiles; configured colours come
   from the palette record. Status hues are platform semantics, not site facts. */
:root {
  --site-fallback-ink: #16181c;
  --site-fallback-ink-strong: #000000;
  --site-fallback-paper: #ffffff;
  --site-fallback-canvas: #fbfbfc;
  --site-fallback-surface-muted: #f4f5f6;
  --site-fallback-text-secondary: #3f454e;
  --site-fallback-text-muted: #5c646e;
  --site-fallback-border: #e4e6ea;
  --site-fallback-border-strong: #c9cdd4;
  --site-status-success: #1f7a4d;
  --site-status-warning: #96620a;
  --site-status-error: #a3261a;
  --site-fallback-shadow-soft: 0 1px 2px rgba(22, 24, 28, 0.05);
  --site-fallback-shadow-card: 0 2px 10px rgba(22, 24, 28, 0.05);
  --site-fallback-shadow-pop: 0 10px 28px rgba(22, 24, 28, 0.09);
}
/* Semantic brand roles activate only after the shared palette reader validates
   the versioned data. Older profiles keep their existing, absent signal roles.
   Raw theme inputs are emitted from the canonical palette, not authored here. */
:root[data-palette-brand] {
  --op-accent: var(--theme-primary);
  --op-accent-strong: var(--theme-secondary);
  --op-accent-on-dark: var(--theme-inverse-text);
  --op-bg-surface: var(--theme-surface);
  --op-bg-muted: var(--theme-surface-muted);
  --op-bg-strong: var(--theme-surface-inverse);
  --op-bg-canvas: var(--op-porcelain, var(--theme-background));
  --op-text-primary: var(--theme-text);
  --op-text-secondary: var(--theme-text-secondary);
  --op-text-muted: var(--theme-text-muted);
  --op-text-on-accent: var(--theme-primary-text);
  --op-text-on-dark: var(--theme-inverse-text);
  --op-on-deep: var(--theme-primary-text);
  --op-on-deep-muted: var(--theme-primary-text);
  --op-border: var(--theme-border);
  --op-border-strong: var(--theme-border-strong);
  --op-border-focus: var(--op-text-primary);
  --op-ok: var(--success, var(--site-status-success));
  --op-warn: var(--warning, var(--site-status-warning));
  --op-err: var(--danger, var(--site-status-error));
  --op-shadow-soft: 0 1px 2px color-mix(in srgb, var(--op-text-primary) 5%, transparent);
  --op-shadow-card: 0 2px 10px color-mix(in srgb, var(--op-text-primary) 5%, transparent);
  --op-shadow-pop: 0 10px 28px color-mix(in srgb, var(--op-text-primary) 9%, transparent);
  --surface-soft: var(--op-bg-muted);

  --op-lock-bg: var(--theme-background);
  --op-lock-panel: var(--op-bg-surface);
  --op-lock-text: var(--op-text-primary);
  --op-lock-muted: var(--op-text-secondary);
  --op-lock-line: var(--op-border-strong);
  --op-lock-accent: var(--role-brand);
  --op-lock-on-accent: var(--role-brand-on);
  --op-lock-accent-hover: var(--role-brand-deep);
  --op-lock-on-accent-hover: var(--role-brand-deep-on);
  --op-lock-badge-bg: var(--op-bg-muted);
  --op-lock-badge-ink: var(--op-text-primary);
  --op-lock-input-bg: var(--op-bg-surface);
  --op-lock-input-border: var(--op-text-muted);
  --op-lock-shell-start: var(--op-porcelain);
  --op-lock-shadow: 0 24px 80px color-mix(in srgb, var(--op-text-primary) 12%, transparent);
  --op-lock-glow-one: color-mix(in srgb, var(--role-brand) 16%, transparent);
  --op-lock-glow-two: color-mix(in srgb, var(--op-accent) 14%, transparent);
  --op-lock-focus-ring: var(--op-text-primary);
  --op-lock-error-bg: var(--op-bg-muted);
  --op-lock-error-border: var(--op-err);
  --op-lock-error-text: var(--op-text-primary);
  --role-brand: var(--theme-brand, var(--op-accent, CanvasText));
  --role-brand-on: var(--theme-brand-on, Canvas);
  --role-brand-foreground: var(--theme-brand-foreground, var(--role-brand));
  --role-brand-hover-on: var(--theme-brand-hover-on, var(--role-brand-on));
  --role-brand-deep: var(--theme-brand-deep, var(--role-brand));
  --role-brand-deep-on: var(--theme-brand-deep-on, var(--role-brand-on));
  --role-brand-deep-foreground: var(--theme-brand-deep-foreground, var(--role-brand-foreground));
  --role-brand-deep-hover-on: var(--theme-brand-deep-hover-on, var(--role-brand-deep-on));
  --role-brand-light: var(--theme-brand-light, var(--op-bg-muted, Canvas));
  --role-brand-light-on: var(--theme-brand-light-on, CanvasText);
  --role-brand-light-foreground: var(--theme-brand-light-foreground, var(--role-brand-foreground));
  --role-brand-light-hover-on: var(--theme-brand-light-hover-on, var(--role-brand-light-on));

  --role-signal: var(--role-brand);
  --role-signal-on: var(--role-brand-on);
  --role-signal-foreground: var(--role-brand-foreground);
  --role-signal-hover-on: var(--role-brand-hover-on);
  --role-signal-deep: var(--role-brand-deep);
  --role-signal-deep-on: var(--role-brand-deep-on);
  --role-signal-deep-foreground: var(--role-brand-deep-foreground);
  --role-signal-deep-hover-on: var(--role-brand-deep-hover-on);
  --role-signal-light: var(--role-brand-light);
  --role-signal-light-on: var(--role-brand-light-on);
  --role-signal-light-foreground: var(--role-brand-light-foreground);
  --role-signal-light-hover-on: var(--role-brand-light-hover-on);

  --op-signal: var(--role-signal);
  --op-signal-on: var(--role-signal-on);
  --op-signal-foreground: var(--role-signal-foreground);
  --op-signal-hover-on: var(--role-signal-hover-on);
  --op-signal-deep: var(--role-signal-deep);
  --op-signal-deep-on: var(--role-signal-deep-on);
  --op-signal-deep-foreground: var(--role-signal-deep-foreground);
  --op-signal-deep-hover-on: var(--role-signal-deep-hover-on);
  --op-signal-light: var(--role-signal-light);
  --op-signal-light-on: var(--role-signal-light-on);
  --op-signal-light-foreground: var(--role-signal-light-foreground);
  --op-signal-light-hover-on: var(--role-signal-light-hover-on);
}

/* Legacy section names are resolved one scope below the root role inputs,
   preventing a role/alias custom-property cycle on the same element. */
:root[data-palette-brand] body {
  --text: var(--op-text-primary);
  --text-primary: var(--op-text-primary);
  --text-secondary: var(--op-text-secondary);
  --text-muted: var(--op-text-muted);
  --text-on-accent: var(--op-text-on-accent);
  --text-inverse: var(--op-text-on-dark);
  --accent: var(--op-accent);
  --accent-strong: var(--op-accent);
  --accent-dark: var(--op-accent-strong);
  --surface: var(--op-bg-surface);
  --surface-soft: var(--op-bg-muted);
  --surface-muted: var(--op-bg-muted);
  --surface-inverse: var(--op-bg-strong);
  --border: var(--op-border);
  --border-strong: var(--op-border-strong);
  --sand: var(--op-bg-canvas);
  --sand-light: var(--op-bg-muted);
}
/* THE CONSENT DIALOG (site-router templates/consent-banner.js). The brand is
   the palette's signal role, else the site accent; the rest are the site's
   surface and text roles. */
:root {
  --op-consent-brand: var(--role-signal, var(--op-accent, var(--accent, #574e3a)));
  --op-consent-brand-foreground: var(--role-signal-foreground, var(--op-consent-brand));
  --op-consent-on-brand: var(--role-signal-on, var(--op-consent-surface));
  --op-consent-on-brand-hover: var(--role-signal-hover-on, var(--op-consent-on-brand));
  --op-consent-surface: var(--op-bg-surface, var(--surface, #ffffff));
  --op-consent-ink: var(--op-text-primary, var(--text, #232a31));
  --op-consent-ink-soft: var(--op-text-secondary, var(--text-secondary, #4a525b));
  --op-consent-hair: var(--op-border, var(--border, #dfe1e0));
  --op-consent-switch-off: var(--op-consent-ink-soft);
  --op-consent-scrim: rgb(35 42 49 / .32);
  --op-consent-shadow: rgb(35 42 49 / .18);
  --op-consent-gold: #d9b45a;
  --op-consent-layer: 999999;
  --op-consent-width: 32rem;
  --op-consent-button: 48px;
  --op-consent-gap: .5rem;
  --op-consent-pad-end: 1.15rem;
  /* Phones: how far above the screen's bottom edge the centre of "Acceptera
     alla" sits, in percent of the screen height. 25 puts it at three
     quarters down, in the one-handed thumb's easy reach on every phone
     height, clear of the bottom edge and the system gesture bar. */
  --op-consent-thumb-rise: 25;
  /* The welcome presentation (hero proposal approved 2026-10-24): the docked
     panel's content measure, the width its text and doors share on a wide
     screen. */
  --op-consent-welcome-width: 82rem;
}

/* THE GALLERY HERO (site-router sections/hero-gallery.js, Operakliniken, hero
   proposal approved 2026-10-24). The brand is the site's accent and the
   surfaces are the site's roles; these are only what the roles do not carry:
   the scrim that darkens a photograph where text sits (an rgb triplet, so the
   section can write each stop's alpha), the ink that sits on that photograph,
   and the sizes of the finder dock and the phone action bar. */
:root {
  --op-gallery-scrim: 26, 15, 10;
  --op-gallery-on-image: #ffffff;
  --op-gallery-on-image-soft: rgb(255 255 255 / .92);
  --op-gallery-glass: color-mix(in srgb, var(--op-bg-canvas, Canvas) 90%, transparent);
  --op-gallery-dock-width: 460px;
  --op-gallery-bar-h: 76px;
}

/* THE PHONE'S APP BAR (site-router sections/nav.js integrateRoutineNavigation,
   Stanley 2026-10-20). The brand is the palette's signal role, else the site
   accent; the raised routine button sits in the middle of the bar. */
:root {
  --op-appbar-brand: var(--role-signal, var(--accent-strong, var(--op-accent, #574e3a)));
  --op-appbar-on-brand: #ffffff;
  --op-appbar-surface: var(--op-bg-surface, var(--surface, #ffffff));
  --op-appbar-ink: var(--op-text-secondary, var(--text-secondary, #4a525b));
  --op-appbar-hair: var(--op-border, var(--border, #dfe1e0));
  --op-appbar-shadow: 0 -6px 24px rgb(35 42 49 / .06);
  --op-appbar-height: 4rem;
  --op-appbar-icon: 1.625rem;
  --op-appbar-gap: 5px;
  --op-appbar-pad-end: .5625rem;
  --op-appbar-badge: 1.375rem;
  --op-appbar-hub-slot: 5.5rem;
  --op-appbar-hub-size: 4.25rem;
  --op-appbar-hub-rise: 2.375rem;
  --op-appbar-hub-ring: 6px;
  --op-appbar-hub-mark: 2.25rem;
  --op-appbar-hub-light: color-mix(in srgb, var(--op-appbar-brand) 70%, var(--op-appbar-on-brand));
  --op-appbar-hub-deep: color-mix(in srgb, var(--op-appbar-brand) 78%, var(--op-text-primary, #232a31));
  --op-appbar-hub-shadow: color-mix(in srgb, var(--op-appbar-brand) 40%, transparent);
  /* The raised routine button while its own sheet is open (Stanley 2026-11-10: the active brown). */
  --op-appbar-hub-active: var(--op-accent, #574e3a);
  --op-appbar-hub-active-light: color-mix(in srgb, var(--op-appbar-hub-active) 70%, var(--op-appbar-on-brand));
  --op-appbar-hub-active-deep: color-mix(in srgb, var(--op-appbar-hub-active) 78%, var(--op-text-primary, #232a31));
  --op-appbar-hub-active-shadow: color-mix(in srgb, var(--op-appbar-hub-active) 40%, transparent);
  /* While a sheet of the bar is open (a tab's, the menu's or the routine's)
     the raised button sinks into the bar: it shrinks to about a tab symbol
     (the scale) and drops until its centre meets the symbols' centre (the
     sink: from the bar's foot up past the padding, the label line, the gap
     and half a symbol, less the raised centre). Its number shrinks with it. */
  --op-appbar-hub-sunk-scale: .44;
  --op-appbar-hub-sink: calc(var(--op-appbar-height) - var(--op-appbar-pad-end) - var(--type-eyebrow-size, .8125rem) - var(--op-appbar-gap) - var(--op-appbar-icon) / 2 + var(--op-appbar-hub-rise) - var(--op-appbar-hub-size) / 2);
  --op-appbar-badge-sunk-scale: .8;
  --op-appbar-icon-stroke: 1.5;
  --op-appbar-spring: cubic-bezier(.34, 1.56, .64, 1);
  --op-appbar-press: 350ms;
  --op-appbar-lift: 450ms;
  /* THE SEARCH BAR (Stanley 2026-10-21): on a phone the search stands as a
     fixed bar at the top of every page (sections/nav.js). A site may lay its
     own colour and texture behind the field. */
  --op-searchbar-height: calc(var(--layout-touch-min, 44px) + 2 * var(--op-space-2, .5rem));
  --op-searchbar-surface: var(--op-appbar-surface);
  --op-searchbar-texture: none;
  --op-searchbar-field: var(--op-bg-canvas, var(--bg, #f6f4f1));
  --op-searchbar-field-shadow: none;
}

/* THE BRAND LOCKUP (site-router sections/nav.js NAV_BYLINE_PREVIEW_STYLE).
   The logo is drawn at its basis height times a scale: the wide bar's, and
   the narrow one in the phone's top row. The basis is the drawn height the
   shell fact byline_preview.wordmark_inset_px is measured at, so the inset
   scales with the logo. The byline's line height, and how far its line rises
   into the empty band at the foot of the logo picture, as a share of the
   logo's height. A site's own sheet sets its scales and rise, and draws its
   logo at the basis times the scale (the lockup is laid out from that
   height); at these defaults the inset is the fact as measured. */
:root {
  --op-nav-logo-basis: 28px;
  --op-nav-logo-scale: 1;
  --op-nav-logo-scale-narrow: 1;
  --op-nav-byline-leading: 1.2;
  --op-nav-byline-rise: 0;
}

/* THE SEARCH HELP (site-router sections/match-field.js REFINE_STYLE, Stanley
   2026-10-21): the round button beside the search field and the sheet it
   opens. The button reads as the field does: in the phone's search bar it
   takes the bar's field colour and shadow (sections/nav.js), elsewhere the
   surface. The sheet rises from the foot of a phone and stands as a panel
   under the bar from the 40rem step. A site may lay its own colour on the
   bar's button (the --op-searchbar-action-* pair). */
:root {
  --op-refine-action-surface: var(--op-bg-surface, var(--surface, #ffffff));
  --op-refine-action-ink: var(--op-text-primary, var(--text, #232a31));
  --op-refine-action-icon: 1.375rem;
  --op-refine-badge: 1.125rem;
  --op-refine-radius: var(--op-radius-xl, 18px);
  --op-refine-sheet-max: 92dvh;
  --op-refine-panel-inline: 26rem;
  --op-refine-motion: 420ms;
  --op-searchbar-action-surface: var(--op-searchbar-field, var(--op-refine-action-surface));
  --op-searchbar-action-ink: var(--op-refine-action-ink);
}

/* THE PRODUCT RESULT (site-router product-card.js productResultStyles,
   Stanley 2026-10-22): one product as a row of the search's hits. The
   thumbnail's side, the share of it the product's picture takes, the name in
   the voice face and the gap between the row's text lines. */
:root {
  --op-result-thumb: 4.25rem;
  --op-result-picture: 84%;
  --op-result-name-size: 1.0625rem;
  --op-result-line-gap: 2px;
}

/* The mock marker (workers/shared/mock-mark.js; plan
   docs/plans/2026-10-11-dermashoppen-shell-first-plan.md section 3.2): the
   reviewer-only expanded icon beside an example. Its expanded-icon
   primitives live in the portal sheet (modules/portal-shell/parts/
   hover-expand.css), which no public page loads, so the public site gets
   the minimal set here: the status colour of a page in preparation (the
   portal value, until the one-status-word cleanup makes a single
   definition) and the marker's own measures, named after its class. A token
   sheet holds only :root blocks (test/hero-v2-fold.test.js checks the
   shape), so the rules that read these live beside the marker
   (mock-mark.js MOCK_MARKER_STYLES) and print once in a reviewer render.
   The label's widest state is in em, so the word and the time fit at the
   text size they inherit (measured: 188px at 16px clipped at 160px). The
   frame tokens place a reviewer's example frame (MOCK_FRAME_STYLES): the
   caption band along its foot, and the marker inset far enough that its
   touch area (--layout-touch-min) stays inside a frame that clips. The
   marker's ink is the status colour darkened (the orange measured 2.57 to
   2.8:1 on the pages' light surfaces; the ink is over 7:1 there), so the
   glyph, the focus ring and the label all read. */
:root {
  --status-preparing: #f97316;
  --th-mock-marker-gap: 6px;
  --th-mock-marker-label-max: 20em;
  --th-mock-marker-ink: color-mix(in srgb, var(--status-preparing) 55%, black);
  --th-mock-frame-inset: 16px;
  --th-mock-frame-pad-block: 4px;
  --th-mock-frame-pad-inline: 8px;
  --th-mock-frame-text: 0.875rem;
  --th-mock-frame-ink: CanvasText;
  --th-mock-frame-surface: color-mix(in srgb, Canvas 88%, transparent);
  --th-mock-marker-radius: 4px;
  --th-mock-marker-focus-width: 2px;
  --th-mock-marker-motion: 180ms;
}

/* Modal dialogs (first reader: the checkout notice, site-router
   templates/checkout-popup.js). The scrim dims the page behind an open modal
   with the page's own text colour; the inline maximum is the widest a
   dialog grows before the viewport gutter takes over. */
:root {
  --op-scrim: color-mix(in srgb, CanvasText 45%, transparent);
  --op-dialog-inline-max: 30rem;
}

/* The checkout page (site-router sections/checkout.js): the picture beside a
   cart line (a narrow and a wide kassa), and the summary panel's column
   beside the lines from the 48rem container step (design pass 2). */
:root {
  --op-checkout-media: 4.5rem;
  --op-checkout-media-wide: 5rem;
  --op-checkout-summary-inline: 22rem;
}

/* THE BRAND TABS (site-router templates/sections/brand-tabs.js). How much of
   a series' colour tints its group picture's plate, the floor line every
   cutout of the group picture stands on (from the plate's top), and the
   narrowest a carousel card gets before fewer cards show in one view. */
:root {
  --op-bt-plate-mix: 24%;
  --op-bt-floor: 84%;
  --op-bt-card-min: 13.5rem;
  /* Brand tab hover (design round 3): the share of the selected look a
     hovered tab takes, and the share the selected tab keeps meanwhile. */
  --op-bt-tab-preview: 55%;
  --op-bt-tab-yield: 45%;
  --op-nav-wordmark-inset: 0px;
  --op-media-feedback-layer: 60;
  --op-guide-choice-min: 9rem;
  /* The guide's own choice mark (design round 3): a ring that fills with the
     accent, the surface showing as an inner ring once chosen. */
  --op-guide-mark-size: 1.25rem;
  --op-guide-mark-border: 2px;
  --op-guide-mark-inset: 3px;
  /* The polished local guide's panel (design round 4, templates/sections/
     guide.js polishedLocalGuideStyles): the most answers one row holds, the
     panel's inset on a phone band and as a card from the 40rem container
     step, the height of a progress track segment, the strength of a control
     that cannot act yet, and the intro column beside the panel from 60rem. */
  --op-guide-choice-cols: 3;
  --op-guide-panel-pad: var(--op-space-5, 1.5rem);
  --op-guide-panel-pad-wide: var(--op-space-7, 2.5rem);
  --op-guide-track-h: 4px;
  --op-guide-disabled-opacity: .45;
  --op-guide-intro-columns: minmax(0, 1fr) minmax(0, 2fr);
  /* The stepped guide (skin analysis, design round 5, templates/sections/
     guide.js steppedLocalGuideStyles). Every colour resolves through the
     brand roles the palette engine owns: the primary door paints the brand
     signal in every state (no faded grey door), a picked answer tile carries
     three signals at once (edge, tint, filled mark), so no state rests on
     colour alone. One curve and one entry distance for the motion, which
     the guide only plays under prefers-reduced-motion: no-preference. */
  --op-guide-door-bg: var(--role-signal, var(--op-accent, CanvasText));
  --op-guide-door-bg-hover: var(--role-signal-deep, var(--op-accent-strong, CanvasText));
  --op-guide-door-on: var(--role-signal-on, var(--op-text-on-accent, Canvas));
  --op-guide-door-h: var(--op-control-h-large, 52px);
  --op-guide-tile-bg: var(--op-bg-surface, Canvas);
  --op-guide-tile-edge: var(--op-hairline-strong, GrayText);
  --op-guide-tile-edge-hover: var(--op-text-secondary, CanvasText);
  --op-guide-tile-picked-bg: color-mix(in oklab, var(--role-brand, CanvasText) 7%, var(--op-bg-surface, Canvas));
  --op-guide-tile-picked-edge: var(--role-brand-deep, CanvasText);
  --op-guide-tile-icon: var(--role-brand-deep, CanvasText);
  --op-guide-tile-min-h: 4.25rem;
  --op-guide-chip-size: 2.75rem;
  --op-guide-flush: #d07f73;
  --op-guide-sheet-head-h: 3.5rem;
  --op-guide-sheet-in: 300ms;
  --op-guide-sheet-shift: 1.5rem;
  --op-guide-mark-edge: var(--op-text-secondary, CanvasText);
  --op-guide-focus: var(--role-brand-deep, CanvasText);
  --op-guide-ease: cubic-bezier(.2, .8, .2, 1);
  --op-guide-step-in: 260ms;
  --op-guide-step-shift: .75rem;
  --op-guide-board-gap: var(--op-space-6, 2rem);
  --op-guide-thumb: 4rem;
  /* The product rail (templates/product-rail.js, design round 3): the most
     cards one view shows below the 30rem step, from it and from 40rem (a
     site's shell fact rail_columns replaces the three counts), the narrowest
     a card gets on a narrow rail, how far the next card peeks in there, and
     the strength of a step button at either end. */
  --op-rail-cols-narrow: 2;
  --op-rail-cols-medium: 3;
  --op-rail-cols-wide: 4;
  --op-rail-card-min-narrow: 9rem;
  --op-rail-peek-narrow: 1.5rem;
  --op-rail-step-disabled-opacity: .4;
  /* The space after the "#" that marks a nav link to a place on a page. */
  --op-nav-anchor-mark-gap: .15em;
  /* The page trail (templates/page-trail.js): its row height, which anchors
     clear below the bar; its layer, under the bar's and over the page; the
     narrowest its list of places opens. */
  --op-page-trail-h: 2.75rem;
  --op-page-trail-layer: 90;
  --op-page-trail-list-min: 14rem;
  /* The retail product page's first phone screen (templates/product-page.js
     productAppLayoutStyles): what a phone's first screen holds besides the
     picture and the fixed bars, the page head that scrolls away above it
     (search and logo rows) and the purchase head under it (thumbnails,
     brand, a two-line name, the series and detail line, price and the
     primary door); and the smallest the square picture stage gets. */
  --op-product-first-screen: 30rem;
  --op-product-stage-min: 12rem;
  --op-product-track-first-screen: 36rem;
  --op-product-thumb: 4.5rem;
  --op-product-play: 3.5rem;
  --op-product-buybar-h: 3.5rem;
  --op-product-bar-reserve: calc(var(--op-shell-bar-bottom, 0px) + var(--op-appbar-hub-rise, 0px) + var(--op-appbar-hub-ring, 0px));
  --op-pv-backdrop: #16130d;
}

/* SOFT MOTION (Stanley 2026-11-09, the storefront's motion pass; rules in
   sites/dermashoppen/assets/css/dermashoppen-hub.css). One ease-out curve and
   durations inside 150-260 ms: feedback on a press, a picture arriving, a
   panel or fold opening and closing, a progress bar moving. Only opacity and
   transform move (and a fold's height), never with an overshoot, and only
   under prefers-reduced-motion: no-preference. The shift is how far a panel
   travels as it fades; the zoom is the most a card picture grows under a
   desktop pointer. */
:root {
  --op-motion-ease-out: cubic-bezier(.2, .8, .2, 1);
  --op-motion-feedback: 160ms;
  --op-motion-image: 220ms;
  --op-motion-panel: 240ms;
  --op-motion-progress: 260ms;
  --op-motion-shift: .375rem;
  --op-hover-zoom: 1.02;
}
