/* ------------------------------------------------------------
   THEME — taken from the Virzi MFG logo
   One field colour, one ink colour, thin ink outline, inverted
   panels, wide squared type. That's the whole system.

   Because the design is built from two complementary colours,
   light and dark mode are the same rules with --field and --ink
   swapped. Dark is the default; [data-theme="light"] on <html>
   flips it. Nothing below this block needs to know which is on.
------------------------------------------------------------ */
:root {
  --field: #000;        /* the page */
  --ink: #fff;          /* type, rules, outlines */
  --grey: #a8a8a8;      /* secondary text on the field */
  --line: #3a3a3a;      /* hairlines on the field */
  --paper: #e4e4e4;     /* photo placeholder inside a panel */
  --paper-ink: #555;    /* placeholder caption on --paper */
  --wash: rgba(0, 0, 0, .05);   /* hover wash inside a panel */
  --wash-ink: rgba(255, 255, 255, .12); /* hover wash on the field */

  --display: "Oxanium", "Eurostile", "Bank Gothic", "Arial Narrow", sans-serif;
  --body: "Barlow", "Helvetica Neue", Arial, sans-serif;

  --page-max: 1120px;                  /* width cap on the page column */
  --shop-max: calc(var(--page-max) * 1.2); /* the product boxes run 20% wider */
  --page: min(var(--page-max), 100% - 2.5rem); 
  --header-h: 64px;
  --gutter: clamp(1rem, 3vw, 2.5rem);  /* side gutter on the full-bleed bars */
  --hero-w: 1088px;                    /* displayed width of the hero logo */
  /* How long the whole page takes to change theme. Every surface, the
     type, the rules and the hero logo all read this one value, so they
     move in lockstep and nothing can drift out of step with the page.

     Note the trade-off if you change it: the two themes are exact
     inverses, so a cross-dissolve between them passes through flat grey.
     At the halfway point the background and everything on it are the same
     #808080 and the page briefly reads as blank. The longer this value,
     the longer that dead spot lasts — it is roughly imperceptible under
     300ms and clearly visible at 1s. */
  --theme-ms: 1s;
  /* The artwork used to sit inside a black margin, and this was how far
     in the visible frame began. The file is now cropped flush to the
     artwork, so it is zero — kept as a token because the header's
     measurements are written in terms of it. */
  --hero-frame: 0;
}

:root[data-theme="light"] {
  --field: #fff;
  --ink: #000;
  --grey: #5c5c5c;
  --line: #d4d4d4;
  --paper: #2a2a2a;
  --paper-ink: #9a9a9a;
  --wash: rgba(255, 255, 255, .07);
  --wash-ink: rgba(0, 0, 0, .08);
}

/* Colour changes are instant on load and eased on toggle; the class
   is added by JS only after the stored preference has been applied.

   .frame is deliberately NOT in this list. It carries data-reveal, and a
   transition shorthand here outranks the one in the reveal rule, which
   would replace it and leave the fade-in with nothing to animate. The
   reveal rule carries border-color at this same speed instead. */
html.theme-ready body,
html.theme-ready .site-header,
html.theme-ready .panel,
html.theme-ready .product__photo,
html.theme-ready .specs,
html.theme-ready .buybar,
html.theme-ready .copy,
html.theme-ready .themeswitch {
  transition: background-color var(--theme-ms) var(--ease, ease),
              color var(--theme-ms) var(--ease, ease),
              border-color var(--theme-ms) var(--ease, ease);
}

*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}

body {
  margin: 0;
  background: var(--field);
  color: var(--ink);
  font: 400 1.0625rem/1.6 var(--body);
  -webkit-font-smoothing: antialiased;
}

img { max-width: 100%; display: block; }
a { color: inherit; }
:focus-visible { outline: 3px solid var(--ink); outline-offset: 3px; }

.wrap { width: var(--page); margin-inline: auto; }
/* Shop is the one wider column; its title sits on the product box's edge. */
#shop .wrap { width: min(var(--shop-max), 100% - 2.5rem); }

/* The header and the sticky buy bar are full-bleed surfaces: their
   backgrounds already span the viewport, so their contents should sit
   against the viewport edges too, not inside the 1120px reading column
   that the page sections share. Both bars are overridden together so the
   two stay on the same gutter. */
.site-header .wrap,
.buybar .wrap {
  width: auto;
  margin-inline: 0;
  padding-inline: var(--gutter);
}

h1, h2, h3 {
  font-family: var(--display);
  font-weight: 800;
  line-height: 1;
  text-transform: uppercase;
  letter-spacing: 0.01em;
  margin: 0;
}

.skip {
  position: absolute; left: 1rem; top: -4rem;
  background: var(--ink); color: var(--field);
  padding: .6rem 1rem; font-weight: 600; z-index: 20;
}
.skip:focus { top: 1rem; }

/* ---------- Buttons ---------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center;
  min-height: 48px; padding: 0 1.6rem;
  border: 2px solid var(--ink);
  font: 700 1rem/1 var(--display);
  text-transform: uppercase; letter-spacing: .05em;
  text-decoration: none;
  transition: background-color .15s, color .15s;
}
.btn--solid { background: var(--ink); color: var(--field); }
.btn--solid:hover { background: var(--field); color: var(--ink); }
.btn--line { background: var(--field); color: var(--ink); }
.btn--line:hover { background: var(--ink); color: var(--field); }

/* ---------- Header ---------- */
.site-header {
  position: sticky; top: 0; z-index: 10;
  height: var(--header-h);
  background: var(--field);
  border-bottom: 1px solid var(--line);
}
.site-header .wrap {
  height: 100%;
  display: flex; align-items: center; justify-content: space-between;
}
/* The header is measured out against the hero logo below it.

   --mark-band is the strip between the left edge of the screen and the
   left edge of the hero logo's white frame. The logo is centred at
   --hero-w, and its frame starts --hero-frame into the artwork, so that
   edge is (viewport - --hero-w) / 2 + the inset. The viewport is
   100% + --gutter here, because the wrap's left padding is dropped below
   and only the right gutter remains. Centring the wordmark in a column of
   exactly that width lands it midway between the screen edge and the
   logo — derived, so it follows the logo at any window size instead of
   being a pixel value that only holds on one screen.

   max() stops the band collapsing under the wordmark on narrow screens.
   The nav keeps its own placement at the 3/5 mark, and the switch stays
   pinned right. Below 940px the flex rule above simply spreads them. */
@media (min-width: 940px) {
  .site-header .wrap {
    --mark-band: max(12rem, calc(
      (100% + var(--gutter) - var(--hero-w)) / 2 + var(--hero-w) * var(--hero-frame)
    ));

    display: grid;
    grid-template-columns:
      var(--mark-band) calc(60% - var(--mark-band)) auto minmax(0, 1fr);
    padding-left: 0; /* the wordmark is placed by the maths, not the gutter */
  }
  .site-header .mark { grid-column: 1; justify-self: center; }
  .site-header .nav { grid-column: 3; }
  .themeswitch { grid-column: 4; justify-self: end; }
}

/* The nav hangs off the geometry of the page below it: Shop centred on
   the hero logo's right edge, About centred on the right edge of the
   product box, and Contact one more step along, so the three sit on an
   even rhythm the layout itself sets rather than a chosen gap.

   --nav-step is the distance between those two edges, which is therefore
   also the distance between the item centres. Each item gets a column of
   that width and is centred in it, so the centres land on the step no
   matter what the individual words measure.

   1910px is where this stops being possible, not a taste call: the
   product box right edge is already at (viewport + 1344) / 2, so below
   that width Contact runs under the slider. The 940px rule above keeps
   its simpler 3/5 placement for narrower screens. */
@media (min-width: 1910px) {
  .site-header .wrap {
    --viewport: calc(100% + var(--gutter));

    /* The step is the distance between the hero logo's right edge and the
       product box's right edge. Both are at their caps above this
       breakpoint, so it is a constant — which it has to be, because it is
       read again inside .nav, where a percentage would resolve against
       the nav's own width and feed back into these very columns.

       The 7rem floor is doing real work now: at --hero-w: 1196px the two
       edges are only 16px apart, far too close to seat three words on.
       Below the floor the derived rhythm is gone and only About stays on
       its edge. Shrink --hero-w back toward 920px and the floor lifts on
       its own, with Shop returning to the logo edge. */
    --nav-step: max(7rem, calc(
      (var(--shop-max) - var(--hero-w)) / 2 + var(--hero-w) * var(--hero-frame)
    ));
    /* About is the middle of the three, so centring the nav on the box's
       right edge is what puts About on it. Only used on .wrap, so the
       percentage inside --viewport resolves against the header. */
    --box-right: calc((var(--viewport) + var(--shop-max)) / 2);

    grid-template-columns:
      var(--mark-band)
      calc(var(--box-right) - var(--nav-step) * 1.5 - var(--mark-band))
      auto
      minmax(0, 1fr);
  }
  .site-header .nav {
    display: grid;
    grid-template-columns: repeat(3, var(--nav-step));
    justify-items: center;
    gap: 0;
  }
}
.mark {
  text-decoration: none;
  display: inline-flex; align-items: flex-end; gap: .35rem;
  font-family: var(--display); font-weight: 800; line-height: 1;
}
.mark b { font-size: 1.6rem; letter-spacing: .04em; font-weight: 800; }
.mark small { font-size: .7rem; font-weight: 700; letter-spacing: .06em; padding-bottom: .2rem; }
.nav { display: flex; gap: 1.75rem; }
.nav a {
  font: 600 1.25rem var(--display);
  text-decoration: none; padding: .5rem 0;
  border-bottom: 2px solid transparent;
}
.nav a:hover { border-bottom-color: var(--ink); }

/* ---------- Theme switch ----------
   A pill track with a 2px ink outline and a solid ink knob. The rest of
   the site is squared, so this is the one rounded thing on the page —
   which is what makes it read as a control rather than a panel. */
.themeswitch {
  /* The track's two dimensions are the only literals in this control;
     the knob size and its travel are both derived from them below. */
  --sw-w: 40px;
  --sw-h: 20px;

  appearance: none; -webkit-appearance: none;
  flex: 0 0 auto;
  position: relative;
  width: var(--sw-w); height: var(--sw-h);
  padding: 0;
  border: 2px solid var(--ink);
  border-radius: 999px;
  background: transparent;
  cursor: pointer;
}
/* A circle, sized by the track's inner height via aspect-ratio. */
.themeswitch__knob {
  position: absolute; inset: 1px auto 1px 1px;
  aspect-ratio: 1;
  border-radius: 50%;
  background: var(--ink);
  transition: transform 1s var(--ease);
}
/* Travel is inner width minus knob width. The border and the 1px inset
   subtract equally from both, so the whole thing reduces to w - h. */
.themeswitch[aria-checked="true"] .themeswitch__knob {
  transform: translateX(calc(var(--sw-w) - var(--sw-h)));
}
/* The control is deliberately small, so it carries an invisible 44px
   tap target rather than making everyone aim at 40x20. */
.themeswitch::after {
  content: "";
  position: absolute; inset: -13px -8px;
}
@media (hover: hover) {
  .themeswitch:hover { background: var(--wash-ink); }
}
@media (prefers-reduced-motion: reduce) {
  .themeswitch__knob { transition: none; }
}
/* Without JS the switch is inert, so it is not offered at all. */
html:not(.js) .themeswitch { display: none; }

/* ---------- Hero ---------- */
.hero { padding: clamp(2.5rem, 7vw, 5.5rem) 0 clamp(3rem, 8vw, 6rem); text-align: center; }
/* The logo is wider than --page-max, so the hero column is sized to the
   logo rather than to the page. Only the logo is that wide: .hero__copy
   keeps its own 34rem measure and stays centred inside it. */
.hero .wrap { width: min(var(--hero-w), 100% - 2.5rem); }
/* The logo file is an opaque PNG: white mark on a solid black field, so
   in light mode it would read as a black slab. Rather than inverting it
   at runtime, the light copy is a real file, stacked on the dark one and
   crossfaded. Two reasons over filter: invert(1) — the browser is not
   re-filtering a 1846px image every frame of the change, and the light
   mark is now an asset in its own right, so it can stop being a pure
   inversion the day the logo gains a colour.

   The two are exact inverses today, so the crossfade passes through flat
   mid-grey at the halfway point. That is inherent to blending anything
   with its own inverse, and it is also what the rest of the page does:
   #fff text over a #000 field meets in the middle at grey too. */
.hero__mark {
  position: relative;
  width: min(var(--hero-w), 100%);
  margin-inline: auto;
  /* The cropped file lost the black margin that used to pad the artwork
     (144px above, 163px below, on a 1680px-wide image). These reinstate
     exactly that much space, so cropping changed what transitions without
     changing the layout. Delete them if you want the hero to tighten up. */
  margin-block: calc(var(--hero-w) * 0.0857) calc(var(--hero-w) * 0.0970);
}
.hero__logo { width: 100%; height: auto; }
/* The dark copy holds the space and stays fully opaque; only the light
   one on top changes opacity.

   Fading BOTH at once looks like the obvious way to crossfade and is
   wrong: while the bottom copy is semi-transparent the page shows through
   it and adds to the copy above, so the logo's field comes out up to 36
   levels brighter than the page and a lighter rectangle appears for the
   duration of the change. With the bottom copy left opaque the result is
   p*light + (1-p)*dark, which is exactly what the page background's own
   #000 -> #fff interpolation produces at the same p. */
.hero__logo--light { position: absolute; top: 0; left: 0; opacity: 0; }
:root[data-theme="light"] .hero__logo--light { opacity: 1; }
/* Matched to the 1s the rest of the page takes. This is safe to put on
   opacity here only because the parallax writes its opacity to .hero__mark,
   the parent — transitioning the element the parallax touches would make
   it lag a frame behind the scroll. */
html.theme-ready .hero__logo { transition: opacity var(--theme-ms) var(--ease); }
.hero__copy { margin: clamp(1.5rem, 3vw, 2.5rem) auto 0; max-width: 34rem; }
.hero__copy p { margin: 0 0 1.75rem; font-size: 1.25rem; color: var(--ink); }
/* The catchphrase: display face, sized like a headline, and allowed wider
   than the 34rem copy measure so it breaks into two even lines, not three. */
.hero__copy .hero__tagline {
  width: min(46rem, 100vw - 2.5rem);
  margin-left: 50%;
  transform: translateX(-50%);
  font-family: var(--display);
  font-weight: 600;
  font-size: clamp(1.6rem, 4vw, 2.75rem);
  line-height: 1.15;
  letter-spacing: .01em;
  text-wrap: balance;
}
.hero__tagline strong { font-weight: 800; color: var(--ink); }

@media (prefers-reduced-motion: no-preference) {
  .hero__mark { animation: settle .9s cubic-bezier(.2,.7,.2,1) both; }
  @keyframes settle { from { opacity: 0; transform: scale(.985); } to { opacity: 1; transform: none; } }
}

/* ---------- Sections ---------- */
section[id] { scroll-margin-top: var(--header-h); }
.section { padding: clamp(3rem, 7vw, 5.5rem) 0; border-top: 1px solid var(--line); }
.section__title { font-size: clamp(2rem, 5vw, 3.25rem); margin-bottom: 2.5rem; }

/* ---------- The frame (the logo's outline + white panel + black band) ---------- */
.frame {
  border: 2px solid var(--ink);
  padding: clamp(.75rem, 2.4vw, 1.6rem) clamp(.75rem, 3vw, 2rem) 0;
}
.frame + .frame { margin-top: 2.5rem; }
.panel { background: var(--ink); color: var(--field); }
.band {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
  gap: 1rem; padding: 1.5rem 0;
}
.band__label { font: 700 1rem var(--display); }
.band__links { display: flex; flex-wrap: wrap; gap: .75rem; }

/* Product */
/* Black in both themes, at the photo/info divider's weight: in light mode
   it edges the photo off the white page; in dark it melts into the field. */
.product { display: grid; grid-template-columns: 1fr 1fr; border: 2px solid #000; }
.product__photo {
  /* No fixed square here: side by side, the info column can run taller than
     the photo, and a locked aspect-ratio pinned the panel height and clipped
     the last spec row. The square returns below, where the grid stacks. */
  min-height: 18rem;
  background: var(--paper);
  display: grid; place-items: center;
  text-align: center; padding: 1.5rem;
  font: 600 1rem var(--display); color: var(--paper-ink);
  border-right: 2px solid var(--field);
}
.product__photo img { width: 100%; height: 100%; object-fit: cover; }
.product__info { padding: clamp(1.5rem, 4vw, 3rem); display: flex; flex-direction: column; }
.product__name { font-size: clamp(2rem, 4.4vw, 3.4rem); }
.product__price { font: 700 1.6rem var(--display); margin: 1rem 0 1.25rem; }
.product__desc { margin: 0 0 1.75rem; max-width: 34rem; }
.specs { margin: auto 0 0; border-top: 2px solid var(--field); }
.specs div {
  display: flex; justify-content: space-between; gap: 1rem;
  padding: .7rem 0; border-bottom: 1px solid var(--field);
}
.specs dt { font-weight: 600; }
.specs dd { margin: 0; text-align: right; }
.specs__made { display: flex; align-items: center; gap: .45rem; }
/* Flat colours, not tokens: the flag is the one thing that must not
   invert with the theme. The hairline keeps its white stripes off a
   white panel. */
.flag { width: 1.35em; height: auto; aspect-ratio: 19 / 10; outline: 1px solid var(--line); }

/* What's included: a native <details>, styled as one more spec row. */
.included { border-bottom: 1px solid var(--field); }
.included summary {
  display: flex; justify-content: space-between; align-items: center;
  padding: .7rem 0; font-weight: 600; cursor: pointer;
  list-style: none;
}
.included summary::-webkit-details-marker { display: none; }
.included summary::after {
  content: "+"; font: 700 1.2rem/1 var(--display);
  transition: transform .2s var(--ease);
}
.included[open] summary::after { transform: rotate(45deg); }
.included summary:focus-visible { outline: 2px solid var(--field); outline-offset: 2px; }
.included ul { margin: 0; padding: 0 0 .9rem 1.1rem; }
.included li { padding: .15rem 0; }

/* Coming soon frame (dashed = not built yet) */
.frame--soon { border-style: dashed; border-color: var(--grey); padding-bottom: 0; }
.soon { padding: clamp(2rem, 5vw, 3.5rem); }
.soon h3 { font-size: clamp(1.5rem, 3.4vw, 2.25rem); margin-bottom: .75rem; }
.soon p { margin: 0; max-width: 32rem; }
.frame--soon .band { justify-content: flex-end; }

/* ---------- About ---------- */
.about { display: grid; grid-template-columns: 1fr 1.4fr; gap: clamp(2rem, 6vw, 5rem); }
.about .section__title { margin-bottom: 0; }
.about p { margin: 0 0 1.1rem; max-width: 36rem; color: var(--ink); }
.about p:last-child { margin-bottom: 0; }

/* ---------- Contact / footer ---------- */
.contact { text-align: left; }
/* The title already carries 2.5rem beneath it, so this only needs to set
   its own gap before the address. The measure is capped so the line
   length stays readable on a wide screen. */
.contact__intro {
  margin: 0 0 1.75rem;
  max-width: 44rem;
  font-size: 1.125rem;
  color: var(--ink);
}
.contact__mail {
  display: inline-block;
  font: 700 clamp(1.4rem, 4.2vw, 2.6rem)/1.1 var(--display);
  text-decoration: none; border-bottom: 3px solid var(--ink);
  word-break: break-word;
}
.contact__mail:hover { background: var(--ink); color: var(--field); }
.contact__links { display: flex; flex-wrap: wrap; gap: .5rem 1.5rem; margin-top: 1.75rem; }
.contact__links a { color: var(--ink); }
.contact__links a:hover { color: var(--ink); }

.site-footer { border-top: 1px solid var(--line); padding: 1.75rem 0 2.5rem; color: var(--ink); font-size: .9rem; }
.site-footer .wrap { display: flex; justify-content: space-between; flex-wrap: wrap; gap: .5rem 1rem; }

/* ---------- Small screens ---------- */
@media (max-width: 820px) {
  .product { grid-template-columns: 1fr; }
  .product__photo { aspect-ratio: 1; min-height: 0; border-right: 0; border-bottom: 2px solid var(--field); }
  .about { grid-template-columns: 1fr; }
  .nav { gap: 1.1rem; }
  .themeswitch { margin-left: 1.1rem; }
}
@media (max-width: 560px) {
  .band { flex-direction: column; align-items: stretch; }
  .band__links { flex-direction: column; }
  .frame--soon .band { align-items: stretch; }
  .mark small { display: none; }
  .themeswitch { margin-left: .75rem; }
}


/* ============================================================
   INTERACTION LAYER
   Everything below is additive. It sharpens the same system —
   hard edges, pure black and white, no rounded corners, no soft
   shadows — and every rule degrades to the static layout above
   when JavaScript or motion is unavailable.

   The `.js` hook is set by an inline snippet in <head> so that
   reveal-on-scroll never flashes unstyled content.
   ============================================================ */

:root {
  --ease: cubic-bezier(.2, .7, .2, 1);
  --bar-h: 2px;
}

/* ---------- Scroll progress: a hairline across the header ---------- */
.progress {
  position: absolute; left: 0; right: 0; bottom: -1px;
  height: var(--bar-h);
  background: var(--ink);
  transform: scaleX(var(--progress, 0));
  transform-origin: left;
  will-change: transform;
  pointer-events: none;
}

/* ---------- Header reacts to scroll ---------- */
.site-header {
  transition: border-color .3s var(--ease);
}
.site-header.is-scrolled { border-bottom-color: var(--ink); }

/* The wordmark tightens slightly once you leave the hero */
.mark b { transition: letter-spacing .35s var(--ease); }
.site-header.is-scrolled .mark b { letter-spacing: .02em; }

/* ---------- Nav: an underline that draws itself ---------- */
.nav a { position: relative; }
.nav a::after {
  content: "";
  position: absolute; left: 0; right: 0; bottom: -2px;
  height: 2px; background: var(--ink);
  transform: scaleX(0); transform-origin: left;
  transition: transform .3s var(--ease);
}
.nav a:hover::after,
.nav a:focus-visible::after,
.nav a.is-active::after { transform: scaleX(1); }
/* the ::after replaces the original border swap, so neutralise it */
.nav a:hover { border-bottom-color: transparent; }

@media (prefers-reduced-motion: reduce) {
  .nav a::after { transition: none; }
}

/* ---------- Reveal on scroll ---------- */
@media (prefers-reduced-motion: no-preference) {
  .js [data-reveal] {
    opacity: 0;
    transform: translateY(16px);
    /* Longhands, not the shorthand: border-color rides along so a .frame
       still eases across a theme change (it is excluded from the theme
       rule above to protect this fade), but it keeps its own 1s timing
       and never inherits the reveal's stagger.
       Leaving: quick and unstaggered, so scrolling up clears the section
       cleanly instead of trailing off one element at a time. */
    transition-property: opacity, transform, border-color;
    transition-duration: .3s, .3s, var(--theme-ms);
    transition-timing-function: var(--ease);
    transition-delay: 0s, 0s, 0s;
  }
  .js [data-reveal].is-revealed {
    opacity: 1;
    transform: none;
    /* Arriving: slower, and staggered per section. Both live here rather than
       on the base rule because a transition reads its timing from the state it
       is moving TO, which is what lets the two directions differ. */
    transition-duration: .7s, .7s, var(--theme-ms);
    transition-delay: var(--reveal-delay, 0s), var(--reveal-delay, 0s), 0s;
  }
  .hero__mark { will-change: transform; }
}

/* ---------- Buttons: the fill wipes in ---------- */
@media (prefers-reduced-motion: no-preference) {
  .btn { position: relative; overflow: hidden; isolation: isolate; }
  .btn::before {
    content: "";
    position: absolute; inset: 0; z-index: -1;
    transform: scaleX(0); transform-origin: left;
    transition: transform .3s var(--ease);
  }
  .btn--solid::before { background: var(--field); }
  .btn--line::before  { background: var(--ink); }
  .btn:hover::before,
  .btn:focus-visible::before { transform: scaleX(1); }

  /* keep the base background; only the label colour flips, mid-wipe */
  .btn { transition: color .2s var(--ease) .08s; }
  .btn--solid:hover, .btn--solid:focus-visible { background: var(--ink); color: var(--ink); }
  .btn--line:hover,  .btn--line:focus-visible  { background: var(--field); color: var(--field); }
}
.btn:active { transform: translateY(1px); }

/* ---------- Frames echo the logo's double outline on hover ---------- */
@media (hover: hover) {
  .frame {
    outline: 1px solid transparent;
    outline-offset: 0;
    transition: outline-offset .3s var(--ease), outline-color .3s var(--ease),
                border-color .3s var(--ease);
  }
  .frame:hover { outline-color: var(--line); outline-offset: 7px; }
  .frame--soon:hover { outline-color: transparent; border-color: var(--ink); }

  /* Spec rows pick up a faint wash under the cursor */
  .specs div:hover { background: var(--wash); }
}
.specs div { transition: background-color .2s var(--ease); }

/* ---------- Product photo: zoomable once a real image is present ---------- */
.product__photo.is-zoomable { cursor: zoom-in; overflow: hidden; position: relative; }
.product__photo.is-zoomable img { transition: transform .5s var(--ease); }
.product__photo.is-zoomable::after {
  content: "Click to enlarge";
  position: absolute; left: 0; right: 0; bottom: 0;
  background: var(--field); color: var(--ink);
  font: 700 .7rem/1 var(--display);
  text-transform: uppercase; letter-spacing: .08em;
  padding: .7rem; text-align: center;
  transform: translateY(100%);
  transition: transform .3s var(--ease);
}
@media (hover: hover) {
  .product__photo.is-zoomable:hover img { transform: scale(1.04); }
  .product__photo.is-zoomable:hover::after { transform: none; }
}

/* ---------- Product photo slideshow ----------
   A horizontal scroll-snap strip, so a finger or trackpad swipe moves it
   natively; code.js only drives the auto-advance, arrows and dots. The
   strip is pinned to the photo cell, which gets its size from the grid
   side by side and from its aspect-ratio once stacked. */
.product__photo.slideshow { background: #fff; padding: 0; display: block; position: relative; overflow: hidden; }
.slideshow__track {
  position: absolute; inset: 0;
  display: flex;
  overflow-x: auto; overflow-y: hidden;
  scroll-snap-type: x mandatory;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
}
.slideshow__track::-webkit-scrollbar { display: none; }
.slideshow__track:focus-visible { outline: 3px solid var(--ink); outline-offset: -3px; }
.slideshow__track img {
  flex: 0 0 100%;
  /* The shots are on pure white, so fitting the whole photo and filling
     the rest of the cell white looks seamless, and nothing is cropped
     when the cell runs taller or wider than square. */
  object-fit: contain; background: #fff;
  scroll-snap-align: start; scroll-snap-stop: always;
}
.slideshow__arrow {
  position: absolute; top: 50%; z-index: 1;
  width: 42px; height: 42px; margin-top: -21px;
  display: grid; place-items: center;
  background: var(--field); color: var(--ink);
  border: 0; cursor: pointer;
  font: 700 1.3rem/1 var(--display);
  opacity: .85;
  transition: opacity .2s;
}
.slideshow__arrow:hover { opacity: 1; }
.slideshow__arrow--prev { left: .75rem; }
.slideshow__arrow--next { right: .75rem; }
.slideshow__dots {
  position: absolute; left: 0; right: 0; bottom: .9rem; z-index: 1;
  display: flex; justify-content: center; gap: .5rem;
  transition: transform .3s var(--ease);
}
.slideshow__dot {
  width: 10px; height: 10px; padding: 0;
  background: var(--field); border: 2px solid var(--field);
  opacity: .55; cursor: pointer;
}
.slideshow__dot[aria-current="true"] { background: var(--ink); opacity: 1; }
/* Clear the "Click to enlarge" bar when it slides up. */
@media (hover: hover) {
  .product__photo.is-zoomable:hover .slideshow__dots { transform: translateY(-2.2rem); }
}
@media (hover: none) {
  .slideshow__arrow { display: none; } /* swipe instead */
}

/* ---------- Lightbox ---------- */
.lightbox {
  position: fixed; inset: 0; z-index: 60;
  background: rgba(0, 0, 0, .94);
  display: grid; place-items: center;
  padding: clamp(1rem, 4vw, 3rem);
  opacity: 0; visibility: hidden;
  transition: opacity .25s var(--ease), visibility .25s;
}
.lightbox.is-open { opacity: 1; visibility: visible; }
.lightbox__figure {
  margin: 0;
  border: 2px solid var(--ink);
  max-width: min(1000px, 100%);
  transform: scale(.98);
  transition: transform .25s var(--ease);
}
.lightbox.is-open .lightbox__figure { transform: none; }
.lightbox__figure img {
  width: 100%; height: auto;
  max-height: calc(100vh - 12rem);
  object-fit: contain;
  background: #fff; /* the shots are on white */
}
.lightbox__cap {
  background: var(--ink); color: var(--field);
  font: 600 .9rem var(--body);
  padding: .8rem 1rem;
}
.lightbox__close {
  position: absolute; top: 1rem; right: 1rem;
  width: 48px; height: 48px;
  background: var(--field); color: var(--ink);
  border: 2px solid var(--ink);
  font: 700 1.4rem/1 var(--display);
  cursor: pointer;
  transition: background-color .2s, color .2s;
}
.lightbox__close:hover { background: var(--ink); color: var(--field); }
.lightbox__arrow {
  position: absolute; top: 50%; z-index: 1;
  width: 48px; height: 64px; margin-top: -32px;
  background: var(--field); color: var(--ink);
  border: 2px solid var(--ink);
  font: 700 1.8rem/1 var(--display);
  cursor: pointer;
  transition: background-color .2s, color .2s;
}
.lightbox__arrow:hover { background: var(--ink); color: var(--field); }
.lightbox__arrow[hidden] { display: none; }
.lightbox__arrow--prev { left: 1rem; }
.lightbox__arrow--next { right: 1rem; }
@media (hover: none) {
  .lightbox__arrow { display: none; } /* swipe instead */
}
@media (prefers-reduced-motion: reduce) {
  .lightbox, .lightbox__figure { transition: none; }
}

/* ---------- Sticky buy bar ---------- */
.buybar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 30;
  background: var(--field);
  border-top: 2px solid var(--ink);
  transform: translateY(101%);
  transition: transform .35s var(--ease);
}
.buybar.is-visible { transform: none; }
.buybar .wrap {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; padding: .85rem 0;
}
.buybar__id { min-width: 0; }
.buybar__name {
  font: 700 1.05rem var(--display);
  text-transform: uppercase; letter-spacing: .02em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.buybar__price { font: 600 .9rem var(--body); color: var(--ink); }
.buybar__links { display: flex; gap: .5rem; flex-shrink: 0; }
.buybar .btn { min-height: 42px; padding: 0 1rem; font-size: .85rem; }
@media (prefers-reduced-motion: reduce) {
  .buybar { transition: none; }
}
@media (max-width: 560px) {
  .buybar__price { display: none; }
  .buybar .wrap { gap: .6rem; }
  .buybar .btn { padding: 0 .7rem; font-size: .78rem; }
}

/* ---------- Copy-to-clipboard ---------- */
.copy {
  background: transparent; color: var(--ink);
  border: 1px solid var(--line);
  font: 600 .9rem var(--body);
  padding: .15rem .7rem;
  cursor: pointer;
  transition: color .2s, border-color .2s;
}
.copy:hover, .copy.is-done { color: var(--ink); border-color: var(--ink); }

.toast {
  position: fixed; z-index: 70;
  top: calc(var(--header-h) + 1rem); right: 1rem;
  background: var(--ink); color: var(--field);
  font: 700 .85rem var(--display);
  text-transform: uppercase; letter-spacing: .06em;
  padding: .8rem 1.1rem;
  opacity: 0; transform: translateY(-8px);
  transition: opacity .25s var(--ease), transform .25s var(--ease);
  pointer-events: none;
}
.toast.is-visible { opacity: 1; transform: none; }

/* Scroll lock while the lightbox is open */
body.is-locked { overflow: hidden; }
