/* dour.store — the upgrade layer.
 *
 * Loaded AFTER `dour.css`, which stays byte-identical to the origin's compiled
 * stylesheet. Nothing here replaces a rule in that file; everything here is
 * additive, and every value is either derived from a token the origin already
 * defines or is a dimension (spacing, target size, leading) the origin never
 * set deliberately.
 *
 * The rule this file obeys: no new hues, no new fonts, no new layout. The theme
 * is the palette, the typeface and the photography, and none of the three is
 * touched. What changes is hierarchy, target size, state and rhythm — the parts
 * of a UI that Odoo's theme panel cannot express, and that were left at
 * whatever the panel defaulted to.
 *
 * Overruled once, in section 11: the header mega menu does change layout. The
 * origin pins that panel to a hard 580px over markup authored for Odoo's
 * 1290px default, which leaves a third of it empty on the right and ~85px
 * empty at the bottom. Section 11 states the case, and is the only place in
 * this file where 'no new layout' does not hold.
 *
 * Everything is scoped to `html[data-ui="upgrade"]`. Drop the attribute and the
 * site is the origin again, byte for byte — which is what `tools/compare-live.mjs`
 * measures, and why that check stays meaningful after this file exists.
 */

/* ==================================================================
 * 1 · Typography
 *
 * The origin's theme carries `--font-size-base: 0.75rem` and
 * `--body-line-height: 1`. That is 12px type set solid, on a bilingual
 * storefront — Bengali needs more leading than Latin, not less, and the returns
 * policy on the cart page is 700 words of it.
 *
 * Bootstrap's own default for both variables is in this same stylesheet
 * (`--body-line-height: 1.5`); the theme overrode it. These lines put the
 * leading back and lift the base one step. No font changes, no colour changes,
 * and nothing above h1 — `--h1-font-size` is absolute (3.125rem), so the hero
 * wordmark is untouched.
 * ================================================================== */
html[data-ui="upgrade"] {
  --font-size-base: 0.875rem;
  --body-font-size: 0.875rem;
  --body-line-height: 1.55;
  --headings-line-height: 1.15;
  --h2-line-height: 1.15;
  --h3-line-height: 1.2;
  --h4-line-height: 1.25;
  --h5-line-height: 1.3;
  --h6-line-height: 1.35;
  --btn-font-size: 0.8125rem;

  /* Spacing scale. The origin has no consistent one; these are read only by
   * rules in this file, so nothing inherits them by accident. */
  --u-1: 4px;
  --u-2: 8px;
  --u-3: 12px;
  --u-4: 16px;
  --u-5: 24px;
  --u-6: 32px;

  /* Interaction. `--o-color-1` is the origin's own brand navy (#00203f). */
  --u-focus: var(--o-color-1, #00203f);
  --u-target: 44px;
  --u-ease: cubic-bezier(0.2, 0, 0, 1);
  --u-fast: 140ms;
  --u-slow: 240ms;
  --u-radius: 10px;
  --u-line: color-mix(in srgb, var(--900, #292121) 12%, transparent);
}

/* Long-form prose gets a measure. Past roughly 70 characters a line stops being
 * read and starts being skimmed, which is the opposite of what a returns policy
 * is for. */
html[data-ui="upgrade"] .custom-desc-content {
  max-width: 68ch;
}

html[data-ui="upgrade"] .custom-desc-content li {
  margin-bottom: var(--u-2);
}

html[data-ui="upgrade"] .custom-desc-content p + p {
  margin-top: var(--u-3);
}

/* Bengali sits taller than Latin at the same nominal size and needs the extra
 * leading to stay legible. The captured policy blocks carry no lang attribute,
 * so this keys on the element that does when one is present. */
html[data-ui="upgrade"] :lang(bn) {
  line-height: 1.75;
}

/* ==================================================================
 * 2 · Targets and controls
 *
 * Measured on the running rebuild at 375px: the size radios are 13x13, the
 * colour swatches 39x39, and "Add to My cart" is 32x128. WCAG 2.2 AA (2.5.8)
 * sets the floor at 24x24; 44x44 is the practical one on a phone. The colour
 * row and the size row also speak two different control languages in the same
 * panel — round swatch chips above, bare radio dots below. These rules give the
 * size row the colour row's language and bring both up to target size.
 * ================================================================== */

/* Size: turn the native radio and its label into a chip. The input stays in the
 * DOM and stays the thing that is checked — only its painting changes, so
 * `js_variant_change` keeps working and the form still submits what it did. */
html[data-ui="upgrade"] .js_attribute_value .form-check {
  padding: 0;
  margin: 0;
  display: block;
}

html[data-ui="upgrade"] .js_attribute_value .form-check-input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
  margin: 0;
  pointer-events: none;
}

html[data-ui="upgrade"] .js_attribute_value label.col-form-label {
  padding: 0;
  margin: 0;
  cursor: pointer;
}

html[data-ui="upgrade"] .js_attribute_value .radio_input_value {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: var(--u-target);
  min-height: var(--u-target);
  padding: 0 var(--u-3);
  border: 1px solid var(--u-line);
  border-radius: var(--u-radius);
  background: var(--white, #fff);
  font-weight: 500;
  line-height: 1;
  cursor: pointer;
  transition:
    border-color var(--u-fast) var(--u-ease),
    background-color var(--u-fast) var(--u-ease),
    color var(--u-fast) var(--u-ease);
}

html[data-ui="upgrade"] .js_attribute_value .radio_input_value:hover {
  border-color: var(--u-focus);
}

html[data-ui="upgrade"] .js_attribute_value .form-check-input:checked + .radio_input_value {
  background: var(--u-focus);
  border-color: var(--u-focus);
  color: var(--white, #fff);
}

html[data-ui="upgrade"] .js_attribute_value .form-check-input:focus-visible + .radio_input_value {
  outline: 2px solid var(--u-focus);
  outline-offset: 2px;
}

/* An unavailable combination is currently signalled only by a message further
 * down the panel. Mark the chip itself. */
html[data-ui="upgrade"] .js_attribute_value .form-check-input:disabled + .radio_input_value {
  opacity: 0.4;
  cursor: not-allowed;
  text-decoration: line-through;
}

/* Colour: same target size, and a selected state that reads as selected rather
 * than as a slightly different border. */
html[data-ui="upgrade"] label.css_attribute_color {
  width: var(--u-target);
  height: var(--u-target);
  border-radius: 50%;
  border: 1px solid var(--u-line);
  padding: 3px;
  background-clip: content-box;
  transition:
    box-shadow var(--u-fast) var(--u-ease),
    transform var(--u-fast) var(--u-ease);
}

html[data-ui="upgrade"] label.css_attribute_color:hover {
  transform: scale(1.06);
}

/* Keyed on the input rather than on `.active`, because `.active` is a class
 * Odoo's own JS moves at runtime and this app deliberately does not ship that
 * JS. `:has(:checked)` is always the truth about which colour is selected. */
html[data-ui="upgrade"] label.css_attribute_color:has(input:checked),
html[data-ui="upgrade"] label.css_attribute_color.active {
  border-color: var(--u-focus);
  border-width: 2px;
  padding: 3px;
  box-shadow: 0 0 0 2px var(--white, #fff) inset;
}

html[data-ui="upgrade"] .o_wsale_product_attribute {
  display: flex;
  flex-wrap: wrap;
  gap: var(--u-2);
  margin-top: var(--u-2);
}

html[data-ui="upgrade"] .o_wsale_product_attribute .list-inline-item {
  margin: 0;
}

html[data-ui="upgrade"] .variant_attribute {
  margin-bottom: var(--u-5);
}

html[data-ui="upgrade"] strong.attribute_name {
  display: block;
  font-size: 0.75rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--700, #574949);
}

/* `Tags: Women's Nova Keyhole Tank` is the merchandiser's taxonomy showing
 * through to the shopper. It is not worth removing — it is a real link surface —
 * but lifting the base size made it louder than the buy button's neighbours,
 * which is backwards. Put it back where it belongs. */
html[data-ui="upgrade"] .o_product_tags {
  font-size: 0.75rem;
  color: var(--600, #7d6c6c);
}

html[data-ui="upgrade"] .o_product_tags strong {
  font-weight: 500;
  color: var(--600, #7d6c6c);
}

/* ==================================================================
 * 3 · Buttons
 *
 * The origin's `--btn-border-radius: 1.25rem` pill is the theme and stays. What
 * changes is height, and that the primary action on a phone is not a 128px-wide
 * target floating in a 375px column.
 * ================================================================== */
html[data-ui="upgrade"] .btn {
  min-height: var(--u-target);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--u-2);
  transition:
    transform var(--u-fast) var(--u-ease),
    box-shadow var(--u-fast) var(--u-ease),
    background-color var(--u-fast) var(--u-ease);
}

html[data-ui="upgrade"] .btn-link {
  min-height: 0;
}

html[data-ui="upgrade"] .btn-primary {
  min-height: 48px;
  padding-inline: var(--u-5);
  font-weight: 600;
  letter-spacing: 0.02em;
}

html[data-ui="upgrade"] .btn-primary:hover {
  box-shadow: 0 6px 20px color-mix(in srgb, var(--o-color-1, #00203f) 28%, transparent);
  transform: translateY(-1px);
}

html[data-ui="upgrade"] .btn:active {
  transform: translateY(0);
}

/* Add to cart, on a phone, is the page's one job. The origin already puts
 * `flex-grow-1` on the anchor, but its wrapper is `d-inline-flex`, so the grow
 * has nothing to grow into — the wrapper is what needs to claim the row. */
@media (max-width: 991.98px) {
  html[data-ui="upgrade"] .js_check_product {
    width: 100%;
  }
  html[data-ui="upgrade"] .js_check_product:not(.btn-link):is(a, button) {
    flex: 1 1 auto;
  }
  html[data-ui="upgrade"] div:has(> .js_check_product) {
    flex: 1 1 auto;
  }
}

/* The quantity stepper is three flush 32px targets. */
html[data-ui="upgrade"] .css_quantity {
  border: 1px solid var(--u-line);
  border-radius: var(--u-radius);
  overflow: hidden;
  height: 48px;
}

html[data-ui="upgrade"] .css_quantity .btn {
  min-height: 46px;
  min-width: var(--u-target);
  border: 0;
  border-radius: 0;
  color: var(--900, #292121);
}

html[data-ui="upgrade"] .css_quantity .btn:hover {
  background: var(--200, #efe9e9);
}

html[data-ui="upgrade"] .css_quantity input.quantity {
  border: 0;
  width: 48px;
  font-weight: 600;
  background: transparent;
}

/* ==================================================================
 * 4 · Focus
 *
 * Odoo's reset removes the outline on nearly everything and puts nothing back
 * that survives its own overrides. A keyboard shopper currently cannot see
 * where they are. `:focus-visible` keeps this off mouse clicks.
 * ================================================================== */
html[data-ui="upgrade"] a:focus-visible,
html[data-ui="upgrade"] button:focus-visible,
html[data-ui="upgrade"] input:focus-visible,
html[data-ui="upgrade"] select:focus-visible,
html[data-ui="upgrade"] textarea:focus-visible,
html[data-ui="upgrade"] [tabindex]:focus-visible {
  outline: 2px solid var(--u-focus);
  outline-offset: 2px;
  border-radius: 4px;
}

/* ==================================================================
 * 5 · Product grid
 *
 * 272 cards over 98 products, so the grid is mostly near-duplicates. Naming the
 * colourway on the card needs markup and belongs to a later tier; what this
 * tier can do is make a card feel like a target and give the photograph the
 * room the design already reserved for it.
 * ================================================================== */
html[data-ui="upgrade"] .oe_product .o_wsale_product_grid_wrapper {
  border-radius: var(--u-radius);
  overflow: hidden;
  transition:
    box-shadow var(--u-slow) var(--u-ease),
    transform var(--u-slow) var(--u-ease);
}

html[data-ui="upgrade"] .oe_product:hover .o_wsale_product_grid_wrapper {
  transform: translateY(-2px);
  box-shadow: 0 10px 30px color-mix(in srgb, var(--900, #292121) 12%, transparent);
}

html[data-ui="upgrade"] .oe_product_image img {
  transition: transform var(--u-slow) var(--u-ease);
}

html[data-ui="upgrade"] .oe_product:hover .oe_product_image img {
  transform: scale(1.04);
}

html[data-ui="upgrade"] .o_wsale_product_information {
  padding-top: var(--u-3);
}

/* The name is what gets scanned; the price is what gets compared. Both were at
 * body weight. */
html[data-ui="upgrade"] .o_wsale_products_item_title {
  font-weight: 600;
  margin-bottom: var(--u-1) !important;
}

/* The colourway, named. Quiet enough that the product name still leads, loud
 * enough to tell six Nova Keyhole Tanks apart at a glance. */
html[data-ui="upgrade"] .o_up_card_colour {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0 0 var(--u-2);
  font-size: 0.6875rem;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--600, #7d6c6c);
  line-height: 1.4;
}

html[data-ui="upgrade"] .o_up_card_swatch {
  width: 11px;
  height: 11px;
  flex: 0 0 auto;
  border-radius: 50%;
  border: 1px solid var(--u-line);
}

html[data-ui="upgrade"] .o_wsale_product_information .oe_currency_value {
  font-weight: 600;
  color: var(--900, #292121);
}

/* ==================================================================
 * 6 · Checkout
 *
 * Height, radius and border only. The field *widths* are wrong too — email
 * 730px, phone 350px, zip 223px on the same form, none of them saying anything
 * about the answer's length — but those come from Bootstrap column spans in the
 * captured markup, and overriding a grid is a layout change, not a skin. That
 * one waits for a tier that owns the markup.
 * ================================================================== */
html[data-ui="upgrade"] .oe_website_sale .form-control,
html[data-ui="upgrade"] .oe_website_sale .form-select {
  min-height: var(--u-target);
  border-radius: var(--u-radius);
  border-color: var(--u-line);
  transition:
    border-color var(--u-fast) var(--u-ease),
    box-shadow var(--u-fast) var(--u-ease);
}

html[data-ui="upgrade"] .oe_website_sale .form-control:focus,
html[data-ui="upgrade"] .oe_website_sale .form-select:focus {
  border-color: var(--u-focus);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--o-color-1, #00203f) 14%, transparent);
}

html[data-ui="upgrade"] .oe_website_sale label {
  font-weight: 500;
  margin-bottom: var(--u-1);
}

/* The wizard paints the completed step in full contrast and the current step in
 * grey — the two states are inverted. This restores the order without touching
 * the markup. */
html[data-ui="upgrade"] .o_wizard_step_active,
html[data-ui="upgrade"] .o_wizard_step_active span {
  color: var(--900, #292121) !important;
  font-weight: 600;
}

html[data-ui="upgrade"] .o_wizard_step_complete,
html[data-ui="upgrade"] .o_wizard_step_complete span {
  color: var(--600, #7d6c6c) !important;
  font-weight: 400;
}

/* ==================================================================
 * 7 · Cart
 * ================================================================== */
html[data-ui="upgrade"] .o_cart_product {
  padding-block: var(--u-4);
  border-bottom: 1px solid var(--u-line);
}

html[data-ui="upgrade"] .o_cart_product a {
  min-height: 32px;
  display: inline-flex;
  align-items: center;
}

/* ==================================================================
 * 8 · Tier 2 — the nodes `UpgradeLayer` adds
 *
 * Everything here is prefixed `o_up_` and exists only because that component
 * created it. Nothing in this block styles captured markup.
 * ================================================================== */

/* The cart's prose, collapsed. The heading is moved into the summary, so it
 * keeps its own type and only gains the marker and the hit area. */
html[data-ui="upgrade"] .o_up_disclosure {
  border-top: 1px solid var(--u-line);
  margin-top: var(--u-5);
}

html[data-ui="upgrade"] .o_up_disclosure_summary {
  display: flex;
  align-items: center;
  gap: var(--u-3);
  min-height: var(--u-target);
  padding: var(--u-3) 0;
  cursor: pointer;
  list-style: none;
  color: var(--900, #292121);
}

html[data-ui="upgrade"] .o_up_disclosure_summary::-webkit-details-marker {
  display: none;
}

html[data-ui="upgrade"] .o_up_disclosure_summary > * {
  margin: 0;
}

/* A caret that turns, rather than a plus that changes meaning. */
html[data-ui="upgrade"] .o_up_disclosure_summary::after {
  content: "";
  width: 8px;
  height: 8px;
  margin-left: auto;
  border-right: 1.5px solid var(--600, #7d6c6c);
  border-bottom: 1.5px solid var(--600, #7d6c6c);
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform var(--u-fast) var(--u-ease);
}

html[data-ui="upgrade"] .o_up_disclosure[open] .o_up_disclosure_summary::after {
  transform: rotate(-135deg) translate(-2px, -2px);
}

html[data-ui="upgrade"] .o_up_disclosure_summary:focus-visible {
  outline: 2px solid var(--u-focus);
  outline-offset: 2px;
}

/* The selected colour's name, beside the attribute label. */
html[data-ui="upgrade"] .o_up_value_name {
  margin-left: var(--u-2);
  color: var(--900, #292121);
  letter-spacing: 0;
  text-transform: none;
  font-weight: 600;
}

html[data-ui="upgrade"] .o_up_value_name:empty {
  display: none;
}

/* The sticky buy bar. Sits above the viewport's own safe area so it does not
 * land under a phone's home indicator. */
html[data-ui="upgrade"] .o_up_buybar {
  position: fixed;
  inset: auto 0 0 0;
  z-index: 1030;
  display: flex;
  align-items: center;
  gap: var(--u-4);
  padding: var(--u-3) var(--u-4);
  padding-bottom: calc(var(--u-3) + env(safe-area-inset-bottom, 0px));
  background: var(--white, #fff);
  border-top: 1px solid var(--u-line);
  box-shadow: 0 -6px 24px color-mix(in srgb, var(--900, #292121) 10%, transparent);
}

html[data-ui="upgrade"] .o_up_buybar_text {
  display: flex;
  flex-direction: column;
  min-width: 0;
  line-height: 1.3;
}

html[data-ui="upgrade"] .o_up_buybar_name {
  font-size: 0.8125rem;
  color: var(--700, #574949);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

html[data-ui="upgrade"] .o_up_buybar_price {
  font-weight: 700;
  font-size: 1rem;
}

html[data-ui="upgrade"] .o_up_buybar_btn {
  margin-left: auto;
  flex: 0 0 auto;
}

/* Zoom. */
html[data-ui="upgrade"] img.o_up_zoomable {
  cursor: zoom-in;
}

html[data-ui="upgrade"] .o_up_zoom {
  position: fixed;
  inset: 0;
  z-index: 2000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--u-5);
  background: color-mix(in srgb, var(--900, #292121) 88%, transparent);
  cursor: zoom-out;
}

html[data-ui="upgrade"] .o_up_zoom img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
}

html[data-ui="upgrade"] .o_up_zoom_close {
  position: absolute;
  top: var(--u-4);
  right: var(--u-4);
  width: var(--u-target);
  height: var(--u-target);
  border: 0;
  border-radius: 50%;
  background: var(--white, #fff);
  color: var(--900, #292121);
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
}

html[data-ui="upgrade"] .o_up_zoom_close:focus-visible {
  outline: 2px solid var(--white, #fff);
  outline-offset: 3px;
}

/* The order summary beside the address form. The card, the table and the
 * totals are the origin's; only the heading and the column are new. */
html[data-ui="upgrade"] .o_up_address_summary {
  margin-top: var(--u-5);
}

html[data-ui="upgrade"] .o_up_address_summary .o_total_card {
  top: var(--u-4);
  border-color: var(--u-line);
  border-radius: var(--u-radius);
}

html[data-ui="upgrade"] .o_up_summary_title {
  font-size: 0.6875rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--700, #574949);
  margin: 0 0 var(--u-3);
}

@media (min-width: 992px) {
  html[data-ui="upgrade"] .o_up_address_summary {
    margin-top: 0;
  }
}

/* Inline validation reuses Bootstrap's own `.invalid-feedback`, which the
 * origin's stylesheet already compiles; this only forces it visible, since
 * Bootstrap reveals it via `.was-validated` on the form and this validates
 * per field instead. */
html[data-ui="upgrade"] .o_up_invalid {
  display: block;
}

/* ==================================================================
 * 9 · Motion
 *
 * Every transition above is a comfort, not information, so all of it goes when
 * the visitor has asked for less. The origin's own scroll reveals go too.
 * ================================================================== */
@media (prefers-reduced-motion: reduce) {
  html[data-ui="upgrade"] *,
  html[data-ui="upgrade"] *::before,
  html[data-ui="upgrade"] *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ==================================================================
 * 10 · The homepage scroll
 *
 * Two jobs, and they turn out to be the same job.
 *
 * THE CROP. The hero photograph is 1594x1920 — portrait — and the hero band is
 * the full window by 908px, which at a 1440px viewport is 1.57:1. Under
 * `background-size: cover` (`.oe_img_bg`) and `background-position: center`
 * (`.o_bg_img_center`) that throws away 47% of the image height, split evenly
 * top and bottom. Measured on the file, the athlete occupies 35.7%–77.6% of
 * the frame; the centred window shows 23.5%–76.5%. The trailing shoe is 1.1%
 * outside it, which is the slice through the sole. It gets worse as the window
 * widens — the visible band is 754/W of the image — so on a 1900px screen the
 * window is 39.7% against a subject that needs 41.9%, and no repositioning can
 * fit it.
 *
 * So: below 1600px, re-anchor the crop on the athlete rather than on the frame
 * (`50% 64%` — derived below, and correct across 992–1600px). At 1600px and up,
 * back the photograph off to 215% of the band's height, which is the scale at
 * which the athlete measures 90% of it and clears both edges by 5%. That leaves
 * a margin either side on the widest screens, and the margin is a blurred copy
 * of the same file — same vertical framing, so the two layers show the same
 * part of the studio sweep where they meet and there is no seam to see. Both
 * layers take the file with `background-image: inherit`, so nothing here
 * hardcodes a media path that a re-capture could re-hash out from under it.
 *
 * THE MOTION. The origin's only homepage animation is `o_anim_pulse`: a 1%
 * scale twitch on the hero block and on the MEN/WOMEN buttons. What replaces it
 * is scroll-driven — `animation-timeline`, so the browser runs it off the main
 * thread and there is no scroll listener anywhere in this app. All of it sits
 * inside `@supports (animation-timeline: view())`; where that is false the page
 * is exactly what it is today, which is the point of a feature query rather
 * than a polyfill.
 *
 * The scale factors are not taste. In the cropped mode the athlete already
 * fills 3.8%–92.8% of the band at the tightest supported width, so a push-in
 * past 1.03 from the bottom edge clips his cap; in the whole-photograph mode
 * there is room for 1.06. Those two numbers are why the drift is subtle.
 * ================================================================== */

/* ---- 10.1 · Hero framing ------------------------------------------
 * Three numbers, all of them the origin's own and all of them constant at
 * every width from 992px to 2400px (measured, not assumed):
 *
 *     band    908px   = pt152 + ten 50px grid rows + pb256
 *     header   86px   = the static bar the band sits under
 *     subject  35.7%–77.6% of the frame, sampled off the file itself
 *
 * The band is taller than a laptop window leaves for it, so framing the
 * photograph to the *band* puts the athlete's trailing shoe under the fold on
 * exactly the screens where the shoe is the thing being complained about.
 * Frame it to the part that is on screen instead — `min(band, 100svh - header)`
 * — and the subject is inside the first screenful at every size.
 *
 * From there the two values fall out. The subject is 41.9% of the frame, so a
 * frame 2.148x the visible height puts him at 90% of it, 5% clear top and
 * bottom; and the offset that centres him is a length, not a percentage of an
 * overflow, which is why it reads as one multiplication:
 *
 *     frame  = visible * 2.148
 *     offset = visible / 2 - frame * 0.5665 = visible * -0.7168
 *
 * Wider than about 1.2x the frame and there is margin either side. That margin
 * is a blurred copy of the same file, positioned to sit on the same slice of
 * the studio sweep, so the two layers meet on the same tone and there is no
 * seam to see — blurring a *differently framed* copy is what puts a grey band
 * down each side of a photograph. Both layers take the file with
 * `background-image: inherit`, so nothing here hardcodes a media path that a
 * re-capture could re-hash out from under it, and `background-size: 0 0` keeps
 * the span itself from painting a third copy underneath them.
 * ------------------------------------------------------------------ */
@media (min-width: 992px) {
  html[data-ui="upgrade"] .homepage .carousel-item.parallax .s_parallax_bg {
    --o-up-hero-visible: min(908px, calc(100svh - 86px));
    background-size: 0 0;
  }

  html[data-ui="upgrade"] .homepage .carousel-item.parallax .s_parallax_bg::before,
  html[data-ui="upgrade"] .homepage .carousel-item.parallax .s_parallax_bg::after {
    content: "";
    position: absolute;
    inset: 0;
    background-image: inherit;
    background-repeat: no-repeat;
  }

  /* The margin. `cover` against the band's own width, offset to the same slice
     the sharp layer is showing. The blurred athlete inside it is always behind
     the sharp one and never reaches the margin — it stops being true past
     about 3200px, which is past every screen this is laid out for. */
  html[data-ui="upgrade"] .homepage .carousel-item.parallax .s_parallax_bg::before {
    background-size: cover;
    background-position: 50% calc(86px + var(--o-up-hero-visible) * 0.5 - 100vw * 0.6825);
    filter: blur(40px) saturate(1.03) brightness(0.98);
    /* A blur samples past its own edges and fades out there; the over-scale
       pushes that softness outside the band. */
    transform: scale(1.07);
  }

  /* The photograph, and the join.
   *
   * The two layers land within about 8% of each other in luminance where they
   * meet — measured on the file at five heights — but 8% across a hard vertical
   * edge is still an edge, and no amount of blur behind it softens the line the
   * sharp layer's own boundary draws. So the sharp layer is masked out over the
   * last 48px of itself. The stops are at the photograph's edges rather than at
   * the band's, which is what `--o-up-hero-frame` is for; when the photograph
   * is wider than the band those stops fall off-screen and the mask is a no-op,
   * so this never vignettes a full-bleed hero. */
  html[data-ui="upgrade"] .homepage .carousel-item.parallax .s_parallax_bg::after {
    --o-up-hero-frame: calc(var(--o-up-hero-visible) * 1.783);
    background-size: auto calc(var(--o-up-hero-visible) * 2.148);
    /* The `86px` is the header, and it is a lead not a fudge. This section was
       written while `<header>` still rendered inside a wrapper `<div>`, which
       kept the origin's `#wrapwrap > header { position: absolute }` from
       matching: the bar was static, the band began under it, and centring the
       athlete on the band centred him on clear space. `Captured` makes the
       header a direct child again, so it overlays the band's first 86px and
       centring on the band puts his cap 45px behind the navigation — measured
       at 1440x900, subject top 41px against an 86px bar. Centre him on what
       the bar leaves instead. */
    background-position: 50% calc(86px + var(--o-up-hero-visible) * -0.7168);
    -webkit-mask-image: linear-gradient(
      to right,
      transparent calc(50% - var(--o-up-hero-frame) / 2),
      #000 calc(50% - var(--o-up-hero-frame) / 2 + 48px),
      #000 calc(50% + var(--o-up-hero-frame) / 2 - 48px),
      transparent calc(50% + var(--o-up-hero-frame) / 2)
    );
    mask-image: linear-gradient(
      to right,
      transparent calc(50% - var(--o-up-hero-frame) / 2),
      #000 calc(50% - var(--o-up-hero-frame) / 2 + 48px),
      #000 calc(50% + var(--o-up-hero-frame) / 2 - 48px),
      transparent calc(50% + var(--o-up-hero-frame) / 2)
    );
  }
}

/* ---- 10.2 · The hero, on arrival ----------------------------------
 * The origin pulses the whole content block once. Two words set at 4.5rem and
 * display-2 deserve to arrive as two words, so the block's own animation is
 * dropped and its paragraphs carry it instead — including the empty ones the
 * editor left behind, which cost nothing to animate and mean this does not
 * depend on counting them.
 *
 * `.o_animate` is `visibility: hidden` until `OdooCompat` reveals it, so these
 * play behind that gate exactly as the origin's did.
 * ------------------------------------------------------------------ */
@keyframes o-up-rise {
  from {
    opacity: 0;
    transform: translate3d(0, 1.15rem, 0);
    filter: blur(7px);
  }
  to {
    opacity: 1;
    transform: none;
    filter: blur(0);
  }
}

html[data-ui="upgrade"] .homepage .carousel-item.parallax .carousel-content {
  position: relative;
  animation-name: none;
}

html[data-ui="upgrade"] .homepage .carousel-item.parallax .carousel-content > * {
  animation: o-up-rise 1s cubic-bezier(0.16, 1, 0.3, 1) both;
}

html[data-ui="upgrade"] .homepage .carousel-item.parallax .carousel-content > :nth-child(1) { animation-delay: 0.04s; }
html[data-ui="upgrade"] .homepage .carousel-item.parallax .carousel-content > :nth-child(2) { animation-delay: 0.12s; }
html[data-ui="upgrade"] .homepage .carousel-item.parallax .carousel-content > :nth-child(3) { animation-delay: 0.22s; }
html[data-ui="upgrade"] .homepage .carousel-item.parallax .carousel-content > :nth-child(4) { animation-delay: 0.3s; }
html[data-ui="upgrade"] .homepage .carousel-item.parallax .carousel-content > :nth-child(5) { animation-delay: 0.36s; }
html[data-ui="upgrade"] .homepage .carousel-item.parallax .carousel-content > :nth-child(n + 6) { animation-delay: 0.42s; }

/* ---- 10.3 · Scroll-driven ----------------------------------------- */
@supports (animation-timeline: view()) {
  /* The photograph pushes in as the hero leaves. 10.1 leaves the athlete 5%
     clear at the top and bottom of the band, so 1.06 spends 3% of that at
     either end and he is still whole when the hero has gone. */
  @keyframes o-up-hero-push {
    from { transform: scale(1); }
    to   { transform: scale(1.06); }
  }

  /* The wording leaves faster than the photograph. That difference is the whole
     effect — parallax is a ratio, not a movement. */
  @keyframes o-up-hero-lift {
    from { transform: translate3d(0, 0, 0); opacity: 1; }
    to   { transform: translate3d(0, -22%, 0); opacity: 0; }
  }

  @keyframes o-up-fade-out {
    from { opacity: 1; }
    to   { opacity: 0; }
  }

  @keyframes o-up-progress {
    from { transform: scaleX(0); }
    to   { transform: scaleX(1); }
  }

  @keyframes o-up-settle {
    from { transform: scale(1.13); clip-path: inset(14% 0 0 0); }
    to   { transform: scale(1); clip-path: inset(0 0 0 0); }
  }

  @keyframes o-up-drift {
    from { transform: translate3d(0, 4%, 0); }
    to   { transform: translate3d(0, -4%, 0); }
  }

  @keyframes o-up-enter {
    from { opacity: 0; transform: translate3d(0, 2.2rem, 0); }
    to   { opacity: 1; transform: none; }
  }

  @keyframes o-up-cue {
    from { background-position: 0 -20px, 0 0; }
    to   { background-position: 0 56px, 0 0; }
  }

  @media (min-width: 992px) {
    html[data-ui="upgrade"] .homepage .carousel-item.parallax .s_parallax_bg {
      animation: o-up-hero-push linear both;
      animation-timeline: scroll(root block);
      animation-range: 0 100svh;
    }
  }

  html[data-ui="upgrade"] .homepage .carousel-item.parallax .carousel-content {
    animation: o-up-hero-lift linear both;
    animation-timeline: scroll(root block);
    animation-range: 0 82svh;
  }

  /* The scroll cue: a rail with a segment running down it, gone by the time the
     first scroll has answered the question it asks.
     Anchored to the window rather than to the hero, because the hero is 908px
     and a laptop viewport is not — pinned to the section's own bottom edge the
     cue lands below the fold on exactly the screens that most need it. */
  html[data-ui="upgrade"] #wrapwrap.homepage::before {
    content: "";
    position: fixed;
    left: 50%;
    bottom: clamp(18px, 4vh, 46px);
    width: 1px;
    height: 56px;
    margin-left: -0.5px;
    pointer-events: none;
    background-image:
      linear-gradient(to bottom, rgba(17, 17, 17, 0), #111, rgba(17, 17, 17, 0)),
      linear-gradient(to bottom, rgba(17, 17, 17, 0.16), rgba(17, 17, 17, 0.16));
    background-size: 1px 20px, 1px 100%;
    background-repeat: no-repeat;
    background-position: 0 -20px, 0 0;
    animation:
      o-up-cue 2.6s cubic-bezier(0.65, 0, 0.35, 1) infinite,
      o-up-fade-out linear both;
    animation-timeline: auto, scroll(root block);
    animation-range: normal, 0 16svh;
    z-index: 1039;
  }

  /* Where you are in the page, as a hairline. Above the header, below the
     search modal and the offcanvas. */
  html[data-ui="upgrade"] #wrapwrap.homepage::after {
    content: "";
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 2px;
    z-index: 1040;
    background: #111;
    transform: scaleX(0);
    transform-origin: 0 50%;
    pointer-events: none;
    animation: o-up-progress linear both;
    animation-timeline: scroll(root block);
  }

  /* The two full-bleed panels. They arrive over-scaled and settle, which is the
     difference between a photograph that is placed and one that lands. */
  html[data-ui="upgrade"] .homepage .s_quadrant .o_grid_item_image {
    overflow: hidden;
  }

  html[data-ui="upgrade"] .homepage .s_quadrant .o_grid_item_image img {
    animation: o-up-settle linear both;
    animation-timeline: view(block);
    animation-range: entry 0% cover 55%;
  }

  /* The origin's pulse on the category buttons is a 1% twitch that reads as a
     rendering artefact next to the panel behind it. Same trigger, same duration
     budget, a movement that means "arrived". */
  html[data-ui="upgrade"] .homepage .s_quadrant .btn.o_animate,
  html[data-ui="upgrade"] .homepage .s_image_text_box .btn.o_animate {
    animation-name: o-up-enter !important;
    animation-duration: 0.85s !important;
    animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1) !important;
  }

  /* The jersey block: the photograph moves against its own column. */
  html[data-ui="upgrade"] .homepage .s_image_text_box .o_grid_item_image {
    overflow: hidden;
  }

  html[data-ui="upgrade"] .homepage .s_image_text_box .o_grid_item_image img {
    animation: o-up-drift linear both;
    animation-timeline: view(block);
    animation-range: cover 0% cover 100%;
    scale: 1.1;
  }

  /* The leggings carousel already cross-fades on a timer; this gives the frame
     behind it somewhere to go. */
  html[data-ui="upgrade"] .homepage .s_carousel_wrapper.o_half_screen_height .carousel-item {
    animation: o-up-hero-push linear both;
    animation-timeline: view(block);
    animation-range: cover 0% cover 100%;
  }
}

/* ---- 10.4 · Asked for less ----------------------------------------
 * Section 9 flattens durations, which is enough for a keyframe that plays once.
 * A scroll-driven animation has no duration to flatten — it is bound to the
 * scroller — so the binding itself has to go, and the transforms with it. The
 * framing rules above are not motion and stay.
 * ------------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
  html[data-ui="upgrade"] .homepage .carousel-item.parallax .s_parallax_bg,
  html[data-ui="upgrade"] .homepage .carousel-item.parallax .carousel-content,
  html[data-ui="upgrade"] .homepage .carousel-item.parallax .carousel-content > *,
  html[data-ui="upgrade"] #wrapwrap.homepage::before,
  html[data-ui="upgrade"] #wrapwrap.homepage::after,
  html[data-ui="upgrade"] .homepage .s_quadrant .o_grid_item_image img,
  html[data-ui="upgrade"] .homepage .s_image_text_box .o_grid_item_image img,
  html[data-ui="upgrade"] .homepage .s_carousel_wrapper.o_half_screen_height .carousel-item {
    animation-timeline: auto !important;
    animation-name: none !important;
    transform: none !important;
    clip-path: none !important;
    opacity: 1 !important;
    filter: none !important;
    scale: 1 !important;
  }

  /* Without its animation the progress hairline would sit at scaleX(0) and
     paint nothing, which is correct — but say so rather than rely on it. */
  html[data-ui="upgrade"] #wrapwrap.homepage::before,
  html[data-ui="upgrade"] #wrapwrap.homepage::after {
    display: none;
  }
}

/* ==================================================================
 * 11 · The header mega menu
 *
 * The origin's own `user_custom_rules.scss` pins this panel:
 *
 *     #top_menu .o_mega_menu_container_size {
 *       width: 580px !important; min-width: 580px !important;
 *       max-width: 580px !important; left: 0 !important;
 *       transform: none !important; }
 *
 * over the top of Odoo's own rule, which is still in `dour.css` and still
 * says `left: 50%; transform: translateX(-50%); max-width: 1290px`. The
 * markup inside was authored against that stock width: a twelve-column
 * grid holding two items, at columns 1-4 and 6-10, over five rows of 50px.
 *
 * Clamped to 580px those two columns measure 137px and 183px, so a third of
 * the panel stands empty on the right; the five forced rows outlast the
 * links by ~85px, and the `padding-bottom` under them is 0. Measured against
 * the live site at a 1440px viewport the replica reproduces all of it to the
 * pixel — 580x268 for MEN, 580x218 for WOMEN — so what follows corrects the
 * origin rather than a drift away from it.
 *
 * Three changes: unpin the width so the panel is as wide as its own content,
 * leave grid mode for a flex row so the panel ends where the links end, and
 * make the menu item the containing block so the panel hangs under the word
 * that opened it rather than under the left edge of the whole nav.
 *
 * Desktop only: this markup lives in the `d-none d-lg-block` navbar, and the
 * offcanvas copy of MEN and WOMEN carries no `.dropdown-menu` at all — true
 * of the origin too, and not something a stylesheet can fix.
 *
 * `DOUR_UI=origin` still serves the 580px box, so `tools/compare-live.mjs`
 * measures what it always did.
 * ================================================================== */
@media (min-width: 992px) {
  /* The origin marks these items `position-static`, which is what makes the
     panel's `left: 0` resolve against the whole of `#top_menu`. SALE is
     already `position: relative` by way of Bootstrap's `.dropdown`. */
  html[data-ui="upgrade"] #top_menu > li.nav-item.dropdown {
    position: relative !important;
  }

  html[data-ui="upgrade"] #top_menu .o_mega_menu_container_size {
    width: max-content !important;
    min-width: 0 !important;
    /* 1290px is Odoo's own ceiling for this panel, in `dour.css`. */
    max-width: min(1290px, calc(100vw - var(--u-6) * 2)) !important;
    margin-left: 0 !important;
  }

  /* `.container` would re-impose a fixed width under a panel that no longer
     has one, and with it the twelve-column basis. 1.1rem is the padding the
     origin already gives every navbar item, so the panel's first heading
     lands directly under the word that opened it. */
  html[data-ui="upgrade"] .o_mega_menu .s_mega_menu_odoo_menu > .container {
    width: auto !important;
    max-width: none !important;
    padding-right: 1.1rem;
    padding-left: 1.1rem;
  }

  /* The origin's `pt16` has no `pb` behind it — at 580px the spare grid rows
     stood in for one. With the rows gone the panel needs a real bottom. */
  html[data-ui="upgrade"] .o_mega_menu .s_mega_menu_odoo_menu {
    padding-bottom: var(--u-5);
  }

  /* Out of grid mode. The inline `grid-area` and the `col-lg-*` widths both
     describe positions in a track that no longer exists, so both go. */
  html[data-ui="upgrade"] .o_mega_menu .row.o_grid_mode {
    display: flex !important;
    flex-wrap: nowrap;
    align-items: flex-start;
    /* The row's negative gutter and the items' matching padding cancelled out
       inside a 12-column track. Out of it they only push the first heading
       off the nav word above it, so both go and `column-gap` carries the
       whole gap: ~64px, against the origin's one empty track plus gutters. */
    margin-right: 0 !important;
    margin-left: 0 !important;
    column-gap: calc(var(--u-6) * 2);
  }

  html[data-ui="upgrade"] .o_mega_menu .row.o_grid_mode > .o_grid_item {
    grid-area: auto !important;
    flex: 0 0 auto;
    width: auto !important;
    max-width: none !important;
    min-height: 0 !important;
    padding-right: 0 !important;
    padding-left: 0 !important;
  }
}

/* ==================================================================
 * 12 · The homepage on a phone, and the brand background
 *
 * Section 10 framed the hero and gave it motion, but every rule that does so
 * sits inside `@media (min-width: 992px)`. Below that breakpoint the origin
 * swaps to a second carousel — `d-lg-none`, `o_full_screen_height`, its own
 * `pt256 pb120` — and none of section 10 reaches it. Measured at 390x844:
 *
 *     band          907px   (pt256 + ten 50px rows + pb120)
 *     header         70px   static on this breakpoint, so the band starts at 70
 *     drawn photo   753x907 under `cover`
 *     window        51.8% of the image width, anchored on the frame's centre
 *
 * The subject is 44.6% of the image width, so it fits — but the window is
 * centred on the *frame* at 50% while the athlete is centred at 47.1%, which
 * spends the whole margin on the wrong side: 0.7% of clearance at his trailing
 * shoe against 6.5% of waste past his leading hand. 0.7% of 1594px is eleven
 * pixels, which is the shoe touching the edge at 390px and cut through at 360px
 * and below. That is the crop that is left.
 *
 * The fix is section 10.1's, turned ninety degrees. There the band is wider
 * than the photograph is tall and the reference dimension is the height; here
 * it is narrower than the photograph is wide and the reference is the width.
 * Same two layers, same blurred margin behind the same file, same derivation
 * from the same measured subject box.
 *
 * THE SUBJECT BOX, measured off the file rather than assumed — the numbers
 * section 10 quotes vertically, plus the horizontal pair it never needed:
 *
 *     x   24.8% -> 69.4%   width  44.6%   centre 47.10%
 *     y   35.7% -> 77.5%   height 41.8%   centre 56.60%
 *
 * Sampled by thresholding luma against the studio sweep. The trailing shoe's
 * sole and sock are near-white and the sweep behind them is luma 248, so they
 * are invisible to that test — which is exactly why they are also invisible as
 * a crop, and why the black upper at 24.8% is the edge that matters.
 * ================================================================== */

/* ---- 12.1 · The hero, framed to the athlete on a phone -------------
 * Put the subject's *width* at 86% of the band's, and the arithmetic is forced
 * from there:
 *
 *     photo width  = 0.86 / 0.446         = 1.928  of the band  -> 192.8%
 *     photo height = 1.928 / (1594/1920)  = 2.3223 of the band's width
 *
 * `background-size` percentages resolve against the positioning area, so the
 * width is unit-free and correct at every phone size without a single vw.
 *
 * The horizontal anchor is the one percentage that puts image point 47.10% at
 * the band's centre. With the image 1.928x the band, a `p%` position offsets
 * its left edge by -0.928 * band * p/100, so:
 *
 *     (0.5 + 0.00928p) / 1.928 = 0.4710   ->   p = 44%
 *
 * which leaves 3.6% of the frame clear at each of his edges — symmetric, and
 * the same at 320px as at 430px because both terms scale with the band.
 *
 * The vertical anchor is a length, because it centres him in the part of the
 * band that is *on screen* rather than in the band: the band is 907px and no
 * phone is, so framing to the band puts him under the fold on the screens that
 * prompted this. His centre sits 0.566 down a frame 2.3223 band-widths tall,
 * hence the single multiplication.
 * ------------------------------------------------------------------ */
@media (max-width: 991.98px) {
  html[data-ui="upgrade"] .homepage .s_carousel_wrapper.d-lg-none .carousel-item.parallax .s_parallax_bg {
    --o-up-hero-visible: min(907px, calc(100svh - 70px));
    --o-up-hero-frame-h: calc(100vw * 2.3223);
    /* `70px` is the phone header, for the reason 10.1's `86px` is the desktop
       one: the bar overlays the band's first rows, so the athlete is centred on
       what it leaves rather than on the band. */
    --o-up-hero-frame-y: calc(70px + var(--o-up-hero-visible) * 0.5 - 100vw * 1.3144);
    background-size: 0 0;
  }

  html[data-ui="upgrade"] .homepage .s_carousel_wrapper.d-lg-none .carousel-item.parallax .s_parallax_bg::before,
  html[data-ui="upgrade"] .homepage .s_carousel_wrapper.d-lg-none .carousel-item.parallax .s_parallax_bg::after {
    content: "";
    position: absolute;
    inset: 0;
    background-image: inherit;
    background-repeat: no-repeat;
  }

  /* The margin, which on a phone lands under the photograph rather than beside
     it. Same file, anchored on the same 44% column, so the tone the blurred
     layer carries at the join is the tone the sharp one ends on. */
  html[data-ui="upgrade"] .homepage .s_carousel_wrapper.d-lg-none .carousel-item.parallax .s_parallax_bg::before {
    background-size: cover;
    background-position: 44% 50%;
    filter: blur(36px) saturate(1.03) brightness(0.99);
    transform: scale(1.08);
  }

  /* The photograph, and the join. The mask stops are the photograph's own top
     and bottom edges, so when the frame overflows the band — which it does at
     390px and up — both stops fall outside and the mask is a no-op. */
  html[data-ui="upgrade"] .homepage .s_carousel_wrapper.d-lg-none .carousel-item.parallax .s_parallax_bg::after {
    background-size: 192.8% auto;
    background-position: 44% var(--o-up-hero-frame-y);
    -webkit-mask-image: linear-gradient(
      to bottom,
      transparent var(--o-up-hero-frame-y),
      #000 calc(var(--o-up-hero-frame-y) + 44px),
      #000 calc(var(--o-up-hero-frame-y) + var(--o-up-hero-frame-h) - 44px),
      transparent calc(var(--o-up-hero-frame-y) + var(--o-up-hero-frame-h))
    );
    mask-image: linear-gradient(
      to bottom,
      transparent var(--o-up-hero-frame-y),
      #000 calc(var(--o-up-hero-frame-y) + 44px),
      #000 calc(var(--o-up-hero-frame-y) + var(--o-up-hero-frame-h) - 44px),
      transparent calc(var(--o-up-hero-frame-y) + var(--o-up-hero-frame-h))
    );
  }
}

/* ---- 12.2 · The hero's motion, on a phone --------------------------
 * `o-up-hero-push` is section 10.3's and is gated to the desktop breakpoint
 * with the rest of it, so the phone hero is the one surface on the page whose
 * background does not move at all. 12.1 leaves the athlete 3.6% clear at each
 * side and roughly 8% top and bottom, so 1.04 is what that clearance affords.
 * ------------------------------------------------------------------ */
@supports (animation-timeline: scroll(root block)) {
  @keyframes o-up-hero-push-sm {
    from { transform: scale(1); }
    to   { transform: scale(1.04); }
  }

  @media (max-width: 991.98px) {
    html[data-ui="upgrade"] .homepage .s_carousel_wrapper.d-lg-none .carousel-item.parallax .s_parallax_bg {
      animation: o-up-hero-push-sm linear both;
      animation-timeline: scroll(root block);
      animation-range: 0 100svh;
    }
  }
}

/* ---- 12.3 · The brand background ----------------------------------
 * The page between the photographs is flat #fff, and flat #fff is the one
 * surface a studio brand never actually has. Two layers put the studio back
 * without adding a colour, a font or a node:
 *
 *   GRAIN — an feTurbulence tile at 3.8%, stepped through six offsets so it
 *   boils the way film does. Stepped on `transform`, not `background-position`,
 *   so it is a composited layer rather than a full-viewport repaint six times a
 *   second; the layer is oversized 25% so the offsets never expose an edge.
 *
 *   SWEEP — two soft neutral pools, one high-left and one low-right, drifting
 *   and opening out as the page scrolls. It is the hero's own studio falloff
 *   continued past the hero, which is the whole reason it reads as the brand's
 *   background rather than as a gradient someone liked.
 *
 * Both hang off `body`, whose `::before` and `::after` the origin never uses
 * (checked: zero occurrences in `dour.css`), at `z-index: -1`. `body`'s white
 * propagates to the canvas, so a negative layer paints above that white and
 * below every box in the page; `#wrapwrap` is `position: relative; z-index: 0`
 * and transparent, as are the sections inside it, so the texture shows through
 * the empty white and is occluded by the photographs and the footer — which is
 * the correct place for it to stop.
 *
 * Scoped with `:has()` to the homepage: this is a landing-page background, and
 * a product grid is not the place to put texture behind the merchandise.
 *
 * No scroll listener, here or anywhere in this app — the sweep is bound to the
 * scroller by `animation-timeline` and runs off the main thread.
 * ------------------------------------------------------------------ */
html[data-ui="upgrade"] body:has(#wrapwrap.homepage)::before,
html[data-ui="upgrade"] body:has(#wrapwrap.homepage)::after {
  content: "";
  position: fixed;
  z-index: -1;
  pointer-events: none;
}

html[data-ui="upgrade"] body:has(#wrapwrap.homepage)::before {
  inset: -25%;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.82' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23g)'/%3E%3C/svg%3E");
  background-repeat: repeat;
  opacity: 0.038;
}

html[data-ui="upgrade"] body:has(#wrapwrap.homepage)::after {
  inset: -20% -10%;
  background-image:
    radial-gradient(58% 40% at 20% 10%, rgba(17, 17, 17, 0.055), rgba(17, 17, 17, 0) 70%),
    radial-gradient(50% 36% at 84% 76%, rgba(17, 17, 17, 0.045), rgba(17, 17, 17, 0) 72%);
}

@supports (animation-timeline: scroll(root block)) {
  @keyframes o-up-grain {
    0%   { transform: translate3d(0, 0, 0); }
    20%  { transform: translate3d(-3%, 2%, 0); }
    40%  { transform: translate3d(2%, -3%, 0); }
    60%  { transform: translate3d(-2%, -2%, 0); }
    80%  { transform: translate3d(3%, 1%, 0); }
    100% { transform: translate3d(0, 0, 0); }
  }

  @keyframes o-up-sweep {
    from { transform: translate3d(0, -4%, 0) scale(1); }
    to   { transform: translate3d(0, 6%, 0) scale(1.09); }
  }

  html[data-ui="upgrade"] body:has(#wrapwrap.homepage)::before {
    animation: o-up-grain 1.4s steps(1) infinite;
  }

  html[data-ui="upgrade"] body:has(#wrapwrap.homepage)::after {
    animation: o-up-sweep linear both;
    animation-timeline: scroll(root block);
  }
}

/* ---- 12.4 · Asked for less ----------------------------------------
 * Same reasoning as 10.4: a scroll-bound animation has no duration to flatten,
 * so the binding goes. The grain's *texture* is not motion and stays — only its
 * boil stops; the sweep is a static gradient once unbound and stays too.
 * ------------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
  html[data-ui="upgrade"] .homepage .s_carousel_wrapper.d-lg-none .carousel-item.parallax .s_parallax_bg,
  html[data-ui="upgrade"] body:has(#wrapwrap.homepage)::before,
  html[data-ui="upgrade"] body:has(#wrapwrap.homepage)::after {
    animation-timeline: auto !important;
    animation-name: none !important;
    transform: none !important;
  }
}

/* ==================================================================
 * 13 · Tier 4 — the finish
 *
 * Asked for on 2026-09-10: a more professional, finished surface — the type,
 * the alignment, animated scrolling, the product grid and the product page,
 * the dropdown boards, the photographs and how they arrive, and the smoothness
 * of every transition.
 *
 * OVERRULE, recorded beside the rule it overrules. The preamble of this file
 * says "no new fonts". The origin names Roboto; the live site loads it from
 * an asset this rebuild never mirrored, so here there was no @font-face for it
 * anywhere in `dour.css` and every visitor read their operating system's
 * fallback — on Windows, Arial. The owner asked for the type to be dealt with,
 * so from here on the site is set in Inter (reading and interface) and Barlow
 * Condensed (display), loaded from the document head. "No new hues" still
 * holds: every colour below is one the origin already uses.
 *
 * Two motion primitives run through everything that follows. `--u-out` is a
 * long, decelerating ease for things that arrive; the existing `--u-ease` is
 * for things that respond. Individual transform properties (`translate`,
 * `scale`) are used for hover states wherever section 10 already owns an
 * element's `transform` through a scroll-driven animation — an animation on
 * `transform` would otherwise win over the hover for as long as the page is
 * open.
 * ================================================================== */
html[data-ui="upgrade"] {
  --font-sans-serif: "Inter", "Roboto", "Segoe UI", "Noto Sans Bengali", "Nirmala UI", system-ui,
    sans-serif;
  --u-display: "Barlow Condensed", "Inter", "Arial Narrow", sans-serif;
  --u-ink: var(--900, #292121);
  --u-ink-2: var(--700, #574949);
  --u-ink-3: var(--600, #7d6c6c);
  --u-navy: var(--o-color-1, #00203f);
  --u-deep: var(--o-color-5, #14212e);
  --u-paper: var(--white, #fff);
  --u-mist: var(--100, #faf8f8);
  --u-out: cubic-bezier(0.16, 1, 0.3, 1);
  --u-shadow-1: 0 1px 2px rgba(41, 33, 33, 0.05), 0 10px 28px -14px rgba(41, 33, 33, 0.18);
  --u-shadow-2: 0 2px 6px rgba(41, 33, 33, 0.06), 0 28px 56px -20px rgba(41, 33, 33, 0.28);
  --u-header-h: 86px;
  scroll-behavior: smooth;
}

@media (max-width: 991.98px) {
  html[data-ui="upgrade"] {
    --u-header-h: 70px;
  }
}

/* ---- 13.1 · Type --------------------------------------------------- */
html[data-ui="upgrade"] body {
  font-family: var(--font-sans-serif);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

html[data-ui="upgrade"] :is(h1, .h1, h2, .h2, .display-1, .display-2, .display-3, .display-3-fs, .category-title) {
  font-family: var(--u-display);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.01em;
  line-height: 0.95;
}

html[data-ui="upgrade"] :is(h1, .h1) {
  font-size: clamp(2.25rem, 1.6rem + 2.2vw, 3.25rem);
}

html[data-ui="upgrade"] :is(h2, .h2) {
  font-size: clamp(1.75rem, 1.35rem + 1.4vw, 2.5rem);
}

html[data-ui="upgrade"] :is(h3, .h3, h4, .h4, h5, .h5, h6, .h6) {
  font-weight: 600;
  letter-spacing: -0.01em;
}

html[data-ui="upgrade"] :is(.oe_currency_value, .product_price, .o_up_count, .o_up_sg td) {
  font-variant-numeric: tabular-nums;
}

/* The origin sets the navigation and every button in Instrument Sans, a
 * third face beside Roboto. One interface face: Inter carries both. */
html[data-ui="upgrade"] .navbar .nav-link,
html[data-ui="upgrade"] .navbar-brand,
html[data-ui="upgrade"] .btn {
  font-family: var(--font-sans-serif);
}

/* ---- 13.2 · Buttons, refined --------------------------------------- */
html[data-ui="upgrade"] .btn-custom {
  border-radius: 999px !important;
  padding: 0 30px !important;
  min-height: 52px;
  font-family: var(--font-sans-serif);
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  border-width: 1px !important;
  transition:
    background-color 0.22s var(--u-ease),
    color 0.22s var(--u-ease),
    border-color 0.22s var(--u-ease),
    box-shadow 0.3s var(--u-ease),
    translate 0.3s var(--u-out);
}

html[data-ui="upgrade"] .btn-custom strong {
  font-weight: 600;
}

html[data-ui="upgrade"] .btn-custom.bg-black {
  background-color: #111 !important;
  border-color: #111 !important;
  color: #fff !important;
}

html[data-ui="upgrade"] .btn-custom.bg-black:hover,
html[data-ui="upgrade"] .btn-custom.bg-black:focus-visible {
  background-color: var(--u-navy) !important;
  border-color: var(--u-navy) !important;
  color: #fff !important;
  box-shadow: 0 16px 32px -14px rgba(0, 32, 63, 0.55);
  translate: 0 -2px;
}

html[data-ui="upgrade"] .btn-primary {
  border-radius: 999px;
  letter-spacing: 0.04em;
}

html[data-ui="upgrade"] .btn .fa-arrow-right,
html[data-ui="upgrade"] .btn .fa-angle-double-right {
  display: inline-block;
  transition: translate 0.28s var(--u-out);
}

html[data-ui="upgrade"] .btn:hover .fa-arrow-right,
html[data-ui="upgrade"] .btn:hover .fa-angle-double-right {
  translate: 4px 0;
}

/* ==================================================================
 * 14 · The header
 * ================================================================== */
html[data-ui="upgrade"] #wrapwrap > header > .navbar {
  transition:
    background-color 0.28s var(--u-ease),
    box-shadow 0.28s var(--u-ease);
}

html[data-ui="upgrade"] #top_menu > .nav-item > .nav-link {
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 0.55rem 0.95rem !important;
  position: relative;
}

html[data-ui="upgrade"] #top_menu > .nav-item > .nav-link > span {
  position: relative;
  display: inline-block;
}

/* The underline draws from the left on hover and stays while the panel is
 * open, so the word and its board read as one object. */
html[data-ui="upgrade"] #top_menu > .nav-item > .nav-link > span::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -5px;
  height: 2px;
  border-radius: 2px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: 0 50%;
  transition: transform 0.32s var(--u-out);
}

html[data-ui="upgrade"] #top_menu > .nav-item:hover > .nav-link > span::after,
html[data-ui="upgrade"] #top_menu > .nav-item.show > .nav-link > span::after,
html[data-ui="upgrade"] #top_menu > .nav-item > .nav-link.active > span::after {
  transform: scaleX(1);
}

html[data-ui="upgrade"] #top_menu > .nav-item > .dropdown-toggle::after {
  margin-left: 0.3em;
  vertical-align: 0.2em;
  border-top-width: 0.32em;
  border-right-width: 0.32em;
  border-left-width: 0.32em;
  opacity: 0.55;
  transition:
    transform 0.26s var(--u-out),
    opacity 0.2s var(--u-ease);
}

html[data-ui="upgrade"] #top_menu > .nav-item.show > .dropdown-toggle::after {
  transform: rotate(180deg);
  opacity: 1;
}

/* The origin's theme draws a stray rule before WOMEN and nowhere else. */
html[data-ui="upgrade"] #o_main_nav #top_menu > li:nth-child(2) > a.nav-link::before {
  display: none;
}

/* The cart, the search and the account: three equal round targets. */
html[data-ui="upgrade"] #o_main_nav .navbar-nav.align-items-center > li > a.btn.rounded-circle,
html[data-ui="upgrade"] #o_main_nav .o_navlink_background_hover > div.btn.rounded-circle {
  width: 40px;
  height: 40px;
  min-height: 40px;
  padding: 0 !important;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  transition:
    background-color 0.18s var(--u-ease),
    transform 0.18s var(--u-ease);
}

html[data-ui="upgrade"] #o_main_nav .navbar-nav.align-items-center > li > a.btn.rounded-circle:hover,
html[data-ui="upgrade"] #o_main_nav .o_navlink_background_hover:hover > div.btn.rounded-circle {
  background-color: color-mix(in srgb, currentColor 9%, transparent);
}

html[data-ui="upgrade"] #o_main_nav .o_navlink_background_hover {
  padding: 0 !important;
  min-height: 0;
}

html[data-ui="upgrade"] .my_cart_quantity {
  font-size: 0.625rem;
  font-weight: 700;
  min-width: 18px;
  height: 18px;
  line-height: 18px;
  padding: 0 5px;
  border-radius: 999px;
  background: var(--u-navy) !important;
  box-shadow: 0 0 0 2px var(--u-paper);
}

/* Affixed: the bar that comes back after the first scroll is glass, so the
 * page reads through it and the shadow says it floats. */
html[data-ui="upgrade"] #wrapwrap > header.o_header_affixed {
  z-index: 1030;
}

html[data-ui="upgrade"] #wrapwrap > header.o_header_affixed > .navbar {
  background-color: rgba(255, 255, 255, 0.86) !important;
  -webkit-backdrop-filter: saturate(1.6) blur(16px);
  backdrop-filter: saturate(1.6) blur(16px);
  box-shadow:
    0 1px 0 rgba(41, 33, 33, 0.06),
    0 12px 32px -20px rgba(41, 33, 33, 0.35) !important;
}

html[data-ui="upgrade"] #wrapwrap > header .navbar-brand img {
  transition: height 0.28s var(--u-out);
}

/* ---- 14.1 · The mega menu boards ----------------------------------- */
@keyframes o-up-menu-in {
  from {
    opacity: 0;
    transform: translateY(-10px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@media (min-width: 992px) {
  html[data-ui="upgrade"] #top_menu .dropdown-menu.o_mega_menu {
    border: 0 !important;
    border-radius: 0 0 22px 22px;
    background: var(--u-paper) !important;
    box-shadow: var(--u-shadow-2);
    overflow: hidden;
  }

  html[data-ui="upgrade"] #top_menu .dropdown-menu.show {
    animation: o-up-menu-in 0.3s var(--u-out) both;
  }

  html[data-ui="upgrade"] .o_mega_menu .s_mega_menu_odoo_menu {
    padding: 28px 12px 30px !important;
  }

  html[data-ui="upgrade"] .o_mega_menu .row.o_grid_mode {
    column-gap: 52px;
  }

  html[data-ui="upgrade"] .o_mega_menu .o_grid_item {
    min-width: 172px;
  }

  html[data-ui="upgrade"] .o_mega_menu h4 {
    font-family: var(--u-display);
    font-size: 1.3125rem !important;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    margin: 0 0 2px !important;
  }

  html[data-ui="upgrade"] .o_mega_menu h4 .nav-link {
    color: var(--u-ink) !important;
  }

  html[data-ui="upgrade"] .o_mega_menu .s_hr {
    padding: 6px 0 12px !important;
  }

  html[data-ui="upgrade"] .o_mega_menu .s_hr hr {
    width: 28px !important;
    margin: 0 !important;
    border-top: 2px solid var(--u-navy) !important;
    opacity: 1;
  }

  html[data-ui="upgrade"] .o_mega_menu nav.nav .nav-link {
    padding: 7px 0 !important;
    font-size: 0.9375rem;
    font-weight: 500;
    color: var(--u-ink-2) !important;
    transition:
      color 0.16s var(--u-ease),
      translate 0.3s var(--u-out);
  }

  html[data-ui="upgrade"] .o_mega_menu nav.nav .nav-link:hover {
    color: var(--u-ink) !important;
    translate: 6px 0;
  }

  /* The photograph. `Chrome` appends one per panel from the homepage's own
   * boards; the word over it is the word that opened the panel. */
  html[data-ui="upgrade"] .o_up_menu_tile {
    position: relative;
    display: block;
    flex: 0 0 auto;
    width: 216px;
    height: 272px;
    border-radius: 16px;
    overflow: hidden;
    background: var(--u-mist);
    text-decoration: none !important;
    isolation: isolate;
  }

  html[data-ui="upgrade"] .o_up_menu_tile img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: 50% 18%;
    transition: transform 1.1s var(--u-out);
  }

  html[data-ui="upgrade"] .o_up_menu_tile:hover img {
    transform: scale(1.06);
  }

  html[data-ui="upgrade"] .o_up_menu_tile_text {
    position: absolute;
    inset: auto 0 0 0;
    padding: 28px 16px 16px;
    display: flex;
    flex-direction: column;
    gap: 3px;
    color: #fff;
    background: linear-gradient(to top, rgba(20, 33, 46, 0.8), rgba(20, 33, 46, 0));
  }

  html[data-ui="upgrade"] .o_up_menu_tile_kicker {
    font-family: var(--u-display);
    font-size: 1.75rem;
    font-weight: 700;
    line-height: 1;
    letter-spacing: 0.02em;
    text-transform: uppercase;
  }

  html[data-ui="upgrade"] .o_up_menu_tile_cta {
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    opacity: 0.92;
  }

  html[data-ui="upgrade"] .o_up_menu_tile_cta::after {
    content: "\2192";
    display: inline-block;
    margin-left: 6px;
    transition: translate 0.3s var(--u-out);
  }

  html[data-ui="upgrade"] .o_up_menu_tile:hover .o_up_menu_tile_cta::after {
    translate: 4px 0;
  }
}

/* ---- 14.2 · Plain dropdowns: SALE, and the sort control ------------ */
html[data-ui="upgrade"] .dropdown-menu:not(.o_mega_menu):not(.o_up_mobile_mega) {
  border: 0;
  border-radius: 14px;
  padding: 8px;
  min-width: 200px;
  background: var(--u-paper);
  box-shadow: var(--u-shadow-2);
}

html[data-ui="upgrade"] .dropdown-menu:not(.o_mega_menu):not(.o_up_mobile_mega).show {
  animation: o-up-menu-in 0.26s var(--u-out) both;
}

html[data-ui="upgrade"] .dropdown-menu:not(.o_mega_menu) .dropdown-item {
  border-radius: 9px;
  padding: 10px 12px;
  font-size: 0.875rem;
  font-weight: 500;
  transition:
    background-color 0.14s var(--u-ease),
    color 0.14s var(--u-ease);
}

html[data-ui="upgrade"] .dropdown-menu:not(.o_mega_menu) .dropdown-item:hover,
html[data-ui="upgrade"] .dropdown-menu:not(.o_mega_menu) .dropdown-item:focus {
  background: var(--200, #efe9e9) !important;
  color: var(--u-ink) !important;
}

html[data-ui="upgrade"] .dropdown-menu:not(.o_mega_menu) .dropdown-item.active {
  background: var(--u-navy) !important;
  color: #fff !important;
}

/* ---- 14.3 · Search ------------------------------------------------- */
html[data-ui="upgrade"] #o_search_modal .modal-content {
  border: 0;
  border-radius: 20px;
  padding: 10px;
  box-shadow: var(--u-shadow-2);
}

html[data-ui="upgrade"] #o_search_modal .oe_search_box {
  font-size: 1.125rem;
  padding: 18px 22px !important;
  border-radius: 14px 0 0 14px !important;
  background: var(--u-mist) !important;
}

html[data-ui="upgrade"] #o_search_modal .oe_search_button {
  border-radius: 0 14px 14px 0 !important;
  background: var(--u-navy) !important;
  color: #fff !important;
  padding: 0 26px !important;
}

html[data-ui="upgrade"] .modal-backdrop {
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
}

html[data-ui="upgrade"] .modal.fade .modal-dialog {
  transition:
    transform 0.34s var(--u-out),
    opacity 0.34s var(--u-ease);
  transform: translateY(-14px) scale(0.985);
}

html[data-ui="upgrade"] .modal.show .modal-dialog {
  transform: none;
}

/* ---- 14.4 · The phone menu ----------------------------------------- */
@media (max-width: 991.98px) {
  html[data-ui="upgrade"] .o_navbar_mobile {
    width: min(88vw, 400px);
    background: rgba(255, 255, 255, 0.96);
    -webkit-backdrop-filter: blur(20px);
    backdrop-filter: blur(20px);
  }

  html[data-ui="upgrade"] .o_navbar_mobile .offcanvas-body {
    padding: 8px 24px 28px;
  }

  html[data-ui="upgrade"] .o_navbar_mobile .top_menu > .nav-item > .nav-link {
    font-family: var(--u-display);
    font-size: 1.75rem;
    font-weight: 700;
    letter-spacing: 0.02em;
    text-transform: uppercase;
    padding: 12px 0;
    border-bottom: 1px solid var(--u-line);
  }

  /* The offcanvas centres its items and sizes them to their words, so a
   * list under MEN was narrower than the word above it. A menu is a column. */
  html[data-ui="upgrade"] .o_navbar_mobile .navbar-nav {
    align-items: stretch !important;
  }

  html[data-ui="upgrade"] .o_navbar_mobile .top_menu > .nav-item {
    width: 100%;
  }

  /* Bootstrap stamps `data-bs-popper="static"` on a static dropdown when it
   * opens, and the stylesheet's `top: 100%` for that attribute then pushes
   * an in-flow list down by the height of its container — the origin's own
   * SALE list here lands below the fold the same way. In flow means in flow. */
  html[data-ui="upgrade"] .o_navbar_mobile .dropdown-menu,
  html[data-ui="upgrade"] .o_navbar_mobile .dropdown-menu[data-bs-popper] {
    position: relative !important;
    top: auto !important;
    left: auto !important;
    margin: 0 !important;
    width: 100%;
    min-width: 0;
    border: 0 !important;
    border-radius: 0 !important;
    padding: 4px 0 12px 6px !important;
    background: transparent !important;
    box-shadow: none !important;
    animation: none !important;
  }

  html[data-ui="upgrade"] .o_navbar_mobile .dropdown-item {
    padding: 9px 8px;
    font-size: 1rem;
  }

  html[data-ui="upgrade"] .o_up_mobile_mega_head .dropdown-item {
    margin-top: 8px;
    font-size: 0.6875rem;
    font-weight: 700;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--u-ink-3) !important;
  }

  html[data-ui="upgrade"] .o_navbar_mobile .oe_search_box {
    padding: 12px 18px !important;
  }
}

/* ==================================================================
 * 15 · The homepage boards
 * ================================================================== */

/* ---- 15.1 · The hero wording --------------------------------------- */
html[data-ui="upgrade"] .homepage .carousel-item.parallax .carousel-content {
  display: flex;
  flex-direction: column;
  justify-content: center;
}

html[data-ui="upgrade"] .homepage .carousel-item.parallax .carousel-content > p {
  margin: 0;
  font-family: var(--u-display);
  font-weight: 800;
  text-transform: uppercase;
  line-height: 0.88;
  letter-spacing: -0.005em;
}

/* The editor left empty paragraphs and an empty heading as spacers. The
 * spacing is set here instead, so they go. */
html[data-ui="upgrade"] .homepage .carousel-item.parallax .carousel-content > p:not(:has(font, .btn, .h2-fs)),
html[data-ui="upgrade"] .homepage .carousel-item.parallax .carousel-content > h1:not(:has(font)) {
  display: none;
}

html[data-ui="upgrade"] .homepage .carousel-item.parallax .carousel-content > p > span[style*="font-size"],
html[data-ui="upgrade"] .homepage .carousel-item.parallax .carousel-content > p.display-2 {
  font-size: clamp(4.5rem, 2.6rem + 6.4vw, 8.5rem) !important;
  font-weight: 800;
  line-height: 0.88;
}

/* 13vw: at 375px that is 49px, which keeps GRAVITY short of the athlete's
 * leg where the two share a line; 17vw ran the word into his shorts. */
html[data-ui="upgrade"] .homepage .carousel-item.parallax .carousel-content .h2-fs {
  display: block;
  font-size: clamp(3rem, 13vw, 5rem) !important;
  font-weight: 800;
  line-height: 0.9;
}

html[data-ui="upgrade"] .homepage .carousel-item.parallax .carousel-content .h2-fs strong {
  font-weight: 800;
}

html[data-ui="upgrade"] .homepage .carousel-item.parallax .carousel-content > p:has(> .btn) {
  margin-top: 30px !important;
}

/* ---- 15.2 · The two boards ----------------------------------------- */
html[data-ui="upgrade"] .homepage .s_quadrant .o_grid_item_image {
  overflow: hidden;
}

html[data-ui="upgrade"] .homepage .s_quadrant .o_grid_item_image img {
  transition: scale 1.2s var(--u-out);
}

html[data-ui="upgrade"] .homepage .s_quadrant .o_grid_item_image:hover img {
  scale: 1.04;
}

html[data-ui="upgrade"] .homepage .s_quadrant .btn.btn-custom {
  min-width: 168px;
  justify-content: center;
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
}

/* ---- 15.3 · The jersey block --------------------------------------- */
html[data-ui="upgrade"] .homepage .s_image_text_box[data-snippet] {
  background-color: transparent !important;
  padding-top: 72px !important;
  padding-bottom: 72px !important;
}

/* The editor's grid is ten fixed 50px rows. A heading set at display size
 * no longer fits the rows with the copy under it, and the block's background
 * stops at the track while the text runs on. Let the rows take the height of
 * what is in them. */
html[data-ui="upgrade"] .homepage .s_image_text_box .row.o_grid_mode {
  grid-auto-rows: minmax(50px, auto) !important;
}

html[data-ui="upgrade"] .homepage .s_image_text_box .o_grid_item.o_cc2 {
  --grid-item-padding-y: 96px !important;
  --grid-item-padding-x: 56px !important;
  /* The block carries Bootstrap's `.rounded`, which is `!important`. */
  border-radius: 28px !important;
  background-color: var(--u-mist) !important;
}

html[data-ui="upgrade"] .homepage .s_image_text_box .o_grid_item.o_cc2 h4 {
  font-family: var(--u-display);
  font-size: clamp(1.9rem, 1.4rem + 1.6vw, 2.75rem);
  font-weight: 700;
  line-height: 0.95;
  letter-spacing: 0.01em;
  text-transform: uppercase;
  max-width: 17ch;
  margin-bottom: 20px;
}

html[data-ui="upgrade"] .homepage .s_image_text_box .o_grid_item.o_cc2 p {
  line-height: 1.6;
}

html[data-ui="upgrade"] .homepage .s_image_text_box .o_grid_item.o_cc2 .text-600 {
  color: var(--u-ink-2) !important;
}

html[data-ui="upgrade"] .homepage .s_image_text_box .o_grid_item_image {
  border-radius: 22px;
  overflow: hidden;
  box-shadow: var(--u-shadow-2) !important;
}

/* ---- 15.4 · The photograph carousel -------------------------------- */
html[data-ui="upgrade"] .homepage .s_carousel_wrapper.o_half_screen_height .carousel-fade .carousel-item {
  transition-duration: 1.2s !important;
}

html[data-ui="upgrade"] .homepage .s_carousel_wrapper.o_half_screen_height .o_we_bg_filter {
  background: linear-gradient(
    100deg,
    rgba(20, 33, 46, 0.64) 0%,
    rgba(20, 33, 46, 0.3) 55%,
    rgba(20, 33, 46, 0.06) 100%
  ) !important;
}

/* Three lines at most: the band is 250px of grid for the wording and the
 * button together, so the size is capped where the longest caption still
 * breaks three times at 992px and up. */
html[data-ui="upgrade"] .homepage .s_carousel_wrapper.o_half_screen_height h2 {
  max-width: 19ch;
  margin-bottom: 20px;
  color: #fff;
  line-height: 0.92;
}

html[data-ui="upgrade"] .homepage .s_carousel_wrapper.o_half_screen_height h2 span {
  font-size: clamp(2.125rem, 1.4rem + 2.1vw, 3.5rem) !important;
}

html[data-ui="upgrade"] .homepage .s_carousel_wrapper.o_half_screen_height .carousel-content p {
  margin: 0;
}

html[data-ui="upgrade"] .homepage .s_carousel_wrapper.o_half_screen_height .btn-primary {
  background: #fff !important;
  border-color: #fff !important;
  color: var(--u-ink) !important;
  padding-inline: 28px;
}

html[data-ui="upgrade"] .homepage .s_carousel_wrapper.o_half_screen_height .btn-primary:hover {
  background: var(--u-navy) !important;
  border-color: var(--u-navy) !important;
  color: #fff !important;
}

/* The origin hides the indicators; three photographs deserve a place marker.
 * Slim rails, the current one longer. */
html[data-ui="upgrade"] .homepage .s_carousel_wrapper.o_half_screen_height .carousel-indicators.s_carousel_indicators_hidden {
  display: flex !important;
  margin-bottom: 22px;
}

html[data-ui="upgrade"] .homepage .s_carousel_wrapper.o_half_screen_height .carousel-indicators [data-bs-target] {
  width: 26px;
  height: 3px;
  margin: 0 4px;
  border: 0;
  border-radius: 3px;
  background: #fff;
  opacity: 0.45;
  transition:
    width 0.3s var(--u-out),
    opacity 0.3s var(--u-ease);
}

html[data-ui="upgrade"] .homepage .s_carousel_wrapper.o_half_screen_height .carousel-indicators [data-bs-target].active {
  width: 44px;
  opacity: 1;
}

/* ==================================================================
 * 16 · The product grid
 * ================================================================== */
html[data-ui="upgrade"] .o_wsale_products_page .container.oe_website_sale {
  padding-top: 28px !important;
}

html[data-ui="upgrade"] #products_grid .breadcrumb {
  margin-bottom: 10px !important;
}

html[data-ui="upgrade"] #products_grid .breadcrumb-item,
html[data-ui="upgrade"] #products_grid .breadcrumb-item a {
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--u-ink-3);
}

html[data-ui="upgrade"] #products_grid .breadcrumb-item a:hover {
  color: var(--u-ink);
}

html[data-ui="upgrade"] #products_grid .category-title {
  font-size: clamp(2.5rem, 1.9rem + 2.4vw, 3.75rem);
  margin: 0 0 8px;
  text-align: left;
}

html[data-ui="upgrade"] #products_grid .products_header {
  margin-bottom: 26px !important;
  padding-bottom: 16px;
  border-bottom: 1px solid var(--u-line);
}

html[data-ui="upgrade"] .o_up_count {
  margin-right: auto;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--u-ink-3);
}

html[data-ui="upgrade"] .products_header .o_sortby_dropdown.me-auto {
  margin-right: 0 !important;
}

html[data-ui="upgrade"] .products_header .btn-light {
  min-height: 40px;
  padding: 0 16px !important;
  border: 1px solid var(--u-line) !important;
  border-radius: 999px !important;
  background: transparent !important;
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--u-ink) !important;
  transition:
    border-color 0.16s var(--u-ease),
    background-color 0.16s var(--u-ease);
}

html[data-ui="upgrade"] .products_header .btn-light:hover {
  border-color: var(--u-ink) !important;
  background: var(--u-mist) !important;
}

html[data-ui="upgrade"] .products_header .dropdown-toggle .text-muted {
  margin-right: 4px;
  color: var(--u-ink-3) !important;
}

/* ---- 16.1 · The filter rail ---------------------------------------- */
@media (min-width: 992px) {
  /* The origin serves the desktop rail `visually-hidden`, and on the live
   * site it stays that way after load — the column is there, 330px of it,
   * with nothing in it (measured 2026-09-10 at 1440px: 0×0 after five
   * seconds). Whatever the theme intended, a shopper at a desktop had no
   * filters. Shown here in the stylesheet so it never waits on a script;
   * `UpgradeLayer` drops the class as well. */
  html[data-ui="upgrade"] .o_wsale_products_grid_before_rail.visually-hidden {
    position: static !important;
    width: auto !important;
    height: auto !important;
    margin: 0 !important;
    overflow: visible !important;
    clip: auto !important;
    clip-path: none !important;
    white-space: normal !important;
    border: 0 !important;
  }

  html[data-ui="upgrade"] .o_wsale_products_grid_before_rail {
    height: auto !important;
    max-height: calc(100vh - var(--u-header-h) - 32px);
    padding: 4px 8px 40px !important;
    margin: 0 !important;
    overflow-y: auto !important;
  }

  html[data-ui="upgrade"] #products_grid_before {
    top: calc(var(--u-header-h) + 20px) !important;
    padding-right: 36px !important;
  }
}

html[data-ui="upgrade"] .products_attributes_filters .accordion-item {
  border: 0 !important;
  margin: 0 !important;
  background: transparent;
}

html[data-ui="upgrade"] .products_attributes_filters .accordion-button {
  padding: 14px 0 !important;
  border-bottom: 1px solid var(--u-line) !important;
  border-radius: 0 !important;
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--u-ink) !important;
}

html[data-ui="upgrade"] .products_attributes_filters .accordion-button::after {
  width: 0.8rem;
  height: 0.8rem;
  background-size: 0.8rem;
  opacity: 0.6;
}

html[data-ui="upgrade"] .products_attributes_filters .accordion-collapse > div {
  padding: 14px 0 8px;
}

html[data-ui="upgrade"] .products_attributes_filters label.css_attribute_color {
  width: 26px;
  height: 26px;
  margin: 3px !important;
  padding: 2px;
  border: 1px solid var(--u-line);
  border-radius: 50%;
  background-clip: content-box;
  cursor: pointer;
  transition:
    transform 0.16s var(--u-ease),
    border-color 0.16s var(--u-ease);
}

html[data-ui="upgrade"] .products_attributes_filters label.css_attribute_color:hover {
  transform: scale(1.12);
  border-color: var(--u-ink-3);
}

html[data-ui="upgrade"] .products_attributes_filters label.css_attribute_color:has(input:checked),
html[data-ui="upgrade"] .products_attributes_filters label.css_attribute_color.active {
  padding: 2px;
  border: 2px solid var(--u-navy);
  box-shadow: none;
}

html[data-ui="upgrade"] .products_attributes_filters label.css_attribute_color input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
  margin: 0;
}

/* Sizes as chips, the same control the product page uses for the same choice. */
html[data-ui="upgrade"] .products_attributes_filters .form-check {
  display: inline-block;
  padding: 0;
  margin: 0 6px 6px 0 !important;
  min-height: 0;
}

html[data-ui="upgrade"] .products_attributes_filters .form-check-input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
  margin: 0;
  pointer-events: none;
}

html[data-ui="upgrade"] .products_attributes_filters .form-check-label {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 42px;
  min-height: 34px;
  padding: 0 10px;
  border: 1px solid var(--u-line);
  border-radius: 9px;
  font-size: 0.8125rem;
  font-weight: 500 !important;
  cursor: pointer;
  transition:
    border-color 0.14s var(--u-ease),
    background-color 0.14s var(--u-ease),
    color 0.14s var(--u-ease);
}

html[data-ui="upgrade"] .products_attributes_filters .form-check-label:hover {
  border-color: var(--u-ink);
}

html[data-ui="upgrade"] .products_attributes_filters .form-check:has(:checked) .form-check-label {
  background: var(--u-navy);
  border-color: var(--u-navy);
  color: #fff;
}

html[data-ui="upgrade"] .products_attributes_filters .form-check-input:focus-visible + .form-check-label {
  outline: 2px solid var(--u-focus);
  outline-offset: 2px;
}

/* ---- 16.2 · The cards ---------------------------------------------- */
/* The gap is an inline custom property on the grid, so the value has to be
 * important to be read at all. */
html[data-ui="upgrade"] #products_grid .o_wsale_products_grid_table {
  --o-wsale-products-grid-gap: 22px !important;
}

/* The card is the origin's own panel. Odoo's "cards" design already draws a
 * white box with a 1px hairline around the photograph *and* the information,
 * and it exposes that box as custom properties — so the geometry is set
 * through them rather than fought with `!important`, and one value still means
 * one thing. What the origin chose was 0.4rem of radius and 0.5rem of padding,
 * which put the product name 2px from the hairline and the price 6px above it;
 * these are the same panel with room in it.
 *
 * `--o-wsale-card-thumb-border-radius` keeps the origin's shape: the
 * photograph *is* the card's top edge, square where it meets the words, and a
 * pixel tighter than the panel so the border curves outside it rather than
 * through it. */
html[data-ui="upgrade"] .o_wsale_design_cards {
  --o-wsale-card-border-radius: 16px;
  --o-wsale-card-info-padding: 13px 14px 15px;
  --o-wsale-card-thumb-border-radius: 15px 15px 0 0;
}

/* Section 5 sets a padding-top on every information block and would otherwise
 * win the shorthand's top edge back; this reads the same variable, so the
 * padding above still has one source. */
html[data-ui="upgrade"] .o_wsale_design_cards .oe_product_cart .o_wsale_product_information {
  padding: var(--o-wsale-card-info-padding);
}

/* Border and ground, both graded, in one paint: the fill is clipped to the
 * padding box and the border colour to the border box, so a 1px transparent
 * border shows the second gradient and nothing else does. The fill stays paper
 * behind the photograph and falls to the theme's mist under the words, which
 * gives the card a bottom without drawing a line across it; the border is
 * darkest at the corners the light would catch. */
html[data-ui="upgrade"] .o_wsale_design_cards .oe_product_cart {
  border: 1px solid transparent !important;
  background:
    linear-gradient(180deg, var(--u-paper) 0%, var(--u-paper) 60%, var(--u-mist) 100%) padding-box,
    linear-gradient(
        158deg,
        rgba(41, 33, 33, 0.17) 0%,
        rgba(41, 33, 33, 0.08) 34%,
        rgba(41, 33, 33, 0.07) 62%,
        rgba(41, 33, 33, 0.15) 100%
      )
      border-box !important;
  box-shadow: 0 1px 2px rgba(41, 33, 33, 0.04);
  overflow: hidden;
  transition:
    box-shadow 0.35s var(--u-ease),
    background 0.35s var(--u-ease),
    translate 0.35s var(--u-out);
}

/* Hover lifts the panel and turns the hairline towards the brand navy. The
 * keyboard gets the same card, because `:focus-within` reaches the product
 * link and the wishlist button both. */
html[data-ui="upgrade"] .o_wsale_design_cards .oe_product:hover .oe_product_cart,
html[data-ui="upgrade"] .o_wsale_design_cards .oe_product:focus-within .oe_product_cart {
  translate: 0 -2px;
  box-shadow: var(--u-shadow-1);
  background:
    linear-gradient(180deg, var(--u-paper) 0%, var(--u-paper) 55%, var(--u-mist) 100%) padding-box,
    linear-gradient(
        158deg,
        rgba(0, 32, 63, 0.3) 0%,
        rgba(0, 32, 63, 0.14) 34%,
        rgba(41, 33, 33, 0.12) 62%,
        rgba(41, 33, 33, 0.2) 100%
      )
      border-box !important;
}

@media (prefers-reduced-motion: reduce) {
  html[data-ui="upgrade"] .o_wsale_design_cards .oe_product:hover .oe_product_cart,
  html[data-ui="upgrade"] .o_wsale_design_cards .oe_product:focus-within .oe_product_cart {
    translate: none;
  }
}

/* Section 5 lifted the whole card by the wrapper; the panel does it now. */
html[data-ui="upgrade"] .oe_product .o_wsale_product_grid_wrapper,
html[data-ui="upgrade"] .oe_product:hover .o_wsale_product_grid_wrapper {
  border-radius: 0;
  overflow: visible;
  transform: none;
  box-shadow: none;
  transition: none;
}

html[data-ui="upgrade"] .o_wsale_design_cards .oe_product_image {
  background: var(--u-paper);
  box-shadow: none !important;
}

/* One hairline where the photograph meets the words, at the weight the panel's
 * border settles to in the middle, so the two read as one drawing. */
html[data-ui="upgrade"] .o_wsale_design_cards .oe_product_image::after {
  content: "";
  position: absolute;
  inset: auto 0 0;
  height: 1px;
  z-index: 1;
  background: rgba(41, 33, 33, 0.07);
  pointer-events: none;
}

html[data-ui="upgrade"] .oe_product .oe_product_image img {
  transition: transform 0.9s var(--u-out);
}

html[data-ui="upgrade"] .oe_product:hover .oe_product_image img {
  transform: scale(1.05);
}

/* The heart. The origin already floats it over the photograph's bottom-left
 * corner, hidden until the card is hovered, with an id-strength selector; the
 * corner and the hover are kept and only restated to move it to the right and
 * to make it reachable where there is no hover. `bottom: calc(100% + 10px)`
 * is measured from the information block the origin positions it against, so
 * the heart sits 10px inside the photograph's bottom edge. */
html[data-ui="upgrade"] #products_grid .o_wsale_products_grid_table .oe_product .o_wsale_product_btn {
  position: absolute !important;
  top: auto !important;
  left: auto !important;
  right: 10px !important;
  bottom: calc(100% + 10px) !important;
  z-index: 3;
  opacity: 0;
  translate: 0 4px;
  transition:
    opacity 0.22s var(--u-ease),
    translate 0.3s var(--u-out);
}

html[data-ui="upgrade"] #products_grid .o_wsale_products_grid_table .oe_product:hover .o_wsale_product_btn,
html[data-ui="upgrade"] #products_grid .o_wsale_products_grid_table .oe_product .o_wsale_product_btn:focus-within,
html[data-ui="upgrade"] #products_grid .o_wsale_products_grid_table .oe_product .o_wsale_product_btn:has(.o_wish_added) {
  opacity: 1 !important;
  translate: 0 0;
}

@media (hover: none) {
  html[data-ui="upgrade"] #products_grid .o_wsale_products_grid_table .oe_product .o_wsale_product_btn {
    opacity: 1 !important;
    translate: 0 0;
  }
}

html[data-ui="upgrade"] .oe_product .o_add_wishlist {
  width: 36px;
  height: 36px;
  min-height: 36px;
  padding: 0 !important;
  border: 0 !important;
  border-radius: 50% !important;
  background: rgba(255, 255, 255, 0.94) !important;
  color: var(--u-ink) !important;
  box-shadow: var(--u-shadow-1);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

html[data-ui="upgrade"] .oe_product .o_add_wishlist .fa {
  font-size: 15px;
  line-height: 1;
}

html[data-ui="upgrade"] .oe_product .o_add_wishlist:hover,
html[data-ui="upgrade"] .oe_product .o_add_wishlist.o_wish_added {
  color: var(--u-navy) !important;
}

html[data-ui="upgrade"] .oe_product .o_wsale_products_item_title {
  margin-bottom: 3px !important;
}

html[data-ui="upgrade"] .oe_product .o_wsale_products_item_title a {
  font-size: 0.9375rem !important;
  font-weight: 600;
  line-height: 1.3;
  color: var(--u-ink) !important;
  transition: color 0.16s var(--u-ease);
}

html[data-ui="upgrade"] .oe_product .o_wsale_products_item_title a:hover {
  color: var(--u-navy) !important;
}

html[data-ui="upgrade"] .oe_product .o_wsale_product_sub {
  margin-top: 6px;
}

html[data-ui="upgrade"] .oe_product .product_price .h6 {
  font-size: 0.9375rem !important;
  font-weight: 600;
  color: var(--u-ink);
}

/* ==================================================================
 * 17 · The product page
 * ================================================================== */
html[data-ui="upgrade"] #product_detail {
  padding-top: 10px;
}

html[data-ui="upgrade"] #product_detail .o_wsale_breadcrumb .breadcrumb-item,
html[data-ui="upgrade"] #product_detail .o_wsale_breadcrumb .breadcrumb-item a {
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--u-ink-3);
}

html[data-ui="upgrade"] #product_detail .o_wsale_breadcrumb .breadcrumb-item a:hover {
  color: var(--u-ink);
}

@media (min-width: 992px) {
  html[data-ui="upgrade"] #product_detail_main > .o_wsale_product_images {
    padding-right: 36px;
  }

  html[data-ui="upgrade"] #product_details {
    padding-left: 28px;
  }

  html[data-ui="upgrade"] #o-carousel-product {
    top: calc(var(--u-header-h) + 20px) !important;
  }
}

/* ---- 17.1 · The gallery -------------------------------------------- */
html[data-ui="upgrade"] #o-carousel-product {
  gap: 14px;
}

html[data-ui="upgrade"] #o-carousel-product .o_carousel_product_outer {
  border-radius: 18px;
  background: var(--u-paper);
  overflow: hidden;
}

html[data-ui="upgrade"] #o-carousel-product .o_carousel_product_outer::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  border-radius: inherit;
  box-shadow: inset 0 0 0 1px rgba(41, 33, 33, 0.08);
  pointer-events: none;
}

html[data-ui="upgrade"] #o-carousel-product.carousel-fade .carousel-item {
  transition: opacity 0.5s var(--u-ease) !important;
  transform: none !important;
}

html[data-ui="upgrade"] #o-carousel-product .carousel-control-prev,
html[data-ui="upgrade"] #o-carousel-product .carousel-control-next {
  width: 64px;
  opacity: 0;
  transition: opacity 0.25s var(--u-ease);
}

html[data-ui="upgrade"] #o-carousel-product:hover .carousel-control-prev,
html[data-ui="upgrade"] #o-carousel-product:hover .carousel-control-next,
html[data-ui="upgrade"] #o-carousel-product .carousel-control-prev:focus-visible,
html[data-ui="upgrade"] #o-carousel-product .carousel-control-next:focus-visible {
  opacity: 1;
}

html[data-ui="upgrade"] #o-carousel-product .carousel-control-prev i.oi,
html[data-ui="upgrade"] #o-carousel-product .carousel-control-next i.oi {
  width: 44px;
  height: 44px;
  border: 0 !important;
  border-radius: 50% !important;
  background: rgba(255, 255, 255, 0.95) !important;
  color: var(--u-ink) !important;
  box-shadow: var(--u-shadow-1);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 15px;
  transition: transform 0.22s var(--u-out);
}

html[data-ui="upgrade"] #o-carousel-product .carousel-control-prev:hover i.oi,
html[data-ui="upgrade"] #o-carousel-product .carousel-control-next:hover i.oi {
  transform: scale(1.08);
}

html[data-ui="upgrade"] .o_carousel_product_indicators .carousel-indicators {
  gap: 8px !important;
  margin: 0 !important;
  padding: 0 !important;
}

html[data-ui="upgrade"] .o_carousel_product_indicators .carousel-indicators > li {
  width: 64px !important;
  height: 80px !important;
  border: 1px solid var(--u-line) !important;
  border-radius: 10px !important;
  background: var(--u-paper);
  opacity: 0.7;
  overflow: hidden !important;
  cursor: pointer;
  transition:
    opacity 0.2s var(--u-ease),
    border-color 0.2s var(--u-ease),
    box-shadow 0.2s var(--u-ease);
}

html[data-ui="upgrade"] .o_carousel_product_indicators .carousel-indicators > li:hover {
  opacity: 1;
  border-color: var(--u-ink-3) !important;
}

html[data-ui="upgrade"] .o_carousel_product_indicators .carousel-indicators > li.active {
  opacity: 1;
  border-color: var(--u-navy) !important;
  box-shadow: 0 0 0 1px var(--u-navy);
}

html[data-ui="upgrade"] .o_carousel_product_indicators .carousel-indicators > li img {
  width: 100% !important;
  height: 100% !important;
  object-fit: cover !important;
  border-radius: 0;
}

/* ---- 17.2 · The details column ------------------------------------- */
html[data-ui="upgrade"] #product_details h1 {
  font-size: clamp(2rem, 1.5rem + 2vw, 3rem);
  margin: 0 0 10px;
}

html[data-ui="upgrade"] #product_details .product_price {
  margin: 4px 0 22px !important;
}

html[data-ui="upgrade"] #product_details .product_price h3 {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
  margin: 0;
  font-family: var(--font-sans-serif);
  font-size: 1.625rem;
  font-weight: 700;
  letter-spacing: -0.01em;
}

html[data-ui="upgrade"] #product_details .product_price .oe_default_price {
  font-size: 1rem;
  font-weight: 500;
  color: var(--u-ink-3) !important;
}

html[data-ui="upgrade"] #product_details .product_price .h6.text-muted {
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 5px 10px;
  border-radius: 999px;
  background: var(--200, #efe9e9);
  color: var(--u-ink-2) !important;
}

html[data-ui="upgrade"] #product_details strong.attribute_name {
  margin-bottom: 8px;
}

html[data-ui="upgrade"] #o_wsale_cta_wrapper {
  gap: 12px;
  margin-top: 8px;
}

html[data-ui="upgrade"] #o_wsale_cta_wrapper .css_quantity {
  height: 52px;
  margin: 0 !important;
  border-radius: 999px;
}

html[data-ui="upgrade"] #o_wsale_cta_wrapper .css_quantity .btn {
  min-height: 50px;
}

html[data-ui="upgrade"] #add_to_cart_wrap {
  flex: 1 1 auto;
  margin: 0 !important;
}

html[data-ui="upgrade"] #add_to_cart {
  width: 100%;
  min-height: 52px;
  border-radius: 999px;
  font-size: 0.9375rem;
  letter-spacing: 0.04em;
  box-shadow: 0 12px 26px -14px rgba(0, 32, 63, 0.6);
}

html[data-ui="upgrade"] #product_details .js_product > .btn-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin: 10px 20px 0 0 !important;
  padding: 8px 0 !important;
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--u-ink-2) !important;
  text-decoration: none !important;
  transition: color 0.16s var(--u-ease);
}

html[data-ui="upgrade"] #product_details .js_product > .btn-link:hover {
  color: var(--u-ink) !important;
}

html[data-ui="upgrade"] .o_up_trust {
  list-style: none;
  margin: 22px 0 0;
  padding: 18px 0 0;
  border-top: 1px solid var(--u-line);
  display: grid;
  gap: 10px;
}

html[data-ui="upgrade"] .o_up_trust a {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  font-size: 0.8125rem;
  line-height: 1.45;
  color: var(--u-ink-2) !important;
  text-decoration: none !important;
}

html[data-ui="upgrade"] .o_up_trust a:hover span {
  color: var(--u-ink);
}

html[data-ui="upgrade"] .o_up_trust svg {
  width: 18px;
  height: 18px;
  flex: 0 0 auto;
  margin-top: 1px;
  color: var(--u-navy);
}

html[data-ui="upgrade"] .o_up_trust strong {
  font-weight: 600;
  color: var(--u-ink);
}

/* "Tags:" with no tags after it is a label for nothing. */
html[data-ui="upgrade"] .o_product_tags:not(:has(a, .badge, span:not(:empty))) {
  display: none !important;
}

html[data-ui="upgrade"] #product_attributes_simple .table {
  margin: 18px 0 0;
  font-size: 0.8125rem;
}

html[data-ui="upgrade"] #product_attributes_simple td {
  border: 0;
  padding: 2px 0;
  color: var(--u-ink-3);
}

/* ---- 17.3 · The description ---------------------------------------- */
html[data-ui="upgrade"] .custom-product-desc {
  margin-top: 56px;
  padding-top: 36px;
  border-top: 1px solid var(--u-line);
}

html[data-ui="upgrade"] .custom-product-desc h2 {
  margin-bottom: 20px !important;
}

html[data-ui="upgrade"] .custom-desc-content {
  font-size: 0.9375rem;
  line-height: 1.65;
  color: var(--u-ink-2);
}

html[data-ui="upgrade"] .custom-desc-content > p:first-child {
  font-size: 1.125rem;
  font-weight: 600;
  color: var(--u-ink);
}

html[data-ui="upgrade"] .custom-desc-content h3 {
  margin: 28px 0 10px;
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--u-ink);
}

html[data-ui="upgrade"] .custom-desc-content ul {
  padding-left: 1.1em;
}

html[data-ui="upgrade"] .custom-desc-content li p {
  margin: 0;
}

html[data-ui="upgrade"] .custom-desc-content b,
html[data-ui="upgrade"] .custom-desc-content strong {
  font-weight: 600;
  color: var(--u-ink);
}

/* "Products you may like" is a heading over a snippet the origin never
 * configured, so it heads nothing. */
html[data-ui="upgrade"] .o_wsale_product_page .container.my-5:has(.o_dynamic_snippet_empty) {
  display: none;
}

/* ---- 17.4 · The size guide dialog ---------------------------------- */
@keyframes o-up-pop {
  from {
    opacity: 0;
    transform: translateY(14px) scale(0.98);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@keyframes o-up-fade-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

html[data-ui="upgrade"] body.o_up_modal_open {
  overflow: hidden;
}

html[data-ui="upgrade"] .o_up_modal {
  position: fixed;
  inset: 0;
  z-index: 2000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: rgba(41, 33, 33, 0.55);
  -webkit-backdrop-filter: blur(5px);
  backdrop-filter: blur(5px);
  animation: o-up-fade-in 0.2s var(--u-ease) both;
}

html[data-ui="upgrade"] .o_up_modal_panel {
  position: relative;
  width: 100%;
  max-width: 640px;
  max-height: 90vh;
  overflow: auto;
  padding: 36px 32px 28px;
  border-radius: 22px;
  background: var(--u-paper);
  box-shadow: var(--u-shadow-2);
  animation: o-up-pop 0.34s var(--u-out) both;
}

html[data-ui="upgrade"] .o_up_modal_close {
  position: absolute;
  top: 14px;
  right: 14px;
  width: var(--u-target);
  height: var(--u-target);
  border: 0;
  border-radius: 50%;
  background: var(--u-mist);
  color: var(--u-ink);
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
  transition: background-color 0.16s var(--u-ease);
}

html[data-ui="upgrade"] .o_up_modal_close:hover {
  background: var(--200, #efe9e9);
}

html[data-ui="upgrade"] .o_up_sg .sg-title {
  font-family: var(--u-display);
  font-size: 1.75rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  text-align: left !important;
  margin-bottom: 6px !important;
}

html[data-ui="upgrade"] .o_up_sg .sg-instructions {
  text-align: left !important;
  font-size: 0.8125rem;
  line-height: 1.5;
  max-width: 48ch;
  margin-bottom: 20px !important;
}

html[data-ui="upgrade"] .o_up_sg .sg-toggle-btn {
  min-height: 34px;
  padding: 0 16px;
  margin: 0 0 0 6px !important;
  border: 1px solid var(--u-line) !important;
  border-radius: 999px;
  background: transparent !important;
  color: var(--u-ink-2) !important;
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.08em;
}

html[data-ui="upgrade"] .o_up_sg .sg-toggle-btn.active {
  background: var(--u-navy) !important;
  border-color: var(--u-navy) !important;
  color: #fff !important;
}

html[data-ui="upgrade"] .o_up_modal_panel[data-unit="inch"] .unit-cm,
html[data-ui="upgrade"] .o_up_modal_panel[data-unit="cm"] .unit-inch {
  display: none;
}

html[data-ui="upgrade"] .o_up_sg .table th {
  padding: 10px 8px;
  border: 0;
  border-bottom: 1px solid var(--u-line);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--u-ink-3);
}

html[data-ui="upgrade"] .o_up_sg .table td {
  padding: 10px 8px;
  border: 0;
  border-bottom: 1px solid var(--u-line);
  font-size: 0.9375rem;
}

html[data-ui="upgrade"] .o_up_sg .table td:first-child {
  font-weight: 600;
}

/* ==================================================================
 * 18 · Cart, checkout and the footer — the finish that carries over
 * ================================================================== */
html[data-ui="upgrade"] .o_cart_product img {
  border-radius: 10px;
}

html[data-ui="upgrade"] .oe_website_sale .o_total_card {
  border-radius: 16px;
  border-color: var(--u-line);
}

html[data-ui="upgrade"] .o_footer .dour-footer-block .dour-footer-heading {
  font-family: var(--u-display);
  font-size: 1.25rem;
  font-weight: 700;
  letter-spacing: 0.18em;
}

html[data-ui="upgrade"] .o_footer .dour-footer-links a {
  font-size: 0.9375rem;
  font-weight: 400;
  color: rgba(255, 255, 255, 0.8) !important;
}

html[data-ui="upgrade"] .o_footer .dour-license p {
  color: rgba(255, 255, 255, 0.8);
}

html[data-ui="upgrade"] .o_footer .dour-footer-icon {
  background: rgba(255, 255, 255, 0.08);
}

html[data-ui="upgrade"] .o_footer .s_embed_code.pt40 {
  padding-top: 64px !important;
}

html[data-ui="upgrade"] .o_footer .s_embed_code.pb16 {
  padding-bottom: 44px !important;
}

html[data-ui="upgrade"] .o_footer .custom-footer {
  padding: 22px 12px;
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  font-size: 0.8125rem;
}

/* ==================================================================
 * 19 · Arrival
 *
 * How things come onto the screen. Reveal on scroll (`UpgradeLayer` marks the
 * elements and flips `o_up_in` as they enter), photographs fading in as they
 * decode, and a cross-fade between pages through the browser's own
 * cross-document view transition — no script, and no page waits on it.
 * Everything under `html.o_up_js` is inert without JS, so nothing is ever
 * hidden that a script did not promise to show.
 * ================================================================== */
@view-transition {
  navigation: auto;
}

::view-transition-old(root) {
  animation-duration: 0.16s;
}

::view-transition-new(root) {
  animation-duration: 0.28s;
}

html.o_up_js[data-ui="upgrade"] .o_up_reveal {
  opacity: 0;
  translate: 0 22px;
  transition:
    opacity 0.7s var(--u-out),
    translate 0.9s var(--u-out);
  transition-delay: var(--o-up-delay, 0ms);
}

html.o_up_js[data-ui="upgrade"] .o_up_reveal.o_up_in {
  opacity: 1;
  translate: 0 0;
}

html.o_up_js[data-ui="upgrade"] img.o_up_img {
  opacity: 0;
  transition: opacity 0.6s var(--u-ease);
}

html.o_up_js[data-ui="upgrade"] img.o_up_img.o_up_loaded {
  opacity: 1;
}

html[data-ui="upgrade"] .o_up_top {
  position: fixed;
  right: 22px;
  bottom: 22px;
  z-index: 1035;
  width: 46px;
  height: 46px;
  border: 1px solid var(--u-line);
  border-radius: 50%;
  background: var(--u-paper);
  color: var(--u-ink);
  box-shadow: var(--u-shadow-2);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  opacity: 0;
  translate: 0 10px;
  pointer-events: none;
  transition:
    opacity 0.3s var(--u-ease),
    translate 0.4s var(--u-out),
    background-color 0.16s var(--u-ease);
}

html[data-ui="upgrade"] .o_up_top svg {
  width: 20px;
  height: 20px;
}

html[data-ui="upgrade"] .o_up_top.o_up_show {
  opacity: 1;
  translate: 0 0;
  pointer-events: auto;
}

html[data-ui="upgrade"] .o_up_top:hover {
  background: var(--u-mist);
}

@media (max-width: 767.98px) {
  html[data-ui="upgrade"] .o_up_top {
    display: none;
  }
}

/* The homepage's scroll cue and progress hairline are fixed at the window's
 * edges; under an open menu, search or dialog they are noise. */
html[data-ui="upgrade"] body:has(.offcanvas.show, .modal.show, .o_up_modal) #wrapwrap.homepage::before,
html[data-ui="upgrade"] body:has(.offcanvas.show, .modal.show, .o_up_modal) #wrapwrap.homepage::after {
  display: none;
}

/* ==================================================================
 * 20 · Phones
 * ================================================================== */
@media (max-width: 767.98px) {
  html[data-ui="upgrade"] #products_grid .o_wsale_products_grid_table {
    --o-wsale-products-grid-gap: 12px !important;
  }

  /* Two cards to a row at 390px is a 174px panel; the desktop radius and
   * padding read as fat on it. Same panel, one step down. */
  html[data-ui="upgrade"] .o_wsale_design_cards {
    --o-wsale-card-border-radius: 13px;
    --o-wsale-card-info-padding: 10px 11px 12px;
    --o-wsale-card-thumb-border-radius: 12px 12px 0 0;
  }

  html[data-ui="upgrade"] .oe_product .o_wsale_products_item_title a {
    font-size: 0.8125rem !important;
  }

  html[data-ui="upgrade"] .oe_product .product_price .h6 {
    font-size: 0.8125rem !important;
  }

  html[data-ui="upgrade"] #products_grid .category-title {
    font-size: 2.25rem;
  }

  /* The phone toolbar repeats the category name beside its back arrow, under
   * a heading that already says it. The arrow stays. */
  html[data-ui="upgrade"] .products_header > .d-lg-none > h4 {
    display: none;
  }

  html[data-ui="upgrade"] .products_header > .d-lg-none > .btn-light {
    width: 40px;
    padding: 0 !important;
    justify-content: center;
  }

  html[data-ui="upgrade"] .homepage .s_image_text_box .o_grid_item.o_cc2 {
    --grid-item-padding-y: 40px !important;
    --grid-item-padding-x: 24px !important;
    border-radius: 20px !important;
  }
}

@media (max-width: 991.98px) {
  html[data-ui="upgrade"] #o-carousel-product {
    position: static !important;
  }

  html[data-ui="upgrade"] #product_details {
    padding-top: 8px;
  }

  html[data-ui="upgrade"] #product_details h1 {
    font-size: 2rem;
  }

  html[data-ui="upgrade"] .o_up_modal_panel {
    padding: 28px 20px 20px;
  }
}

/* ==================================================================
 * 21 · Asked for less — the finish
 *
 * Section 9 already flattens every transition and keyframe. What it cannot
 * reach: the view transition, which is the browser's; and the reveal, which
 * `UpgradeLayer` does not apply at all when motion is reduced.
 * ================================================================== */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }

  html[data-ui="upgrade"] {
    scroll-behavior: auto;
  }
}

/* ==================================================================
 * 22b · Not offered, and not for sale
 *
 * The captured cart carries Odoo's coupon and gift-card controls. dour has
 * no codes to apply, and the form's own action used to answer a dead page,
 * so the control goes -- in the upgrade tier only. `DOUR_UI=origin` keeps
 * it, so `compare:live` still matches the origin's cart; the route behind
 * it now sends a shopper back to their bag unchanged.
 *
 * A product the back office has marked unsellable renders with the origin's
 * own `css_not_available` state, which `dour.css` already styles: the
 * stepper and the price go and "Not Available." appears. What that state
 * leaves standing is the button, so it goes here, and the sentence the
 * upgrade layer writes beside it when the bag refused an add gets its shape.
 * ================================================================== */
html[data-ui="upgrade"] .coupon_form,
html[data-ui="upgrade"] tr.oe_website_sale_gift_card {
  display: none;
}

html[data-ui="upgrade"] .js_product.css_not_available #add_to_cart_wrap {
  display: none;
}

html[data-ui="upgrade"] .o_up_stock_note {
  margin: 8px 0 0;
  font-size: 0.9rem;
  font-weight: 600;
  color: #dc3545;
}
