/* ==================================================================
   LIQUID GLASS — the material layer.

   Loaded after every other sheet, this file is the site's single
   authority on glass. It follows the apple-design skill's §12
   (materials & depth) and §1 (response):

   1. MATERIAL WEIGHT ENCODES HIERARCHY. Three tiers, and every
      surface belongs to exactly one:
        chip  — small controls (language picker, back button):
                light blur, whisper fill. Draws the eye to what is
                interactive without competing with content.
        card  — content surfaces (tiles, product cards, contact
                cards, process steps): heavier blur, a rim whose
                brightness VARIES around the edge, a specular dome
                across the top. Bigger surfaces read THICKER.
        sheet — structural regions (menus, dropdowns, footer):
                the heaviest blur and the darkest fill, so structure
                separates from content.

   2. THE RECIPE that makes a surface read as glass rather than a
      grey box, in order of importance:
        a. a rim drawn as a GRADIENT (light does not hit an edge
           evenly) — via mask-composite where a pseudo-element is
           free, via border + top-highlight where it is not;
        b. a specular dome — a soft radial bloom in the top third,
           painted as a background layer above the backdrop blur;
        c. a vertical luminance gradient in the fill — glass is
           lighter where the light enters;
        d. a shadow PAIR — a tight inset top light plus a deep soft
           drop — so the surface both catches light and floats.

   3. RESPONSE IS INSTANT. Press states fire on :active (pointer-
      down, not release) in 60ms; releases ease back over 260ms via
      the shared pressable rules in components.css — the shorthand
      here always carries `transform` so it never erases them.

   4. THE FALLBACKS ARE PART OF THE DESIGN. prefers-reduced-
      transparency and prefers-contrast get frosted-solid surfaces;
      no backdrop-filter support gets near-solid fills. A visitor
      who asked for less transparency is not given a worse site —
      just a more opaque one.
   ================================================================== */

:root {
  /* Rim stops, reused everywhere so every edge catches the same key
     light from the upper left. */
  --lg-rim-hi:  rgba(255, 255, 255, 0.60);
  --lg-rim-mid: rgba(255, 255, 255, 0.09);
  --lg-rim-lo:  rgba(255, 255, 255, 0.05);
  --lg-rim-end: rgba(255, 255, 255, 0.22);

  /* The specular dome, painted above the blur on every card. */
  --lg-specular: radial-gradient(120% 70% at 18% -12%,
                   rgba(255, 255, 255, 0.16), transparent 58%);

  /* Tier fills — vertical luminance gradient, lighter at the top. */
  --lg-fill-chip:  linear-gradient(180deg,
                     rgba(255, 255, 255, 0.085), rgba(255, 255, 255, 0.04));
  --lg-fill-card:  linear-gradient(180deg,
                     rgba(255, 255, 255, 0.115) 0%,
                     rgba(255, 255, 255, 0.05) 42%,
                     rgba(255, 255, 255, 0.028) 100%);
  --lg-fill-sheet: linear-gradient(180deg,
                     rgba(18, 28, 44, 0.72), rgba(10, 17, 30, 0.78));

  --lg-blur-chip:  blur(16px) saturate(170%);
  --lg-blur-card:  blur(30px) saturate(185%) brightness(1.04);
  --lg-blur-sheet: blur(40px) saturate(170%);

  --lg-shadow-card:
    0 20px 48px -20px rgba(0, 0, 0, 0.78);
  --lg-shadow-card-hover:
    0 34px 72px -24px rgba(0, 0, 0, 0.88);
}

/* ------------------------------------------------------------------
   HEADER CLOCK — boxless. It is ambient information, not a control,
   so it gets NO material at all: the header's own whisper glass is
   its surface. (Chip material on a non-interactive element was the
   mistake — material weight is supposed to mark interactivity.)
   ------------------------------------------------------------------ */
.cochin-now {
  background: transparent;
  border: none;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  padding: 0;
  gap: 0.6rem;
}
/* Bigger, and the place name in white — it is the first word read. */
.cochin-now__place { font-size: 0.98rem; color: var(--pearl); font-weight: 500; }
.cochin-now__time  { font-size: 1.1rem; }
.cochin-now__zone  { font-size: 0.74rem; }
.cochin-now__glyph { display: inline-flex; width: 1.3rem; height: 1.3rem; color: var(--pearl); }
.cochin-now__glyph svg { width: 100%; height: 100%; }
.cochin-now__temp  { font-size: 1.1rem; }
.cochin-now__cond  { font-size: 0.92rem; }
.cochin-now__sep   { font-size: 1rem; }

/* ------------------------------------------------------------------
   FOOTER LOGO — was distorted: a width:100% grid stretch fought the
   fixed 34px height (natural aspect is 4.06:1; it rendered at 7.8:1).
   Width leads, height follows the aspect ratio.
   ------------------------------------------------------------------ */
.site-footer__mark {
  width: clamp(130px, 12vw, 165px);
  height: auto;
}

/* ------------------------------------------------------------------
   FORM TILES — the four-across hero row. Card tier, turned up: this
   is the surface the whole page's material language hangs off.
   ------------------------------------------------------------------ */
.form-tile__link {
  border-radius: 24px;
  background:
    var(--lg-specular),
    var(--lg-fill-card);
  -webkit-backdrop-filter: var(--lg-blur-card);
  backdrop-filter: var(--lg-blur-card);
  box-shadow:
    /* each tile glows faintly in its own hue, from inside */
    inset 0 0 44px -18px var(--tile-glow),
    0 20px 48px -20px rgba(0, 0, 0, 0.78);
}
/* The rim, brighter and keyed to the tile's hue at the tail. */
.form-tile__link::before {
  padding: 1.2px;
  background: linear-gradient(150deg,
    var(--lg-rim-hi) 0%,
    var(--lg-rim-mid) 36%,
    var(--lg-rim-lo) 62%,
    var(--tile-edge) 100%);
}
@media (hover: hover) {
  .form-tile__link:hover {
    transform: translateY(-10px);
    background:
      radial-gradient(120% 70% at 18% -12%, rgba(255, 255, 255, 0.22), transparent 58%),
      linear-gradient(180deg,
        rgba(255, 255, 255, 0.15) 0%,
        rgba(255, 255, 255, 0.07) 42%,
        rgba(255, 255, 255, 0.04) 100%);
    box-shadow:
      inset 0 0 56px -16px var(--tile-glow),
      0 36px 76px -24px rgba(0, 0, 0, 0.9);
  }
}
/* Press on the tile itself — a card this size barely moves (§1). */
.form-tile__link:active {
  transform: scale(0.988) translateY(-4px);
  transition: transform 60ms linear;
}

/* ------------------------------------------------------------------
   BUTTONS — luminous glass pills. The old primary was an opaque
   pearl slab; the ghost was a hairline. Both become material:
   the primary carries the brightest fill on the page (lighter
   material draws attention to interactive elements — §12), the
   ghost the quietest. Text goes heavier for vibrancy: over glass,
   flat mid-grey collapses; weight + spacing carry legibility.
   ------------------------------------------------------------------ */
.btn,
.enquiry__submit {
  border-radius: 999px;
  font-weight: 600;
  letter-spacing: 0.14em;
}
.btn--primary {
  position: relative;
  isolation: isolate;
  background:
    var(--lg-specular),
    linear-gradient(180deg,
      rgba(245, 243, 238, 0.26) 0%,
      rgba(245, 243, 238, 0.13) 50%,
      rgba(245, 243, 238, 0.09) 100%);
  -webkit-backdrop-filter: blur(20px) saturate(190%) brightness(1.08);
  backdrop-filter: blur(20px) saturate(190%) brightness(1.08);
  color: var(--pearl);
  border: none;
  box-shadow:
    0 10px 30px -10px rgba(0, 0, 0, 0.7);
}
.btn--primary::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  padding: 1px;
  background: linear-gradient(150deg,
    rgba(255, 255, 255, 0.75) 0%,
    rgba(255, 255, 255, 0.16) 40%,
    rgba(255, 255, 255, 0.1) 64%,
    rgba(255, 255, 255, 0.38) 100%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  pointer-events: none;
}
.btn--primary:hover {
  transform: translateY(-2px);
  background:
    radial-gradient(120% 70% at 18% -12%, rgba(255, 255, 255, 0.24), transparent 58%),
    linear-gradient(180deg,
      rgba(245, 243, 238, 0.34) 0%,
      rgba(245, 243, 238, 0.18) 50%,
      rgba(245, 243, 238, 0.12) 100%);
  box-shadow:
    0 16px 40px -12px rgba(0, 0, 0, 0.8);
}

.btn--ghost {
  border: 1px solid rgba(245, 243, 238, 0.28);
  background: var(--lg-fill-chip);
  -webkit-backdrop-filter: var(--lg-blur-chip);
  backdrop-filter: var(--lg-blur-chip);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.18);
}
/* The old fill-from-left wipe fought the material read; hover now
   simply brightens the glass. */
.btn--ghost::before { display: none; }
.btn--ghost:hover {
  color: var(--pearl);
  border-color: rgba(245, 243, 238, 0.45);
  background: linear-gradient(180deg,
    rgba(255, 255, 255, 0.13), rgba(255, 255, 255, 0.06));
}

/* The enquiry submit is the primary of its page. */
.enquiry__submit {
  background:
    var(--lg-specular),
    linear-gradient(180deg,
      rgba(245, 243, 238, 0.26) 0%,
      rgba(245, 243, 238, 0.12) 100%);
  -webkit-backdrop-filter: blur(20px) saturate(190%);
  backdrop-filter: blur(20px) saturate(190%);
  color: var(--pearl);
  border: 1px solid rgba(255, 255, 255, 0.28);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.45),
    0 10px 30px -10px rgba(0, 0, 0, 0.7);
}

/* ------------------------------------------------------------------
   CONTACT CARDS — card tier. Their pseudo-elements are already in
   use elsewhere, so the rim is a border and the top highlight rides
   in the shadow stack instead of a mask-composite gradient.
   ------------------------------------------------------------------ */
.contact-card.glass--card {
  border-radius: 22px;
  border: 1px solid rgba(190, 220, 255, 0.28);
  /* Blue glass. The tint is a cool cerulean lifted from the water
     itself, kept light enough that pearl text sits at ~9:1 on it. No
     inset shading anywhere: the pane is one clear sheet, and it CASTS
     a shadow onto the page rather than carrying one. */
  background:
    radial-gradient(120% 70% at 18% -12%, rgba(255, 255, 255, 0.22), transparent 58%),
    linear-gradient(180deg,
      rgba(150, 200, 255, 0.24) 0%,
      rgba(110, 170, 240, 0.16) 48%,
      rgba(80, 140, 220, 0.14) 100%);
  -webkit-backdrop-filter: blur(34px) saturate(200%) brightness(1.08);
  backdrop-filter: blur(34px) saturate(200%) brightness(1.08);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.5),
    0 28px 64px -24px rgba(0, 20, 60, 0.75),
    0 8px 20px -10px rgba(0, 20, 60, 0.45);
}
.contact-card.glass--card:hover {
  transform: translateY(-8px);
  border-color: rgba(200, 228, 255, 0.4);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.6),
    0 42px 88px -28px rgba(0, 20, 60, 0.85),
    0 12px 26px -12px rgba(0, 20, 60, 0.5);
}
/* Text on the blue pane goes full pearl so it stays effortless. */
.contact-card.glass--card .contact-card__body { color: rgba(255, 255, 255, 0.9); }
.contact-card.glass--card .contact-card__eyebrow { color: rgba(255, 255, 255, 0.78); }

/* ------------------------------------------------------------------
   PROCESS — "From the harbour to your destination." Four hairline
   rows become four cards, so the section speaks the same material
   language as the tiles above it.
   ------------------------------------------------------------------ */
.process-step {
  border-top: none;
  padding: clamp(1.25rem, 2vw, 1.75rem);
  border-radius: 18px;
  border: 1px solid rgba(245, 243, 238, 0.11);
  background:
    var(--lg-specular),
    linear-gradient(180deg,
      rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.03));
  -webkit-backdrop-filter: blur(22px) saturate(170%);
  backdrop-filter: blur(22px) saturate(170%);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.2),
    0 14px 36px -18px rgba(0, 0, 0, 0.7);
}

/* ------------------------------------------------------------------
   LANGUAGE PICKER — chip tier trigger, sheet tier menu.
   ------------------------------------------------------------------ */
.lang-select__trigger {
  background:
    radial-gradient(120% 80% at 20% -10%, rgba(255, 255, 255, 0.12), transparent 60%),
    var(--lg-fill-chip);
  -webkit-backdrop-filter: var(--lg-blur-chip);
  backdrop-filter: var(--lg-blur-chip);
  border-color: rgba(245, 243, 238, 0.18);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.22);
}
.lang-select__menu {
  border-radius: 16px;
  border-color: rgba(245, 243, 238, 0.16);
  background: var(--lg-fill-sheet);
  -webkit-backdrop-filter: var(--lg-blur-sheet);
  backdrop-filter: var(--lg-blur-sheet);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.16),
    0 26px 56px -16px rgba(0, 0, 0, 0.85);
}

/* ------------------------------------------------------------------
   PRODUCTS PAGE — cards and notes join the same card tier.
   ------------------------------------------------------------------ */
.product-card {
  border-radius: 20px;
  background:
    var(--lg-specular),
    var(--lg-fill-card);
  -webkit-backdrop-filter: var(--lg-blur-card);
  backdrop-filter: var(--lg-blur-card);
  box-shadow: var(--lg-shadow-card);
}
.product-card::before {
  padding: 1.2px;
  background: linear-gradient(150deg,
    var(--lg-rim-hi) 0%,
    var(--lg-rim-mid) 40%,
    var(--lg-rim-lo) 66%,
    var(--lg-rim-end) 100%);
}
@media (hover: hover) {
  .product-card:hover {
    transform: translateY(-4px);
    box-shadow: var(--lg-shadow-card-hover);
  }
}
.form-chapter__note {
  border-radius: 18px;
  background:
    radial-gradient(120% 80% at 16% -10%, rgba(255, 255, 255, 0.09), transparent 60%),
    linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.025));
  -webkit-backdrop-filter: blur(20px) saturate(165%);
  backdrop-filter: blur(20px) saturate(165%);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15);
}

/* ------------------------------------------------------------------
   ENQUIRY FIELDS — the underline stays (boxing every field would
   stack light-on-light material); focus grows a soft glass glow so
   the active field is unmistakable over the moving water.
   ------------------------------------------------------------------ */
.enquiry__input:focus,
.enquiry__select:focus,
.enquiry__textarea:focus {
  border-bottom-color: var(--pearl);
  /* The 2px bar is the WCAG focus indicator from pages.css — this
     rule must CARRY it, not replace it, because at equal specificity
     the later file wins the whole box-shadow longhand. The glow is
     decoration on top. */
  box-shadow:
    0 2px 0 0 var(--pearl),
    0 12px 22px -18px rgba(245, 243, 238, 0.55);
}

/* ------------------------------------------------------------------
   ACCESSIBILITY FALLBACKS (§14) — three independent signals.
   ------------------------------------------------------------------ */
@media (prefers-reduced-transparency: reduce) {
  .form-tile__link,
  .btn--primary,
  .btn--ghost,
  .enquiry__submit,
  .contact-card.glass--card,
  .process-step,
  .lang-select__trigger,
  .lang-select__menu,
  .product-card,
  .form-chapter__note,
  .chapter-nav__list,
  .menu-sub__back {
    background: rgba(13, 20, 33, 0.97);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
}
@media (prefers-contrast: more) {
  .form-tile__link,
  .btn--primary,
  .btn--ghost,
  .contact-card.glass--card,
  .process-step,
  .product-card,
  .lang-select__trigger,
  .lang-select__menu {
    background: rgba(8, 13, 22, 0.98);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
    border: 1px solid rgba(245, 243, 238, 0.55);
  }
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .form-tile__link,
  .contact-card.glass--card,
  .process-step,
  .product-card,
  .lang-select__menu {
    background: rgba(13, 20, 33, 0.94);
  }
  .btn--primary { background: rgba(245, 243, 238, 0.2); }
}
@media (prefers-reduced-motion: reduce) {
  .form-tile__link:hover,
  .btn--primary:hover,
  .product-card:hover { transform: none; }
}

/* ------------------------------------------------------------------
   TILE ALIGNMENT — the hidden item lists differ in length per tile,
   and with bottom-anchored content the taller list pushed its title
   higher: four titles, four heights. Content anchors to the top, so
   every name sits on the same line across the row and the reveal
   grows downward into the tile's reserved space.
   ------------------------------------------------------------------ */
.form-tile__body { justify-content: flex-start; }

/* ------------------------------------------------------------------
   HOME PAGE CHROME — no bars, just the stuff.

   HEADER: when the top bar drops in after the hero sequence it used
   to arrive as a whisper-glass strip with a hairline and a specular
   edge. On the home page the ocean IS the surface — the logo, clock,
   language picker and menu button now float directly on it.

   FOOTER: the footer carried its own video layer and its own glass
   pane, which met the page's single frost pane at a visible seam —
   a lighter bar where the footer began. On the home page the footer
   now paints the SAME frost recipe as the page pane (same tint
   tokens, same blur), so the page reads as one continuous sheet of
   glass from the hero to the last line.
   ------------------------------------------------------------------ */
.page-home .site-header.is-visible {
  /* Not a bar — a breath of tint fading to nothing, just enough for
     the burger and the clock to hold against bright water. */
  background: linear-gradient(180deg,
    rgba(4, 10, 20, 0.44) 0%,
    rgba(4, 10, 20, 0.18) 62%,
    transparent 100%);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  border-bottom: none;
}
.page-home .site-header.is-visible::after { display: none; }

/* FOOTER — no surface of its own, on any page.

   Three things used to fight here, and between them they made the
   footer read as a separate slab welded to the bottom of the page:

     1. On the home page the footer re-declared the page tint and blur
        itself. The values matched main::after exactly, which is why
        this looked like it should work — but two adjacent
        backdrop-filters each clamp their blur at their own edges, so
        the join was still visible. Fixed properly at the source: the
        pane is one element spanning both now (css/pages.css).

     2. On about/products/quality/contact the footer instead used
        .site-footer__glass — a DIFFERENT material: rgba(26,35,48,0.5)
        over a 20px blur, against the page's rgba(8,20,40,0.54) over
        32px. Lighter, bluer and sharper than the ground it sat on.

     3. A 1px divider line across the top of it.

   All three are gone. The footer is transparent and the page pane
   shows through, so every page ends in the same continuous sheet. */
.site-footer {
  border-top: none;
  background: transparent;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}
.site-footer__glass,
.site-footer__bg-video { display: none; }

/* ------------------------------------------------------------------
   PRODUCTS SECTION BAR — was welded to the hero's bottom edge; it
   breathes now, and the pill is unmistakably glass: luminance-graded
   fill over a heavy blur, a specular bloom, a brighter rim.
   ------------------------------------------------------------------ */
.chapter-nav { margin-top: clamp(1.5rem, 3.5vh, 2.75rem); }
.chapter-nav__list {
  padding: 0.45rem;
  background:
    radial-gradient(130% 90% at 20% -20%, rgba(255, 255, 255, 0.14), transparent 55%),
    linear-gradient(180deg, rgba(255, 255, 255, 0.10), rgba(255, 255, 255, 0.02)),
    rgba(12, 22, 38, 0.44);
  -webkit-backdrop-filter: blur(36px) saturate(185%) brightness(1.05);
  backdrop-filter: blur(36px) saturate(185%) brightness(1.05);
  border: 1px solid rgba(245, 243, 238, 0.2);
  box-shadow:
    0 18px 44px -16px rgba(0, 0, 0, 0.8);
}

/* ------------------------------------------------------------------
   HERITAGE HEADING — the slanted serif IS this heading's identity;
   uprighting it read as a different face entirely. The one deliberate
   exception to the no-italics rule, at the client's request.
   ------------------------------------------------------------------ */
.about-excerpt__heading em { font-style: italic; }

/* Focus rings are for keyboard users. nav.js stamps the modality on
   <html>; a pointer user never sees a ring on the menu chrome, a
   keyboard user always does. */
html[data-modality="pointer"] .menu-overlay :focus-visible,
html[data-modality="pointer"] .menu-toggle:focus-visible,
html[data-modality="pointer"] .lang-select__trigger:focus-visible {
  outline: none;
}

/* Hero eyebrow — one size up. It is the first line anyone reads. */
.hero__eyebrow {
  font-size: clamp(0.95rem, 1.15vw, 1.1rem);
  letter-spacing: 0.2em;
  opacity: 1;
}

/* ------------------------------------------------------------------
   FOOTER SOCIAL — ONE glass rail holding five channels.

   This replaces five individually-glassed pills, and the reason is
   material, not taste. The footer already sits on the page pane, which
   is itself a translucent surface; five more blurred, brand-tinted
   pills on top of it made a third layer of glass on glass, which is
   exactly where legibility collapses and where "premium" tips into
   noisy. Two channels were tolerable, five were not.

   So the RAIL is the material -- one blur, one rim, one shadow -- and
   the buttons inside it are flat. Each one carries its brand hue only
   in the icon and only in the wash that arrives on hover, so the group
   reads as one object at rest and the colour is a response to the
   pointer rather than five competing badges.

   Labels stay on desktop, where there is room for a word to read as an
   invitation. Below 640px they collapse to icons and the accessible
   name carries on doing the work through aria-label, so five channels
   still fit one row on a phone instead of wrapping into a block.
   ------------------------------------------------------------------ */
.site-footer__social {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.6rem;
  margin-block: clamp(1.25rem, 3vh, 2rem) clamp(0.5rem, 1.5vh, 1rem);
}
.social-rail__label {
  margin: 0;
  font-family: var(--font-sans);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--pearl-muted);
}
.social-rail,
.dock__rail {
  position: relative;
  isolation: isolate;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.25rem;
  padding: 0.4rem;
  border-radius: 999px;
  background:
    radial-gradient(120% 140% at 22% -30%, rgba(255, 255, 255, 0.16), transparent 58%),
    linear-gradient(180deg, rgba(255, 255, 255, 0.10), rgba(255, 255, 255, 0.02)),
    rgba(12, 22, 38, 0.42);
  -webkit-backdrop-filter: blur(30px) saturate(190%) brightness(1.04);
  backdrop-filter: blur(30px) saturate(190%) brightness(1.04);
  box-shadow: 0 18px 40px -18px rgba(0, 0, 0, 0.65);
}
/* Gradient rim, drawn as a masked ring so the edge catches light along
   the top-left and fades away underneath -- the same treatment every
   other glass surface on the site uses. */
.social-rail::before,
.dock__rail::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  padding: 1px;
  background: linear-gradient(150deg,
    rgba(255, 255, 255, 0.62) 0%,
    rgba(255, 255, 255, 0.10) 44%,
    rgba(255, 255, 255, 0.05) 64%,
    rgba(255, 255, 255, 0.34) 100%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  pointer-events: none;
}

.social-btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.55rem;
  /* 44px is the floor for a touch target; the rail's own padding keeps
     the outer edges clear of the viewport. */
  min-height: 44px;
  min-width: 44px;
  padding: 0 0.95rem;
  border-radius: 999px;
  text-decoration: none;
  color: var(--pearl);
  font-family: var(--font-sans);
  font-size: 0.9rem;
  font-weight: 600;
  letter-spacing: 0.01em;
  white-space: nowrap;
  --sb-hue: 96, 165, 250;
  background: transparent;
  transition:
    background-color 200ms var(--ease-out),
    color 200ms var(--ease-out),
    transform 200ms var(--ease-out);
}
.social-btn__icon {
  width: 20px;
  height: 20px;
  fill: currentColor;
  flex: none;
  /* The hue lives in the icon at rest. Muted enough that five of them
     sit together without shouting, full strength on hover. */
  color: rgb(var(--sb-hue));
  opacity: 0.92;
  transition: opacity 200ms var(--ease-out);
}
.social-btn--whatsapp { --sb-hue:  37, 211, 102; }
.social-btn--linkedin { --sb-hue:  10, 102, 194; }
.social-btn--facebook { --sb-hue:  24, 119, 242; }
.social-btn--wechat   { --sb-hue:   7, 193,  96; }
.social-btn--zalo     { --sb-hue:   0, 104, 255; }

@media (hover: hover) and (pointer: fine) {
  .social-btn:hover { background-color: rgba(var(--sb-hue), 0.22); }
  .social-btn:hover .social-btn__icon { opacity: 1; }
}
/* Feedback on the press, not on release (Apple: respond to touch-down). */
.social-btn:active { transform: scale(0.96); transition: transform 60ms linear; }
.social-btn:focus-visible {
  outline: 2px solid var(--pearl);
  outline-offset: 2px;
}

/* Phones: icons only. The label stays in the accessibility tree via
   aria-label on the anchor, so nothing is lost to a screen reader. */
@media (max-width: 640px) {
  .site-footer__social { align-items: flex-start; }
  .social-btn { padding: 0; }
  .social-btn__label {
    position: absolute;
    width: 1px; height: 1px;
    margin: -1px; padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
  }
}

@media (prefers-reduced-transparency: reduce) {
  .dock__hint {
    background: rgba(10, 18, 32, 0.96);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
  .social-rail,
  .dock__rail {
    background: rgba(12, 22, 38, 0.92);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
}
@media (prefers-reduced-motion: reduce) {
  .social-btn { transition: background-color 120ms linear, color 120ms linear; }
  .social-btn:active { transform: none; }
}

/* ------------------------------------------------------------------
   ENQUIRY FORM — five fields, and labels a person can actually read.
   The old 10px tracked-caps labels were legible only as texture.
   ------------------------------------------------------------------ */
.enquiry__grid--simple {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(1.25rem, 2.5vh, 1.75rem) clamp(1.25rem, 2.5vw, 2rem);
}
@media (max-width: 640px) { .enquiry__grid--simple { grid-template-columns: minmax(0, 1fr); } }
.enquiry__label {
  font-family: var(--font-sans);
  font-size: 0.98rem;
  font-weight: 500;
  letter-spacing: 0.02em;
  text-transform: none;
  color: var(--pearl);
  margin-bottom: 0.35rem;
}
.enquiry__input,
.enquiry__select,
.enquiry__textarea {
  font-size: 1.05rem;
  padding: 0.7rem 0;
  border-bottom-color: rgba(255, 255, 255, 0.4);
}
.enquiry__textarea { min-height: 180px; line-height: 1.6; }
.enquiry__input::placeholder,
.enquiry__textarea::placeholder { color: rgba(245, 243, 238, 0.42); }

/* ------------------------------------------------------------------
   TILE HOVER LIST — the products should be the event. Body size, not
   eyebrow size, with a little more air between lines.
   ------------------------------------------------------------------ */
.form-tile__items { gap: 0.3rem; }
.form-tile__items li {
  font-size: clamp(1rem, 1.15vw, 1.15rem);
  line-height: 1.35;
  color: var(--pearl);
}
.form-tile__items li::before { color: rgba(245, 243, 238, 0.55); }

/* ==================================================================
   THE LIGHT — pointer-tracked specular highlight (js/glassLight.js).

   A soft radial bloom that sits where the pointer is, painted on a
   child span above the surface's fill and media but under its text
   (screen blend never darkens, so white type is untouched even where
   the span does overlap it). It appears on hover and follows the hand;
   this is what makes the panes read as glass rather than as pictures
   of glass. Never on touch, reduced motion, or reduced transparency —
   glassLight.js does not run there, and the CSS is guarded twice.
   ================================================================== */
.has-glass-light { position: relative; }
.glass-light {
  position: absolute;
  inset: 0;
  z-index: 3;
  border-radius: inherit;
  pointer-events: none;
  opacity: 0;
  mix-blend-mode: screen;
  background:
    radial-gradient(220px circle at var(--lx, 50%) var(--ly, 40%),
      rgba(255, 255, 255, 0.26) 0%,
      rgba(255, 255, 255, 0.10) 32%,
      transparent 68%);
  transition: opacity 420ms var(--ease-out-expo);
}
.has-glass-light:hover > .glass-light { opacity: 1; }
/* Small controls get a tighter, brighter bloom. */
.btn--primary > .glass-light,
.social-btn   > .glass-light {
  background:
    radial-gradient(120px circle at var(--lx, 50%) var(--ly, 40%),
      rgba(255, 255, 255, 0.34) 0%,
      rgba(255, 255, 255, 0.1) 40%,
      transparent 70%);
}
/* Text and controls inside lit surfaces stay above the light. */
/* Lift real content above the light layer — but NOT the light layer
   itself. `> *` matched .glass-light too and reset it from absolute to
   relative, which killed the pointer highlight on every surface styled
   this way (process steps, contact cards, certs). */
/* Lift real content above the effect layers — but never the effect
   layers themselves. `> *` matched .glass-light and .glass-facet and
   reset them from absolute to relative: the highlight died, and the
   facet collapsed to a 0x0 inline span. Any layer this file injects
   must be excluded here, so they are named once, together. */
.product-card__body,
.contact-card > *:not(.glass-light):not(.glass-facet),
.process-step > *:not(.glass-light):not(.glass-facet),
.cert > *:not(.glass-light):not(.glass-facet) { position: relative; z-index: 4; }
.chapter-nav__link { position: relative; z-index: 4; }
@media (hover: none), (prefers-reduced-motion: reduce), (prefers-reduced-transparency: reduce) {
  .glass-light { display: none; }
}

/* ==================================================================
   ABOUT · QUALITY — surfaces that predated the material system.
   ================================================================== */
/* Certification cells were a bordered table; they are cards now, on
   the same card tier as everything else, with air between them. */
.certs__grid {
  gap: clamp(0.75rem, 1.2vw, 1rem);
  border: none;
}
.cert {
  border: 1px solid rgba(245, 243, 238, 0.12);
  border-radius: 18px;
  min-height: 160px;
  background:
    var(--lg-specular),
    linear-gradient(180deg, rgba(255, 255, 255, 0.085), rgba(255, 255, 255, 0.03));
  -webkit-backdrop-filter: blur(22px) saturate(170%);
  backdrop-filter: blur(22px) saturate(170%);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.22),
    0 14px 36px -18px rgba(0, 0, 0, 0.7);
  transition:
    transform 260ms var(--ease-out-expo),
    border-color 300ms var(--ease-out-expo);
}
@media (hover: hover) {
  .cert:hover { border-color: rgba(245, 243, 238, 0.24); }
}
/* Legacy hover on the about-page principle cards reached for the old
   brand colour and an opaque near-black fill; both are gone. */
.principle {
  border-radius: 18px;
  border: 1px solid rgba(245, 243, 238, 0.11);
  background:
    var(--lg-specular),
    linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.03));
  -webkit-backdrop-filter: blur(22px) saturate(170%);
  backdrop-filter: blur(22px) saturate(170%);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.2),
    0 14px 36px -18px rgba(0, 0, 0, 0.7);
}
@media (hover: hover) and (pointer: fine) {
  .principle:hover {
    border-color: rgba(245, 243, 238, 0.24);
    background:
      var(--lg-specular),
      linear-gradient(180deg, rgba(255, 255, 255, 0.11), rgba(255, 255, 255, 0.045));
    transform: translateY(-4px);
  }
}
.principle__num { color: var(--pearl-quiet); }
/* Process-step hover lift, so the four cards answer the pointer the
   way the tiles above them do. */
@media (hover: hover) {
  .process-step {
    transition:
      transform 300ms var(--ease-out-expo),
      border-color 300ms var(--ease-out-expo),
      box-shadow 300ms var(--ease-out-expo);
  }
  .process-step:hover {
    transform: translateY(-4px);
    border-color: rgba(245, 243, 238, 0.22);
    box-shadow:
      inset 0 1px 0 rgba(255, 255, 255, 0.28),
      0 22px 48px -18px rgba(0, 0, 0, 0.8);
  }
}

/* ==================================================================
   TOUCH TARGETS · SMALL POLISH
   ================================================================== */
/* The floating section bar's links reach 44px on touch devices. */
@media (hover: none) {
  .chapter-nav__link { min-height: 44px; align-items: center; }
}
/* The photo reel joins the radius system (was 10px on a 16–24 page). */
.reel { border-radius: 18px; }
/* Section eyebrows read a touch stronger — they are the wayfinding. */
.eyebrow { color: rgba(245, 243, 238, 0.82); }
.metric__label.eyebrow { color: var(--pearl-muted); }

/* ==================================================================
   CONTACT PAGE — the enquiry pane and the clock face.
   ================================================================== */
/* .glass--card was referenced by the form and defined by nothing —
   the fields sat bare on the page. A large surface reads THICKER
   (§12): heavier blur, a slightly deeper tint for a form's worth of
   type, the rim highlight, and a shadow that lifts it off the frost. */
.enquiry.glass--card {
  border-radius: 24px;
  border: 1px solid rgba(245, 243, 238, 0.14);
  background:
    radial-gradient(120% 60% at 16% -10%, rgba(255, 255, 255, 0.14), transparent 58%),
    linear-gradient(180deg,
      rgba(255, 255, 255, 0.075) 0%,
      rgba(255, 255, 255, 0.035) 40%,
      rgba(8, 20, 40, 0.18) 100%);
  -webkit-backdrop-filter: blur(36px) saturate(180%) brightness(1.03);
  backdrop-filter: blur(36px) saturate(180%) brightness(1.03);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.28),
    0 30px 70px -28px rgba(0, 0, 0, 0.8);
}

/* The analogue dial: a glass watch face — luminance-graded, a specular
   catch at eleven o'clock, a rim that brightens toward the light. */
.clock {
  background:
    radial-gradient(70% 55% at 30% 18%, rgba(255, 255, 255, 0.2), transparent 60%),
    linear-gradient(180deg, rgba(255, 255, 255, 0.11), rgba(255, 255, 255, 0.03) 60%, rgba(8, 20, 40, 0.2));
  -webkit-backdrop-filter: blur(28px) saturate(180%);
  backdrop-filter: blur(28px) saturate(180%);
  border: 1px solid rgba(245, 243, 238, 0.2);
  box-shadow:
    0 34px 70px -24px rgba(0, 0, 0, 0.75);
}

/* ==================================================================
   SAFE AREAS — the notch, the rounded corners, the home indicator.

   viewport-fit=cover opts the page into the full display, so every
   fixed or edge-anchored surface must now keep itself out from under
   the hardware. max() is the right operator: it takes the design
   value on a device with no insets and the inset where there is one,
   so nothing moves on a laptop.

   The landscape case is the one that actually bites: on a notched
   iPhone turned sideways the left inset is ~47px, which would eat
   the first two characters of every line of body copy.
   ================================================================== */
.container {
  padding-inline:
    max(var(--container-pad), env(safe-area-inset-left))
    max(var(--container-pad), env(safe-area-inset-right));
}
.site-header__inner {
  padding-inline:
    max(var(--gutter-x), env(safe-area-inset-left))
    max(var(--gutter-x), env(safe-area-inset-right));
}
.menu-overlay__inner {
  padding-inline:
    max(var(--gutter-x), env(safe-area-inset-left))
    max(var(--gutter-x), env(safe-area-inset-right));
}
/* The dock floats above the home indicator, not under it. */
.dock {
  right: max(clamp(1rem, 2.5vw, 2rem), env(safe-area-inset-right));
  bottom: max(clamp(1rem, 2.5vw, 2rem), env(safe-area-inset-bottom));
}
/* The last line of the page clears the indicator too. */
.site-footer {
  padding-bottom: max(var(--space-6), calc(env(safe-area-inset-bottom) + var(--space-4)));
}
.skip-link { left: max(var(--space-2), env(safe-area-inset-left)); }

/* ==================================================================
   MINIMUM READABLE SIZE — nothing on the page below 12px.

   Four live cases, found by rendering every page at 390px and reading
   computed styles rather than grepping declarations. These are all
   tracked uppercase micro-labels, which is exactly the type that is
   hardest to read small; letter-spacing is eased a touch as they grow
   so the rhythm is unchanged.
   ================================================================== */
.cochin-now__zone      { font-size: 0.78rem; }          /* was 11.8px */
.hero__cue-text        { font-size: 0.75rem; letter-spacing: 0.26em; }  /* was 10px */
.contact-card__eyebrow { font-size: 0.75rem; letter-spacing: 0.28em; }  /* was 11px */
/* NOT raised: .clock__wordmark and .clock__zone are engraving on an
   ornamental watch dial, inside an aria-hidden SVG, in user units on a
   200-unit viewBox. A maker's line on a real dial is tiny; setting it
   to 12 would deform the object, and Hard Rule 1 forbids changing the
   design. The time itself is available as real text in the header
   clock and in the dial's sr-only label, so nothing is lost. The
   verification script exempts decorative SVG text explicitly. */

/* ==================================================================
   OVERFLOW SAFETY NET — the two rules that stop a phone layout
   blowing out, applied once instead of per-component.

   1. Flex and grid items default to `min-width: auto`, which means
      "never shrink below my content's intrinsic width". One long word,
      one email address, one un-broken product code, and the track
      refuses to shrink and the page grows a horizontal scrollbar.
      This project has already been bitten by it once (a table with a
      34rem min-width silently clipping a chapter at 311px). Setting
      min-width: 0 on the layout children lets tracks shrink honestly.

   2. Long tokens then need somewhere to break. Body copy gets
      overflow-wrap: anywhere — the exporter's own address,
      exports@algamarseafoods.com, is 30 characters and appears in the
      footer and in the contact cards at 320px.

   Deliberately scoped to layout containers rather than `* { }`:
   min-width: 0 on everything would collapse the intrinsic sizing that
   buttons, chips and the language picker depend on.
   ================================================================== */
.container > *,
.forms > *,
.metrics__grid > *,
.process__grid > *,
.contacts__grid > *,
.product-grid > *,
.principles__grid > *,
.certs__grid > *,
.reach-row > *,
.story__grid > *,
.timeline__grid > *,
.timeline-row > *,
.capabilities__grid > *,
.glossary__item > *,
.cap-row > *,
.office__grid > *,
.faq__grid > *,
.localtime__grid > *,
.enquiry__grid > *,
.site-footer__grid > *,
.pipeline-stage > * { min-width: 0; }

/* Long tokens break rather than push. Scoped to prose: applying this
   to headings would let a display line break mid-word. */
.hero__sub,
.about-excerpt__body,
.form-tile__line,
.process-step__body,
.reach-row__markets,
.reach__note,
.contact-card__body,
.contact-card__cta,
.product-card__detail,
.form-intro__body,
.form-closing__body,
.story__body,
.timeline-row__body,
.glossary__def,
.cap-row__val,
.faq__a,
.office__address,
.office__meta,
.site-footer__col,
.enquiry__intro {
  overflow-wrap: anywhere;
  /* overflow-wrap only, and deliberately NO hyphens: auto.
     Automatic hyphenation was breaking ordinary words mid-line and
     inserting a dash to mark the break -- "Indi-vidual Quick Frozen"
     on the quality page being the one that got noticed. overflow-wrap
     stays as the safety net for genuinely unbreakable strings like
     long URLs and email addresses: it only acts when a word cannot
     fit at all, and it never adds a dash. Nothing on the site is
     justified, so there is no ragged-edge cost to dropping it. */
}

/* iframes were the one media element missing from the reset. */
iframe { max-width: 100%; }

/* ==================================================================
   SECTION BAR ON A PHONE — fit, don't scroll.

   At 320-375px the four chapter links needed ~500px, so the pill
   became a horizontal scroller and two of the four forms sat off the
   edge behind a scroll a thumb has no reason to try. The numerals are
   decorative (aria-hidden) and the first to go; the words themselves
   are the navigation and all four now fit. Desktop is untouched.
   ================================================================== */
@media (max-width: 560px) {
  .chapter-nav__list { padding: 0.3rem; gap: 0.1rem; }
  .chapter-nav__link {
    padding: 0.5rem 0.6rem;
    font-size: 0.85rem;
    gap: 0;
  }
  .chapter-nav__link .num { display: none; }
}

/* ==================================================================
   HARD FLOORS — objects whose minimum size ignored their container.

   Surfaced by the 200%-zoom pass. Several objects were sized with a
   pixel floor (`clamp(200px, …)`, `clamp(130px, …)`) which, below a
   certain viewport, is wider than the space available — the element
   then pushes the page sideways no matter how well the layout around
   it behaves. Wrapping each in min(…, 100%) keeps the intended size
   everywhere it fits and lets it yield where it does not. Nothing
   changes at any normal viewport.
   ================================================================== */
.clock            { width: min(clamp(200px, 26vw, 280px), 100%); }
.site-footer__mark { width: min(clamp(130px, 12vw, 165px), 100%); }
.reel__item       { width: min(clamp(230px, 26vw, 360px), 100%); }

/* Display headings may break a word as a LAST resort. break-word only
   acts when a word genuinely cannot fit — unlike `anywhere`, it never
   affects normal line-breaking, so the display type is untouched at
   every size where it fits. */
.hero__headline,
.page-hero__title,
.about-excerpt__heading,
.range__heading,
.process__heading,
.reach__heading,
.contacts__heading,
.form-chapter__heading,
.story__heading,
.timeline__heading,
.principles__heading,
.pipeline__heading,
.faq__heading,
.office__heading,
.enquiry__heading,
.story-closing__heading,
.contact-card__heading,
.product-card__name,
.form-tile__name { overflow-wrap: break-word; }

/* The header's utility cluster is the last thing that can compress.
   Below the WCAG floor it may still clip; the menu button is the one
   control that must survive, so it keeps its 44px and the language
   picker yields first. */
@media (max-width: 380px) {
  .lang-select__trigger { padding-inline: 8px; }
  .site-header__inner { column-gap: 0.25rem; }
}

/* Grid children the first pass missed, plus the inline-flex link that
   could not wrap its own arrow. Both surfaced at extreme zoom; both
   are free everywhere else. */
.about-excerpt__grid > *,
.reach__grid > *,
.metrics__header > *,
.enquiry__grid--simple > *,
.enquiry__field > *,
.pipeline__list > *,
.chapter-nav__list > * { min-width: 0; }

.link-arrow { flex-wrap: wrap; }

/* ==================================================================
   TOUCH TARGETS — 44px hit boxes without moving a single pixel.

   The gate found exactly one class of failure across 100 page-size
   pairs: three controls whose visual height is under 44px. The brief
   allows exactly the right technique for this — "extend hit area
   beyond visual bounds if needed" — so each grows by padding and
   gives the space straight back with an equal negative margin. The
   hit box reaches 44px; the layout footprint is unchanged to the
   pixel, which is what Hard Rule 1 requires.

   .link-arrow's underline is drawn on its inner <span>, so padding on
   the link itself cannot move it. Verified: the rule below changes
   height only.
   ================================================================== */
/* 6px, not 4: the natural height varies 34-36px with the type scale,
   and 6 clears 44 in both cases. Overshooting a hit area is free. */
.link-arrow {
  padding-block: 6px;
  margin-block: -6px;
}
.site-footer__mark-link {
  display: inline-flex;
  align-items: center;
  padding-block: 6px;
  margin-block: -6px;
}
.lang-select__trigger {
  min-height: 44px;
  margin-block: -2px;
}

/* ==================================================================
   HERO MEDIA — the state machine that guarantees no play button.

   js/heroMedia.js writes data-video-state on .hero-bg:

     loading — first paint, before play() has answered
     playing — the video is running; it is what you see
     static  — play() was refused, the file failed, or the visitor
               asked for reduced motion

   In `static` the <video> is REMOVED FROM THE RENDER (display: none),
   not merely covered. A paused <video> is what iOS Safari draws
   ::-webkit-media-controls-start-playback-button on top of, so the
   only reliable way to never show that button is to not have a paused
   video on screen. The still underneath is a frame from the clip, so
   the hero reads the same either way.

   The suppression rules further down are the second layer only. They
   exist for the window between first paint and play() resolving, and
   for engines that decorate a video we have not hidden yet. They are
   not the fix — heroMedia.js is.
   ================================================================== */
.hero-bg__fallback {
  position: absolute;
  inset: 0;
  background-position: center;
  background-size: cover;
  background-repeat: no-repeat;
  opacity: 0;
  transition: opacity 600ms var(--ease-out-expo);
  pointer-events: none;
}
/* Shown whenever the video is not the thing playing. `loading` counts:
   the still is the correct first paint, and it cross-fades out under
   the video the moment playback actually starts. */
.hero-bg[data-video-state="loading"] .hero-bg__fallback,
.hero-bg[data-video-state="static"]  .hero-bg__fallback { opacity: 1; }

.hero-bg[data-video-state="static"] .hero-video {
  display: none !important;
}

/* Reduced motion: heroMedia.js never fetches or plays the file, and
   this guarantees the still even if a state write is missed. */
@media (prefers-reduced-motion: reduce) {
  .hero-bg .hero-video { display: none !important; }
  .hero-bg__fallback { opacity: 1 !important; transition: none; }
}

/* ------------------------------------------------------------------
   Residual native controls — every part, every engine. The existing
   block covered .hero-video and a dead .menu-video class; this adds
   the enclosure part, Firefox, and the cast button, and applies to
   any video the site may add later.
   ------------------------------------------------------------------ */
video::-webkit-media-controls,
video::-webkit-media-controls-enclosure,
video::-webkit-media-controls-panel,
video::-webkit-media-controls-panel-container,
video::-webkit-media-controls-play-button,
video::-webkit-media-controls-overlay-play-button,
video::-webkit-media-controls-start-playback-button,
video::-webkit-media-controls-overlay-enclosure {
  display: none !important;
  -webkit-appearance: none !important;
  appearance: none !important;
  opacity: 0 !important;
  pointer-events: none !important;
}
video::-moz-media-controls { display: none !important; }
video::-internal-media-controls-overlay-cast-button { display: none !important; }

/* The footer lost its Compliance column, so the grid is three tracks
   now, not four — otherwise the mark and two lists sat squeezed into
   the left three-quarters with a hole where the certifications were. */
.site-footer--slim .site-footer__grid { grid-template-columns: 1.4fr 1fr 1fr; }
@media (max-width: 900px) {
  .site-footer--slim .site-footer__grid { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 640px) {
  .site-footer--slim .site-footer__grid { grid-template-columns: 1fr; }
}

/* ==================================================================
   HOW WE WORK — cut crystal.

   A frosted pane and a block of cut crystal are different materials.
   Frost scatters light evenly; crystal has a BODY — it bends what is
   behind it, throws hard highlights, splits light into colour at the
   bevels, and pools a caustic where it focuses. Four optical cues,
   each a real layer:

   1. REFRACTION AT THE BEVEL. ::before carries its OWN backdrop-filter
      — a different blur, saturation and brightness from the body —
      masked to a ring around the perimeter. The edge therefore
      re-samples the ocean behind it differently from the centre, which
      is literally what a bevel does to light. This is the cue that
      separates crystal from frosted plastic, and no gradient can fake
      it.

   2. DISPERSION. The rim gradient runs cool at the top-left and warm
      at the bottom-right — a few percent of blue and amber, the sign
      of white light being split. Push this further and it reads as a
      rendering error; kept this faint it reads as glass.

   3. HARD SPECULARS. Cut glass throws sharp streaks, not soft blooms.
      ::after is a narrow diagonal band at high contrast with a tight
      falloff, plus a second, dimmer one below it.

   4. A CAUSTIC. The pool of light a lens focuses, low and off-centre,
      warm against the cool body.

   On hover the streak SWEEPS across and the body deepens — the tile
   reads as tilting to catch the light, not as a box changing colour
   (apple-design §12: materialise, don't just fade).
   ================================================================== */
.process-step {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  border: 1px solid rgba(214, 232, 255, 0.16);
  border-radius: 20px;
  /* CRYSTAL IS COLOURLESS. The first attempt tinted the body blue and
     pushed saturation to 200%, which turned the ocean behind it into a
     cobalt slab — the opposite of glass. A real block of crystal has
     almost no colour of its own; it takes the scene behind it and
     bends it. So the fill is now near-neutral and very light, the blur
     is LOW (crystal transmits, frost is what blurs), and all the
     colour lives at the bevel where dispersion actually happens. */
  background:
    /* caustic — the pool a lens focuses, warm, low and left */
    radial-gradient(58% 38% at 26% 98%, rgba(255, 228, 178, 0.1), transparent 72%),
    /* the specular dome at the light source */
    radial-gradient(125% 72% at 16% -16%, rgba(255, 255, 255, 0.22), transparent 62%),
    /* the body — a whisper, so the water reads through it */
    linear-gradient(168deg,
      rgba(255, 255, 255, 0.1) 0%,
      rgba(255, 255, 255, 0.045) 42%,
      rgba(255, 255, 255, 0.02) 72%,
      rgba(8, 22, 42, 0.07) 100%);
  -webkit-backdrop-filter: blur(14px) saturate(148%) brightness(1.04);
  backdrop-filter: blur(14px) saturate(148%) brightness(1.04);
  box-shadow:
    /* the lit top facet */
    /* interior bounce, so it reads solid rather than hollow */
    inset 0 0 40px -24px rgba(210, 235, 255, 0.42),
    0 18px 42px -20px rgba(0, 0, 0, 0.72),
    0 4px 12px -6px rgba(0, 0, 0, 0.45);
  transition:
    transform 520ms var(--ease-out-expo),
    box-shadow 520ms var(--ease-out-expo),
    border-color 520ms var(--ease-out-expo),
    backdrop-filter 520ms var(--ease-out-expo);
}

/* 1 + 2 — the refracting, dispersing bevel. */
.process-step::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  border-radius: inherit;
  padding: 1.4px;
  background: linear-gradient(142deg,
    rgba(196, 226, 255, 0.85) 0%,     /* cool — light entering */
    rgba(255, 255, 255, 0.30) 18%,
    rgba(255, 255, 255, 0.06) 46%,
    rgba(255, 255, 255, 0.05) 62%,
    rgba(255, 214, 160, 0.42) 88%,    /* warm — light leaving */
    rgba(255, 236, 200, 0.62) 100%);
  -webkit-backdrop-filter: blur(7px) saturate(230%) brightness(1.2);
  backdrop-filter: blur(7px) saturate(230%) brightness(1.2);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  pointer-events: none;
}

/* 3 — the hard speculars, parked off to the left until hover. */
.process-step::after {
  content: "";
  position: absolute;
  inset: -40% -10%;
  z-index: 2;
  pointer-events: none;
  background:
    linear-gradient(104deg,
      transparent 34%,
      rgba(255, 255, 255, 0.16) 43%,
      rgba(255, 255, 255, 0.42) 47%,
      rgba(255, 255, 255, 0.10) 51%,
      transparent 58%),
    linear-gradient(104deg,
      transparent 62%,
      rgba(214, 236, 255, 0.16) 67%,
      transparent 72%);
  transform: translateX(-38%);
  opacity: 0.75;
  transition:
    transform 900ms var(--ease-out-expo),
    opacity 640ms var(--ease-out-expo);
}

@media (hover: hover) {
  .process-step:hover {
    transform: translateY(-6px);
    border-color: rgba(214, 232, 255, 0.3);
    -webkit-backdrop-filter: blur(17px) saturate(160%) brightness(1.08);
    backdrop-filter: blur(17px) saturate(160%) brightness(1.08);
    box-shadow:
      inset 0 0 46px -20px rgba(210, 235, 255, 0.55),
      0 30px 64px -22px rgba(0, 0, 0, 0.82),
      0 6px 16px -8px rgba(0, 0, 0, 0.5);
  }
  /* the sweep */
  .process-step:hover::after { transform: translateX(38%); opacity: 1; }
}

/* The numeral is engraved into the glass rather than printed on it:
   a dark cut with a lit lower edge, the way an incision in a solid
   catches light from below. */
.process-step__num {
  color: rgba(226, 240, 255, 0.34);
  text-shadow:
    0 1px 0 rgba(255, 255, 255, 0.22),
    0 -1px 1px rgba(4, 14, 30, 0.6);
}

/* The pointer light is brighter and tighter here — a crystal's
   highlight is a hotspot, not a wash. */
.process-step > .glass-light {
  background:
    radial-gradient(180px circle at var(--lx, 50%) var(--ly, 40%),
      rgba(255, 255, 255, 0.34) 0%,
      rgba(206, 232, 255, 0.14) 34%,
      transparent 66%);
}

@media (prefers-reduced-motion: reduce) {
  .process-step::after { transition: none; }
  .process-step:hover { transform: none; }
}
/* Reduced transparency / high contrast already flatten .process-step
   via the blocks above; the bevel and sweep must go with them. */
@media (prefers-reduced-transparency: reduce), (prefers-contrast: more) {
  .process-step::before,
  .process-step::after { display: none; }
}

/* ==================================================================
   CRYSTAL, EVERYWHERE — the process-step treatment generalised.

   Three optical layers, applied to every tile on the site. What is
   deliberately NOT generalised is colour: the form tiles keep their
   per-form hue glow, the contact cards keep the blue the client asked
   for, the certs and principles stay neutral. Crystal is colourless —
   it takes its colour from what is behind it and from its own tint,
   so the same optics sit correctly on top of every one of them.

   LAYER 1 — THE REFRACTING BEVEL, on ::before.
   Most of these surfaces already had a gradient rim there. A rim is a
   drawing of an edge; a bevel is an edge that BENDS LIGHT. Each rim
   now carries its own backdrop-filter — a different blur, saturation
   and brightness from the body — so the perimeter re-samples the
   background differently from the centre. That difference is the
   whole effect, and no gradient can imitate it.

   LAYER 2 — DISPERSION, in the same gradient.
   Cool at the light-entry corner, warm at the exit. A few percent of
   blue and amber is the signature of white light being split; more
   reads as a rendering fault.

   LAYER 3 — THE SWEPT FACET, on an injected .glass-facet span.
   A hard, narrow streak that crosses the surface as it turns to the
   light. It lives in a span rather than ::after because ::after is
   already the specular dome on the form tiles and the dock buttons —
   a shared treatment cannot depend on which slots a card has free.

   Small controls (buttons, pills, the dock) get the bevel and no
   sweep: on a 54px circle a travelling streak reads as a glitch.
   ================================================================== */

/* ---- Layer 1 + 2: the bevel, on surfaces whose ::before is the rim ---- */
.form-tile__link::before,
.product-card::before,
.social-btn::before,
.btn--primary::before {
  -webkit-backdrop-filter: blur(6px) saturate(220%) brightness(1.16);
  backdrop-filter: blur(6px) saturate(220%) brightness(1.16);
}
/* Dispersion added to the big cards' rims. The small controls keep
   their existing neutral rims — a chromatic edge on a 44px pill is
   noise at that size. */
.form-tile__link::before {
  background: linear-gradient(142deg,
    rgba(196, 226, 255, 0.8) 0%,
    rgba(255, 255, 255, 0.3) 20%,
    var(--lg-rim-mid) 46%,
    var(--lg-rim-lo) 64%,
    rgba(255, 214, 160, 0.34) 86%,
    var(--tile-edge) 100%);
}
.product-card::before {
  background: linear-gradient(142deg,
    rgba(196, 226, 255, 0.78) 0%,
    rgba(255, 255, 255, 0.28) 20%,
    var(--lg-rim-mid) 46%,
    var(--lg-rim-lo) 64%,
    rgba(255, 214, 160, 0.36) 88%,
    rgba(255, 236, 200, 0.5) 100%);
}

/* ---- The surfaces that had no rim at all: give them one ---- */
.contact-card.glass--card,
.principle,
.cert,
.form-chapter__note {
  position: relative;
  isolation: isolate;
  overflow: hidden;
}
.contact-card.glass--card::before,
.principle::before,
.cert::before,
.form-chapter__note::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  border-radius: inherit;
  padding: 1.4px;
  background: linear-gradient(142deg,
    rgba(196, 226, 255, 0.82) 0%,
    rgba(255, 255, 255, 0.3) 18%,
    rgba(255, 255, 255, 0.06) 46%,
    rgba(255, 255, 255, 0.05) 63%,
    rgba(255, 214, 160, 0.4) 88%,
    rgba(255, 236, 200, 0.58) 100%);
  -webkit-backdrop-filter: blur(7px) saturate(230%) brightness(1.2);
  backdrop-filter: blur(7px) saturate(230%) brightness(1.2);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  pointer-events: none;
}

/* ---- Layer 3: the swept facet ---- */
.has-glass-facet { position: relative; }
.glass-facet {
  position: absolute;
  inset: -40% -12%;
  z-index: 2;
  pointer-events: none;
  border-radius: inherit;
  background:
    linear-gradient(104deg,
      transparent 34%,
      rgba(255, 255, 255, 0.15) 43%,
      rgba(255, 255, 255, 0.4) 47%,
      rgba(255, 255, 255, 0.1) 51%,
      transparent 58%),
    linear-gradient(104deg,
      transparent 62%,
      rgba(214, 236, 255, 0.15) 67%,
      transparent 72%);
  transform: translateX(-38%);
  opacity: 0.7;
  transition:
    transform 900ms var(--ease-out-expo),
    opacity 640ms var(--ease-out-expo);
}
@media (hover: hover) {
  .has-glass-facet:hover > .glass-facet { transform: translateX(38%); opacity: 1; }
}

/* Content must clear the facet (z 2) and the light (z 3). The tiles
   and product cards already lift their own bodies; these are the
   surfaces that did not. */
.principle > *:not(.glass-light):not(.glass-facet),
.form-chapter__note > *:not(.glass-light):not(.glass-facet) {
  position: relative;
  z-index: 4;
}

/* A caustic for the cards that had none — the pool a lens focuses.
   Layered UNDER each surface's existing background so no tint the
   client chose is overwritten; it only adds the warm pool at the foot. */
.principle,
.cert,
.form-chapter__note {
  background-image:
    radial-gradient(58% 38% at 26% 98%, rgba(255, 228, 178, 0.09), transparent 72%),
    var(--lg-specular),
    linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.03));
}

/* Honour the same preference gates as the rest of the material. */
@media (prefers-reduced-motion: reduce) {
  .glass-facet { transition: none; }
}
@media (prefers-reduced-transparency: reduce), (prefers-contrast: more) {
  .glass-facet { display: none; }
  .contact-card.glass--card::before,
  .principle::before,
  .cert::before,
  .form-chapter__note::before { display: none; }
  .form-tile__link::before,
  .product-card::before,
  .social-btn::before,
  .btn--primary::before {
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
}

/* The facet cannot be collapsed by a stray `> *` rule again: it
   declares its own box, and every host clips to its own radius so the
   streak can never run past a corner. */
.glass-facet {
  display: block !important;
  position: absolute !important;
}
.has-glass-facet {
  overflow: hidden;
  border-radius: inherit;
}
.form-tile__link.has-glass-facet,
.product-card.has-glass-facet,
.contact-card.has-glass-facet,
.process-step.has-glass-facet,
.principle.has-glass-facet,
.cert.has-glass-facet,
.form-chapter__note.has-glass-facet { overflow: hidden; }

/* ==================================================================
   FLAT AND LIGHT — the final surface pass.

   Three changes, and one consequence worth naming.

   NO CAST SHADOW. Every drop shadow comes off the tiles. That removes
   the last depth cue, which means THE EDGE IS NOW THE ONLY THING
   separating a tile from the water behind it. So the rim is no longer
   decoration — it is load-bearing, and it is brightened to match. A
   surface with neither shadow nor a legible edge dissolves into its
   background, which is the failure mode this guards against.

   FLAT. The luminance gradients that gave the glass a top-lit body
   are flattened to an even fill. What stays is a whisper of specular
   at the light source and the bevel — enough that the material still
   reads as glass rather than as a flat translucent rectangle, without
   the modelling that made it look thick.

   LIGHT. Fills go up a little and even out, so the tiles read as
   luminous panes rather than tinted ones.

   ROUNDER. Every radius up by 6px, which at these sizes is the "tiny
   bit" that softens the corner without turning a card into a pill.

   The contrast floor still governs: pearl body copy over these
   lighter fills was measured after the change, not assumed.
   ================================================================== */

/* ---- No shadows on any tile, cast or painted ---- */
.form-tile__link,
.form-tile__link:hover,
.product-card,
.product-card:hover,
.contact-card.glass--card,
.contact-card.glass--card:hover,
.process-step,
.process-step:hover,
.principle,
.principle:hover,
.cert,
.cert:hover,
.form-chapter__note,
.metric,
.enquiry.glass--card,
.chapter-nav__list {
  box-shadow: none;
}

/* ---- Flat, light fills. Even translucency, one soft specular. ---- */
.form-tile__link,
.product-card,
.process-step,
.principle,
.cert,
.form-chapter__note {
  background-image:
    radial-gradient(120% 62% at 18% -12%, rgba(255, 255, 255, 0.16), transparent 58%),
    linear-gradient(180deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.085));
  -webkit-backdrop-filter: blur(16px) saturate(150%) brightness(1.06);
  backdrop-filter: blur(16px) saturate(150%) brightness(1.06);
}
/* The contact cards keep the blue you chose — flattened and lifted,
   not recoloured. */
.contact-card.glass--card {
  background-image:
    radial-gradient(120% 62% at 18% -12%, rgba(255, 255, 255, 0.18), transparent 58%),
    linear-gradient(180deg, rgba(150, 200, 255, 0.2), rgba(130, 185, 245, 0.17));
  -webkit-backdrop-filter: blur(18px) saturate(165%) brightness(1.06);
  backdrop-filter: blur(18px) saturate(165%) brightness(1.06);
}

/* ---- The edge, now load-bearing ---- */
.form-tile__link,
.product-card,
.contact-card.glass--card,
.process-step,
.principle,
.cert,
.form-chapter__note {
  border: 1px solid rgba(255, 255, 255, 0.3);
}
.form-tile__link::before,
.product-card::before,
.contact-card.glass--card::before,
.process-step::before,
.principle::before,
.cert::before,
.form-chapter__note::before {
  background: linear-gradient(142deg,
    rgba(214, 236, 255, 0.9) 0%,
    rgba(255, 255, 255, 0.5) 22%,
    rgba(255, 255, 255, 0.28) 48%,
    rgba(255, 255, 255, 0.3) 66%,
    rgba(255, 224, 180, 0.55) 88%,
    rgba(255, 240, 214, 0.75) 100%);
}

/* ---- Rounder by a touch ---- */
.form-tile__link      { border-radius: 30px; }
.contact-card.glass--card { border-radius: 28px; }
.product-card,
.process-step         { border-radius: 26px; }
.principle,
.cert,
.form-chapter__note   { border-radius: 24px; }
.metric               { border-radius: 22px; }

/* Hover stays a lift and a brightening — with no shadow to deepen,
   the response has to come from the material itself. */
@media (hover: hover) {
  .form-tile__link:hover,
  .product-card:hover,
  .contact-card.glass--card:hover,
  .process-step:hover,
  .principle:hover,
  .cert:hover {
    border-color: rgba(255, 255, 255, 0.46);
    background-image:
      radial-gradient(120% 62% at 18% -12%, rgba(255, 255, 255, 0.24), transparent 58%),
      linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0.125));
  }
  .contact-card.glass--card:hover {
    background-image:
      radial-gradient(120% 62% at 18% -12%, rgba(255, 255, 255, 0.26), transparent 58%),
      linear-gradient(180deg, rgba(160, 208, 255, 0.26), rgba(140, 192, 250, 0.22));
  }
}

/* ==================================================================
   CONTRAST ON THE LIGHTER TILES — measured, not assumed.

   With the fills lightened, the composited background inside a tile
   was sampled from real screenshots (pixel-decoded, not estimated).
   Two body styles were failing badly against it:

     .product-card__detail   2.43:1
     .cert__status           2.47:1

   Both used --pearl-quiet (#8a8780), a grey chosen for captions on a
   near-black page, not for body copy on a luminous pane. Promoted to
   --pearl-muted, which measures ~4.8:1 in the same sample. The floor
   is 4.5:1 (ui-ux-pro-max colour-contrast; WCAG 1.4.3).

   The swept facet also had to come down. At 0.4 white it crossed
   under the text during hover and pushed the local background to
   roughly 1.9:1 — a contrast failure that only existed while the
   pointer was on the card, which is exactly the kind that never shows
   up in a static audit. Narrowed and halved; it still reads as a
   streak of light across glass.
   ================================================================== */
.product-card__detail,
.cert__status { color: var(--pearl-muted); }

.glass-facet {
  background:
    linear-gradient(104deg,
      transparent 38%,
      rgba(255, 255, 255, 0.08) 45%,
      rgba(255, 255, 255, 0.2) 47.5%,
      rgba(255, 255, 255, 0.06) 50%,
      transparent 56%),
    linear-gradient(104deg,
      transparent 63%,
      rgba(214, 236, 255, 0.08) 67%,
      transparent 71%);
}

/* Vibrancy (apple-design §12): over a surface whose luminance changes
   — water moving behind it, a specular sweep crossing it — flat body
   copy needs its glyph edges held. Measured: the text region averages
   5.3:1, but the brightest pixels under the sweep touch ~3.1, and a
   contrast failure that exists only while the pointer is on the card
   is one no static audit would ever catch. A hairline dark shadow
   costs nothing at these sizes and holds the letterforms through it. */
.form-tile__line,
.form-tile__items li,
.process-step__body,
.product-card__detail,
.contact-card__body,
.cert__status,
.form-chapter__note-body {
  text-shadow: 0 1px 2px rgba(6, 16, 32, 0.55);
}
.form-tile__name,
.process-step__title,
.product-card__name,
.contact-card__heading,
.cert__code {
  text-shadow: 0 1px 3px rgba(6, 16, 32, 0.45);
}

/* ==================================================================
   SQUARE TILES. aspect-ratio does the work; the old min-height would
   fight it, so it is released to `auto`. Content is bottom-anchored
   already, so a square gives the hover list more room, not less.

   The one place a literal square would be wrong is the single-column
   phone layout, where a full-width square is a 360px-tall block and
   four of them are a very long scroll. There the tile keeps its
   proportion but caps its height.
   ================================================================== */
.form-tile__link {
  aspect-ratio: 1 / 1;
  min-height: 0;
}
/* Square at every width, including the single-column phone layout —
   asked for twice, so the long scroll is the accepted trade. */

/* ==================================================================
   THE CLOCK — a commissioned instrument.

   A dial is not a card, and the glass rules that govern the tiles do
   not transfer to it. A tile is a sheet you look THROUGH; an
   instrument is an object you look AT. So this is built the way a
   real one is: a machined case, a dial recessed inside it, and a
   curved crystal over the top.

   FOUR PARTS, outside in:

   1. THE CASE — a knurled ring. The conic gradient is a fine repeat
      of light and shadow, which is what knurling is: a milled edge
      that catches light at every facet. Two shadow pairs give it
      thickness, and the ring is a hair wider than the dial so the
      dial reads as SET INTO it, not printed on it.

   2. THE WELL — the dial sits recessed. That is the inner shadow
      under the top edge: light falls into the case and is stopped by
      the wall. Without it the dial floats and the case reads as a
      sticker.

   3. THE CRYSTAL — two reflections on a curved disc. A wide, soft arc
      across the upper-left is the sky in the dome; a tight, hard one
      is the light source itself. Both use screen blending, which can
      only ever lighten, so they lie over the dial WITHOUT hiding it.
      This is the point: the reflection must not cost you the time.

   4. LEGIBILITY FIRST. The batons and hands are brightened and given
      their own shadow so they cut through both reflections. An
      instrument that is beautiful and unreadable has failed at the
      only thing it does.

   Restraint is the brief. No chrome gradients, no lens flare, no
   colour that is not already in the palette — the object should look
   commissioned, not decorated.
   ================================================================== */
.clock-case {
  position: relative;
  display: grid;
  place-items: center;
  width: min(clamp(216px, 28vw, 300px), 100%);
  aspect-ratio: 1;
  border-radius: 50%;
  padding: clamp(8px, 1.2vw, 13px);
  isolation: isolate;
  /* The knurled ring — REPEATING, so the facets run the whole way
     round. A plain conic-gradient holds its final stop for the
     remaining 350 degrees, which is a smooth bright ring, not a
     milled one. 6deg per facet gives 60 of them: fine enough to read
     as machining rather than as a pattern. */
  background:
    repeating-conic-gradient(from 210deg,
      rgba(255, 255, 255, 0.26) 0deg,
      rgba(255, 255, 255, 0.03) 2.4deg,
      rgba(255, 255, 255, 0.22) 4deg,
      rgba(255, 255, 255, 0.04) 6deg),
    /* the light falling across the case, so one side is the lit side */
    linear-gradient(146deg,
      rgba(255, 255, 255, 0.16) 0%,
      rgba(255, 255, 255, 0.02) 46%,
      rgba(255, 226, 186, 0.12) 100%);
  -webkit-backdrop-filter: blur(18px) saturate(160%);
  backdrop-filter: blur(18px) saturate(160%);
}
/* The case's own lit and shaded edges — a ring has an outside and an
   inside, and they catch light from opposite directions. */
.clock-case::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  padding: 1.5px;
  background: linear-gradient(150deg,
    rgba(255, 255, 255, 0.9) 0%,
    rgba(255, 255, 255, 0.28) 26%,
    rgba(255, 255, 255, 0.1) 52%,
    rgba(255, 226, 186, 0.4) 84%,
    rgba(255, 244, 224, 0.72) 100%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
  pointer-events: none;
  z-index: 3;
}

/* 2 — the dial, recessed into the case. */
.clock {
  position: relative;
  width: 100%;
  height: 100%;
  isolation: isolate;
  border: none;
  background:
    radial-gradient(120% 120% at 34% 24%, rgba(46, 62, 86, 0.5), transparent 62%),
    linear-gradient(180deg, rgba(12, 20, 34, 0.62), rgba(6, 12, 24, 0.72));
  -webkit-backdrop-filter: blur(22px) saturate(150%);
  backdrop-filter: blur(22px) saturate(150%);
  box-shadow:
    /* the case wall stopping the light — this is what recesses it */
    inset 0 6px 14px -6px rgba(0, 0, 0, 0.85),
    inset 0 -3px 10px -6px rgba(255, 255, 255, 0.18),
    inset 0 0 0 1px rgba(255, 255, 255, 0.12);
}

/* 3 — THE CRYSTAL. Four layers, each one thing a real dome does.
   All screen-blended: they can only lighten, so the dial is never
   hidden by its own reflection. Positions come from clockCrystal.js,
   which solves the mirror geometry; the values here are the resting
   state used before it runs and under reduced motion. */
.clock__crystal,
.clock__fresnel,
.clock__ghost,
.clock__glint {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  pointer-events: none;
  mix-blend-mode: screen;
  z-index: 2;
}

/* (a) THE ENVIRONMENT. A dome reflects the room: bright sky above the
   horizon, dark ground below it. The boundary sits square to the
   light, so it tips as the dial travels — the single strongest cue
   that you are looking at something curved and not a flat disc. */
.clock__crystal {
  background:
    linear-gradient(var(--horizon, 160deg),
      rgba(214, 234, 255, 0.26) 0%,
      rgba(190, 216, 245, 0.12) 38%,
      rgba(150, 180, 215, 0.03) 52%,
      rgba(10, 20, 36, 0.1) 62%,
      rgba(6, 14, 26, 0.16) 100%),
    /* the soft pool of sky, opposite the light */
    radial-gradient(64% 48% at var(--sx, 34%) var(--sy, 22%),
      rgba(226, 240, 255, 0.2) 0%,
      rgba(196, 222, 250, 0.07) 46%,
      transparent 74%);
}

/* (b) FRESNEL. Reflectance climbs steeply toward grazing incidence,
   so a dome is dim in the middle and bright around its edge. This is
   the layer that stops the crystal reading as a flat overlay — and it
   brightens as a whole when the light goes off-axis, because that is
   when more of the dome is at a grazing angle to it. */
.clock__fresnel {
  background:
    radial-gradient(closest-side circle at 50% 50%,
      transparent 62%,
      rgba(206, 230, 255, calc(0.1 + var(--fres, 0.4) * 0.16)) 86%,
      rgba(230, 242, 255, calc(0.16 + var(--fres, 0.4) * 0.3)) 97%,
      transparent 100%);
}

/* (c) THE SECOND IMAGE. Glass has thickness, so the inner surface
   returns a fainter, offset copy of the same highlight. Small, dim,
   and on the opposite side — but its absence is what makes a CG dome
   look like plastic. */
.clock__ghost {
  background:
    radial-gradient(9% 5% at var(--bx, 62%) var(--by, 70%),
      rgba(198, 226, 255, 0.22) 0%,
      rgba(198, 226, 255, 0.07) 52%,
      transparent 80%);
  filter: blur(1.4px);
}

/* (d) THE SPECULAR. The light source itself. An ellipse, because near
   the rim you see it at an angle: it compresses along its direction
   of travel and stretches across it, and rotates to follow. */
.clock__glint {
  background:
    radial-gradient(var(--gw, 14%) var(--gh, 7%) at var(--gx, 33%) var(--gy, 18%),
      rgba(255, 255, 255, calc(0.5 + var(--fres, 0.4) * 0.38)) 0%,
      rgba(240, 248, 255, 0.24) 40%,
      rgba(220, 236, 255, 0.06) 68%,
      transparent 84%);
  transform: rotate(var(--gang, 0deg));
  transform-origin: var(--gx, 33%) var(--gy, 18%);
  filter: blur(0.5px);
}

/* 4 — the dial must out-read its own crystal. */
.clock__batons line { stroke-opacity: 0.9; }
.clock__batons--cardinal line { stroke-opacity: 1; stroke-width: 3; }
.clock__ticks line { stroke-opacity: 0.4; }
.clock__hand line {
  stroke: #ffffff;
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.75));
}
.clock__hand--second line,
.clock__hand--second circle { filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.6)); }
.clock__wordmark { fill-opacity: 0.72; }
.clock__zone { fill-opacity: 0.46; }

/* The dial is an object, not a control: no hover lift, no pointer
   highlight. It sits still and is read. */
@media (prefers-reduced-transparency: reduce), (prefers-contrast: more) {
  .clock-case { background: rgba(12, 20, 34, 0.98); backdrop-filter: none; -webkit-backdrop-filter: none; }
  .clock { background: rgba(8, 14, 26, 0.99); backdrop-filter: none; -webkit-backdrop-filter: none; }
  .clock__crystal, .clock__glint { display: none; }
}

/* ==================================================================
   BLEEDING CORNERS — backdrop-filter is not clipped by border-radius.

   The bevel ::before carries its own backdrop-filter, and Chrome
   paints that filtered region to the pseudo-element's BOX, not to its
   rounded shape. The result was a square corner of brightened
   backdrop sticking out past every tile's radius — visible on the
   process steps, and on every other surface that got the same bevel.

   `overflow: hidden` on the parent does not save it: the ancestor
   clip is applied rectangularly to the filtered layer. `clip-path`
   does, because it clips the element's whole rendering including the
   backdrop. Each surface therefore publishes its own radius as
   --cr-radius, and the bevel clips to exactly that.

   Verified by bisection in the browser: disabling the pseudo's
   backdrop-filter removed the leak; restoring it with this clip
   removed the leak while keeping the refraction.
   ================================================================== */
.form-tile__link        { --cr-radius: 30px; }
.contact-card.glass--card { --cr-radius: 28px; }
.product-card,
.process-step           { --cr-radius: 26px; }
.principle,
.cert,
.form-chapter__note     { --cr-radius: 24px; }
.btn--primary,
.social-btn,
.chapter-nav__list      { --cr-radius: 999px; }

.form-tile__link::before,
.product-card::before,
.contact-card.glass--card::before,
.process-step::before,
.principle::before,
.cert::before,
.form-chapter__note::before,
.btn--primary::before,
.social-btn::before {
  clip-path: inset(0 round var(--cr-radius, 20px));
}

/* The facet is a plain element with no backdrop-filter, so the
   parent's overflow does clip it — but pinning its shape as well
   costs nothing and removes any doubt at a corner. */
.glass-facet { clip-path: inset(0 round var(--cr-radius, 20px)); }

/* ==================================================================
   THE PRODUCT PORTFOLIO LINK — aligned to the tiles it belongs to.

   It sat tight under the grid and only as wide as its own text, so it
   read as a loose fragment rather than the row's closing line. Given
   more air above, aligned to the tile column's left edge, and the
   underline runs the full width of the label so it reads as a
   deliberate rule rather than a wrapped bit of text.
   ================================================================== */
.range__more {
  margin-top: clamp(2.75rem, 5.5vw, 4.25rem);
  padding-left: 0;
  align-self: start;
}
.range__more > span:not(.arrow) { padding-bottom: 6px; }

/* ==================================================================
   CLOCK — clearer glass.

   Two things were making the crystal read as frosted rather than
   optical: the dial's own backdrop blur was high enough to fog what
   was behind it, and the reflections were soft everywhere. Real
   sapphire is the opposite — it transmits almost perfectly and its
   reflections are SHARP. So the blur comes down, the fills come down
   with it, and every reflection edge is tightened.

   The dial itself stays dark. That is not frost, it is the dial: a
   watch face needs a dark ground for the hands to read against, and
   clarity in the crystal must never be bought with legibility in the
   thing the crystal is protecting.
   ================================================================== */
.clock {
  /* Transmits rather than frosts. */
  -webkit-backdrop-filter: blur(9px) saturate(155%) brightness(1.02);
  backdrop-filter: blur(9px) saturate(155%) brightness(1.02);
  /* Nothing on the crystal may escape the dial — the rotated glint
     was pushing its box outside the circle and taking the document
     13px wider at 320px. A disc clips to a disc. */
  overflow: hidden;
}

/* The sky wash: tighter and cleaner, so it reads as a reflection with
   an edge rather than a haze over the whole face. */
.clock__crystal {
  background:
    linear-gradient(var(--horizon, 160deg),
      rgba(220, 238, 255, 0.22) 0%,
      rgba(196, 222, 250, 0.09) 34%,
      rgba(150, 180, 215, 0.01) 48%,
      rgba(8, 18, 34, 0.09) 60%,
      rgba(5, 12, 24, 0.15) 100%),
    radial-gradient(52% 38% at var(--sx, 34%) var(--sy, 22%),
      rgba(232, 244, 255, 0.17) 0%,
      rgba(200, 226, 252, 0.045) 44%,
      transparent 70%);
}

/* A crisper rim: the Fresnel band narrows and gains a hard inner
   edge, which is what an optical surface looks like at grazing. */
.clock__fresnel {
  background:
    radial-gradient(closest-side circle at 50% 50%,
      transparent 74%,
      rgba(212, 234, 255, calc(0.06 + var(--fres, 0.4) * 0.14)) 88%,
      rgba(238, 247, 255, calc(0.2 + var(--fres, 0.4) * 0.34)) 96.5%,
      rgba(255, 255, 255, calc(0.1 + var(--fres, 0.4) * 0.2)) 99%,
      transparent 100%);
}

/* The specular gets its hard core back — a real one has a bright
   centre that falls off fast, not a soft bloom. */
.clock__glint {
  background:
    radial-gradient(var(--gw, 14%) var(--gh, 7%) at var(--gx, 33%) var(--gy, 18%),
      rgba(255, 255, 255, calc(0.62 + var(--fres, 0.4) * 0.36)) 0%,
      rgba(255, 255, 255, calc(0.34 + var(--fres, 0.4) * 0.2)) 26%,
      rgba(226, 240, 255, 0.12) 56%,
      transparent 78%);
  filter: blur(0.25px);
}
.clock__ghost { filter: blur(1px); }

/* The maker's mark on the dial. Printed, not applied: held back from
   pure white so it sits in the dial's own light rather than glowing
   off it, and given the same faint engraving shadow as the other
   dial furniture so it reads as part of the face. */
.clock__mark {
  /* Definition, not decoration. The old treatment cost it on three
     fronts at once: 82% opacity flattened the contrast, a blurred
     drop-shadow softened every edge, and the uncropped lockup meant
     the wordmark was rendering at roughly a quarter of the pixels it
     had available. Full opacity, a hard shadow with no blur — it
     still reads as printed into the dial — and a touch of contrast to
     hold the strokes through a 12x downscale. */
  opacity: 1;
  filter: drop-shadow(0 0.5px 0 rgba(0, 0, 0, 0.6)) contrast(1.12) brightness(1.04);
  -webkit-backface-visibility: hidden;
}

/* ==================================================================
   THE REEL — a run that never stops and never jumps.

   Three separate things could break the loop, and all three are shut
   off here rather than left to chance:

   1. A frame that fails to load collapses to zero width, which shifts
      the seam. min-width holds the box whatever happens to the file.

   2. The animation must never be paused by anything except the
      client's own hover intent — and not even that, since a reel that
      stops mid-run is exactly what "the loop breaks" describes.

   3. prefers-reduced-motion must stop it honestly (a still strip),
      not slow it to a crawl where the seam becomes visible.
   ================================================================== */
.reel__item {
  min-width: clamp(230px, 26vw, 360px);
  background: rgba(10, 20, 36, 0.5);
}
/* The run is continuous. Hover no longer holds it: pausing a marquee
   leaves it stopped at an arbitrary offset, and on a wide screen with
   the pointer resting anywhere near it, the reel simply sat still. */
@media (hover: hover) {
  .reel:hover .reel__track { animation-play-state: running; }
}
@media (prefers-reduced-motion: reduce) {
  .reel__track { animation: none; transform: none; }
}

/* ==================================================================
   HEADER STANDS DOWN AT THE FOOTER.

   js/headerRetire.js adds .is-retired once the footer's top edge rises
   above the header's bottom edge, which is where the footer's ALGAMAR
   mark starts sharing the screen with the header's. See that file for
   the measurements.

   !important is load-bearing here, not laziness. The homepage hero
   choreography animates the header with GSAP and leaves inline
   `opacity` and `transform` on the element -- renderFinalState() sets
   header.style.opacity = '1' outright. An inline style beats a class
   selector at any specificity, so without !important this rule would
   do nothing on the one page the effect is most visible.

   visibility, not just opacity: a header at zero opacity is still in
   the tab order, so a keyboard user at the foot of the page would tab
   into controls they cannot see. It flips after the fade so the
   transition still reads, and back instantly on the way in.
   ================================================================== */
.site-header {
  transition:
    background-color var(--dur-short) var(--ease-out),
    backdrop-filter  var(--dur-short) var(--ease-out),
    opacity          var(--dur-short) var(--ease-out),
    visibility       0s linear 0s;
}
.site-header.is-retired {
  opacity: 0 !important;
  visibility: hidden !important;
  pointer-events: none !important;
  transition:
    opacity    var(--dur-short) var(--ease-out),
    visibility 0s linear var(--dur-short);
}

/* ==================================================================
   CONTACT DOCK — one pill that unfurls into the footer's rail.

   THE IDEA. At rest the dock is a single glass pill reading "Contact
   us". Pressed, it becomes the same six-channel rail the footer
   carries: the pill's own glass grows leftward and the channels
   emerge from under the toggle, nearest first, each sliding out along
   the direction the rail is growing. Pressed again, they retreat back
   into the toggle in the reverse order and the pill closes over them.
   Enter and exit are the same path, reversed — the one rule that makes
   a disclosure feel spatial rather than scripted.

   ONE MATERIAL. The rail shares its glass with .social-rail (the
   selector list on that rule now includes .dock__rail), so the dock and
   the footer are literally the same surface. The buttons inside are
   flat and carry brand colour in the icon only, exactly as in the
   footer, so the site has ONE way of presenting these six channels.

   NO CROSSFADE, NO SCALE-FROM-CENTRE. Two easy choices were rejected:
   fading the rail in over the pill (two materials briefly stacked =
   the glass-on-glass legibility trap), and scaling a finished rail out
   from the corner (reads as a popover, not as the pill itself
   changing). Instead the pill's width is what animates. It is the same
   element throughout.

   HOW THE WIDTH ANIMATES. .dock__channels is the clip window
   (overflow:hidden) and its width transitions 0 -> var(--dock-ch-w),
   which js/dock.js measures from the inner strip's max-content just
   before every open. The inner strip is width:max-content and sits at
   the window's right edge (margin-left:auto), so as the window grows it
   reveals from the toggle outward -- the same direction the per-channel
   stagger runs. Both cues agree.

   A first version animated grid-template-columns 0fr -> 1fr instead, to
   avoid measuring. Reviewed and rejected, with numbers: inside a
   shrink-to-fit flex item the grid box grows as f (its intrinsic
   contribution) while the track inside it is f x box, so the clip
   window lagged at f^2 -- 65px of window inside a 136px box 68ms into
   the open, and a widening band of empty glass between the icons and
   the toggle. The icons detached from the toggle instead of unfurling
   from it. Measuring one number is the honest fix; the CSS below has
   no fr anywhere.

   TIMING. Nothing overshoots. A click carried no momentum, and the
   Apple guidance is exact on this: overshoot belongs on a flick, not
   on a control that just opened. Open uses a long ease-out so the
   first 60% of the travel happens fast and the last 40% settles;
   close is shorter and eases in, so it reads as the rail being drawn
   back rather than falling shut.
   ================================================================== */

/* The old .dock was a flex row of three circles. It is now the fixed
   anchor for one rail plus a hover hint that must escape the rail's
   clipping, so it stops being a flex container. */
.dock {
  display: block;
  /* The resting state carries the same clock as .dock.is-retired, so
     scrolling back up from the footer is the retire played in reverse.
     Without it the un-retire was an instant pop on every page but the
     home page: CSS uses the AFTER-change state's transition list, and
     the resting .dock had none. */
  transition:
    opacity 320ms var(--ease-out-expo),
    transform 320ms var(--ease-out-expo),
    visibility 0s linear 0s;
  /* One clock for everything that moves the rail's width. */
  --dock-open-t:  520ms;
  --dock-open-e:  var(--ease-out-expo);
  --dock-close-t: 400ms;
  --dock-close-e: var(--ease-in-out-cubic);
}

/* Rail: material from .social-rail; layout here. Toggle first in DOM
   for keyboard order, on the right visually via row-reverse. */
.dock__rail {
  display: flex;
  flex-direction: row-reverse;
  flex-wrap: nowrap;
  align-items: center;
  gap: 0;
  padding: 0.4rem;
  transition: transform 220ms var(--ease-out);
}
/* Feedback lives on the press. */
.dock__rail:active { transform: scale(0.985); transition-duration: 60ms; }
/* Content sits above the rail's pointer light (same lesson as the cards:
   lift the content, never the effect layer itself). */
.dock__rail > *:not(.glass-light) { position: relative; z-index: 4; }

/* ---- the toggle: chat glyph + label, becomes a bare × when open ---- */
.dock__toggle {
  appearance: none;
  border: 0;
  margin: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  min-width: 44px;
  padding: 0 1rem 0 0.85rem;
  border-radius: 999px;
  background: transparent;
  color: var(--pearl);
  font-family: var(--font-sans);
  font-size: 0.9rem;
  font-weight: 600;
  letter-spacing: 0.01em;
  white-space: nowrap;
  cursor: pointer;
  /* Every piece that changes the rail's WIDTH -- this padding, the label's
     track, the channels' track, the gap -- runs on the same duration and
     curve, deliberately. The rail's width is their sum, and interpolating
     each piece by the same fraction f(t) makes the sum monotonic. With
     the toggle collapsing on a shorter curve than the channels grew, the
     rail overshot by ~8px mid-flight and pulled back: an accidental
     bounce on a control that had no momentum to bounce with. */
  transition:
    padding var(--dock-open-t) var(--dock-open-e),
    background-color 200ms var(--ease-out);
}
.dock:not(.is-open) .dock__toggle { transition-duration: var(--dock-close-t), 200ms; transition-timing-function: var(--dock-close-e), var(--ease-out); }
@media (hover: hover) and (pointer: fine) {
  .dock__toggle:hover { background-color: rgba(255, 255, 255, 0.08); }
}
.dock__toggle:focus-visible {
  outline: 2px solid var(--pearl);
  outline-offset: -2px;
}
.dock.is-open .dock__toggle { padding: 0; }

.dock__toggle-glyph {
  position: relative;
  width: 22px;
  height: 22px;
  flex: none;
}
.dock__glyph {
  position: absolute;
  inset: 0;
  width: 22px;
  height: 22px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.9;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition:
    opacity 260ms var(--ease-out),
    transform 380ms var(--ease-out-expo);
}
.dock__glyph--close { opacity: 0; transform: rotate(-90deg) scale(0.6); }
.dock.is-open .dock__glyph--chat  { opacity: 0; transform: rotate(90deg) scale(0.6); }
.dock.is-open .dock__glyph--close { opacity: 1; transform: rotate(0) scale(1); }

/* The label collapses with the same 0fr trick, so its width animates
   to exactly zero rather than to a guessed max-width. */
.dock__toggle-label {
  display: inline-grid;
  grid-template-columns: 1fr;
  margin-left: 0.55rem;
  opacity: 1;
  /* On close the label's opacity waits 220ms before fading in, so it
     arrives on a track that has mostly opened. Fading at once revealed
     "Contact us" through a hard clip edge at full opacity -- a 200ms
     typewriter wipe -- and was asymmetric with the open, where the text
     fades out first and the track closes behind it. */
  transition:
    grid-template-columns var(--dock-close-t) var(--dock-close-e),
    margin-left var(--dock-close-t) var(--dock-close-e),
    opacity 180ms var(--ease-out) 220ms;
}
.dock__toggle-text {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
}
.dock.is-open .dock__toggle-label {
  grid-template-columns: 0fr;
  margin-left: 0;
  opacity: 0;
  transition:
    grid-template-columns var(--dock-open-t) var(--dock-open-e),
    margin-left var(--dock-open-t) var(--dock-open-e),
    opacity 120ms var(--ease-out);
}

/* ---- the channels: a clip window whose width is measured ---- */
.dock__channels {
  width: 0;
  overflow: hidden;
  flex: none;
  /* The window is itself a flex container packing its one child to the
     END. That is what pins the strip to the toggle side while the
     window is narrower than the strip: with negative free space,
     flex-end overflows at the START, so the hidden part is always the
     far end and the reveal always begins at the toggle. (margin-left:
     auto cannot do this -- auto margins resolve to zero once the child
     overflows, which left-aligned the strip and revealed WhatsApp
     first, 120px from the toggle.) */
  display: flex;
  justify-content: flex-end;
  align-items: center;
  margin-right: 0;
  /* Hidden as a WHOLE while collapsed, not only its children: otherwise
     assistive tech still meets an empty group named "Contact channels".
     The flip to hidden waits for the close animation; to visible it is
     immediate. */
  visibility: hidden;
  transition:
    width var(--dock-open-t) var(--dock-open-e),
    margin-right var(--dock-open-t) var(--dock-open-e),
    visibility 0s linear var(--dock-close-t);
}
.dock__channels-inner {
  flex: none;
  width: max-content;                          /* always its full width */
  display: flex;
  align-items: center;
  gap: 0.25rem;
}
.dock.is-open .dock__channels {
  width: var(--dock-ch-w, 284px);
  margin-right: 0.25rem;
  visibility: visible;
  transition:
    width var(--dock-open-t) var(--dock-open-e),
    margin-right var(--dock-open-t) var(--dock-open-e),
    visibility 0s linear 0s;
}
.dock:not(.is-open) .dock__channels {
  transition:
    width var(--dock-close-t) var(--dock-close-e),
    margin-right var(--dock-close-t) var(--dock-close-e),
    visibility 0s linear var(--dock-close-t);
}

/* Each channel is a footer .social-btn with its label removed. It
   starts under the toggle (shifted toward it, small, invisible) and
   arrives at rest; --i is its position from the left, set inline, and
   the delays are computed from it in both directions. */
.dock__ch {
  padding: 0;
  opacity: 0;
  visibility: hidden;                          /* out of the tab order too */
  transform: translateX(18px) scale(0.72);
  transition:
    transform 260ms var(--ease-in-out-cubic),
    opacity 180ms var(--ease-out),
    visibility 0s linear 340ms,
    background-color 200ms var(--ease-out);
  /* CLOSE order: farthest from the toggle goes first. The visibility flip
     waits for the LAST icon's fade to finish: index 5 starts at 140ms and
     fades for 180ms, so anything under 320ms would snap it to hidden at
     a third of its opacity. 340ms leaves a margin. */
  transition-delay: calc(var(--i) * 28ms), calc(var(--i) * 28ms), 340ms, 0s;
}
.dock.is-open .dock__ch {
  opacity: 1;
  visibility: visible;
  transform: none;
  transition:
    transform 460ms var(--ease-out-expo),
    opacity 300ms var(--ease-out),
    visibility 0s linear 0s,
    background-color 200ms var(--ease-out);
  /* OPEN order: nearest the toggle goes first. Six channels, so the
     nearest (index 5) gets 0ms and the farthest (index 0) gets 200ms. */
  transition-delay: calc((5 - var(--i)) * 40ms), calc((5 - var(--i)) * 40ms), 0s, 0s;
}
.dock__ch:focus-visible {
  outline: 2px solid var(--pearl);
  outline-offset: -2px;                        /* inside the clip window */
}
/* .dock.is-open .dock__ch { transform: none } out-specifies the footer's
   .social-btn:active press, so restate it here at the higher weight. */
.dock.is-open .dock__ch:active { transform: scale(0.96); transition: transform 60ms linear; }

/* Email has no brand colour; a soft pearl-blue keeps it in the family. */
.social-btn--email { --sb-hue: 148, 190, 240; }

/* ---- hover hint: the channel's name, risen above the icon ---- */
.dock__hint {
  position: absolute;
  right: var(--hr, 50%);
  bottom: calc(100% + 10px);
  transform: translate(50%, 6px);
  padding: 0.35rem 0.6rem;
  border-radius: 999px;
  background: rgba(10, 18, 32, 0.78);
  -webkit-backdrop-filter: blur(14px) saturate(160%);
  backdrop-filter: blur(14px) saturate(160%);
  box-shadow: 0 8px 22px -10px rgba(0, 0, 0, 0.7), inset 0 0 0 1px rgba(255, 255, 255, 0.10);
  color: var(--pearl);
  font-family: var(--font-sans);
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;                        /* inert while hidden */
  transition:
    opacity 160ms var(--ease-out),
    transform 220ms var(--ease-out-expo),
    right 220ms var(--ease-out-expo);
}
/* Hoverable while shown (WCAG 1.4.13): the pointer can travel up onto
   the caption to read it without dismissing it. Two things make that
   true rather than merely intended: the caption's hit area extends
   down over the 10px gap beneath it (::before), so on the way up the
   pointer is never over bare page; and js/dock.js only dismisses when
   the pointer reaches the toggle or leaves the dock, and even then on a
   short grace timer that any re-entry cancels. */
.dock__hint.is-on { opacity: 1; transform: translate(50%, 0); pointer-events: auto; }
.dock__hint.is-on::before {
  content: "";
  position: absolute;
  left: -8px;
  right: -8px;
  top: 100%;
  height: 16px;                                /* the gap plus a margin */
}

/* ---- narrow phones: the rail unfurls UPWARD instead.

   Six channels plus the close button are seven 44px targets: 308px
   before any padding, and a 320px screen has 288px between its insets.
   No amount of tightening fits that in one row without going under the
   44px floor. So at and below 360px the same mechanism turns 90
   degrees: the rail becomes a column, the channels' grid animates its
   ROW from 0fr to 1fr, and each icon rises out of the toggle beneath it.
   This is also the native phone idiom for a speed dial, so it reads as
   expected rather than as a fallback. The pill's width contracts to a
   circle at the same time as the column grows -- one continuous morph.

   The hover hint is dropped here: these are touch screens, and the
   caption's geometry (above the icon) has no natural place beside a
   column hugging the right edge. ---- */
@media (max-width: 360px) {
  .dock__rail {
    flex-direction: column-reverse;   /* toggle first in DOM -> bottom */
    align-items: center;
  }
  .dock__channels {
    width: auto;                      /* the width no longer animates */
    height: 0;                        /* the height does */
    margin-right: 0;
    margin-bottom: 0;
    transition:
      height var(--dock-open-t) var(--dock-open-e),
      margin-bottom var(--dock-open-t) var(--dock-open-e),
      visibility 0s linear var(--dock-close-t);
  }
  .dock.is-open .dock__channels {
    width: auto;
    height: var(--dock-ch-h, 284px);
    margin-right: 0;
    margin-bottom: 0.25rem;
    transition:
      height var(--dock-open-t) var(--dock-open-e),
      margin-bottom var(--dock-open-t) var(--dock-open-e),
      visibility 0s linear 0s;
  }
  .dock:not(.is-open) .dock__channels {
    transition:
      height var(--dock-close-t) var(--dock-close-e),
      margin-bottom var(--dock-close-t) var(--dock-close-e),
      visibility 0s linear var(--dock-close-t);
  }
  .dock__channels {
    flex-direction: column;           /* pack the strip to the bottom: */
    justify-content: flex-end;        /* it overflows upward, reveal begins at the toggle */
    align-items: center;
  }
  .dock__channels-inner {
    width: auto;
    height: max-content;
    flex-direction: column;
  }
  .dock__ch { transform: translateY(18px) scale(0.72); }
  .dock__hint { display: none; }
}
/* Narrow AND short (a small phone held sideways, a split-screen pane):
   a six-high column is 345px and would run up into the header below
   437px of height. Two columns of three is 140px, fits down to ~300px,
   and still rises out of the toggle.

   Applied in BOTH states, deliberately. Scoped to .is-open it snapped
   the strip back to the 6-high column on the first frame of the close,
   which threw the rail 140px upward into the header before the height
   animated back down. The strip is clipped while closed, so applying
   the grid unconditionally costs nothing and the height animates
   0 <-> 140px in both directions. */
@media (max-width: 360px) and (max-height: 440px) {
  .dock__channels-inner {
    display: grid;
    grid-template-columns: repeat(2, 44px);
    justify-content: end;
    align-content: end;
    gap: 0.25rem;
  }
}

/* ---- preferences ---- */
@media (prefers-reduced-motion: reduce) {
  /* A crossfade and nothing else. Sizes snap: at once on open (then the
     icons fade in), and 150ms into the close (after the icons have
     faded out) -- snapping the width at 0ms clipped the fade before it
     could be seen. Every transform is pinned at its resting value so no
     glyph rotates, no icon slides, no caption drops, and the retire
     does not translate. */
  .dock__rail, .dock__toggle, .dock__toggle-label { transition: none !important; }
  .dock__channels {
    transition: width 0s linear 150ms, height 0s linear 150ms,
                margin-right 0s linear 150ms, margin-bottom 0s linear 150ms,
                visibility 0s linear 150ms !important;
  }
  .dock.is-open .dock__channels {
    transition: width 0s, height 0s, margin-right 0s, margin-bottom 0s, visibility 0s !important;
  }
  .dock__ch {
    transform: none !important;
    transition: opacity 150ms linear, visibility 0s linear 150ms !important;
    transition-delay: 0s, 150ms !important;
  }
  .dock.is-open .dock__ch { transition: opacity 150ms linear, visibility 0s !important; transition-delay: 0s !important; }
  .dock.is-open .dock__ch:active { transform: none !important; }
  .dock__glyph { transform: none !important; transition: opacity 150ms linear !important; }
  .dock__hint  { transform: translate(50%, 0) !important; transition: opacity 150ms linear !important; }
  .dock__rail:active { transform: none; }
  .dock, .dock.is-retired { transform: none !important; }
}

/* Home page: the hero-gating rule (.page-home.hero-complete .dock)
   rewrites the transition shorthand without a visibility delay, so on
   that page alone the retire over the footer went visibility:hidden on
   the first frame -- a pop, and now one that cut a 400ms rail-close
   mid-flight. Give the retired state its own list, matching the plain
   .dock.is-retired one. */
.page-home.hero-complete .dock.is-retired {
  transition:
    opacity 320ms var(--ease-out-expo),
    transform 320ms var(--ease-out-expo),
    visibility 0s linear 320ms;
}

/* A retired dock that still holds keyboard focus stays on screen until
   focus leaves it. Without this, a keyboard user who focused a channel
   and then paged down to the footer had the dock vanish from under
   their focus, which was dropped to <body>. Beats both the plain and
   the homepage-gated retire rules.

   :has(:focus-visible) rather than :focus-within, deliberately: in
   Chrome a mouse click leaves the button focused, so :focus-within
   would hold the dock on screen over the footer -- whose identical rail
   the retire exists to avoid duplicating -- for every mouse user who
   had ever pressed it. :focus-visible fires for keyboard focus only,
   which is the one case that needs the protection. */
.dock.is-retired:has(:focus-visible),
.page-home.hero-complete .dock.is-retired:has(:focus-visible) {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transform: none;
}

/* ==================================================================
   PRODUCT CARD — NAME FIRST, THEN WHAT IT IS, THEN HOW IT COMES.

   A card that read "Short arm and long arm. Whole, cleaned, and cut."
   ran two different kinds of fact together: the first list is the
   SPECIES, the second is the MEDIUMS it is cut and packed in. They are
   on separate lines now — but separating them is only half the job, and
   the first attempt got the other half backwards.

   THE MISTAKE, RECORDED. The mediums line was set at `font-size: 0.86em`.
   `em` resolves against the PARENT, and this line's parent is
   .product-card__body, not the detail paragraph beside it — so 0.86em
   computed to 16.34px against the detail's 14.40px. The quietest fact on
   the card rendered as the loudest thing under the name. Sizes here are
   now clamped in rem, on the same curve as the two lines above, so the
   ladder is explicit and cannot invert:

     name    clamp(1rem,   1.25vw, 1.12rem)   600  --pearl
     species clamp(0.9rem, 1.05vw, 0.98rem)   400  --pearl-muted
     mediums clamp(0.8rem, 0.92vw, 0.86rem)   500  --pearl-muted

   The mediums line is a step SMALLER but slightly heavier and a little
   wider-tracked — the treatment small specification type wants, and the
   reason it stays legible at 0.8rem rather than fading out. It keeps
   --pearl-muted rather than the quieter token: --pearl-quiet measured
   2.43:1 on these very cards once before and had to be promoted, and
   smaller type needs more contrast, not less.

   HOVER. The product NAME is what a buyer scans for, so it is the thing
   that lights up: it brightens to full white and takes a soft blue
   bloom, on the card's hover rather than the name's own, so the whole
   tile is the target. Pointer devices only — there is no hover on a
   phone, and a sticky :hover state there would leave one card lit at
   random. Suppressed under reduced motion, where the colour still
   changes but without the transition.
   ================================================================== */
.product-card__forms {
  margin: 0.4rem 0 0;
  font-family: var(--font-sans);
  font-size: clamp(0.8rem, 0.92vw, 0.86rem);
  font-weight: 500;
  line-height: 1.5;
  letter-spacing: 0.014em;
  color: var(--pearl-muted);
  text-shadow: 0 1px 2px rgba(6, 16, 32, 0.55);
}
/* Cards that carry only mediums (cuttlefish) have no species line above,
   so the mediums line takes the species line's spacing instead. */
.product-card__name + .product-card__forms { margin-top: 0; }

.product-card__name {
  transition:
    color 260ms var(--ease-out),
    text-shadow 260ms var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .product-card:hover .product-card__name,
  .product-card:focus-within .product-card__name {
    color: #fff;
    text-shadow:
      0 1px 2px rgba(6, 16, 32, 0.55),
      0 0 18px rgba(168, 214, 255, 0.55),
      0 0 34px rgba(120, 180, 245, 0.28);
  }
}
@media (prefers-reduced-motion: reduce) {
  .product-card__name { transition: none; }
}
