/* ==========================================================================
   Thorix — thorix.shop
   Identity: anodised aluminium. A mid-tone graphite surface (no other build
   in this workspace is mid-tone), with the accent used the way keycap sets
   use one — a single bright key on an otherwise neutral field.
   The Forge renders are dark-lit on black, so a light page would make them
   read as cutouts lit for another room. The surface is chosen to hold them.
   ========================================================================== */

:root {
  /* --- surfaces: bead-blasted anodised aluminium, mid-tone ---------------- */
  --graphite:        #2E3133;  /* page */
  --graphite-deep:   #1B1D1F;  /* render wells, the Forge stage, footer */
  --graphite-raised: #3C4042;  /* cards, inputs */
  --line:            #4A4E50;  /* hairlines */
  --line-strong:     #5E6365;

  /* --- text: contrast checked against both --graphite and --graphite-raised
         --muted is 6.1:1 on graphite and 4.8:1 on raised, so secondary text
         stays AA even when a card sits on the page. ------------------------ */
  --arctic:          #ECEAE5;  /* primary text — 10.2:1 on graphite */
  --muted:           #B0B4B5;  /* secondary text */

  /* --- the accent: Thorix copper -----------------------------------------
         The house colour, shared with thorixsoftware.com — same company, so
         the brand carries across. Three values because one cannot do every
         job at AA on a dark surface:
           --accent      fill only. --arctic on it is 2.4:1, so a fill always
                         takes --accent-on, not white.
           --accent-on   label on an accent fill (5.9:1).
           --accent-text accent-coloured TEXT on graphite (5.9:1) and on a
                         raised card (4.6:1).
         Hover goes lighter rather than darker: darkening copper drops the
         dark label to 4.0:1, lightening lifts it to 7.2:1. ---------------- */
  --accent:          #D9823B;
  --accent-hover:    #E9964D;
  --accent-on:       #1B1D1F;
  --accent-text:     #E7A366;
  /* The lit wordmark only. Display-size type on a scrimmed photo, where it
     measures 8:1 — nothing at body size ever uses it. */
  --accent-lit:      #F0A052;

  /* --- state -------------------------------------------------------------
         No amber. It sat a few degrees of hue from copper and read as the
         brand colour rather than a warning. Low stock is carried by weight
         and a real count instead — on a field of muted grey, bold white with
         a number is louder than another orange. Every SKU on the old site
         said "Limited stock", which told a buyer nothing. ----------------- */
  --ok:              #6FBF95;  /* the one success moment — clearly not copper */
  /* Destructive actions only, and only in the admin. A real red rather than a
     darker copper, so "delete for good" cannot be mistaken for a brand accent
     at a glance. 4.8:1 on --graphite. */
  --danger:          #EE7A7A;

  /* --- type ------------------------------------------------------------- */
  --display: "Saira Semi Condensed", "Arial Narrow", sans-serif;
  --body:    "Public Sans", system-ui, -apple-system, sans-serif;
  --mono:    "Azeret Mono", ui-monospace, "SF Mono", Menlo, monospace;

  /* --- the keyboard unit --------------------------------------------------
         1u is the width of a single keycap. The catalog grid is measured in
         it, so card widths are multiples of the thing being sold. ---------- */
  --u: 5.5rem;

  --radius: 3px;   /* keycaps and cases have a small, machined radius */
  --pad: clamp(1.25rem, 4vw, 3rem);
  --maxw: 82rem;

  /* --- surface texture ----------------------------------------------------
         A bead-blasted anodised panel is not a flat fill. It has two things
         going on, and both are here as background layers rather than overlay
         elements, so nothing has to be stacked above the page content:
           --grain  the blast finish — fine desaturated noise, one 140px tile.
           --brush  the directional marks the finish leaves, at the same angle
                    everywhere so the whole site reads as one machined part.
         Both are deliberately near the threshold of visibility: they should
         register as "this surface has a finish", never as a pattern. -------- */
  --grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23g)' opacity='0.24'/%3E%3C/svg%3E");
  --brush: repeating-linear-gradient(102deg, rgba(236,234,229,0.018) 0 1px, rgba(0,0,0,0.022) 1px 3px);
}

/* --------------------------------------------------------------------------
   reset + base
   -------------------------------------------------------------------------- */

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  /* Split, not shorthand: the texture and the fill are set separately so a
     later rule can change one without silently dropping the other. The grain
     scrolls with the page — a fixed attachment repaints the whole viewport on
     every scroll, and the tile is far too fine for anyone to see it move. */
  background-color: var(--graphite);
  background-image: var(--grain);
  color: var(--arctic);
  font-family: var(--body);
  font-size: 1rem;
  line-height: 1.6;
  font-synthesis-weight: none;
}

h1, h2, h3, h4 {
  font-family: var(--display);
  font-weight: 600;
  line-height: 1.05;
  letter-spacing: 0.005em;
  margin: 0;
  text-wrap: balance;
}

h1 { font-size: clamp(2.5rem, 7vw, 5rem); }
h2 { font-size: clamp(1.75rem, 4vw, 2.75rem); }
h3 { font-size: clamp(1.15rem, 2vw, 1.4rem); }

p { margin: 0 0 1em; max-width: 62ch; }

a { color: var(--accent-text); text-decoration-thickness: 1px; text-underline-offset: 0.2em; }
a:hover { color: var(--arctic); }

img { max-width: 100%; height: auto; display: block; }

/* Visible, high-contrast focus everywhere — never removed. */
:focus-visible {
  outline: 2px solid var(--accent-text);
  outline-offset: 2px;
}

/* The one place a number is a number: prices, forces, stock counts. */
.mono, .price, .spec-table td, .stock {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em;
}

.eyebrow {
  font-family: var(--mono);
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.18em;
  color: var(--muted);
  margin: 0 0 0.75rem;
}

.wrap { max-width: var(--maxw); margin-inline: auto; padding-inline: var(--pad); }

.skip-link {
  position: absolute; left: -9999px;
  background: var(--accent); color: var(--accent-on);
  padding: 0.75rem 1rem; z-index: 100;
}
.skip-link:focus { left: 1rem; top: 1rem; }

/* --------------------------------------------------------------------------
   header / nav
   -------------------------------------------------------------------------- */

.site-header {
  position: sticky; top: 0; z-index: 50;
  background-color: var(--graphite-deep);
  background-image: var(--grain);
  border-bottom: 1px solid var(--line);
}

.site-header__inner {
  display: flex; align-items: center; gap: 1.5rem;
  max-width: var(--maxw); margin-inline: auto;
  padding: 0.75rem var(--pad);
}

.brand {
  font-family: var(--display);
  font-size: 1.5rem; font-weight: 700;
  letter-spacing: 0.02em; text-transform: uppercase;
  color: var(--arctic); text-decoration: none;
  margin-right: auto;
  display: inline-flex; align-items: center; gap: 0.5rem;
  min-height: 44px;
}
/* The runic TX monogram, shared with thorixsoftware.com. It carries the mark,
   so the accent dot that used to sit after the wordmark is gone — one of them
   was doing the other's job. */
.brand__mark { width: 1.5rem; height: 1.5rem; flex: none; }

.nav { display: flex; align-items: center; gap: 0.25rem; }

.nav a {
  font-family: var(--display);
  font-size: 1.05rem; font-weight: 500;
  letter-spacing: 0.03em;
  color: var(--muted); text-decoration: none;
  padding: 0.65rem 0.75rem;
  border-radius: var(--radius);
  min-height: 44px; display: inline-flex; align-items: center;
}
.nav a:hover, .nav a[aria-current="page"] { color: var(--arctic); background: var(--graphite-raised); }

.cart-link { position: relative; }
.cart-count {
  font-family: var(--mono); font-size: 0.7rem; font-weight: 500;
  background: var(--accent); color: var(--accent-on);
  border-radius: 999px; padding: 0.05em 0.45em;
  margin-left: 0.4rem;
}
.cart-count[hidden] { display: none; }

.nav-toggle {
  display: none;
  background: none; border: 1px solid var(--line);
  color: var(--arctic); border-radius: var(--radius);
  min-width: 44px; min-height: 44px;
  font-family: var(--mono); font-size: 0.75rem;
  cursor: pointer;
}

@media (max-width: 800px) {
  .nav-toggle { display: inline-flex; align-items: center; justify-content: center; }
  .nav {
    display: none;
    position: absolute; inset: 100% 0 auto 0;
    flex-direction: column; align-items: stretch;
    background-color: var(--graphite-deep);
    background-image: var(--grain);
    border-bottom: 1px solid var(--line);
    padding: 0.5rem var(--pad) 1rem;
  }
  .nav[data-open="true"] { display: flex; }
  .nav a { font-size: 1.2rem; }
}

/* --------------------------------------------------------------------------
   hero — opens on a real photograph of a real board
   The Forge's render layers are an assembly set built for the configurator's
   exploded stack; at hero size they read as CGI. A photograph of the actual
   product on the actual deskmat we sell is the more characteristic thing, so
   the page opens on that.

   The photograph sits behind the copy at every width. It is the same frame
   cropped two ways, not one frame squeezed: the wide crop's empty stretch of
   mat is to the LEFT of the board, which is the first thing a narrow viewport
   throws away, so below 760px an upright crop puts that emptiness ABOVE the
   board instead. Each crop's scrim runs along the axis its copy sits on.
   -------------------------------------------------------------------------- */

.hero {
  position: relative;
  display: grid; align-items: center;
  min-height: min(41rem, 82vh);
  background-color: var(--graphite-deep);
  background-image: var(--grain);
  border-bottom: 1px solid var(--line);
}

.hero__photo { position: absolute; inset: 0; z-index: 0; }
/* Biased to the top of the upright crop. On a phone the box is narrower than
   the crop, so nothing is trimmed vertically and this does nothing; it only
   takes effect on a wide-but-short viewport, where centring would trim the bare
   mat off the top and slide the board straight up under the copy. */
.hero__photo img { width: 100%; height: 100%; object-fit: cover; object-position: 50% 20%; }

/* The scrim is what guarantees the copy's contrast, so it is dense where the
   text is and lifts where only the buttons are. Vertical here: on the upright
   crop the copy is stacked above the board, not beside it. The stops are set
   against the crop — bare mat down to 42%, board below — so the board reads
   through the lower half while every line of body text stays above 4.5:1. */
.hero__photo::after {
  content: "";
  position: absolute; inset: 0;
  background: linear-gradient(180deg,
    rgba(19,21,23,0.95) 0%,
    rgba(19,21,23,0.94) 45%,
    rgba(19,21,23,0.88) 60%,
    rgba(19,21,23,0.66) 78%,
    rgba(19,21,23,0.42) 100%);
}

.hero__inner {
  position: relative; z-index: 1;
  /* .wrap's `margin-inline: auto` would otherwise suppress the grid item's
     stretch and shrink this to fit-content. */
  width: 100%;
  padding-block: clamp(2.25rem, 8vw, 4rem) clamp(1.75rem, 6vw, 3rem);
}
.hero__copy { max-width: 32rem; }

@media (min-width: 760px) {
  /* Kept close to the wide crop's own 2.4:1 so `cover` barely crops: any taller
     and the frame zooms in, and the empty left of the mat — the whole reason
     this frame was chosen — goes with it. */
  .hero { min-height: clamp(24rem, 41vw, 36rem); }
  .hero__photo img { object-position: 58% 50%; }
  .hero__photo::after {
    background: linear-gradient(100deg,
      rgba(19,21,23,0.96) 0%,
      rgba(19,21,23,0.94) 34%,
      rgba(19,21,23,0.86) 52%,
      rgba(19,21,23,0.30) 74%,
      rgba(19,21,23,0) 100%);
  }
  .hero__inner { padding-block: clamp(3rem, 7vw, 5rem); }
  /* The percentage is the load-bearing half: the board enters at roughly 53%
     of the frame at every width, so the copy has to stop before it rather than
     rely on a fixed measure that only clears at desktop. */
  .hero__copy { max-width: min(32rem, 52%); }
}

/* --- the wordmark ---------------------------------------------------------
   The one orchestrated moment on the site: six letters drop in, and the last
   one throws a spark as it lands. It runs once, on load, and nothing else on
   the page animates on arrival.
   -------------------------------------------------------------------------- */

.wordmark {
  position: relative;
  display: inline-flex;
  font-family: var(--display);
  font-weight: 700;
  font-size: clamp(3.5rem, 13vw, 8rem);
  line-height: 0.88;
  letter-spacing: 0.055em;
  text-transform: uppercase;
  color: var(--accent-lit);
}

.wordmark__l {
  display: inline-block;
  /* The glow is three stops: a hot core, the copper bloom, and a wide falloff
     that lifts the letter off the photograph without blurring its edge. */
  text-shadow:
    0 0 0.03em rgba(255, 226, 190, 0.5),
    0 0 0.26em rgba(217, 130, 59, 0.55),
    0 0 0.72em rgba(217, 130, 59, 0.32);
  /* transform and opacity only. An earlier pass animated `filter: blur()` too,
     which forces the browser to re-rasterise the glyph and its 0.72em shadow
     every single frame — six of those at display size is what made the entrance
     stutter. These two properties are composited, so nothing re-rasterises. */
  animation: wordmark-drop 520ms cubic-bezier(0.16, 1, 0.3, 1) both;
  animation-delay: calc(var(--i) * 55ms);
}

@keyframes wordmark-drop {
  0%   { opacity: 0; transform: translate3d(0, -0.4em, 0) scale(1.05) rotate(-3deg); }
  60%  { opacity: 1; }
  100% { opacity: 1; transform: translate3d(0, 0, 0) scale(1) rotate(0deg); }
}

/* --- the strike -----------------------------------------------------------
   A zero-size origin pinned to the bottom-right corner of the X. It fires at
   780ms, which is when the last letter lands (5 * 55ms + 520ms), so the burst
   reads as caused by the impact rather than as a separate effect.
   -------------------------------------------------------------------------- */

.wordmark__strike {
  position: absolute;
  right: 0.07em; bottom: 0.13em;
  width: 0; height: 0;
}

/* The flash at the point of impact — brief, and gone before the sparks land. */
.wordmark__strike::before {
  content: "";
  position: absolute; inset: -0.11em;
  border-radius: 50%;
  background: radial-gradient(circle,
    rgba(255, 242, 219, 0.95) 0%,
    rgba(255, 176, 96, 0.5) 42%,
    rgba(217, 130, 59, 0) 72%);
  opacity: 0;
  animation: strike-flash 320ms cubic-bezier(0.2, 0.9, 0.3, 1) 780ms both;
}

@keyframes strike-flash {
  0%   { opacity: 0; transform: scale(0.2); }
  16%  { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(1.9); }
}

.spark {
  position: absolute; left: 0; top: 0;
  width: var(--sz); height: var(--sz);
  margin: calc(var(--sz) / -2);
  border-radius: 50%;
  background: #FFF0D6;
  box-shadow: 0 0 0.09em 0.012em rgba(255, 198, 133, 0.9), 0 0 0.4em rgba(217, 130, 59, 0.75);
  opacity: 0;
  animation: strike-spark var(--dur) cubic-bezier(0.05, 0.8, 0.15, 1) calc(780ms + var(--t)) both;
}

/* Out fast along its own vector, then dragged 0.3em down as it dies — the
   parabola is what stops six dots reading as a starburst decal. */
@keyframes strike-spark {
  0%   { opacity: 0; transform: translate3d(0, 0, 0) scale(0.25); }
  9%   { opacity: 1; transform: translate3d(calc(var(--x) * 0.26), calc(var(--y) * 0.26), 0) scale(1); }
  55%  { opacity: 0.9; transform: translate3d(calc(var(--x) * 0.74), calc(var(--y) * 0.96), 0) scale(0.6); }
  100% { opacity: 0; transform: translate3d(var(--x), calc(var(--y) + 0.3em), 0) scale(0.1); }
}

@media (prefers-reduced-motion: reduce) {
  .wordmark__l { animation: none; }
  .wordmark__strike { display: none; }
}

/* The dateline under the wordmark — a masthead, so the rule belongs to it. */
.hero__dateline {
  display: flex; align-items: center; gap: 0.75rem;
  font-family: var(--mono); font-size: 0.7rem;
  text-transform: uppercase; letter-spacing: 0.22em;
  color: var(--muted);
  margin: 0.9rem 0 1.5rem;
}
.hero__dateline::after {
  content: ""; flex: 1 1 auto;
  height: 1px; background: var(--line-strong);
  max-width: 8rem;
}

.hero__title { text-transform: uppercase; font-size: clamp(1.9rem, 4.4vw, 3rem); }
.hero__title em {
  font-style: normal;
  color: var(--accent-text);
}
.hero__lede { color: var(--muted); font-size: 1.05rem; margin-top: 0.9rem; }
.hero__actions { display: flex; flex-wrap: wrap; gap: 0.75rem; margin: 1.5rem 0 0; }

/* --------------------------------------------------------------------------
   board-stack — the composited keyboard
   Every layer shares one crop frame (images/forge/README.md), so stacking them
   in the same box reproduces the assembled board exactly. The first layer is
   in flow and sets the height; the rest are absolutely positioned on top.
   -------------------------------------------------------------------------- */

.board-stack {
  position: relative;
  aspect-ratio: 1400 / 1039;   /* the one shared frame every layer is cropped to */
  width: 100%;
}
.board-stack > img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: contain;
}
.board-stack > img:first-child { position: relative; }

/* --------------------------------------------------------------------------
   the keyboard-unit grid
   Card widths are multiples of 1u, so the catalog is measured in the object it
   sells rather than in arbitrary fractions.
   -------------------------------------------------------------------------- */

/* The hairlines between tiles are drawn by the CARDS, not by this container.
   It used to paint itself --line and let a 1px gap show through, which works
   only while every cell is filled: a last row holding 2 of 4 cards left the
   remaining two cells painting container background, i.e. a solid grey block
   sitting next to the products. Nothing in CSS can target an empty grid cell,
   so the fix is to stop painting the thing that has empty cells. */
.board-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(var(--u), 1fr));
  gap: 0;
}

/* Product cards span 3u — roughly a spacebar-adjacent width — dropping to two
   per row below 640px, where three would leave nothing readable. */
.product-card { grid-column: span 3; }

@media (max-width: 640px) {
  .board-grid { grid-template-columns: repeat(2, 1fr); }
  .product-card { grid-column: span 1; }
}

/* Fixed column counts rather than auto-fill. auto-fill derives its count from
   the viewport, and that count is almost never a multiple of the 3u a card
   spans: at 1440px it resolved to 13 columns, so four span-3 cards filled 12
   and left a dead ~95px track of container background down the right of every
   row (and 7 columns / 99px dead at 768px). A count divisible by 3 keeps the
   right edge flush at every width, and makes cards-per-row a known number
   rather than something the viewport decides.
   Both counts preserve the density auto-fill already produced: 2 per row on
   tablet, 4 on desktop.
   Declared before .board-grid--pair / --presets so those later, equal-
   specificity overrides still win their own column counts. */
@media (min-width: 641px)  { .board-grid { grid-template-columns: repeat(6, 1fr); } }
@media (min-width: 1024px) { .board-grid { grid-template-columns: repeat(12, 1fr); } }

/* There is deliberately no per-row stagger here any more. It indented the first
   card of each row by 0.25u/0.5u to echo a keyboard's row offset, but on a
   catalogue that meant one arbitrary card sat 22px right of the column above it
   AND rendered ~22px narrower than its neighbours. A structural device has to
   encode something true about the content (§A6); this encoded nothing about the
   products and read as a layout bug, which is precisely what it looked like. */

.product-card {
  background-color: var(--graphite-raised);
  background-image: var(--brush);
  /* Each tile draws its own hairline and overlaps the next by exactly that 1px,
     so shared edges stay a single line rather than doubling to 2px — the ruled
     sheet the container background used to fake, minus the empty-cell block. */
  border: 1px solid var(--line);
  margin: 0 -1px -1px 0;
  padding: 1rem;
  display: flex; flex-direction: column; gap: 0.75rem;
  text-decoration: none; color: inherit;
  transition: background-color 0.15s ease;
}
.product-card:hover { background-color: #464b4d; }
@media (prefers-reduced-motion: reduce) { .product-card { transition: none; } }

.product-card__media {
  background: var(--graphite-deep);
  border-radius: 2px;
  aspect-ratio: 1 / 1;
  overflow: hidden;
  display: grid; place-items: center;
}
.product-card__media img { width: 100%; height: 100%; object-fit: cover; }

.product-card__title {
  font-family: var(--display); font-size: 1.1rem; font-weight: 600;
  line-height: 1.15; margin: 0;
}
.product-card__vendor {
  font-family: var(--mono); font-size: 0.68rem;
  text-transform: uppercase; letter-spacing: 0.12em; color: var(--muted);
}
.product-card__foot { margin-top: auto; display: flex; align-items: baseline; gap: 0.6rem; flex-wrap: wrap; }

.price { font-size: 1rem; font-weight: 500; }
.price--was { color: var(--muted); text-decoration: line-through; font-size: 0.85rem; }

/* Stock is a real number, not a badge that says "Limited" on everything. */
.stock { font-size: 0.72rem; letter-spacing: 0.02em; }
/* Low stock is the loud one, carried by weight rather than hue — on a field
   of muted grey, bold white with a real count reads faster than another
   orange would, and it leaves copper meaning only one thing. */
.stock--low { color: var(--arctic); font-weight: 600; }
.stock--out { color: var(--muted); }
.stock--in  { color: var(--muted); }

/* Two-up, for a pair of category tiles rather than a run of products.
   These used to sit in a 16:10 band, on the reasoning that a category is not a
   product and should not borrow the shop grid's square tile. The photography
   does not support it: every product shot is square, so a 16:10 crop took 38%
   of each one away through the middle — the switch lost its stem and its pins.
   A band needs a photograph made for a band, like the deskmats one below.
   These two get the square their pictures actually are.

   Held to three columns' width rather than filling the row, so a pair of
   square tiles does not tower over the section it introduces. */
.board-grid--pair > .product-card { grid-column: span 3; }
@media (min-width: 641px) {
  .board-grid--pair { grid-template-columns: repeat(6, 1fr); max-width: 46rem; }
}

/* Presets — a small, curated set (six today), never the large open-ended
   catalog auto-fill was built for. auto-fill's column count depends on
   viewport width, so at some widths 6 cards don't divide evenly into full
   rows. Fixed column counts instead: 6 divides evenly into both, which keeps
   the last row full when all six presets are active. An incomplete row is no
   longer a visual problem either way — cards carry their own hairlines now, so
   a short row simply ends. */
.board-grid--presets > .product-card { grid-column: span 1; }
@media (min-width: 641px) { .board-grid--presets { grid-template-columns: repeat(3, 1fr); } }

/* --------------------------------------------------------------------------
   feature band — one wide row, for a category with a single line in it
   -------------------------------------------------------------------------- */

.feature {
  display: grid; gap: 0;
  background-color: var(--graphite-raised);
  background-image: var(--brush);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  text-decoration: none; color: inherit;
  transition: border-color 0.15s ease;
}
.feature:hover { border-color: var(--accent); }
@media (prefers-reduced-motion: reduce) { .feature { transition: none; } }

.feature__media { background: var(--graphite-deep); aspect-ratio: 16 / 10; overflow: hidden; }
.feature__media img { width: 100%; height: 100%; object-fit: cover; }
.feature__body { padding: clamp(1.25rem, 3vw, 2.5rem); }

/* After the base rules, not before: a media query adds no specificity, so an
   override written above `.feature__media`'s own aspect-ratio would lose to it
   and stretch the box to 16:10 of the row height — straight over the copy. */
@media (min-width: 800px) {
  /* Stretch, not centre: the photograph runs the full height of the band, so
     the card reads as one panel rather than an image parked inside one. */
  .feature { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: stretch; }
  .feature__media { aspect-ratio: auto; }
  .feature__body { align-self: center; }
}
.feature__title { font-family: var(--display); font-size: clamp(1.5rem, 3vw, 2rem); margin: 0.35rem 0 0.75rem; }
.feature__body p { color: var(--muted); }
.feature__cta {
  font-family: var(--display); font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.04em; color: var(--accent-text);
}
.feature:hover .feature__cta { color: var(--arctic); }

/* Real measurements off the product, in the face reserved for numbers — not a
   stat row. Every value here appears in the deskmat's own spec sheet. */
.feature__specs {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr));
  gap: 0.75rem 1.5rem;
  margin: 0 0 1.1rem; padding: 0.85rem 0;
  border-block: 1px solid var(--line);
}
.feature__specs > div { display: grid; gap: 0.15rem; align-content: start; }
.feature__specs dt {
  font-family: var(--mono); font-size: 0.6rem;
  text-transform: uppercase; letter-spacing: 0.14em; color: var(--muted);
}
.feature__specs dd {
  margin: 0; font-family: var(--mono); font-size: 0.85rem;
  font-variant-numeric: tabular-nums; letter-spacing: -0.02em;
}

/* --------------------------------------------------------------------------
   pitch & feel — the signature readout
   Renders only where the source copy actually states a value, so it is never
   showing an invented number.
   -------------------------------------------------------------------------- */

.pitch {
  display: grid; gap: 0.5rem;
  padding: 1rem 0;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}

.pitch__row { display: grid; grid-template-columns: 5.5rem 1fr; align-items: center; gap: 0.75rem; }

.pitch__label {
  font-family: var(--mono); font-size: 0.68rem;
  text-transform: uppercase; letter-spacing: 0.12em; color: var(--muted);
}

.pitch__scale {
  position: relative;
  display: grid; grid-template-columns: repeat(5, 1fr); gap: 3px;
}
.pitch__tick { height: 0.5rem; background: var(--line-strong); border-radius: 1px; }
.pitch__tick[data-on="true"] { background: var(--accent); }

.pitch__ends {
  display: flex; justify-content: space-between;
  font-family: var(--mono); font-size: 0.62rem;
  text-transform: uppercase; letter-spacing: 0.1em; color: var(--muted);
}

/* --------------------------------------------------------------------------
   buttons
   -------------------------------------------------------------------------- */

.btn {
  font-family: var(--display); font-size: 1.05rem; font-weight: 600;
  letter-spacing: 0.04em; text-transform: uppercase;
  display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem;
  min-height: 48px; padding: 0.6rem 1.5rem;
  border: 1px solid transparent; border-radius: var(--radius);
  cursor: pointer; text-decoration: none;
  /* Copper is a light fill: white on it is 2.4:1. The label is the dark
     surface colour, which gives 5.9:1. */
  background: var(--accent); color: var(--accent-on);
  transition: background 0.15s ease;
}
.btn:hover:not(:disabled) { background: var(--accent-hover); color: var(--accent-on); }
.btn:disabled { background: var(--graphite-raised); color: var(--muted); cursor: not-allowed; border-color: var(--line); }
@media (prefers-reduced-motion: reduce) { .btn { transition: none; } }

.btn--ghost { background: transparent; border-color: var(--line-strong); color: var(--arctic); }
.btn--ghost:hover:not(:disabled) { background: var(--graphite-raised); border-color: var(--arctic); }

/* --------------------------------------------------------------------------
   product detail
   -------------------------------------------------------------------------- */

/* The way back, built as the thing this shop sells. A keycap is a top surface
   sitting on a skirt, so the control is a face with a 2px edge under it and a
   lit top lip; pressing it takes the skirt away and drops the face onto the
   page by exactly that much. The legend is set in the mono face the rest of
   the site uses for anything stamped rather than written. */
.back-key {
  display: inline-flex; align-items: center; gap: 0.55rem;
  margin: 1.5rem 0 0;
  min-height: 44px; padding: 0.5rem 1rem;
  background-color: var(--graphite-raised);
  background-image: var(--brush);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  box-shadow: inset 0 1px 0 rgba(236, 234, 229, 0.07), 0 2px 0 var(--line-strong);
  color: var(--arctic); text-decoration: none;
  font-family: var(--mono); font-size: 0.74rem;
  text-transform: uppercase; letter-spacing: 0.11em;
  transition: transform 0.1s ease-out, box-shadow 0.1s ease-out, border-color 0.15s;
}
.back-key:hover { border-color: var(--arctic); }
.back-key:active {
  transform: translateY(2px);
  box-shadow: inset 0 1px 0 rgba(236, 234, 229, 0.07), 0 0 0 var(--line-strong);
}
/* The arrow is the legend on the cap, not a separate moving part — it is the
   only thing here that is not text, so it carries the accent. */
.back-key__arrow { color: var(--accent-text); font-size: 0.95rem; line-height: 1; }
@media (prefers-reduced-motion: reduce) {
  .back-key { transition: none; }
  .back-key:active { transform: none; }
}

.product { display: grid; gap: clamp(1.5rem, 4vw, 3rem); padding: clamp(1.5rem, 4vw, 3rem) 0; }
@media (min-width: 900px) { .product { grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); align-items: start; } }

.gallery { display: grid; gap: 0.5rem; }
.gallery__main {
  background: var(--graphite-deep); border: 1px solid var(--line);
  border-radius: var(--radius); aspect-ratio: 1 / 1; overflow: hidden;
}
.gallery__main img { width: 100%; height: 100%; object-fit: cover; }
.gallery__thumbs { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.gallery__thumb {
  width: 4.5rem; height: 4.5rem; padding: 0;
  background: var(--graphite-deep); border: 1px solid var(--line);
  border-radius: 2px; overflow: hidden; cursor: pointer;
}
.gallery__thumb[aria-pressed="true"] { border-color: var(--accent-text); }

.variants { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0 0 1.25rem; padding: 0; border: 0; }
.variants legend {
  font-family: var(--mono); font-size: 0.68rem; text-transform: uppercase;
  letter-spacing: 0.12em; color: var(--muted); padding: 0; margin-bottom: 0.6rem;
}
.variant {
  background: var(--graphite-raised); border: 1px solid var(--line);
  color: var(--arctic); border-radius: var(--radius);
  padding: 0.5rem 0.9rem; min-height: 44px; cursor: pointer;
  font-family: var(--mono); font-size: 0.85rem;
  display: flex; flex-direction: column; align-items: flex-start; line-height: 1.3;
}
.variant span { font-size: 0.65rem; color: var(--muted); }
.variant[aria-pressed="true"] { border-color: var(--accent-text); background: var(--graphite-deep); }
.variant:disabled { opacity: 0.45; cursor: not-allowed; }

.spec-table { width: 100%; border-collapse: collapse; margin: 1rem 0 2rem; font-size: 0.85rem; }
.spec-table th, .spec-table td { text-align: left; padding: 0.55rem 0; border-bottom: 1px solid var(--line); vertical-align: top; }
.spec-table th { color: var(--muted); font-weight: 500; width: 42%; font-family: var(--body); }

.prose h3 { margin: 1.75rem 0 0.5rem; }
.prose ul { margin: 0 0 1em; padding-left: 1.1rem; color: var(--muted); }
.prose li { margin-bottom: 0.3em; }
.prose p { color: var(--muted); }

/* --------------------------------------------------------------------------
   The Forge
   The stage shows only the layers up to the step being chosen, so the board
   builds up as you work through it. That is not decoration: once keycaps go
   on, the plate and switches are completely hidden, so steps 4 and 5 would
   otherwise appear to change nothing.
   -------------------------------------------------------------------------- */

.forge { display: grid; gap: clamp(1.5rem, 3vw, 2.5rem); padding: clamp(1.5rem, 4vw, 2.5rem) 0; }
@media (min-width: 1000px) {
  .forge { grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr); align-items: start; }
  .forge__stage { position: sticky; top: 5.5rem; }
}

.forge__stage {
  background-color: var(--graphite-deep);
  background-image: var(--grain);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: clamp(0.75rem, 2vw, 1.5rem);
}
.forge__caption {
  font-family: var(--mono); font-size: 0.7rem;
  text-transform: uppercase; letter-spacing: 0.12em;
  color: var(--muted); margin: 0.75rem 0 0;
}

.step { border: 0; padding: 0; margin: 0 0 2rem; }
.step__head { display: flex; align-items: baseline; gap: 0.6rem; margin-bottom: 0.75rem; }
.step__num {
  font-family: var(--mono); font-size: 0.7rem; color: var(--muted);
  border: 1px solid var(--line-strong); border-radius: 2px;
  padding: 0.1rem 0.4rem; flex: none;
}
.step__title { font-family: var(--display); font-size: 1.25rem; font-weight: 600; }
.step__note { color: var(--muted); font-size: 0.85rem; margin: 0 0 0.75rem; }

.option-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr)); gap: 0.5rem; }

.option {
  display: flex; flex-direction: column; gap: 0.15rem; text-align: left;
  background: var(--graphite-raised); border: 1px solid var(--line);
  border-radius: var(--radius); color: var(--arctic);
  padding: 0.6rem 0.75rem; min-height: 44px; cursor: pointer;
  font-family: var(--body); font-size: 0.9rem; line-height: 1.25;
}
.option:hover { border-color: var(--line-strong); }
.option[aria-pressed="true"] {
  border-color: var(--accent);
  box-shadow: inset 3px 0 0 var(--accent);
  background: var(--graphite-deep);
}
.option small { color: var(--muted); font-size: 0.72rem; }
.option:disabled { opacity: 0.45; cursor: not-allowed; }

/* --------------------------------------------------------------------------
   build summary
   -------------------------------------------------------------------------- */

.summary { border-top: 1px solid var(--line); padding-top: 1.25rem; }
.summary__row {
  display: flex; justify-content: space-between; gap: 1rem;
  padding: 0.35rem 0; font-size: 0.9rem;
  border-bottom: 1px solid var(--line);
}
.summary__row dt { color: var(--muted); font-family: var(--mono); font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.1em; }
.summary__row dd { margin: 0; text-align: right; }
.summary__row dd[data-empty="true"] { color: var(--muted); font-style: italic; }
.summary dl { margin: 0 0 1.25rem; }

/* --------------------------------------------------------------------------
   cart
   -------------------------------------------------------------------------- */

.cart-row {
  display: grid; grid-template-columns: 4.5rem 1fr auto; gap: 1rem;
  align-items: center; padding: 1rem 0; border-bottom: 1px solid var(--line);
}
.cart-row__media { width: 4.5rem; height: 4.5rem; background: var(--graphite-deep); border-radius: 2px; overflow: hidden; }
.cart-row__media img { width: 100%; height: 100%; object-fit: cover; }
.cart-row__name { font-family: var(--display); font-size: 1.1rem; }
.cart-row__meta { font-family: var(--mono); font-size: 0.72rem; color: var(--muted); }

.qty { display: inline-flex; align-items: center; border: 1px solid var(--line); border-radius: var(--radius); }
.qty button {
  min-width: 44px; min-height: 44px; background: none; border: 0;
  color: var(--arctic); font-family: var(--mono); font-size: 1rem; cursor: pointer;
}
.qty button:hover { background: var(--graphite-raised); }
.qty output { min-width: 2.5rem; text-align: center; font-family: var(--mono); }

.cart-total { display: flex; justify-content: space-between; align-items: baseline; padding: 1.25rem 0; font-size: 1.25rem; }

/* --------------------------------------------------------------------------
   forms
   -------------------------------------------------------------------------- */

.field {
  display: block; width: 100%;
  background: var(--graphite-raised);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  color: var(--arctic);
  font-family: var(--body); font-size: 1rem;
  padding: 0.7rem 0.85rem; margin-bottom: 0.6rem;
  min-height: 48px;
}
form label {
  display: block;
  font-family: var(--mono); font-size: 0.7rem;
  text-transform: uppercase; letter-spacing: 0.12em;
  color: var(--muted); margin-bottom: 0.3rem;
}
.field::placeholder { color: var(--muted); }
.field:focus-visible { border-color: var(--accent); }
textarea.field { resize: vertical; min-height: 4.5rem; }

/* Honeypot — off-screen, skipped by keyboard and by autofill. A real visitor
   never sees it; a bot that fills it gets a silent success and no email. */
.hp {
  position: absolute; left: -9999px;
  width: 1px; height: 1px; overflow: hidden;
}

/* --------------------------------------------------------------------------
   long-form: the guide
   Chapters, spec cards and paired pros/cons — the shape the content already
   has. A single muted column threw all of it away.
   -------------------------------------------------------------------------- */

.page-lede {
  color: var(--arctic); font-size: clamp(1.05rem, 2vw, 1.25rem);
  max-width: 46ch; line-height: 1.5;
}

.chapter-nav {
  display: flex; flex-wrap: wrap; gap: 0.5rem;
  position: sticky; top: 4.25rem; z-index: 20;
  /* Opaque, and carrying the same finish as the page it scrolls over. */
  background-color: var(--graphite);
  background-image: var(--grain);
  padding: 1rem 0; margin: 2rem 0 1rem;
  border-bottom: 1px solid var(--line);
}
.chapter-nav a {
  display: inline-flex; align-items: baseline; gap: 0.5rem;
  font-family: var(--display); font-size: 1rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.03em;
  color: var(--muted); text-decoration: none;
  border: 1px solid var(--line); border-radius: var(--radius);
  padding: 0.55rem 0.9rem; min-height: 44px;
}
.chapter-nav a span { font-family: var(--mono); font-size: 0.7rem; color: var(--accent-text); }
.chapter-nav a:hover { color: var(--arctic); border-color: var(--accent); }

/* The home page's teaser for the Guide: the guide's own three chapters, in the
   order it reads them, each linking to where it starts. The numbers are the
   guide's numbers — front to back of a build — not decoration. */
.chapter-list { list-style: none; margin: 1.75rem 0 0; padding: 0; display: grid; }
.chapter-list li { border-top: 1px solid var(--line); }
.chapter-list li:last-child { border-bottom: 1px solid var(--line); }
.chapter-list a {
  display: grid; grid-template-columns: 3rem 1fr auto; align-items: baseline;
  gap: 0.5rem 1rem;
  padding: 1.1rem 0.25rem; min-height: 44px;
  color: inherit; text-decoration: none;
}
.chapter-list a:hover { background-color: rgba(236, 234, 229, 0.035); }
.chapter-list b {
  font-family: var(--mono); font-size: 0.72rem; font-weight: 400;
  letter-spacing: 0.08em; color: var(--accent-text);
}
.chapter-list strong {
  font-family: var(--display); font-weight: 600;
  font-size: clamp(1.2rem, 2.6vw, 1.6rem);
  text-transform: uppercase; letter-spacing: 0.02em;
}
.chapter-list span { color: var(--muted); font-size: 0.9rem; grid-column: 2 / -1; }
.chapter-list em {
  font-style: normal; font-family: var(--mono); font-size: 0.7rem;
  text-transform: uppercase; letter-spacing: 0.12em; color: var(--muted);
  grid-row: 1; grid-column: 3;
}
.chapter-list a:hover em, .chapter-list a:hover strong { color: var(--accent-text); }
@media (max-width: 560px) {
  .chapter-list a { grid-template-columns: 2.25rem 1fr; }
  .chapter-list em { display: none; }
}

.chapter { padding: clamp(2rem, 5vw, 3.5rem) 0; border-bottom: 1px solid var(--line); scroll-margin-top: 9rem; }
.chapter:last-of-type { border-bottom: 0; }

.chapter__head { display: flex; align-items: center; gap: 1rem; margin-bottom: 1.25rem; }
/* The numbers are real: three chapters read in order, front to back of a build. */
.chapter__num {
  font-family: var(--mono); font-size: 0.85rem;
  color: var(--accent-on); background: var(--accent);
  border-radius: var(--radius); padding: 0.3rem 0.55rem; flex: none;
}
.chapter__head h2 { font-size: clamp(1.9rem, 5vw, 3rem); text-transform: uppercase; }
.chapter__lead { max-width: 64ch; }
.chapter__lead p { color: var(--muted); }
.chapter__lead ul { color: var(--muted); padding-left: 1.1rem; }
.chapter__cta { margin-top: 1.5rem; }

/* spec cards — one per switch/case/plate type */
.spec-cards { display: grid; gap: 0.75rem; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); margin: 1rem 0 0; }
.spec-card {
  background-color: var(--graphite-raised);
  background-image: var(--brush);
  border: 1px solid var(--line);
  border-top: 2px solid var(--accent);
  border-radius: var(--radius);
  padding: 1rem 1.1rem;
}
.spec-card h4 {
  font-family: var(--display); font-size: 1.1rem; font-weight: 600;
  margin: 0 0 0.6rem; text-transform: uppercase; letter-spacing: 0.02em;
}
.spec-card dl { margin: 0; display: grid; gap: 0.4rem; }
.spec-card dl > div { display: grid; grid-template-columns: 5.5rem 1fr; gap: 0.6rem; align-items: baseline; }
.spec-card dt {
  font-family: var(--mono); font-size: 0.62rem; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--muted);
}
.spec-card dd { margin: 0; font-size: 0.88rem; line-height: 1.4; }

/* panels: pros, cons, mistakes, takeaways */
.panel {
  background-color: var(--graphite-raised);
  background-image: var(--brush);
  border: 1px solid var(--line);
  border-left: 3px solid var(--line-strong);
  border-radius: 0 var(--radius) var(--radius) 0;
  padding: 1.1rem 1.25rem; margin: 1rem 0;
}
.panel h3 {
  font-family: var(--mono); font-size: 0.7rem; font-weight: 500;
  text-transform: uppercase; letter-spacing: 0.14em;
  color: var(--muted); margin: 0 0 0.6rem;
}
.panel ul { margin: 0; padding-left: 1.1rem; }
.panel li { margin-bottom: 0.4rem; font-size: 0.92rem; }
.panel li strong { color: var(--arctic); }
.panel p { color: var(--muted); }

.proscons { display: grid; gap: 1rem; margin: 1rem 0; }
@media (min-width: 800px) { .proscons { grid-template-columns: 1fr 1fr; align-items: start; } }
/* Pros and cons are told apart by the marker and the rule, not by red and
   green — the accent is the only hue on the page and it means "Thorix". */
.panel--pro { border-left-color: var(--arctic); }
.panel--pro ul { list-style: none; padding-left: 0; }
.panel--pro li::before { content: "+"; font-family: var(--mono); color: var(--arctic); margin-right: 0.6rem; }
.panel--con ul { list-style: none; padding-left: 0; }
.panel--con li::before { content: "\2212"; font-family: var(--mono); color: var(--muted); margin-right: 0.6rem; }
.panel--warn { border-left-color: var(--accent); }
.panel--key {
  border-left-color: var(--accent);
  background-color: var(--graphite-deep);
  background-image: var(--grain);
}
.panel--key li { font-size: 1rem; }

/* --------------------------------------------------------------------------
   contact
   -------------------------------------------------------------------------- */

.contact-grid { display: grid; gap: clamp(1.5rem, 4vw, 3rem); margin-top: 2rem; }
@media (min-width: 850px) { .contact-grid { grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr); } }

.contact-list { margin: 1rem 0 0; }
.contact-list dt {
  font-family: var(--mono); font-size: 0.7rem;
  text-transform: uppercase; letter-spacing: 0.12em;
  color: var(--muted); margin-top: 1.25rem;
}
.contact-list dd { margin: 0.25rem 0 0; }

/* --------------------------------------------------------------------------
   states — designed, not blank
   -------------------------------------------------------------------------- */

.note {
  border-left: 3px solid var(--line-strong);
  padding: 0.85rem 1rem; margin: 1rem 0;
  background: var(--graphite-raised); border-radius: 0 var(--radius) var(--radius) 0;
  color: var(--muted); font-size: 0.9rem;
}
/* Errors get weight and a rule rather than a second red, so the accent keeps
   meaning exactly one thing. */
.note--error { border-left-color: var(--arctic); color: var(--arctic); font-weight: 600; }
.note--warn  { border-left-color: var(--accent); }

.empty { padding: clamp(2rem, 6vw, 4rem) 0; text-align: left; }
.empty h2 { margin-bottom: 0.5rem; }

.skeleton {
  background: var(--graphite-raised);
  border-radius: var(--radius);
  min-height: 12rem;
  position: relative; overflow: hidden;
}
.skeleton::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(90deg, transparent, rgba(236,234,229,0.06), transparent);
  transform: translateX(-100%);
  animation: sweep 1.4s infinite;
}
@keyframes sweep { to { transform: translateX(100%); } }
@media (prefers-reduced-motion: reduce) { .skeleton::after { animation: none; } }

/* --------------------------------------------------------------------------
   sections + footer
   -------------------------------------------------------------------------- */

.section { padding: clamp(2.5rem, 6vw, 5rem) 0; }
.section--deep {
  background-color: var(--graphite-deep);
  background-image: var(--grain);
  border-block: 1px solid var(--line);
}
.section__head { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; flex-wrap: wrap; margin-bottom: 1.75rem; }

.site-footer {
  background-color: var(--graphite-deep);
  background-image: var(--grain);
  border-top: 1px solid var(--line);
  padding: clamp(2rem, 5vw, 3.5rem) 0 2rem;
  color: var(--muted); font-size: 0.9rem;
}
.site-footer__grid { display: grid; gap: 2rem; }
@media (min-width: 700px) { .site-footer__grid { grid-template-columns: 2fr 1fr 1fr; } }
.site-footer h4 { font-size: 0.72rem; font-family: var(--mono); text-transform: uppercase; letter-spacing: 0.14em; color: var(--arctic); margin-bottom: 0.75rem; }
.site-footer ul { list-style: none; margin: 0; padding: 0; }
.site-footer li { margin-bottom: 0.1rem; }
/* Footer links are real tap targets, not 17px-tall text. inline-flex + a 44px
   min-height meets the touch floor without needing extra list spacing. */
.site-footer a {
  color: var(--muted); text-decoration: none;
  display: inline-flex; align-items: center;
  min-height: 44px; padding-right: 0.5rem;
}
.site-footer a:hover { color: var(--arctic); text-decoration: underline; }
.site-footer .brand { min-height: 44px; }
.site-footer__legal { border-top: 1px solid var(--line); margin-top: 2rem; padding-top: 1.25rem; font-size: 0.78rem; }

/* Social links sit inline under the mark rather than as a row of icon
   glyphs — the names are unambiguous and need no icon font. */
.social { display: flex; flex-wrap: wrap; gap: 0.25rem 1rem; margin-top: 0.5rem; }
.social a { font-family: var(--mono); font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.1em; }
.social a:hover { color: var(--accent-text); text-decoration: none; }

@media (min-width: 700px) { .site-footer__grid { grid-template-columns: 2fr 1fr 1fr 1fr; } }

.visually-hidden {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}
