/* ===================================================================
   DTF Station Vietnam — DESIGN SYSTEM
   Ported verbatim from the DTF Station US redesign
   (~/Projects/dtfstation-us-site/shared/design/).
   tokens.css + components.css, concatenated in that order.

   DO NOT EDIT THIS FILE BY HAND. It is a copy of the US source of
   truth so the two sites cannot drift. To change the design, change
   it in the US repo and re-copy (see tools/sync-design.mjs).

   Everything is scoped to .gs, which sits on <body>.
   VN-only components (announcement bar, language toggle, downloads
   accordion, product page, blog) live in main.css, also .gs-scoped.
   =================================================================== */

/* ===================================================================
   DTF Station — design tokens.  SHARED by the Astro site and the
   Shopify theme.  Edit here only; tools/sync.mjs copies this into
   theme/assets/design.css.

   WHY EVERYTHING IS SCOPED TO .gs
   The Shopify theme already ships a complete Apple-style design system
   in theme/assets/main.css. It collides with this one on 76 class names
   (.card .btn .wrap .band .nav .chip .cta …) and on 11 custom properties
   that hold DIFFERENT values:

       --accent   theme #34c9d4   here #49BFBC
       --maxw     theme 1120px    here 1240px
       --nav-h    theme 52px      here 64px
       --ink      theme #1a1a1c   here #202124

   During the page-by-page conversion both systems are live on the same
   page (converted sections + not-yet-converted header/footer), so they
   have to be isolated. `.gs` ("Google Store") is that isolation: it is
   put on the wrapper of every converted section, and on <body> in the
   Astro site where the whole page is this design.

   Specificity works out: `.gs .card` (0,2,0) beats the theme's `.card`
   (0,1,0), so converted markup always wins without !important.

   When the theme is fully converted and main.css is deleted, `.gs` can
   be stripped in one pass — or just left, it costs nothing.
   =================================================================== */

.gs {
  /* --- ink --- */
  --ink: #202124;          /* primary text */
  --ink-soft: #5f6368;     /* secondary text */
  /* 2026-09-03 — was #80868b, which is 3.56:1 on the page ground and FAILS
     WCAG AA for normal text (4.5). This token carries every eyebrow, spec
     label, card kicker, form hint and the footer legal line, so the failure
     was sitewide and long-standing — it predates the brand wash and was found
     by the contrast probe run over ten pages before going live.
     Deepened AGAIN the same day when the wash was turned up. The ground this
     has to clear is no longer a flat colour — it is the strongest pixel of
     the brand wash inside the content column, sampled from a rendered plate:
     rgb(205,235,236) in the teal corner at 1440x900. #686e75 was 4.09:1 there.
     #5c6167 is 4.98:1 against that and clears 4.5 on every card and tint
     ground too. Do not lighten it back without re-sampling the plate. */
  --ink-faint: #5c6167;    /* captions, meta — AA-safe over the wash, see above */
  --line: #dadce0;         /* hairlines, outline buttons */
  --line-soft: #e8eaed;

  /* --- ground --- */
  /* 2026-09-03 — the page ground is now the brand canvas, not pure white.
     Taken from the LOCKED social card system Jason approved on 2026-08-09
     (~/Projects/cmo-agents/.claude/knowledge/design-system/social-card-system.md)
     and used by every social post and the 2026 brand guidelines. */
  --bg: #fbfbfd;
  --bg-alt: #f8f9fa;

  /* --- the brand wash ---
     Three soft radials over --bg: gold from the top-left, teal from the
     top-right and the bottom-right. Jason: *"I kind of like that subtle
     gradient of teal and yellow in the general background. Can you emulate
     that throughout the page?"*

     Positions, opacities and falloffs are the locked card values verbatim.
     The only change is the TEAL, which uses this site's --accent (73,191,188)
     rather than the card system's #49C1BE (73,193,190) — a two-value
     difference nobody can see, and it keeps ONE teal in the codebase. The
     third accent in this project caused a contrast bug every time it was
     duplicated; see the note above.

     ⚠️ --wash-gold IS A BACKGROUND VALUE AND NEVER A TEXT COLOUR. Gold was
     removed from --grad precisely because gold text fails on white. It is
     back only as a wash behind content, at 32% and fading to nothing.

     The card system adds: "slides with a bottom-anchored machine layer drop
     the bottom-right gradient so the wash never sits behind the product."
     That rule holds here for free — every product on this site sits inside an
     opaque white card, so nothing is ever photographed against the wash. */
  --wash-gold: 253, 205, 129;   /* #FDCD81 */
  --wash-teal: 73, 191, 188;    /* --accent */
  /* 2026-09-03, second pass — Jason: "make the gradients in the background
     stronger and spread out a little wider." Opacity up ~1.5x, radii up ~1.35x,
     falloff pushed further out so each one reaches further into the page
     before it dies. The card-system positions are unchanged; this is the same
     wash turned up, not a different one.
     Ceiling checked, not guessed: see the note under --wash-gold. */
  --brand-wash:
    radial-gradient(80% 86% at -8% -6%,  rgba(var(--wash-gold), .50), rgba(var(--wash-gold), 0) 72%),
    radial-gradient(76% 82% at 108% -4%, rgba(var(--wash-teal), .32), rgba(var(--wash-teal), 0) 70%),
    radial-gradient(74% 80% at 106% 104%, rgba(var(--wash-teal), .36), rgba(var(--wash-teal), 0) 72%);

  /* The same wash tuned for a CARD rather than the page — half strength and
     pulled to the corners, because a card is a fraction of the viewport and
     the page values would read as a colour cast rather than a wash. This is
     what makes the flagship card special now that it is no longer black. */
  --brand-wash-card:
    radial-gradient(92% 104% at -4% -10%, rgba(var(--wash-gold), .34), rgba(var(--wash-gold), 0) 70%),
    radial-gradient(88% 100% at 104% 108%, rgba(var(--wash-teal), .30), rgba(var(--wash-teal), 0) 72%);

  /* --- card tints ---
     One per product family, so a card is recognizable before it is read.
     Derived from the DTF Station gold->teal gradient plus two neutrals.

     NOTE: cards and banners no longer USE the colour tints — components.css
     collapses all six to --tint-neutral. These stay defined because the
     selected states on .cfg-opt / .chip-printer still read --tint-teal, and
     because restoring per-family colour should be a one-line change there,
     not a re-derivation here. */
  --tint-neutral: #ffffff;  /* the single card/banner surface */

  /* Cards and banners sit on a white page, so a white surface needs an edge
     to read as a card at all. This is that edge — a hairline drawn INSIDE the
     border-radius via inset box-shadow, so it never fights `overflow: hidden`
     on the media well the way a real `border` does (a border would also add
     2px to the box and knock the mosaic's 6-column maths out). */
  --edge: inset 0 0 0 1px var(--line-soft);
  --edge-hover: inset 0 0 0 1px var(--line);
  --tint-teal: #ECF8F8;   /* brand hue at 95% — same value as --accent-tint */
  --tint-sand: #fdf3e0;
  --tint-mist: #eef1f5;
  --tint-sage: #e7f3ec;
  --tint-blush: #fbeceb;
  --tint-slate: #f1f3f4;
  --tint-ink: #202124;     /* dark card, for the flagship */

  /* --- brand ---
     2026-08-30: Jason set the brand colour to #49BFBC.

     It is a FILL colour, not a text colour, and the difference is not
     cosmetic — measured against white it is 2.22:1, which fails WCAG AA for
     text (4.5) and even the 3.0 floor for UI. White text on it is the same
     2.22:1. So:

       --accent       #49BFBC  fills, rules, graphics, anything on dark
                               (8.05:1 on the dark slab — excellent there)
       --accent-hover #40B5B2  the fill, one step down, for button hover
       --accent-ink   #28716F  SAME HUE darkened until it passes on white AND
                               over the brand wash (was #2D807E; see below)
                               (4.67:1) — text, links, small marks on light

     Buttons filled with --accent therefore take DARK text, not white
     (7.86:1). Do not "fix" that back to white: it is the thing that makes
     this colour usable at full strength instead of muddied.

     This replaced #0e93a1, which was itself chosen over the theme's #34c9d4
     for exactly these contrast reasons. The lesson keeps repeating: pick the
     brand colour, then derive a text shade from it — do not use one value for
     both jobs. */
  /* Paints TEXT (.grad-text), so every stop has to be readable on white. It
     runs ink -> --accent-ink rather than through the brand fill: #49BFBC as
     gradient text would be 2.22:1 and the last words of every heading using it
     would disappear. Was a gold -> green -> teal sweep from the old brand. */
  --grad: linear-gradient(100deg, #141b1c 0%, #28716F 100%);  /* end stop = --accent-ink */
  --accent: #49BFBC;
  --accent-hover: #40B5B2;
  /* Deepened from #2D807E on 2026-09-03 when the wash was strengthened. The
     old value is 4.51:1 on white but only 3.71:1 over the teal corner of the
     wash, and this token paints LINKS, which sit on the page ground all over
     the site. Same hue, 4.56:1 against the worst pixel of the wash. */
  --accent-ink: #28716F;
  --accent-tint: #ECF8F8;  /* the same hue at 95% lightness, for selected states */
  --link: #28716F;         /* = --accent-ink; was Google-blue #1a73e8 — off-brand next to the teal */

  /* --- geometry --- */
  --r-card: 28px;
  --r-banner: 32px;
  --r-chip: 999px;
  --nav-h: 64px;
  --maxw: 1240px;
  --gap: 16px;

  /* Poppins, 2026-08-30, Jason's call. Loaded from Google Fonts in
     site/src/layouts/Base.astro and theme/layout/theme.liquid — the token
     alone does nothing if the face is not fetched.

     Four weights are loaded and each has a job. Poppins is geometric with a
     modest x-height, so it separates weights much more visibly than the old
     Google Sans stack did; using one weight everywhere wastes that.

       400  body copy
       500  eyebrows, labels, buttons, nav
       600  headings
       700  the big figures

     Poppins is also noticeably WIDER per character, so headline tracking was
     eased off across the board — see --track-* below. Its own geometry is
     already tight; piling on -0.035em made caps collide. */
  --font: Poppins, "Google Sans", Inter, -apple-system, BlinkMacSystemFont,
          "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

  /* Tracking, named rather than repeated, so the whole system moves together
     if the face changes again. */
  --track-display: -0.02em;
  --track-heading: -0.015em;
  --track-eyebrow: 0.08em;

  /* SHADOW LANGUAGE — 2026-09-01. This is the single biggest "premium vs.
     utility" tell on the page and it had been the wrong one.

     What was here was Material's elevation shadow: tight, close, dark, two
     small offsets. It says "this is a control you can press". The original
     Apple-style theme used the opposite — a large, soft, far-thrown shadow at
     low opacity (0 30px 60px -30px), which says "this object is sitting on a
     surface under a big soft light". Same element, completely different
     register.

     The shape that matters is the NEGATIVE SPREAD. A big blur with no spread
     pull-back reads as grey haze around the whole box; pulling it back by
     roughly half the blur keeps the shadow under the object where a real one
     would be, so the card looks lifted rather than smudged.

     Tinted with the ink hue rather than neutral grey — a pure-grey shadow on
     a warm-white page reads as dirt. */
  --shadow-card: 0 1px 2px rgba(20, 24, 26, .04), 0 18px 40px -24px rgba(20, 24, 26, .22);
  --shadow-lift: 0 2px 6px rgba(20, 24, 26, .05), 0 40px 72px -34px rgba(20, 24, 26, .34);

  /* Product shots are cut-outs on flat colour, which is why they looked
     pasted on. A drop-shadow filter follows the alpha channel — the actual
     silhouette of the machine — instead of its bounding box, so the printer
     casts a shadow shaped like a printer. This is what the old theme did with
     `filter: drop-shadow(0 34px 50px ...)` on the hero product and it is most
     of why that page felt like a showroom. */
  --shadow-product: drop-shadow(0 24px 34px rgba(20, 24, 26, .18));
  --shadow-product-lg: drop-shadow(0 40px 56px rgba(20, 24, 26, .26));
  /* On the dark treatments a black shadow does nothing; these read as the
     object occluding its own lit ground. */
  --shadow-product-dark: drop-shadow(0 40px 60px rgba(0, 0, 0, .65));

  /* The theme's body font is Apple-system; a converted section has to
     restate it or it inherits the old stack inside .gs. */
  font-family: var(--font);
  color: var(--ink);
}

@media (max-width: 640px) {
  .gs { --r-card: 22px; --r-banner: 24px; --gap: 12px; }
}

/* ===================================================================
   DTF Station — component system.  SHARED by the Astro site and the
   Shopify theme.  Every selector is scoped to .gs — see tokens.css for
   why. Edit here only; tools/sync.mjs generates theme/assets/design.css.

   Design language: Google Store. Clean white canvas, but image-forward —
   content lives inside large tinted rounded cards rather than on bare
   white. Banners are structural here, not decoration.
   =================================================================== */

.gs a { color: inherit; text-decoration: none; }
.gs img { max-width: 100%; display: block; }
.gs button { font-family: inherit; }
.gs *, .gs *::before, .gs *::after { box-sizing: border-box; }

.gs .wrap { max-width: var(--maxw); margin: 0 auto; padding: 0 24px; }
.gs .grad-text {
  background: var(--grad);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* ---------- Type scale ----------
   2026-09-01 — REBUILT UPWARDS. Jason: the site stopped feeling premium.

   The largest single cause was that the whole scale had shrunk. The original
   Apple-style theme ran its hero at clamp(44px, 7.2vw, 88px) and its section
   heads at clamp(32px, 4.6vw, 52px); this system had come in at 56 and 40.
   Every individual number looked defensible and the sum of them was a page
   where nothing was big, so nothing read as important.

   The effect being restored is SCALE CONTRAST — one very large line against
   small, quiet body copy. That ratio is doing almost all the work in the look
   Jason is asking for; it is not the typeface, the colour or the radii, all of
   which were already right. A display line that is only 3x the body size reads
   as a document heading. At 5x it reads as a statement.

   Body copy deliberately did NOT grow to match. Widening the gap is the whole
   point; scaling everything up together just makes a bigger document. */
/* Poppins separates weights clearly, so headings sit at 600 rather than the
   500 the old face needed to look substantial. */
.gs h1, .gs h2, .gs h3 { letter-spacing: var(--track-heading); font-weight: 600; }
.gs .display  { font-size: clamp(40px, 6.6vw, 82px); line-height: 1.03; letter-spacing: var(--track-display); font-weight: 600; }
.gs .title-lg { font-size: clamp(30px, 4.4vw, 52px); line-height: 1.07; letter-spacing: var(--track-display); }
.gs .title-md { font-size: clamp(23px, 2.7vw, 32px); line-height: 1.2; }
/* Grew one step, not four. It has to stay clearly subordinate to .title-lg. */
.gs .lede { font-size: clamp(17px, 1.5vw, 20px); color: var(--ink-soft); line-height: 1.55; }
.gs .meta { font-size: 13px; color: var(--ink-faint); }
/* Uppercase Poppins is wider than the old face and needs a touch more air
   between letters to stop reading as a solid block. */
.gs .eyebrow {
  font-size: 12.5px; font-weight: 500; letter-spacing: var(--track-eyebrow);
  text-transform: uppercase; color: var(--ink-faint);
}

/* ---------- Buttons ---------- */
.gs .btn {
  display: inline-flex; align-items: center; gap: .4em;
  font-size: 15px; font-weight: 500;
  padding: 11px 24px; border-radius: var(--r-chip);
  border: 1px solid transparent; cursor: pointer;
  transition: background .2s, box-shadow .2s, border-color .2s, transform .2s;
  white-space: nowrap; justify-content: center;
  font-family: var(--font);
}
.gs .btn-primary { background: var(--ink); color: #fff; }
.gs .btn-primary:hover { background: #000; box-shadow: var(--shadow-lift); }
/* Dark text on the accent fill, not white. #49BFBC with white type is 2.22:1
   and unreadable; with near-black it is 7.86:1. See the note in tokens.css. */
.gs .btn-accent { background: var(--accent); color: var(--ink); }
.gs .btn-accent:hover { background: var(--accent-hover); box-shadow: var(--shadow-lift); }
.gs .btn-ghost { border-color: var(--line); color: var(--ink); background: transparent; }
.gs .btn-ghost:hover { background: rgba(60, 64, 67, .06); border-color: #c4c7c5; }
.gs .on-dark .btn-ghost { color: #fff; border-color: rgba(255, 255, 255, .4); }
.gs .on-dark .btn-ghost:hover { background: rgba(255, 255, 255, .12); }
.gs .on-dark .btn-primary { background: #fff; color: var(--ink); }

/* Text link with a sliding arrow — Google's "Learn more ›" */
/* min-height 24px, not padding: these are standalone actions on cards, not
   links inside a sentence, so WCAG 2.2's inline exception does not cover them
   and they measured 23px on a phone. A min-height grows the hit area without
   adding vertical space around the text, which padding would. */
.gs .btn-text {
  display: inline-flex; align-items: center; gap: 5px;
  min-height: 24px;
  font-size: 15px; font-weight: 500; color: var(--link);
}
.gs .btn-text .arw { transition: transform .2s; }
.gs .btn-text:hover .arw { transform: translateX(4px); }
.gs .on-dark .btn-text { color: var(--accent); }

/* ---------- Section rhythm ----------
   2026-09-01. Was a flat 56px / 28px. The original theme ran `section{padding:
   100px 0}` and the difference is not subtle: the space around a thing is what
   tells you it is worth looking at. 56px between sections reads as efficient
   use of the page, which is the opposite of the intent. Fluid now so a phone
   does not inherit desktop air it has no room for. */
.gs.band, .gs .band { padding: clamp(64px, 7.5vw, 104px) 0; }
.gs.band.tight, .gs .band.tight { padding: clamp(32px, 3.6vw, 52px) 0; }
.gs .sec-head { margin-bottom: clamp(32px, 3.6vw, 52px); }
.gs .sec-head p { margin-top: 12px; }

/* ---------- Section lead-in ----------
   eyebrow -> large heading -> paragraph, in a measure narrow enough to read.
   Lifted out of the homepage on 2026-09-02 when that page was split into
   reusable sections for the layout options; it was already the shape of every
   section opener on the site, just copy-pasted. `.lead-tight` is the
   subordinate version for a minor section head.

   The measure is 40rem on purpose. These headings run to 58px and the page is
   1240px wide — a heading allowed the full width would run to 90 characters a
   line and stop reading as a statement. */
.gs .lead { max-width: 40rem; margin-bottom: clamp(40px, 5vw, 64px); }
.gs .lead-tight { margin-bottom: 32px; }
.gs .lead h2 {
  font-size: clamp(32px, 4.6vw, 58px);
  line-height: 1.04;
  letter-spacing: var(--track-display);
  margin: 14px 0 0;
  text-wrap: balance;
}
.gs .lead-tight h2 { font-size: clamp(26px, 3.2vw, 40px); }
.gs .lead p {
  font-size: clamp(16px, 1.35vw, 19px);
  line-height: 1.6;
  color: var(--ink-soft);
  margin: 20px 0 0;
}

/* Was a tonal shift — first the opaque #f8f9fa, then a translucent white veil.
   2026-09-03, second pass: NO GROUND AT ALL. Once the wash was strengthened,
   any veil over it read as a pale horizontal stripe cutting across a diagonal
   gradient — the seam was the most visible edge on the page, which is exactly
   what the veil was introduced to avoid.

   The wash is the tonal interest now, and spacing carries the structure (it
   already had to, on a page with no dark bands left). Kept as a no-op class
   rather than deleted because both storefronts write it into markup. */
.gs .band-alt { background: none; }

/* ---------- Category chips (Google's round browse row) ---------- */
.gs .chips {
  display: flex; gap: 10px; overflow-x: auto; padding: 4px 0 12px;
  scrollbar-width: none; -ms-overflow-style: none;
}
.gs .chips::-webkit-scrollbar { display: none; }
.gs .chip {
  flex: 0 0 auto; display: inline-flex; align-items: center; gap: 9px;
  padding: 8px 20px 8px 8px; border-radius: var(--r-chip);
  border: 1px solid var(--line); background: #fff;
  font-size: 14px; font-weight: 500; color: var(--ink);
  transition: background .18s, border-color .18s, box-shadow .18s;
}
.gs .chip:hover { background: var(--bg-alt); border-color: #c4c7c5; box-shadow: var(--shadow-card); }
.gs .chip .ic {
  width: 42px; height: 42px; border-radius: 50%;
  display: grid; place-items: center; overflow: hidden;
  background: var(--tint-slate); flex: 0 0 auto;
}
.gs .chip .ic img { width: 84%; height: 84%; object-fit: contain; }

/* ===================================================================
   BANNERS — the reason for this design.
   .banner is a single wide rounded card; .mosaic is the grid of them.
   =================================================================== */
.gs .banner {
  position: relative; overflow: hidden;
  border-radius: var(--r-banner);
  background: var(--tint-neutral); box-shadow: var(--edge);
  display: grid; align-items: center;
  /* 380, was 460. Jason: the main cards on every page are really tall. These
     banners are one product each and the copy never fills that height, so the
     extra was empty space that pushed everything below the fold. */
  min-height: 380px;
}
.gs .banner .banner-body { padding: 48px clamp(28px, 4vw, 64px); max-width: 560px; z-index: 2; }
.gs .banner .banner-body h1,
.gs .banner .banner-body h2 { margin-bottom: 14px; }

/* 2026-09-01 — a banner body is a 560px column, not the full page, so section
   type set for the full width overshoots inside it. When .title-lg went from
   40px to 52px the short banner titles ("Prestige R1") were fine but the long
   editorial ones on the Why page — "US-based support and warranty, not a box
   with no one to call." — went to five lines and grew the card by ~200px.
   Jason cut banner heights this morning for exactly that reason, so the scale
   increase must not quietly give it back.

   One step down, not back to 40: still clearly bigger than it was, but sized
   for the column it actually occupies. `.display` is deliberately NOT included
   — an h1 banner is the page's headline and is meant to fill the box. */
.gs .banner .banner-body .title-lg { font-size: clamp(27px, 3.3vw, 42px); }
.gs .banner .banner-body p { margin-bottom: 24px; }
.gs .banner .banner-actions { display: flex; flex-wrap: wrap; gap: 12px; }
.gs .banner .banner-art {
  position: absolute; inset: 0 0 0 auto; width: 52%;
  display: grid; place-items: center; padding: 32px;
}
/* Rounded so that the product shots which are JPGs — and therefore carry a
   baked-in white background — read as a deliberate photo plate on a tinted
   banner rather than a hard white box. Transparent PNGs are unaffected:
   there is no opaque area for the radius to clip. */
/* The cap is on the IMAGE, not just the cell. A tall or near-square product
   shot (ink bottles, a floor-standing shaker) will otherwise grow the whole
   banner to fit itself and tower over the copy beside it. */
/* 2026-09-01 — the product now casts a shadow. These are cut-outs dropped on
   flat colour and without one they read as clip-art pasted onto the card,
   which was a large part of why the page stopped feeling like a showroom.
   drop-shadow follows the alpha silhouette, so the machine's own outline casts
   it. Heights are UNCHANGED: Jason cut these this morning because the banners
   were pushing the first section below the fold, and a shadow costs no height.
   The gain in presence comes from light, not size. */
.gs .banner .banner-art img {
  max-height: 280px; width: auto; object-fit: contain;  /* was 380 */
  border-radius: 18px;
  filter: var(--shadow-product);
}
.gs .banner.split .banner-art img { max-height: 260px; }  /* was 340 */
/* Black-on-black does nothing; on the dark treatment the shadow has to read as
   the object occluding its own lit ground instead. */
.gs .banner.on-dark .banner-art img,
.gs .banner.t-ink .banner-art img { filter: var(--shadow-product-dark); }
/* A JPG with a baked-in white background is a rectangle, not a silhouette, so
   drop-shadow would trace a hard box around it. Those get the plate treatment
   the border-radius above was written for and no shadow at all. */
.gs .banner .banner-art img[src$=".jpg"],
.gs .banner .banner-art img[src$=".jpeg"] { filter: none; }

/* split layout: text left, art right, both in flow (better on tall art) */
.gs .banner.split { grid-template-columns: 1fr 1fr; min-height: 340px; }  /* was 420 */
.gs .banner.split .banner-art { position: static; width: auto; }

/* art on the left instead of the right — lets a run of stacked banners
   alternate instead of marching down the page in one rhythm.
   The TEXT STAYS LEFT-ALIGNED. Only the columns swap. Right-aligning a
   paragraph to match the flip makes it noticeably harder to read: every
   line starts at a different x, so the eye loses the return point. */
.gs .banner.flip .banner-art { inset: 0 auto 0 0; }
.gs .banner.flip .banner-body { justify-self: end; }
.gs .banner.split.flip { direction: rtl; }
.gs .banner.split.flip > * { direction: ltr; }

/* Flagship banner treatment.
   2026-09-03 — Jason: *"I don't like the black background. Let's keep
   everything white and clean."* A flat near-black fill is no longer used
   anywhere on this site; the flagship now reads as special through the brand
   wash, scale and type instead of through a black rectangle.

   ⚠️ THE `:not(.has-bg)` IS LOAD-BEARING. Banner.astro puts `on-dark` on
   PHOTOGRAPHIC banners too (`isDark = dark || Boolean(bgImg)`), where the
   surface is a lit photograph under a scrim and the type has to stay white.
   Only the flat fill changes; the photographs are untouched. */
.gs .banner.on-dark:not(.has-bg) {
  background-color: var(--bg);
  background-image: var(--brand-wash-card);
  color: var(--ink);
  box-shadow: var(--edge);
}
.gs .banner.on-dark:not(.has-bg) .lede { color: var(--ink-soft); }
.gs .banner.on-dark:not(.has-bg) .eyebrow { color: var(--ink-faint); }
/* The ghost button reverts to its light-ground styling, and the primary is
   near-black again rather than the white it flips to on a dark card. */
.gs .banner.on-dark:not(.has-bg) .btn-ghost { color: var(--ink); border-color: var(--line); }
.gs .banner.on-dark:not(.has-bg) .btn-ghost:hover { background: rgba(60, 64, 67, .06); }
.gs .banner.on-dark:not(.has-bg) .btn-primary { background: var(--ink); color: #fff; }
.gs .banner.on-dark:not(.has-bg) .banner-art img { filter: var(--shadow-product); }

/* Photographic banners keep the near-black ground the scrim is built on. */
.gs .banner.on-dark.has-bg { background: var(--tint-ink); color: #fff; }
.gs .banner.on-dark.has-bg .lede { color: rgba(255, 255, 255, .72); }
.gs .banner.on-dark.has-bg .eyebrow { color: rgba(255, 255, 255, .6); }

/* A banner whose whole surface is a link (campaign artwork, no CTA). */
.gs a.banner { display: grid; }
.gs a.banner:hover { box-shadow: var(--shadow-lift); }

/* Full-bleed artwork banner: a designed image with its own baked-in type.
   No overlay is drawn, so nothing can collide with the artwork. */
.gs .banner.is-art { min-height: 0; padding: 0; display: block; }
.gs .banner.is-art img { width: 100%; height: auto; border-radius: 0; }

/* ---------- Mosaic grid ---------- */
.gs .mosaic {
  display: grid; gap: var(--gap);
  grid-template-columns: repeat(6, 1fr);
}
.gs .mosaic > * { grid-column: span 2; }      /* default: thirds */
.gs .mosaic > .span-half { grid-column: span 3; }
.gs .mosaic > .span-full { grid-column: span 6; }
.gs .mosaic > .span-two-thirds { grid-column: span 4; }

/* ---------- Product / content card ---------- */
.gs .card {
  position: relative; display: flex; flex-direction: column;
  border-radius: var(--r-card); overflow: hidden;
  background: var(--tint-neutral); min-height: 340px;
  box-shadow: var(--edge);
  transition: transform .22s ease, box-shadow .22s ease;
}
/* The lift shadow must RESTATE the edge — box-shadow is one property, so a
   hover rule that sets only --shadow-lift drops the hairline mid-transition
   and the card visibly loses its outline under the cursor. */
.gs .card:hover { transform: translateY(-3px); box-shadow: var(--edge-hover), var(--shadow-lift); }
.gs .card .card-media {
  flex: 1; display: grid; place-items: center;
  padding: 28px 28px 8px; min-height: 180px;
}
.gs .card .card-media img {
  max-height: 210px; width: auto; object-fit: contain;
  filter: var(--shadow-product);
}
.gs .card.t-ink .card-media img { filter: var(--shadow-product-dark); }
.gs .card .card-media img[src$=".jpg"],
.gs .card .card-media img[src$=".jpeg"] { filter: none; }
.gs .card .card-body { padding: 0 28px 28px; }
.gs .card .card-body h3 { font-size: 19px; margin-bottom: 6px; }
.gs .card .card-body p { font-size: 14px; color: var(--ink-soft); margin-bottom: 14px; }
.gs .card .card-kicker {
  font-size: 12px; font-weight: 500; letter-spacing: .06em;
  text-transform: uppercase; color: var(--ink-faint); margin-bottom: 8px;
}

/* text-first card (no product shot) */
.gs .card.text-only { min-height: 240px; justify-content: flex-end; }
.gs .card.text-only .card-body { padding: 28px; }

/* ---------- Spec rows (shared with build page) ---------- */
.gs .spec-row {
  display: flex; justify-content: space-between; gap: 24px;
  padding: 14px 0; border-bottom: 1px solid var(--line-soft);
  font-size: 15px;
}
.gs .spec-row .l { color: var(--ink-soft); }
.gs .spec-row .v { font-weight: 500; text-align: right; }
.gs .pdp-specs { max-width: 720px; }

/* ---------- Key-spec chips ---------- */
.gs .pdp-keyspecs { display: flex; flex-wrap: wrap; gap: 10px; margin: 20px 0; }
.gs .pdp-keyspecs .kv {
  background: var(--bg-alt); border-radius: 16px;
  padding: 12px 18px; min-width: 116px;
}
.gs .pdp-keyspecs .kv .n { font-size: 19px; font-weight: 500; }
.gs .pdp-keyspecs .kv .l { font-size: 12px; color: var(--ink-faint); margin-top: 2px; }

/* ===================================================================
   BUILD CONFIGURATOR
   Class names kept from the Shopify theme so the ported JS is unchanged.
   =================================================================== */
.gs .cfg { display: grid; grid-template-columns: 1fr 1fr; gap: 48px; align-items: start; }
.gs .cfg-hero { position: sticky; top: calc(var(--nav-h) + 24px); }
.gs .cfg-hero .panel {
  background: var(--tint-mist); border-radius: var(--r-banner);
  padding: 40px; display: grid; place-items: center; min-height: 360px;  /* was 420 */
}
.gs .cfg-hero .panel img { max-height: 340px; width: auto; object-fit: contain; }
.gs .cfg-hero .cap { text-align: center; margin-top: 14px; font-size: 13px; color: var(--ink-faint); }

.gs .cfg-div { height: 1px; background: var(--line-soft); margin: 28px 0 22px; }
.gs .cfg-label { font-size: 13px; font-weight: 500; letter-spacing: .06em; text-transform: uppercase; color: var(--ink-faint); margin-bottom: 18px; }
.gs .cfg-step { margin-bottom: 26px; }
.gs .cfg-step .sh { font-size: 17px; font-weight: 500; margin-bottom: 2px; }
.gs .cfg-step .ss { font-size: 13px; color: var(--ink-faint); margin-bottom: 12px; }

.gs .cfg-opt {
  width: 100%; display: flex; align-items: center; gap: 14px;
  text-align: left; padding: 12px 16px; margin-bottom: 8px;
  background: #fff; border: 1px solid var(--line);
  border-radius: 16px; cursor: pointer;
  transition: border-color .18s, background .18s, box-shadow .18s;
}
.gs .cfg-opt:hover { border-color: #9aa0a6; background: var(--bg-alt); }
.gs .cfg-opt.on { border-color: var(--accent); background: var(--accent-tint); box-shadow: inset 0 0 0 1px var(--accent); }
.gs .cfg-opt.fixed { cursor: default; background: var(--bg-alt); border-style: dashed; }
.gs .cfg-opt.fixed:hover { border-color: var(--line); }
.gs .cfg-opt img { width: 52px; height: 52px; object-fit: contain; flex: 0 0 auto; }
.gs .cfg-opt .txt { flex: 1; min-width: 0; }
.gs .cfg-opt .txt .n { font-size: 14px; font-weight: 500; }
.gs .cfg-opt .txt .d { font-size: 12px; color: var(--ink-faint); margin-top: 1px; }
.gs .cfg-opt .tick {
  width: 20px; height: 20px; border-radius: 50%; flex: 0 0 auto;
  border: 2px solid var(--line); position: relative;
}
.gs .cfg-opt.on .tick { border-color: var(--accent); background: var(--accent); }
.gs .cfg-opt.on .tick::after {
  content: ""; position: absolute; left: 5px; top: 2px;
  width: 5px; height: 10px; border: solid #fff;
  border-width: 0 2px 2px 0; transform: rotate(45deg);
}
.gs .cfg-opt .badge {
  font-size: 11px; font-weight: 500; padding: 4px 10px;
  border-radius: var(--r-chip); background: var(--tint-slate); color: var(--ink-soft);
  flex: 0 0 auto;
}

.gs .cfg-summary { background: var(--bg-alt); border-radius: var(--r-card); padding: 24px; margin-top: 28px; }
.gs .sum-line { display: flex; justify-content: space-between; gap: 16px; padding: 9px 0; font-size: 14px; border-bottom: 1px solid var(--line-soft); }
.gs .sum-line:last-of-type { border-bottom: 0; }
.gs .sum-line .sl-l { color: var(--ink-soft); }
.gs .sum-line .sl-v { font-weight: 500; text-align: right; }
.gs .sum-note { font-size: 13px; color: var(--ink-faint); margin: 14px 0 18px; }
.gs .pdp-actions { display: flex; gap: 10px; }

/* ===================================================================
   COMPARE TABLE
   =================================================================== */
.gs .cmp-picker { display: flex; gap: 10px; overflow-x: auto; padding-bottom: 12px; scrollbar-width: none; }
.gs .cmp-picker::-webkit-scrollbar { display: none; }
.gs .chip-printer {
  flex: 0 0 auto; width: 116px; padding: 14px 10px;
  background: #fff; border: 1px solid var(--line); border-radius: 18px;
  cursor: pointer; text-align: center; transition: .18s;
}
.gs .chip-printer:hover { border-color: #9aa0a6; background: var(--bg-alt); }
.gs .chip-printer.on { border-color: var(--accent); background: var(--accent-tint); box-shadow: inset 0 0 0 1px var(--accent); }
.gs .chip-printer img { height: 56px; width: auto; margin: 0 auto 8px; object-fit: contain; }
.gs .chip-printer .nm { display: block; font-size: 13px; font-weight: 500; }
.gs .chip-printer .w { display: block; font-size: 11px; color: var(--ink-faint); margin-top: 1px; }
.gs .cmp-hint { font-size: 13px; color: var(--ink-faint); margin: 6px 0 24px; }

.gs .cmp-scroll { overflow-x: auto; }
/* table-layout:fixed is load-bearing, not tidiness. Left to auto, the browser
   sizes the two product columns by their CONTENT — the X6's longer spec
   strings made it 560px against the XL2's 442px, so the two machines sat at
   visibly different widths and neither lined up with the other's image.
   Fixed layout takes the 190px label column off the top and splits what is
   left equally, whatever pair is selected. Jason, 2026-09-07. */
.gs .cmp-table { width: 100%; border-collapse: collapse; min-width: 560px; table-layout: fixed; }
.gs .cmp-table th, .gs .cmp-table td { padding: 16px 20px; text-align: left; vertical-align: top; border-bottom: 1px solid var(--line-soft); }
.gs .cmp-col-label { font-size: 14px; color: var(--ink-soft); width: 190px; }
.gs .cmp-prod { background: var(--bg-alt); border-radius: var(--r-card) var(--r-card) 0 0; text-align: center !important; }
.gs .cmp-prod-img { display: grid; place-items: center; min-height: 130px; margin-bottom: 10px; }
.gs .cmp-prod-name { font-size: 17px; font-weight: 500; }
.gs .cmp-prod-w { font-size: 13px; color: var(--ink-faint); margin-bottom: 12px; }
.gs .cmp-val { font-size: 15px; font-weight: 500; }
.gs .cmp-compat { font-size: 13px; color: var(--ink-soft); }
.gs .cmp-compat .lbl { display: block; font-size: 11px; letter-spacing: .06em; text-transform: uppercase; color: var(--ink-faint); margin-bottom: 3px; }
.gs .cmp-compat .or { color: var(--ink-faint); }

/* ---------- CTA band ---------- */
/* The closing CTA, on every page. Was a near-black slab; washed and light
   since 2026-09-03 for the same reason as the banner above. It still reads as
   a distinct object because of the wash, the radius and the centred type —
   it no longer has to shout in black to do that. */
.gs .cta {
  border-radius: var(--r-banner);
  background-color: var(--bg);
  background-image: var(--brand-wash-card);
  color: var(--ink);
  box-shadow: var(--edge);
  padding: clamp(52px, 6.5vw, 88px) clamp(28px, 5vw, 64px); text-align: center;
}
.gs .cta h2 { margin-bottom: 14px; }
.gs .cta p { color: var(--ink-soft); max-width: 34rem; margin: 0 auto 30px; font-size: clamp(16px, 1.4vw, 19px); line-height: 1.55; }
/* .cta carries `on-dark` in the markup on several pages, which would otherwise
   flip the button to white-on-white and the ghost border to translucent. */
.gs .cta.on-dark .btn-primary { background: var(--ink); color: #fff; }
.gs .cta.on-dark .btn-ghost { color: var(--ink); border-color: var(--line); }
.gs .cta.on-dark .btn-ghost:hover { background: rgba(60, 64, 67, .06); }

/* ===================================================================
   FORMS
   Used by the Shopify contact / distributor-signup templates and by the
   Astro contact page. Shopify's {% form %} tag decides the markup around
   these, so the styling hangs off .field / .form-grid rather than off
   element selectors that Shopify might wrap differently.
   =================================================================== */
.gs .form-wrap {
  background: var(--tint-slate); border-radius: var(--r-banner);
  padding: clamp(24px, 4vw, 40px);
}
.gs .form-grid {
  display: grid; grid-template-columns: 1fr 1fr; gap: 16px;
}
.gs .field { display: flex; flex-direction: column; gap: 6px; }
.gs .field.full { grid-column: 1 / -1; }
.gs .field label {
  font-size: 13px; font-weight: 500; color: var(--ink-soft);
}
.gs .field input,
.gs .field select,
.gs .field textarea {
  font-family: var(--font); font-size: 15px; color: var(--ink);
  background: #fff; border: 1px solid var(--line);
  border-radius: 12px; padding: 11px 14px; width: 100%;
  transition: border-color .18s, box-shadow .18s;
}
.gs .field textarea { min-height: 132px; resize: vertical; }
.gs .field input:focus,
.gs .field select:focus,
.gs .field textarea:focus {
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
}
/* A visible ring for keyboard focus only. `outline: none` had been the rule
   on every form field, leaving a border-colour change as the sole focus cue —
   which is invisible to anyone tabbing through a form. :focus-visible keeps
   the ring off mouse clicks so the fields still look the way they were
   designed to. Fixed 2026-09-06. */
.gs .field :is(input, select, textarea):focus-visible,
.gs .form-card :is(input, select, textarea):focus-visible {
  outline: 2px solid var(--accent-ink);
  outline-offset: 2px;
}
.gs .form-note { font-size: 13px; color: var(--ink-faint); margin-top: 14px; }

/* ---------- Form rendered directly on a card ---------- */
/* The Astro pages post their own forms rather than going through Shopify's
   {% form %} tag, so they hang the fields off .fields / <label> on a
   `<form class="card form-card">` instead of the .field markup above.

   Two things this has to fight, and both are why it lives here rather than in
   a page's scoped <style>:

   1. `.card` is in the collision reset, which zeroes padding at (0,2,0). A
      page-level `.aff-form { padding }` is (0,1,0) in dev and only ties in the
      build, so the fields sat flush against the card edge in any renderer that
      loaded the page's styles first — the admin editor preview among them.
      `.gs .card.form-card` is (0,3,0) and wins everywhere.
   2. contact and distributor-signup each carried a scoped COPY of the control
      styling with a note to promote it "once a third form appears". Affiliate
      and SMS were the third and fourth, were written without it, and rendered
      as raw browser inputs. One definition now; new forms get it for free. */
.gs .card.form-card { min-height: 0; padding: clamp(24px, 3vw, 36px); }
.gs .form-card .fields { display: grid; gap: 16px; }
.gs .form-card .field-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.gs .form-card .field-grid > label.full { grid-column: 1 / -1; }
.gs .form-card label { display: grid; gap: 6px; }
.gs .form-card .opt { color: var(--ink-faint); font-weight: 400; text-transform: none; letter-spacing: 0; }
/* Checkboxes and radios are excluded: they are sized by .consent and friends,
   and a 100%-wide checkbox is a full-bleed grey bar. */
.gs .form-card input:not([type="checkbox"]):not([type="radio"]),
.gs .form-card select,
.gs .form-card textarea {
  font-family: var(--font); font-size: 15px; color: var(--ink);
  background: #fff; border: 1px solid var(--line);
  border-radius: 12px; padding: 12px 14px; width: 100%;
  transition: border-color .18s, box-shadow .18s;
}
.gs .form-card textarea { resize: vertical; }
.gs .form-card input:not([type="checkbox"]):not([type="radio"]):focus,
.gs .form-card select:focus,
.gs .form-card textarea:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px rgba(14, 147, 161, .15);
}
.gs .form-card ::placeholder { color: var(--ink-faint); }

@media (max-width: 720px) {
  .gs .form-card .field-grid { grid-template-columns: 1fr; }
}
.gs .form-ok    { color: var(--accent-ink); font-weight: 500; }
.gs .form-error { color: #c5221f; font-weight: 500; }

@media (max-width: 640px) {
  .gs .form-grid { grid-template-columns: 1fr; }
}

/* ---------- Distributor logo strip ---------- */
/* The pills are `span`s, not links, as of 2026-09-04 — see the DEALER NETWORK
   SHOWCASE block at the end of this file for why. `a` is still matched so the
   Shopify sections, which have not been converted yet, keep their styling. */
.gs .dist-strip { display: flex; flex-wrap: wrap; gap: 12px; justify-content: center; }
.gs .dist-strip a,
.gs .dist-strip span {
  padding: 11px 20px; border-radius: var(--r-chip);
  background: #fff; border: 1px solid var(--line);
  font-size: 14px; color: var(--ink-soft); transition: .18s;
}
.gs .dist-strip a:hover { border-color: #9aa0a6; color: var(--ink); box-shadow: var(--shadow-card); }

/* ===================================================================
   NAV + FOOTER
   The Astro site renders these as components; the Shopify theme renders
   them from sections/header.liquid + sections/footer.liquid. Same class
   names on both sides, so this styles either.
   =================================================================== */
/* top honours --anno-h so the nav sits under the Shopify theme's fixed
   announcement bar (html.has-anno sets it to 40px, and an inline script sets
   it before paint so the nav never jumps on load). On the Astro site the
   variable is undefined and the fallback puts the nav flush at 0. */
/* 2026-09-01 — the bar was 92% opaque over a 12px blur, which is a white bar
   with a rounding error: nothing visibly passes under it, so the frosted-glass
   effect was being paid for and not seen. Dropped to 72% over a 20px saturated
   blur, where content genuinely blurs through as it scrolls beneath.

   2026-09-07 — 72% went too far, and it shows up worse on DESKTOP than on a
   phone, which is what Jason reported: "the desktop version is more
   see-through and makes the text harder to read." That is not a perception
   error. Measured with the bar scrolled over a product page, sampling the real
   rendered pixels: the darkest 5% of the strip was 193/255 at 1440px versus
   212/255 at 390px. Two reasons. The bar spans the full width on desktop, so
   far more dark photography passes beneath it, and the six nav links are
   spread across that whole width, so the type actually sits over the dark
   parts. On a phone the bar holds a logo and one button and the content column
   under it is narrow and pale.

   86% keeps the frosted effect Jason likes on mobile ("you can kind of see it
   behind, but it's still easy to read") and takes the worst case back to a
   near-white ground. The blur goes up with it: more blur, less opacity needed
   for the same legibility, because it is the high-frequency detail that
   breaks up letterforms. */
.gs header.nav, header.nav.gs {
  position: sticky; top: var(--anno-h, 0px); z-index: 100;
  background: rgba(255, 255, 255, .86);
  backdrop-filter: saturate(180%) blur(28px);
  -webkit-backdrop-filter: saturate(180%) blur(28px);
  border-bottom: 1px solid rgba(20, 24, 26, .07);
  transition: transform .28s ease;
}
/* Desktop goes higher again, at Jason's request on 2026-09-07 after seeing
   86%: "can you make that bar a little bit more opaque still?" It is the
   screen where far more dark photography passes under a full-width bar, and
   where the links are spread across all of it. The phone keeps 86%, which he
   already liked. */
@media (min-width: 901px) {
  .gs header.nav, header.nav.gs { background: rgba(255, 255, 255, .94); }
}

/* Hidden while scrolling down, back on any upward flick. The class is set by
   the script in Nav.astro; the product section bar rides up with it so it
   takes the top spot rather than leaving a gap. */
.gs .pnav { transition: transform .28s ease; }
html.nav-up .gs header.nav, html.nav-up header.nav.gs { transform: translateY(-100%); }
html.nav-up .gs .pnav, html.nav-up .pnav { transform: translateY(calc(-1 * var(--nav-h))); }

@media (prefers-reduced-motion: reduce) {
  .gs header.nav, header.nav.gs, .gs .pnav { transition: none; }
}
/* Without a backdrop-filter the 72% fill is just a see-through bar and the
   links collide with whatever scrolls under them. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .gs header.nav, header.nav.gs { background: rgba(255, 255, 255, .97); }
}
.gs .nav-inner {
  max-width: var(--maxw); margin: 0 auto; height: var(--nav-h);
  padding: 0 24px; display: flex; align-items: center; gap: 28px;
}
/* 34px, up from 28. Jason, 2026-09-04: a little bit bigger. The nav is 64px
   tall, so this still leaves 15px of clear space above and below — the mark
   grows without the bar feeling crowded. */
.gs .nav-logo img { height: 34px; width: auto; }
.gs .nav-links { display: flex; align-items: center; gap: 4px; list-style: none; margin-left: 8px; }
.gs .nav-links > li > a,
.gs .nav-links .drop-toggle {
  display: inline-flex; align-items: center; gap: 4px;
  font-size: 14px; font-weight: 500; color: var(--ink-soft);
  padding: 8px 12px; border-radius: var(--r-chip);
  background: none; border: 0; cursor: pointer; transition: .18s;
}
.gs .nav-links > li > a:hover,
.gs .nav-links .drop-toggle:hover { background: rgba(60, 64, 67, .07); color: var(--ink); }
.gs .nav-links a.active { color: var(--ink); background: var(--tint-slate); }

/* Separates the category links in a dropdown from the tools beneath them
   (Compare / Build), which were moved out of the top-level bar. */
.gs .drop-menu .drop-sep {
  height: 1px;
  margin: 6px 12px;
  background: var(--line-soft);
}
.gs .nav-right { margin-left: auto; display: flex; align-items: center; gap: 10px; }

.gs .has-drop { position: relative; }
.gs .drop-toggle .caret { font-size: 10px; opacity: .6; transform: rotate(90deg); }
.gs .drop-menu {
  list-style: none; position: absolute; top: calc(100% + 8px); left: 0;
  min-width: 240px; background: #fff;
  border: 1px solid var(--line-soft); border-radius: 16px;
  box-shadow: var(--shadow-lift); padding: 8px;
  opacity: 0; visibility: hidden; transform: translateY(6px);
  transition: opacity .18s, transform .18s, visibility .18s;
}
.gs .drop-menu::before { content: ""; position: absolute; top: -12px; left: 0; right: 0; height: 12px; }
.gs .has-drop:hover .drop-menu,
.gs .has-drop:focus-within .drop-menu,
/* .is-open is set by a tap — see the script in Nav.astro. Hover and focus
   cover a mouse and a keyboard; without this a finger on a tablet wider than
   the burger breakpoint had no way to open a menu at all. */
.gs .has-drop.is-open .drop-menu { opacity: 1; visibility: visible; transform: translateY(0); }
.gs .drop-menu a {
  display: block; padding: 10px 14px; border-radius: 10px;
  font-size: 14px; color: var(--ink-soft); white-space: nowrap;
}
.gs .drop-menu a:hover { background: var(--bg-alt); color: var(--ink); }

.gs .burger { display: none; background: none; border: 0; cursor: pointer; flex-direction: column; gap: 5px; padding: 8px; }
.gs .burger span { width: 20px; height: 2px; background: var(--ink); border-radius: 2px; transition: .3s; }

/* Skip link: the first thing in the tab order on every page. Off-screen until
   it receives focus, then a solid pill at the top-left. Added 2026-09-06 —
   there was no way past the ~35 nav links without tabbing through them. */
/* Visually hidden, still read aloud. Added 2026-09-07 for the "(opens in a
   new tab)" note on outbound buttons; a sighted user sees the arrow, a screen
   reader user gets told. Not display:none, which removes it from the
   accessibility tree entirely and defeats the point. */
.gs .vh {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}

.gs .skip-link {
  position: absolute; left: 12px; top: -80px; z-index: 1000;
  padding: 10px 16px; border-radius: 999px;
  background: var(--ink); color: #fff; font-weight: 500; font-size: 14px;
  transition: top .15s;
}
.gs .skip-link:focus { top: 12px; outline: 2px solid var(--accent); outline-offset: 2px; }

/* Translucent, so the brand wash carries through to the bottom of the page
   instead of stopping at a flat grey slab. Enough opacity to still read as a
   distinct footer zone. */
.gs footer.site, footer.site.gs {
  background: rgba(240, 242, 244, .55); margin-top: 72px;
  padding: 56px 0 32px; font-size: 14px;
}
.gs .foot-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 32px; }
/* The column titles are <h2> in the markup now (they were <h4>, which made
   every page's outline jump from h1 straight to h4). Same visual size. */
.gs .foot-grid h2, .gs .foot-grid h4 { font-size: 13px; font-weight: 500; margin: 0 0 14px; color: var(--ink); }
.gs .foot-grid ul { list-style: none; margin: 0; padding: 0; }
.gs .foot-grid li { margin-bottom: 4px; }
/* Links are laid out as blocks with vertical padding so each is at least 24px
   tall — the footer links measured 20px on a phone, under the WCAG 2.2 target
   minimum, and they are the densest cluster of links on every page. */
.gs .foot-grid a { display: inline-block; padding: 4px 0; color: var(--ink-soft); }
.gs .foot-grid a:hover { color: var(--ink); text-decoration: underline; }
.gs .foot-contact { margin-top: 10px; color: var(--ink-soft); line-height: 1.6; }
.gs .foot-contact a { display: inline-flex; align-items: center; min-height: 24px; color: var(--ink); }
.gs .foot-hours { color: var(--ink-faint); }
.gs .foot-legal { display: flex; flex-wrap: wrap; gap: 2px 14px; }
.gs .foot-legal a { display: inline-block; padding: 4px 0; }
.gs .foot-logo img { height: 26px; width: auto; }
.gs .foot-base {
  margin-top: 40px; padding-top: 24px; border-top: 1px solid var(--line);
  display: flex; flex-wrap: wrap; gap: 14px; justify-content: space-between;
  color: var(--ink-faint); font-size: 13px;
}

/* ===================================================================
   Responsive
   =================================================================== */
@media (max-width: 1024px) {
  .gs .mosaic > * { grid-column: span 3; }
  .gs .mosaic > .span-two-thirds,
  .gs .mosaic > .span-full { grid-column: span 6; }
  .gs .cfg { grid-template-columns: 1fr; gap: 32px; }
  .gs .cfg-hero { position: static; }
}

@media (max-width: 860px) {
  .gs .nav-links {
    display: none; position: absolute; top: var(--nav-h); left: 0; right: 0;
    flex-direction: column; align-items: stretch; gap: 0;
    background: #fff; border-bottom: 1px solid var(--line-soft);
    padding: 12px; box-shadow: var(--shadow-lift);
  }
  .gs .nav-links.open { display: flex; }
  .gs .nav-links > li > a, .gs .nav-links .drop-toggle { width: 100%; justify-content: flex-start; padding: 12px 14px; }
  .gs .drop-menu {
    position: static; opacity: 1; visibility: visible; transform: none;
    box-shadow: none; border: 0; border-radius: 0; padding: 0 0 8px 14px;
    min-width: 0;
  }
  .gs .burger { display: flex; }
  .gs .banner { min-height: 0; }
  .gs .banner.split { grid-template-columns: 1fr; }
  .gs .banner .banner-art { position: static; width: auto; padding: 0 24px 32px; }
  .gs .banner .banner-body { padding: 36px 24px 24px; max-width: none; }
  .gs .banner .banner-art img { max-height: 260px; }
  /* the flip variant collapses to the normal stacked order on mobile */
  .gs .banner.flip .banner-art { inset: auto; }
  .gs .banner.flip .banner-body { justify-self: stretch; text-align: left; }
  .gs .banner.flip .banner-actions { justify-content: flex-start; }
  .gs .banner.split.flip { direction: ltr; }
}

@media (max-width: 640px) {
  .gs .mosaic > *,
  .gs .mosaic > .span-half,
  .gs .mosaic > .span-two-thirds { grid-column: span 6; }
  .gs.band, .gs .band { padding: 40px 0; }
  .gs .cmp-col-label { width: 130px; }
  .gs .cmp-table th, .gs .cmp-table td { padding: 12px 14px; }

  /* ---- compare table on a phone ----
     A label column plus two products cannot fit 342px of usable width, so the
     table used to be 560px wide inside a sideways-scrolling box: you had to
     drag left and right to see the second machine, which is the one thing a
     comparison must never make you do. Below 640px the table is reflowed to
     blocks — the row label becomes a full-width caption and the two values sit
     side by side underneath it. Both machines stay on screen and the page
     reads straight down. Jason, 2026-09-07.

     This is CSS-only on purpose: the table is rendered twice (server-side in
     compare.astro and again by the client script when you swap printers), so
     anything depending on markup changes would have to be made in two places
     and would drift. */
  .gs .cmp-scroll { overflow-x: visible; }
  .gs .cmp-table { display: block; width: 100%; min-width: 0; }
  .gs .cmp-table thead, .gs .cmp-table tbody { display: block; }
  .gs .cmp-table tr { display: flex; flex-wrap: wrap; }
  .gs .cmp-table th, .gs .cmp-table td { padding: 0; border-bottom: 0; }

  /* header: empty label cell dropped, the two machines split the width */
  .gs .cmp-table thead .cmp-col-label { display: none; }
  .gs .cmp-prod { flex: 1 1 0; min-width: 0; padding: 18px 10px 16px; }
  .gs .cmp-prod-img { min-height: 92px; margin-bottom: 8px; }
  /* the inline max-height on each img is per-printer and set server-side —
     it has to be overridden here or the taller machines blow the header up */
  .gs .cmp-prod-img img { max-height: 92px !important; max-width: 100%; object-fit: contain; }
  .gs .cmp-prod-name { font-size: 15px; }
  .gs .cmp-prod-w { margin-bottom: 10px; }

  /* body: label on its own line, values side by side below it */
  .gs .cmp-table tbody tr { padding: 12px 0 11px; border-bottom: 1px solid var(--line-soft); }
  .gs .cmp-table tbody .cmp-col-label {
    flex: 0 0 100%; width: auto; padding: 0 12px; margin-bottom: 5px;
    font-size: 11px; letter-spacing: .06em; text-transform: uppercase;
    color: var(--ink-faint);
  }
  .gs .cmp-table tbody td:not(.cmp-col-label) { flex: 1 1 0; min-width: 0; padding: 0 12px; }
  .gs .cmp-val { font-size: 14px; }
}

@media (prefers-reduced-motion: reduce) {
  .gs * { animation: none !important; transition: none !important; }
}

/* ===================================================================
   Tint utilities.
   Declared last, and scoped to the components they modify, so they win
   over the default background on .card / .banner regardless of the order
   those components are defined in above. This is load-bearing: when these
   were unscoped and declared earlier, every card rendered grey.

   2026-08-26 — cards and banners are DELIBERATELY ONE SURFACE: white, with a
   hairline edge (--edge) so they still read as cards against a white page.
   Jason's call:
   a page of teal / sand / blush / sage cards read as busy rather than as a
   system. The per-family colour tints are collapsed to the neutral here
   rather than deleted, so the hundreds of `t-teal` / `t-sand` classes already
   sitting in templates/*.json and in the Astro components stay valid and keep
   rendering correctly. The theme editor's Tint dropdown was cut down to match
   (see banner-card.liquid / card-mosaic.liquid) — do not re-expose colours in
   those schemas without restoring real values here, or merchants get a
   dropdown that silently does nothing.

   `ink` is NOT collapsed. It is the dark flagship treatment, it carries white
   type, and it is the only colour left on the page — that is the point.

   The --tint-* tokens themselves are untouched: --tint-teal still backs the
   selected states on .cfg-opt and .chip-printer, which are not cards.
   =================================================================== */
.gs .banner.t-teal,  .gs .card.t-teal,
.gs .banner.t-sand,  .gs .card.t-sand,
.gs .banner.t-mist,  .gs .card.t-mist,
.gs .banner.t-sage,  .gs .card.t-sage,
.gs .banner.t-blush, .gs .card.t-blush,
.gs .banner.t-slate,   .gs .card.t-slate,
.gs .banner.t-neutral, .gs .card.t-neutral { background: var(--tint-neutral); }
/* `ink` was the dark flagship treatment. 2026-09-03 it became the WASHED
   flagship treatment — still the one surface that differs from a plain white
   card, still reserved for the flagship, no longer black. The class name is
   kept because it is written into templates and components across both
   storefronts; only what it paints has changed. */
.gs .banner.t-ink,   .gs .card.t-ink   {
  background-color: var(--bg);
  background-image: var(--brand-wash-card);
  color: var(--ink);
  box-shadow: var(--edge);
}
.gs .card.t-ink:hover { box-shadow: var(--edge-hover), var(--shadow-lift); }
.gs .banner.t-ink .lede,   .gs .card.t-ink .card-body p { color: var(--ink-soft); }
.gs .banner.t-ink .eyebrow, .gs .card.t-ink .card-kicker { color: var(--ink-faint); }
.gs .banner.t-ink .banner-art img, .gs .card.t-ink .card-media img { filter: var(--shadow-product); }

/* ===================================================================
   DEALER NETWORK SHOWCASE  (/where-to-buy/)

   NOT a directory and deliberately NOT clickable. Jason's rule, 2026-09-04:
   every buying route on this site goes through the inquiry form so DTF
   Station sees the lead first and routes it to the right dealer. A name that
   links straight out is a lead we never counted. The names are still shown in
   full, because the credibility of the network IS the argument — hiding them
   would read as though we had something to hide.

   `.dist-strip` above is the older centred pill row and is still used by the
   homepage and the Shopify sections; this is the richer grouped grid the
   Where-to-buy page uses, with the coverage filter.
   =================================================================== */
.gs .dealer-stats {
  display: grid; grid-template-columns: repeat(3, 1fr);
  gap: 1px; background: var(--line-soft);
  border: 1px solid var(--line-soft); border-radius: var(--r-banner);
  overflow: hidden; margin-bottom: clamp(32px, 4vw, 52px);
}
.gs .dealer-stats > div { background: #fff; padding: clamp(20px, 2.6vw, 30px) 20px; text-align: center; }
.gs .dealer-stats .n {
  display: block; font-size: clamp(28px, 3.4vw, 42px); font-weight: 600;
  letter-spacing: -.02em; color: var(--accent-ink); line-height: 1.1;
}
.gs .dealer-stats .l {
  display: block; margin-top: 6px; font-size: 13px; color: var(--ink-soft);
}

/* Coverage filter. Purely a view control — with JS off every group renders,
   which is the correct fallback for a list this short. */
.gs .dealer-filter { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin-bottom: 28px; }
.gs .dealer-filter button {
  font-family: var(--font); font-size: 13.5px; color: var(--ink-soft);
  background: #fff; border: 1px solid var(--line); border-radius: var(--r-chip);
  padding: 9px 18px; cursor: pointer; transition: .18s;
}
.gs .dealer-filter button:hover { border-color: #9aa0a6; color: var(--ink); }
.gs .dealer-filter button[aria-pressed="true"] {
  border-color: var(--accent); background: var(--accent-tint);
  color: var(--accent-ink); font-weight: 500;
  box-shadow: inset 0 0 0 1px var(--accent);
}

.gs .dealer-grid {
  display: grid; gap: 12px;
  grid-template-columns: repeat(auto-fill, minmax(232px, 1fr));
}
.gs .dealer-card {
  background: #fff; border-radius: 16px; box-shadow: var(--edge);
  padding: 20px 22px; display: flex; flex-direction: column; gap: 8px;
  transition: box-shadow .18s;
}
.gs .dealer-card:hover { box-shadow: var(--edge-hover), var(--shadow-card); }
.gs .dealer-card .nm {
  font-size: 15px; font-weight: 500; color: var(--ink);
  letter-spacing: -.01em; line-height: 1.35;
}
.gs .dealer-card .cv {
  font-size: 11px; font-weight: 500; letter-spacing: .06em; text-transform: uppercase;
  color: var(--accent-ink); background: var(--accent-tint);
  border-radius: 999px; padding: 4px 10px; align-self: flex-start; margin-top: auto;
}
.gs .dealer-card[hidden] { display: none; }

/* Shown when a filter matches nothing. Cannot happen with today's data, but a
   coverage label removed from the seed list would otherwise leave a blank page
   with no explanation. */
.gs .dealer-empty { text-align: center; color: var(--ink-faint); font-size: 14px; padding: 32px 0; }

@media (max-width: 560px) {
  .gs .dealer-stats { grid-template-columns: 1fr; }
  .gs .dealer-grid { grid-template-columns: 1fr 1fr; gap: 10px; }
  .gs .dealer-card { padding: 16px; }
}
