/*
  Washa Group static profile site.
  Colour taken from the client's own company profile PDF.

  The document is a light one: a near-white ground (#FAFEFF), a dark slate
  (#283A44) doing the heavy lifting for text, and three accents: orange
  #E36F1B, cyan #00EFFF and purple #A066CB, used as fills, rings and
  diagonal shapes around a roundel logo.

  On a light ground none of the three accents carries small text at WCAG AA:
  orange is 3.2:1, purple 3.9:1, cyan only 1.4:1. So each has two values:
  the document's own hue for fills, rules and large display type, and a
  darkened sibling (--orange-t, --cyan-t, --purple-t) for anything set small.
  The one inverted band and the footer use the slate, where the accents flip
  the other way (--orange-l, --purple-l). Every pair was measured after
  compositing.

  Each colour keeps one job throughout:
    orange  the domestic channel: entity one, and the port-call stage keys
    cyan    the primary voice: eyebrows, emphasis, buttons, focus, entity two
    purple  the system voice: placeholder fields, hairlines, the scroll cue,
            the quote mark, the closing band

  No fonts are fetched and no request leaves this origin. The scroll reveals
  are driven by reveal.js, the one script on the page: it puts "js" on <html>
  and adds .is-in to each block as it enters the viewport. Every hidden state
  in this file is scoped to html.js, so with the script blocked or absent
  nothing is hidden and the page reads as if every reveal had finished.
*/

/* ---------- Tokens ---------- */
:root {
  color-scheme: light;

  --bg: #fafeff;               /* page ground, and text on the slate band */
  --bg2: #edf5f9;              /* alternating section */
  --panel: #ffffff;            /* raised slabs */
  --deep: #283a44;             /* the port call, the closing band, the footer */
  --fg: #283a44;               /* primary text, 11.6:1 on bg */
  --muted: #5b6b76;            /* secondary text, 5.4:1 on bg */

  --orange: #e36f1b;           /* fills, bars, display type */
  --orange-t: #a8500c;         /* orange as small text on light, 5.4:1 */
  --orange-l: #ff904d;         /* orange on the slate band, 5.3:1 */

  --cyan: #00efff;             /* fills and hairlines; 8.3:1 on the slate */
  --cyan-t: #067a86;           /* cyan as small text on light, 5.0:1 */

  --purple: #a066cb;           /* fills and hairlines */
  --purple-t: #7c3fae;         /* purple as small text on light, 6.5:1 */
  --purple-l: #c39aec;         /* purple on the slate band, 5.1:1 */

  /* the cut edge of a card, and the darker line where it meets its shadow */
  --edge: rgba(40, 58, 68, 0.13);
  --edge-deep: rgba(40, 58, 68, 0.24);

  --gold: #c4a94f;             /* the frame around the supplied group mark */

  --line: rgba(40, 58, 68, 0.16);
  --lineL: rgba(250, 254, 255, 0.22);
  --dim: rgba(250, 254, 255, 0.78);
  --wash-dim: rgba(250, 254, 255, 0.22);

  --disp: "Playfair Display", Didot, "Bodoni 72", "Hoefler Text", Georgia, "Times New Roman", serif;
  --fn: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Consolas, Menlo, monospace;

  --pad: clamp(20px, 5vw, 84px);
  --max: 1240px;
  --r3d: clamp(240px, 31vw, 400px);

  --e: cubic-bezier(0.2, 0.7, 0.2, 1);
  --e-out: cubic-bezier(0.16, 1, 0.3, 1);
}

/* ---------- Reset ---------- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; scroll-behavior: smooth; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--fn);
  font-size: 1rem;
  line-height: 1.7;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}
a { color: inherit; text-decoration: none; }
ol, ul { margin: 0; padding: 0; list-style: none; }
dl, dd { margin: 0; }
h1, h2, h3, p { margin: 0; }
input { font: inherit; color: inherit; }
::selection { background: var(--cyan); color: var(--fg); }

.disp {
  font-family: var(--disp);
  font-weight: 400;
  line-height: 1.06;
  letter-spacing: -0.005em;
}
.disp em { font-style: italic; }

/* ---------- Focus ---------- */
:focus { outline: none; }
a:focus-visible,
label:focus-visible,
.lang-toggle-input:focus-visible ~ .site .lang-switch {
  outline: 2px solid var(--cyan);
  outline-offset: 4px;
}
.dark a:focus-visible,
.quote a:focus-visible,
.open a:focus-visible { outline-color: var(--purple); }

/* ---------- Bilingual toggle (CSS-only) ---------- */
.lang-toggle-input {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
[lang="id"].t { display: none; }
#lang-toggle:checked ~ .site [lang="id"].t { display: revert; }
#lang-toggle:checked ~ .site [lang="en"].t { display: none; }

/* ---------- Skip link ---------- */
.skip-link {
  position: absolute;
  left: 1rem;
  top: -3rem;
  z-index: 200;
  background: var(--fg);
  color: var(--bg);
  padding: 0.7rem 1rem;
  font-size: 0.9375rem;
  transition: top 0.2s var(--e);
}
.skip-link:focus { top: 1rem; }

/* ---------- Shell ---------- */
.wrap { max-width: var(--max); margin-inline: auto; padding-inline: var(--pad); }
.center { text-align: center; }
.sec { padding-block: clamp(6rem, 13vh, 11rem); }
.tint { background: var(--bg2); }
.dark { background: var(--deep); color: var(--bg); }
main section { scroll-margin-top: calc(var(--header-h, 5rem) + 1rem); }
/* Reveals slide blocks in from 38px to the right of where they land. Until
   they arrive that widens the page, which on a phone shows as a horizontal
   scroll of exactly that much. clip, not hidden: hidden would make this a
   scroll container and break the gallery pin, which is position: sticky. */
main { overflow-x: clip; }

/* ---------- Micro type ---------- */
.eyebrow {
  display: inline-block;
  font-size: 0.6875rem;
  font-weight: 500;
  letter-spacing: 0.34em;
  text-transform: uppercase;
  color: var(--cyan-t);
}
.eyebrow-flank {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: clamp(0.7rem, 1.7vw, 1.25rem);
}
.eyebrow-flank::before,
.eyebrow-flank::after {
  content: "";
  width: clamp(26px, 5vw, 54px);
  height: 1px;
  background: var(--purple);
  flex: none;
}
.eyebrow-light { color: var(--purple-l); }
.dark .eyebrow { color: var(--orange-l); }

.lede {
  color: var(--muted);
  max-width: 56ch;
  margin-top: 1.6rem;
  font-size: clamp(1rem, 0.96rem + 0.25vw, 1.125rem);
  text-wrap: pretty;
}
.center .lede { margin-inline: auto; }

.head { max-width: 68ch; margin-inline: auto; }
h2.disp {
  font-size: clamp(2rem, 5vw, 4rem);
  max-width: 20ch;
  margin: 1.4rem auto 0;
  text-wrap: balance;
}
h2.disp em { color: var(--cyan-t); }
.dark h2.disp em { color: var(--cyan); }

/* the vertical hairline under a centred statement */
.rule {
  display: block;
  width: 1px;
  height: clamp(48px, 8vh, 76px);
  margin: clamp(2rem, 4vw, 2.8rem) auto 0;
  background: linear-gradient(var(--cyan-t), transparent);
  transform-origin: top center;
}

/* ---------- The mark ----------
   washa_group_logo.svg, inlined in three places. It is the client's own
   artwork: three hexagonal strokes forming a W, in the three colours this
   whole palette was taken from: #E26E1A, #00F0FD, #8952FE. Their fills are
   left exactly as supplied rather than swapped for palette tokens, because a
   logo recoloured to suit a page is no longer the logo.

   Inline rather than an <img> so it can be animated per path, and so it costs
   no request. Every copy is aria-hidden and focusable="false": the wordmark
   text beside it already names the company, and a screen reader announcing the
   mark as well would only repeat it. */

/* beside the wordmark */
/* The supplied lockup carries "WASHA GROUP" inside its frame, which is a
   grey smear below 110px and duplicates the live text beside it here. This is
   the same file with those 22 wordmark paths dropped. Nothing else changed,
   and the 112 that remain are byte-for-byte the originals. */
.mark {
  width: 2.9em;
  height: auto;
  flex: none;
  transform-origin: 50% 50%;
  transition: transform 0.6s var(--e-out);
}
.wordmark:hover .mark { transform: translateY(-2px) scale(1.06); }

/* very faint behind the cold open, turning with the scroll */

/* signing off in the footer, drawn on arrival */
/* The footer gets the whole lockup, wordmark and all, at a size where it can
   read properly, which is why the duplicate "Washa Group" line that used
   to sit under it is gone. */
.signoff {
  display: block;
  width: clamp(104px, 12vw, 132px);
  height: auto;
  margin-bottom: 0.6rem;
}

/* ---------- Header ---------- */
.site-header {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 80;
  color: var(--fg);
  background-color: color-mix(in srgb, var(--bg) 86%, transparent);
  border-bottom: 1px solid var(--line);
  backdrop-filter: blur(10px);
}
.header-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.6rem 1.6rem;
  padding-block: 1.05rem;
}
.wordmark {
  display: inline-flex;
  align-items: center;
  gap: 0.62em;
  font-family: var(--disp);
  font-weight: 500;
  font-size: clamp(1.05rem, 1.6vw, 1.3rem);
  letter-spacing: 0.3em;
  padding-left: 0.3em;
  white-space: nowrap;
}
.menu {
  order: 3;
  width: 100%;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.6rem;
}
.menu a {
  position: relative;
  font-size: 0.625rem;
  font-weight: 600;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  padding-block: 0.3em;
  opacity: 0.78;
  transition: opacity 0.4s var(--e);
}
.menu a::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: right;
  transition: transform 0.45s var(--e);
}
.menu a:hover { opacity: 1; }
/* the section you are reading, marked by reveal.js */
.menu a.on { opacity: 1; color: var(--cyan-t); }
.menu a.on::after { transform: scaleX(1); transform-origin: left; background: var(--cyan-t); }
.menu a:hover::after { transform: scaleX(1); transform-origin: left; }
@media (max-width: 55.99rem) {
  /* Six bilingual links wrapped onto three rows and pushed the fixed header
     over the content. One line that scrolls sideways keeps the header a
     predictable height at any width. */
  .menu {
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
    padding-bottom: 0.15rem;
    -webkit-mask-image: linear-gradient(90deg, #000 88%, transparent);
    mask-image: linear-gradient(90deg, #000 88%, transparent);
  }
  .menu::-webkit-scrollbar { display: none; }
  .menu a { white-space: nowrap; }
}
@media (min-width: 56rem) {
  .menu { order: 0; width: auto; }
}
.lang-switch {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  font-size: 0.625rem;
  font-weight: 600;
  letter-spacing: 0.2em;
  cursor: pointer;
  padding: 0.55em 0.9em;
  border: 1px solid currentColor;
  border-radius: 999px;
  opacity: 0.85;
  user-select: none;
  transition: opacity 0.4s var(--e);
}
.lang-switch:hover { opacity: 1; }
.lang-switch em { font-style: normal; opacity: 0.45; }
.lang-switch .lang-en,
.lang-switch .lang-id { transition: opacity 0.25s var(--e); }
#lang-toggle:not(:checked) ~ .site .lang-switch .lang-id { opacity: 0.45; }
#lang-toggle:checked ~ .site .lang-switch .lang-en { opacity: 0.45; }

/* ---------- Buttons ---------- */
.btn {
  display: inline-flex;
  align-items: center;
  font-size: 0.625rem;
  font-weight: 600;
  letter-spacing: 0.26em;
  text-transform: uppercase;
  padding: 1.25em 2.4em;
  border: 1px solid currentColor;
  border-radius: 999px;
  background: var(--fg);
  color: var(--bg);
  transition: background 0.5s var(--e), color 0.5s var(--e), border-color 0.5s var(--e);
}
.btn:hover { background: var(--cyan); border-color: var(--cyan); color: var(--deep); }
.btn-quiet { background: transparent; color: inherit; }
.btn-quiet:hover { background: var(--fg); border-color: var(--fg); color: var(--bg); }

/* ---------- Cold open ---------- */
.open {
  position: relative;
  min-height: 100svh;
  display: grid;
  place-items: center;
  text-align: center;
  color: var(--fg);
  overflow: clip;
  padding: calc(var(--header-h, 5rem) + clamp(2rem, 6vh, 5rem)) var(--pad) clamp(5rem, 10vh, 8rem);
}
.open-bg {
  position: absolute;
  inset: 0;
  z-index: 0;
  background:
    linear-gradient(158deg, rgba(0, 239, 255, 0.13) 0%, transparent 46%),
    linear-gradient(200deg, rgba(227, 111, 27, 0.16) 0%, transparent 40%),
    var(--bg);
}
.open-bg::before {
  content: "";
  position: absolute;
  inset: -12%;
  background:
    radial-gradient(40% 32% at 18% 22%, rgba(227, 111, 27, 0.2), transparent 64%),
    radial-gradient(36% 28% at 82% 70%, rgba(0, 239, 255, 0.22), transparent 64%),
    radial-gradient(30% 24% at 64% 14%, rgba(160, 102, 203, 0.16), transparent 64%);
}
.open-bg::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(120% 100% at 50% 42%, rgba(250, 254, 255, 0.72) 26%, transparent 68%);
}
/* The photograph sits between the ambient wash below it and the white
   radial above: ::before paints before any child, ::after paints after, so
   the gradient that keeps the headline legible still covers the picture. */
.open-bg img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* the drifting accents exist to fill an empty slot; a photograph hides them
   completely, so stop animating what nobody can see. Browsers without :has()
   simply keep the invisible animation running. */
.open-bg:has(img)::before { animation: none; }

.open-ct { position: relative; z-index: 3; max-width: 62rem; }
.open h1 {
  font-size: clamp(2.5rem, 7.4vw, 6rem);
  line-height: 1.02;
  margin-top: 1.7rem;
  text-wrap: balance;
}
.open h1 em { color: var(--orange); }
.open-sub {
  margin: 1.8rem auto 0;
  max-width: 48ch;
  font-size: clamp(1rem, 1.4vw, 1.2rem);
  color: var(--muted);
}
.open-cta {
  margin-top: clamp(2.2rem, 4vw, 3rem);
  display: flex;
  gap: 0.9rem;
  flex-wrap: wrap;
  justify-content: center;
}
/* ---------- The cold open's photograph ----------
   A background, not an illustration: it fills the section edge to edge and
   the type sits on it. That only works because the picture is darkened and
   the type over it goes light. Measured, the light palette could not carry
   it. See README, "Why the cold open is dark".
   Two files, each at its own orientation so neither is badly cropped: the
   3200×1800 frame from 48rem up, the 1200×2400 one below. */
.open-bg img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: brightness(0.44) saturate(0.88);
}
/* Uneven on purpose: darkest at the foot, where the smallest type sits. */
.open-bg:has(img)::after {
  background: linear-gradient(
    180deg,
    rgba(14, 24, 30, 0.66) 0%,
    rgba(14, 24, 30, 0.34) 38%,
    rgba(14, 24, 30, 0.74) 100%);
}
/* The drifting accents and the watermark were furniture for an empty slot.
   A photograph does both jobs, and under one they only muddy it. */
.open-bg:has(img)::before { display: none; }

/* the type flips to light over the picture */
.open:has(.open-bg img) { color: var(--bg); }
.open:has(.open-bg img) h1 { color: var(--bg); }
.open:has(.open-bg img) h1 em { color: var(--orange-l); }
.open:has(.open-bg img) .eyebrow { color: #7fe9f3; }
.open:has(.open-bg img) .eyebrow-flank::before,
.open:has(.open-bg img) .eyebrow-flank::after { background: var(--purple-l); }
.open:has(.open-bg img) .open-sub { color: rgba(250, 254, 255, 0.92); }
.open:has(.open-bg img) .btn {
  background: var(--bg);
  color: var(--deep);
  border-color: transparent;
}
.open:has(.open-bg img) .btn-quiet {
  background: transparent;
  color: var(--bg);
  border-color: rgba(250, 254, 255, 0.62);
}
.open:has(.open-bg img) .scrollcue { background: rgba(250, 254, 255, 0.34); }

.scrollcue {
  position: absolute;
  z-index: 3;
  left: 50%;
  bottom: clamp(18px, 4vh, 42px);
  width: 1px;
  height: clamp(40px, 7vh, 74px);
  margin-left: -0.5px;
  background: rgba(40, 58, 68, 0.18);
  overflow: hidden;
}
.scrollcue i {
  position: absolute;
  inset: 0 0 auto 0;
  height: 42%;
  background: linear-gradient(var(--cyan-t), var(--purple));
}

/* ---------- The monogram as decoration ----------
   asset_bg.svg is three hexagonal strokes forming a W, and the file the whole
   palette was read off. It is not any entity's logo, so it is used here as
   what its name says it is: a graphic. Large, faint, behind the type, and it
   drifts a little against the scroll so a flat section has some depth. */
.graphic {
  position: absolute;
  z-index: 0;
  height: auto;
  pointer-events: none;
  user-select: none;
  transform-origin: 50% 50%;
}
/* ---------- The monogram, drawn large ----------
   Inlined rather than loaded, because the three strokes have to be reachable
   one at a time, and an <img> cannot be drawn line by line. Stroke only, no
   fill: at this size a filled monogram would be a wall, and the outline is
   the thing that can be animated. Each stroke keeps its own supplied colour.
   It bleeds off the edge on purpose; only the inner half is ever on screen. */
.glyph {
  position: absolute;
  z-index: 0;
  top: 50%;
  /* One variable drives both the size and the bleed, so the same fraction
     stays on screen at every width. Offsetting by a percentage of the section
     instead put it almost entirely off the page at 2560px. */
  --gw: clamp(300px, 34vw, 620px);
  width: var(--gw);
  height: auto;
  translate: 0 -50%;
  fill: none;
  stroke-width: 3;
  stroke-linejoin: round;
  opacity: 0.24;
  overflow: visible;
  pointer-events: none;
}
.glyph-left  { left: calc(var(--gw) * -0.58); }   /* 42% of it on screen */
.glyph-right { right: calc(var(--gw) * -0.63); }  /* 37% */

/* Below 68rem the copy runs the full width and there is no side to sit beside;
   measured at 1024px the outline landed under a paragraph. */
@media (max-width: 68rem) {
  .glyph { display: none; }
}

/* Five nodes to a field, set on a loose hexagonal lattice rather than
   scattered: offset rows, so the eye reads a pattern instead of confetti.
   Percentages, so the lattice keeps its shape at any width. */
.field {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}
.g1 { left:  4%;  top: 14%; width: clamp(90px, 13vw, 210px); opacity: 0.075; }
.g2 { left: 27%;  top: 66%; width: clamp(64px,  9vw, 150px); opacity: 0.055; }
.g3 { left: 49%;  top: 22%; width: clamp(52px,  7vw, 118px); opacity: 0.045; }
.g4 { right: 6%;  top: 58%; width: clamp(104px,15vw, 250px); opacity: 0.07;  }
.g5 { right: 22%; top: 4%;  width: clamp(72px, 10vw, 168px); opacity: 0.05;  }

/* the dark band swallows a 5% mark, so everything there is lifted */
.dark .g1, .dark .g4 { opacity: 0.13; }
.dark .g2, .dark .g5 { opacity: 0.1; }
.dark .g3 { opacity: 0.08; }

/* Every second field mirrors, so two in a row never read as a repeat. */
#vision .field { transform: scaleX(-1); }
/* #coverage has a ground of its own now. The drifting hexagons used to show
   up through the water as pale cyan and peach patches that read like islands,
   and the old fix was to paint the map's box in the page colour. There is no
   box to paint any more, so the field goes instead. */
#coverage .field { display: none; }

/* Every section that carries a decorative layer behind its content needs the
   same two things: somewhere for that layer to position against, and a wrap
   that sits over it. This was written three times over, once beside each
   layer, with #coverage in two of the three. One list, in one place.

   clip rather than hidden: hidden would make these scroll containers and
   break position: sticky inside them. */
#intro, #vision, #flag-scope, #services, #coverage, #org {
  position: relative;
  overflow: clip;
}
#intro > .wrap,
#vision > .wrap,
#flag-scope > .wrap,
#services > .wrap,
#coverage > .wrap,
#coverage > .portmap,
#org > .wrap {
  position: relative;
  z-index: 1;
}

/* The hover. translate and rotate rather than transform, because reveal.js
   owns transform on these for the scroll parallax and the two would fight.
   Negative delays start each one mid-cycle so the field never pulses as a
   block; the periods are coprime-ish so it takes minutes to repeat. */
@media (prefers-reduced-motion: no-preference) {
  .graphic { animation: hover-drift 19s ease-in-out infinite both; }
  .g1 { animation-duration: 23s; animation-delay: -3s;  }
  .g2 { animation-duration: 17s; animation-delay: -11s; }
  .g3 { animation-duration: 29s; animation-delay: -7s;  }
  .g4 { animation-duration: 21s; animation-delay: -16s; }
  .g5 { animation-duration: 26s; animation-delay: -2s;  }
}
@keyframes hover-drift {
  0%   { translate: 0 0;      rotate: 0deg;    }
  33%  { translate: 6px -18px; rotate: 3.5deg; }
  66%  { translate: -5px -9px; rotate: -2.5deg; }
  100% { translate: 0 0;      rotate: 0deg;    }
}

@media (prefers-reduced-motion: reduce) {
  .graphic { transform: none !important; }
}

/* ---------- Feature splits ---------- */
.feat {
  display: grid;
  gap: clamp(2rem, 5vw, 4.5rem);
  align-items: center;
  margin-top: clamp(3.5rem, 8vw, 6.5rem);
}
.panel {
  position: relative;
  aspect-ratio: 4 / 5;
  display: grid;
  align-content: end;
  gap: 0.7rem;
  padding: clamp(1.6rem, 3.2vw, 2.6rem);
  overflow: hidden;
}
.panel-a {
  color: var(--fg);
  background:
    radial-gradient(120% 90% at 18% 8%, rgba(227, 111, 27, 0.22), transparent 62%),
    var(--panel);
  box-shadow: inset 0 3px 0 0 var(--orange);
}
.panel-b {
  color: var(--fg);
  background:
    radial-gradient(120% 90% at 18% 8%, rgba(0, 239, 255, 0.26), transparent 62%),
    var(--panel);
  box-shadow: inset 0 3px 0 0 var(--cyan);
}
.panel-k {
  font-size: 0.625rem;
  font-weight: 600;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  opacity: 0.75;
}
.panel-t {
  font-family: var(--disp);
  font-size: clamp(1.6rem, 3.4vw, 2.5rem);
  line-height: 1.08;
}
.panel-r {
  margin-top: 0.9rem;
  padding-top: 0.9rem;
  border-top: 1px solid currentColor;
  font-size: 0.6875rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  opacity: 0.8;
}
.panel-a .panel-k { color: var(--orange-t); opacity: 1; }
.panel-b .panel-k { color: var(--cyan-t); opacity: 1; }
.copy h3 {
  font-size: clamp(1.6rem, 3.4vw, 2.4rem);
  margin: 0.9rem 0 1.1rem;
}
.copy p { color: var(--muted); max-width: 46ch; }
.note {
  margin-top: clamp(3rem, 6vw, 4.5rem);
  margin-inline: auto;
  max-width: 62ch;
  color: var(--muted);
  font-size: 0.9375rem;
  text-align: center;
}
@media (min-width: 52rem) {
  .feat { grid-template-columns: 0.92fr 1.08fr; }
  .feat.rev { grid-template-columns: 1.08fr 0.92fr; }
  .feat.rev .panel { order: 2; }
}

/* ---------- Label / value rows ---------- */
.det { margin-top: 1.8rem; border-top: 1px solid var(--line); }
.det-row {
  display: grid;
  gap: 0.35rem;
  padding-block: 1rem;
  border-bottom: 1px solid var(--line);
}
.det dt {
  font-size: 0.625rem;
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--muted);
}
.det .v { font-family: var(--disp); font-size: 1.125rem; }

/* The Group's rows carry registration codes, not prose. Playfair at 18px in a
   half-width column broke them mid-token: "0303/KA-A3-" on one line and
   "A3.5/LB.00.00/11/2025" on the next. A narrower label column and a smaller
   value give each code the room to stay whole. */
#group .det-row { align-items: start; }
#group .det { margin-top: 1.4rem; }
#group .det .v { font-size: 1rem; line-height: 1.5; }
#group .det .none { color: var(--muted); font-family: var(--fn); font-size: 0.9375rem; }
@media (min-width: 40rem) {
  #group .det-row { grid-template-columns: 9.5rem minmax(0, 1fr); gap: 1rem; }
}
@media (min-width: 40rem) {
  .det-row {
    grid-template-columns: 12rem minmax(0, 1fr);
    gap: 1.2rem;
    align-items: baseline;
  }
}

/* ---------- Placeholders ----------
   One format for every unsupplied value on the page, text or image:
   a dashed gold field reading PLACEHOLDER — <what goes here>. */
.placeholder {
  display: inline-block;
  padding: 0.45em 0.75em;
  border: 1px dashed var(--purple);
  background: rgba(160, 102, 203, 0.1);
  font-family: var(--mono);
  font-size: 0.625rem;
  letter-spacing: 0.14em;
  line-height: 1.35;
  text-transform: uppercase;
  color: var(--purple-t);
  white-space: nowrap;
}
.placeholder::before { content: "PLACEHOLDER — "; opacity: 0.72; }

/* ---------- Gallery carousel ---------- */
/* The gallery is pinned while the ring completes one turn: the section is a
   tall runway, and the box inside it sticks under the header for the length
   of that runway. reveal.js maps how far through the runway you have scrolled
   onto the rotation, so the turn finishes exactly as the pin lets go.

   Scoped to html.js. Without the script the ring cannot turn, and pinning a
   still image for two screen heights would be an obstacle rather than an
   effect, so the section stays in normal flow instead. Same under
   prefers-reduced-motion, further down. */
html.js #gallery {
  --pin-h: calc(100svh - var(--header-h, 5rem));
  height: calc(var(--pin-h) * 3);
  padding-block: 0;
}
html.js #gallery .gallery-pin {
  position: sticky;
  top: var(--header-h, 5rem);
  height: var(--pin-h);
  display: grid;
  align-content: center;
  overflow: hidden;
}
/* cap the ring by the height actually available, letting aspect-ratio set the
   width, so a short window crops nothing */
html.js #gallery .carousel {
  max-width: calc((100svh - var(--header-h, 5rem) - 11rem) * 1.45);
}

/* ---------- The wall behind the ring ----------
   .gallery-pin is the box that holds still while the carousel turns, so the
   backdrop belongs to it rather than to the section: it stays put with the
   ring instead of scrolling past behind it.

   position and overflow are set here as well as in the html.js pin rule
   further up. Without a script that rule never applies, and an absolutely
   positioned wall would then resolve against whatever ancestor happened to
   be positioned and spill out of the section. */
.gallery-pin { position: relative; overflow: hidden; }
.gallery-pin > .wrap,
.gallery-pin > .carousel { position: relative; z-index: 1; }

/* The plate the heading sits on.

   --cyan-t measures 4.60:1 on the plain tint and 1.53:1 against the darkest
   pixel a photograph can put behind it at 0.42. The eyebrow is that colour
   too, at 0.625rem, where the bar is 4.5:1 rather than the 3:1 display type
   gets. So the words need the tint back underneath them, solidly, not as a
   wash.

   The plate hangs off the heading box, not off the pin. .gallery-pin is a
   centred grid, so the heading lands in the middle of it and where exactly
   depends on the height of the glass; anything positioned by percentage from
   the top of the pin misses it on most screens.

   Full-bleed and masked at the ends, so it reads as a band the wall runs
   behind rather than a panel sitting on it. */
.gallery-pin > .wrap::before {
  content: "";
  position: absolute;
  z-index: -1;
  top: -2.8rem;
  bottom: -2.8rem;
  left: 50%;
  width: 100vw;
  transform: translateX(-50%);
  pointer-events: none;
  /* Solid from 14% to 86%, not 26% to 74%. The heading box sits at 21.6% to
     78.8% of this plate, so the narrower band left the eyebrow standing on the
     ramp. That eyebrow is cyan at 0.625rem, which needs 4.5:1 rather than 3:1. */
  background: linear-gradient(180deg,
    transparent 0%, var(--bg2) 14%, var(--bg2) 86%, transparent 100%);
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 20%, #000 80%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 20%, #000 80%, transparent);
}

.wall {
  position: absolute;
  inset: 0;
  z-index: 0;
  display: grid;
  grid-template-rows: 1fr 1fr;
  gap: clamp(6px, 1.2vw, 16px);
  pointer-events: none;
  /* Sharp, and meant to be seen. At 0.16 behind a blur this was texture; at
     0.42 with neither it is a wall of photographs that the ring turns in
     front of. Still grey, so it stays behind the palette rather than adding
     a third set of colours to a section that already has orange and cyan on
     the faces.

     That is 1.53:1 for the cyan in the heading against the darkest pixel a
     photograph can put behind it, which is why the plate below exists and is
     not optional. */
  opacity: 0.42;
  filter: grayscale(1);
  /* dissolve at both ends rather than stopping at a cut edge */
  /* 18%, not 10%: on a 375px screen a tenth is 37px and the band still
     looked cut rather than dissolved. */
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 18%, #000 82%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 18%, #000 82%, transparent);
}
.wall-row {
  display: flex;
  width: max-content;
  min-height: 0;
}
.wall-a { animation: run-left 96s linear infinite; }
.wall-b { animation: run-right 120s linear infinite; }
.wall-set {
  display: flex;
  gap: clamp(6px, 1.2vw, 16px);
  /* the trailing gap, so the two sets are exactly half the row each */
  padding-right: clamp(6px, 1.2vw, 16px);
}
.wall-set img {
  height: 100%;
  width: auto;
  max-width: none;
  display: block;
  object-fit: cover;
}
/* Gone entirely under reduced motion, and not only because it drifts. That
   media query also takes position off .gallery-pin, because the ring collapses
   to a single static card there and the pin has no job left. An absolutely
   positioned wall would stop resolving against it and spill up the page.
   Hiding it settles both at once. */
@media (prefers-reduced-motion: reduce) {
  .wall { display: none; }
}

.carousel {
  position: relative;
  width: min(100%, 56rem);
  margin: clamp(2.5rem, 6vw, 4.5rem) auto 0;
  aspect-ratio: 16 / 11;
  perspective: clamp(900px, 70vw, 1500px);
  perspective-origin: 50% 48%;
}
.carousel::before {
  content: "";
  position: absolute;
  inset: 12% 6%;
  z-index: -1;
  border-radius: 50%;
  background: radial-gradient(56% 50% at 50% 58%, rgba(40, 58, 68, 0.16), transparent 72%);
  filter: blur(28px);
}
.carousel-stage { position: absolute; inset: 0; transform-style: preserve-3d; }

.cface {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 34%;
  aspect-ratio: 4 / 5;
  transform-style: preserve-3d;
}
/* the card inside counter-rotates against the ring, so every face keeps
   looking straight out of the screen instead of turning edge-on */
.cface-in {
  position: absolute;
  inset: 0;
  display: grid;
  align-content: end;
  justify-items: start;
  gap: 0.55rem;
  padding: 1.3rem;
  color: var(--fg);
  border: 1px solid var(--line);
  background:
    linear-gradient(168deg, rgba(0, 239, 255, 0.2) 0%, transparent 52%),
    linear-gradient(340deg, rgba(227, 111, 27, 0.16) 0%, transparent 46%),
    var(--panel);
  box-shadow: 0 26px 50px -32px rgba(40, 58, 68, 0.55);
}
.cface-in::after {
  content: "";
  position: absolute;
  left: 1.3rem;
  right: 1.3rem;
  top: 1.3rem;
  height: 1px;
  background: var(--cyan);
}
/* The ring position lives on .cface, the counter-rotation that turns the card
   back towards the reader lives on .cface-in. Both are plain CSS, so before
   any script runs the ring already reads as five cards facing forward rather
   than four edge-on slivers. reveal.js then animates the same two transforms
   against the scroll. */
.cface:nth-child(1) .cface-in { transform: rotateY(0deg); }
.cface:nth-child(2) .cface-in { transform: rotateY(-72deg); }
.cface:nth-child(3) .cface-in { transform: rotateY(-144deg); }
.cface:nth-child(4) .cface-in { transform: rotateY(-216deg); }
.cface:nth-child(5) .cface-in { transform: rotateY(-288deg); }
.cface:nth-child(1) { transform: translate(-50%, -50%) rotateY(0deg) translateZ(var(--r3d)); }
.cface:nth-child(2) { transform: translate(-50%, -50%) rotateY(72deg) translateZ(var(--r3d)); }
.cface:nth-child(3) { transform: translate(-50%, -50%) rotateY(144deg) translateZ(var(--r3d)); }
.cface:nth-child(4) { transform: translate(-50%, -50%) rotateY(216deg) translateZ(var(--r3d)); }
.cface:nth-child(5) { transform: translate(-50%, -50%) rotateY(288deg) translateZ(var(--r3d)); }
.cface-t { font-size: clamp(0.95rem, 1.5vw, 1.3rem); line-height: 1.15; }

/* ---------- A photograph on a carousel face ----------
   The slot is ready before the pictures are: drop an <img> in as the first
   child of .cface-in and everything below applies. Written now because the
   card slot taught the lesson the hard way. An <img> in a grid that centres
   its items sizes to its own content and overflows, so it has to be taken out
   of flow and told to cover. */
.cface-in img {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* The caption is slate on a pale ground today. Over a photograph that fails
   the same way the cold open did, so a picture brings its own scrim with it
   and the caption goes light. Darkest at the foot, where the words sit. */
.cface-in:has(img) {
  background: var(--deep);
}
.cface-in:has(img)::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(
    to top,
    rgba(14, 24, 30, 0.82) 0%,
    rgba(14, 24, 30, 0.34) 40%,
    rgba(14, 24, 30, 0.08) 70%);
}
.cface-in:has(img) .cface-t {
  position: relative;
  z-index: 2;
  color: var(--bg);
}
.cface-in:has(img)::after { background: var(--cyan); opacity: 0.85; z-index: 2; }

/* ---------- Vision & mission ---------- */
/* The opening line of MISI, which is not one of the numbered points. It sits
   above the grid the way it sits above the bullets in the profile. */
.mission-lead {
  margin: clamp(2.4rem, 5vw, 3.5rem) auto 0;
  max-width: min(100%, 46ch);
  text-align: center;
  font-size: clamp(1.05rem, 1.9vw, 1.3rem);
  line-height: 1.6;
}
.mission {
  display: grid;
  grid-template-columns: 1fr;
  gap: clamp(1rem, 2vw, 1.6rem);
  margin-top: clamp(1.6rem, 3vw, 2.4rem);
}
.mission-item {
  display: grid;
  gap: 0.7rem;
  align-content: start;
  padding: clamp(1.4rem, 3vw, 2rem);
  background: var(--panel);
  border: 1px solid var(--line);
  border-top: 3px solid var(--cyan);
}
.mission-item p { color: var(--muted); }
.mission-k {
  font-family: var(--mono);
  font-size: 0.625rem;
  letter-spacing: 0.3em;
  color: var(--cyan-t);
}
.mission-item:nth-child(1) { border-top-color: var(--orange); }
.mission-item:nth-child(1) .mission-k { color: var(--orange-t); }
.mission-item:nth-child(3) { border-top-color: var(--purple); }
.mission-item:nth-child(3) .mission-k { color: var(--purple-t); }
/* Three lines now, not three words, so the size comes down and the measure
   goes up. At the old 26ch and 2.125rem the closing clause alone wrapped to
   four lines and the block read as a paragraph in display type. */
.motto {
  margin: clamp(2.5rem, 5vw, 4rem) auto 0;
  max-width: min(100%, 40ch);
  text-align: center;
  font-size: clamp(1.05rem, 2.1vw, 1.55rem);
  line-height: 1.45;
  color: var(--orange-t);
}
@media (min-width: 44rem) {
  .mission { grid-template-columns: 1fr 1fr; }
}
/* At two columns three points leave an empty cell; at three they do not. */
@media (min-width: 62rem) {
  .mission { grid-template-columns: repeat(3, 1fr); }
}

/* a five-card ring cannot fit a phone; tighten it so only the neighbours
   crop, never the front card */
@media (max-width: 44rem) {
  .carousel { aspect-ratio: 5 / 4; }
  .cface { width: 40%; }   /* 52% could not fit five on a ring without cropping */
}

/* ---------- The port call (dark) ---------- */
.stages { max-width: 64rem; margin: clamp(3rem, 6vw, 5rem) auto 0; }
.stage {
  display: grid;
  gap: 0.55rem;
  padding-block: clamp(2rem, 4vw, 3.2rem);
  border-top: 1px solid var(--lineL);
}
.stage:last-child { border-bottom: 1px solid var(--lineL); }
.stage-k {
  font-size: 0.625rem;
  font-weight: 600;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--orange-l);
}
.stage h3 { font-family: var(--disp); font-weight: 400; font-size: clamp(1.6rem, 3.4vw, 2.6rem); line-height: 1.08; }
.stage p { color: var(--dim); max-width: 60ch; }
@media (min-width: 56rem) {
  .stage { grid-template-columns: 13rem minmax(0, 1fr); column-gap: 2.5rem; align-items: start; }
  .stage-k { grid-row: 1; padding-top: 0.55rem; }
  .stage h3 { grid-column: 2; }
  .stage p { grid-column: 2; }
}

/* ---------- Coverage ---------- */

/* ---------- Where the work is: the map ----------
   This replaced six COVERAGE STATUS placeholders. There was never a per-area
   status to fill them with, and six boxes each saying nothing is worse than
   not asking the question, so the section answers a question it can answer
   instead: where these places actually are.

   The frame runs from Pulau Karimun east to Pulau Batam and holds 117 islands,
   all of them OpenStreetMap's own coastline, projected equirectangular with x
   scaled by cos(1.09°) and reduced by Douglas-Peucker to 1.2px at a frame of
   1000 by 343 units. One path of 117 subpaths, 19.8KB, and 1px of the frame is
   110m of water. One path rather than 117 elements because the coastline draw
   in reveal.js dashes a single element end to end.

   The build is written up in the README under "The map"; the data is ODbL, so
   the credit line under the map is a licence condition, not a courtesy. */
.portmap {
  /* No cap, no padding, no panel. The map sits outside .wrap and runs the
     full width of the section, and the fade on .batam below takes care of
     the top and bottom edges. A boxed map with four visible sides read as a
     diagram dropped on the page; this reads as the ground under it. */
  margin-block: clamp(2rem, 4vw, 3rem) clamp(1.5rem, 3vw, 2.25rem);
}
.portmap-in {
  position: relative;
  /* a pin near the edge is allowed to hang into the padding above */
  overflow: visible;
  /* The frame keeps its 2.9 to 1 shape at every width, so on a very wide
     monitor it would grow until it owned the screen. 1800px caps it at about
     620px tall and only bites above that width. The cap goes here rather than
     on the svg because the pins are percentages of this box: cap the svg and
     the two would come apart. */
  max-width: 1800px;
  margin-inline: auto;
}

/* On a phone the frame is 3.4 to 1 inside about 300px, which leaves Pulau
   Batam roughly 85px wide carrying seven names. They collide at any type
   size, so below 48rem the frame keeps a usable width and the reader swipes
   it instead. The nav strip at the top of the page already moves that way.

   min-width sits on .portmap-in rather than on the svg because the pins are
   positioned as percentages of .portmap-in. Widen anything else and the
   names come away from their dots.

   direction: rtl starts the scroll at the right-hand end, which is Batam and
   its six areas. Left to itself the reader would open on the empty water west
   of Karimun. .portmap-in puts the direction back so nothing inside reverses.

   The padding is room for the names that hang above and below their dots,
   which overflow-y would otherwise clip; the negative margin takes the space
   back out of the layout. */
@media (max-width: 47.99rem) {
  .portmap-scroll {
    direction: rtl;
    overflow-x: auto;
    overflow-y: hidden;
    padding-block: 1.1rem;
    margin-block: -1.1rem;
    overscroll-behavior-x: contain;
    scrollbar-width: thin;
  }
  .portmap-in {
    direction: ltr;
    min-width: 40rem;
  }
}
.batam {
  display: block;
  width: 100%;
  height: auto;
  /* The top and bottom dissolve into the page instead of stopping at an
     edge. The frame carries 60 units of open water at each end for exactly
     this, so what the fade eats is sea and the part-islands the fetch box
     sliced on its way past, never a whole one. 17% of 343 units is 58, which
     is that band almost exactly.

     Every pin sits between 19% and 81%, so no name is ever printed on land
     that is busy fading out. Check that again if a pin moves. */
  -webkit-mask-image: linear-gradient(to bottom,
    transparent 0%, #000 17%, #000 83%, transparent 100%);
  mask-image: linear-gradient(to bottom,
    transparent 0%, #000 17%, #000 83%, transparent 100%);
}
.batam-coast {
  /* --bg2 carried 18% toward the slate. Grey land on the near-white page,
     rather than white land on a grey panel. The same reading, one box
     fewer. */
  fill: #d4dbdd;
  /* Softened from full slate now that the fill carries the shape. At full
     strength the outline competed with the six names sitting over it. */
  stroke: rgba(40, 58, 68, 0.5);
  /* The frame is 1000 units wide whatever it draws at, and it draws at
     anything from 640px inside the phone scroller to the 1800px cap. One unit
     is therefore worth 0.64 of a CSS pixel at one end and 1.8 at the other,
     and no single width survives that. These four steps each land the line
     between 1.3 and 1.9 real pixels, which is thin enough to leave the names
     over the land readable and thick enough that the inlets do not wash out.

     vector-effect: non-scaling-stroke would be the tidy fix and hold one
     width everywhere. It also measures the dash pattern in screen units
     instead of user units, and a dash pattern is how the coast draws itself,
     so it stays out. */
  stroke-width: 2.6;
  stroke-linejoin: round;
  paint-order: stroke fill;
}
@media (min-width: 48rem) { .batam-coast { stroke-width: 1.7; } }
@media (min-width: 64rem) { .batam-coast { stroke-width: 1.3; } }
@media (min-width: 90rem) { .batam-coast { stroke-width: 1.05; } }

/* ---------- The pins ----------
   HTML over the SVG rather than <text> inside it. Text in a 1000-unit viewBox
   scales with the frame, so a size that reads on a desktop is six or seven
   pixels on a phone; these keep a real CSS size at every width. Their
   positions are percentages of the same frame, generated from the projected
   coordinates so the two cannot drift apart. See the list at the foot.

   Each carries its own name from the start. The numbered chips they replaced
   meant reading a key: a marker that says "01" tells you nothing until you
   have found 01 somewhere else. */
.pin {
  position: absolute;
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: var(--muted);
  cursor: pointer;
  white-space: nowrap;
  transition: color 0.3s var(--e);
}
.pin-dot {
  flex: none;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--deep);
  box-shadow: 0 0 0 3px var(--bg);   /* a collar, so a dot on the coast reads */
  transition: background 0.3s var(--e), transform 0.3s var(--e-out);
}
.pin-name {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.05em;
}
.pin:hover,
.pin:focus-visible { color: var(--fg); }
.pin:focus-visible { outline: 2px solid var(--cyan); outline-offset: 4px; }
.pin.is-on { color: var(--fg); }
.pin.is-on .pin-dot { background: var(--orange); transform: scale(1.45); }

/* The box that marks the picked one. It is a ::after rather than padding on
   the marker itself, because the label sides are percentage transforms of
   the marker box, and growing that box would shift the marker off its
   coordinate the moment it was pressed. This paints outside the box and
   moves nothing.

   z-index: -1 keeps it behind the name. It cannot fall behind the island,
   because the transform that places the marker makes it a stacking context
   of its own and the negative index is trapped inside it. */
.pin::after {
  content: "";
  position: absolute;
  inset: -0.3rem -0.55rem;
  z-index: -1;
  border: 1px solid var(--edge-deep);
  border-radius: 3px;
  background: var(--panel);
  box-shadow: 0 2px 10px -4px rgba(40, 58, 68, 0.45);
  opacity: 0;
  transition: opacity 0.28s var(--e);
}
.pin.is-on::after { opacity: 1; }

/* Which side the name hangs off. Six names on an island this size collide on
   any single rule, so each is placed by hand: north-coast markers put their
   name over the water above, Kabil is at 96% across and can only go left,
   Sekupang sits below its dot to clear Batu Ampar. Move a pin and these have
   to be checked again. */
.pin.lab-r  { transform: translate(-4.5px, -50%); }
.pin.lab-l  { transform: translate(-100%, -50%); flex-direction: row-reverse; }
.pin.lab-l  { margin-left: 4.5px; }
.pin.lab-up { transform: translate(-50%, -100%); flex-direction: column; gap: 0.15rem; }
.pin.lab-dn { transform: translate(-50%, 0); flex-direction: column-reverse; gap: 0.15rem; }
.pin.lab-up .pin-dot,
.pin.lab-dn .pin-dot { order: 2; }
.pin.lab-up { margin-top: 4.5px; }
.pin.lab-dn { margin-top: -4.5px; }

/* ---------- What the picked area handles ----------
   One copy of the text, correct with or without a script. With none, all six
   lines show at once and the block reads as a plain list. Under html.js the
   hint shows until something is picked, then only that one line. */
.mapsay {
  margin-top: clamp(1.2rem, 2.4vw, 1.6rem);
  /* Held open so switching areas does not shift the credit line and
     everything under it. One line is enough at full width; a phone wraps the
     longer pairings onto two. */
  min-height: 3.05em;
}
@media (max-width: 34rem) {
  .mapsay { min-height: 4.65em; }
}
.say {
  margin: 0 0 0.35rem;
  padding: 0.7rem 0.9rem;
  color: var(--muted);
  font-size: 0.9375rem;
  line-height: 1.55;
}
/* The resting line carries the same borders in nothing, so the block does
   not change height the moment the first area is picked. */
.say-hint {
  border: 1px solid transparent;
  border-left-width: 2px;
}
/* Only one of these is ever on screen at a time under html.js, so the box
   can sit on all six rather than being repeated per selector below. */
.say[data-say] {
  border: 1px solid var(--line);
  border-left: 2px solid var(--orange);
  background: var(--panel);
}
.say-k {
  color: var(--fg);
  font-weight: 600;
  letter-spacing: 0.02em;
}
.say-k::after { content: " \2014"; color: var(--muted); font-weight: 400; }
.say-hint { display: none; }
html.js .say { display: none; }
html.js .say-hint { display: block; }
html.js .mapsay[data-show] .say-hint { display: none; }
html.js .mapsay[data-show="batu-ampar"] [data-say="batu-ampar"],
html.js .mapsay[data-show="sekupang"] [data-say="sekupang"],
html.js .mapsay[data-show="nongsa"] [data-say="nongsa"],
html.js .mapsay[data-show="batam-center"] [data-say="batam-center"],
html.js .mapsay[data-show="kabil"] [data-say="kabil"],
html.js .mapsay[data-show="tanjung-uncang"] [data-say="tanjung-uncang"],
html.js .mapsay[data-show="sambu"] [data-say="sambu"],
html.js .mapsay[data-show="karimun"] [data-say="karimun"] {
  display: block;
}

/* Everything under the map reads as one column. The map itself is the full
   width of the section now, so left-aligning these to the wrap would leave
   them stranded against an edge the map does not have. 46rem is the reading
   measure used elsewhere on the page. */
.mapsay, .mapnote, .mapcred { max-width: 46rem; margin-inline: auto; }

/* Set apart from the captions above it, because it is not one of them: the
   place it names has no pin to press. */
.mapnote {
  margin-block: 1.1rem 0;
  color: var(--muted);
  font-size: 0.8125rem;
  line-height: 1.6;
}
.mapcred {
  margin-block: 0.9rem 0;
  color: var(--muted);
  font-size: 0.75rem;
  letter-spacing: 0.02em;
}


/* Each pin's place on the frame, generated with the path in one pass. Editing
   one of these by hand puts the marker somewhere the place is not. */
.pin-batu-ampar { left: 76.379%; top: 27.765%; }   /* Batu Ampar: 1.16498, 104.00213 */
.pin-kabil { left: 91.653%; top: 44.155%; }   /* Kabil: 1.10964, 104.15249 */
.pin-sekupang { left: 68.567%; top: 39.696%; }   /* Sekupang: 1.12469, 103.92521 */
.pin-nongsa { left: 85.827%; top: 19.290%; }   /* Nongsa: 1.19359, 104.09514 */
.pin-tanjung-uncang { left: 66.326%; top: 55.788%; }   /* Tanjung Uncang: 1.07036, 103.90316 */
.pin-batam-center { left: 81.747%; top: 37.758%; }   /* Batam Center: 1.13124, 104.05497 */
.pin-sambu { left: 66.053%; top: 28.701%; }   /* Pulau Sambu: 1.16182, 103.90047 */
.pin-karimun { left: 18.901%; top: 80.046%; }   /* Tanjung Balai Karimun: 0.98846, 103.43625 */

/* ---------- Activity: ten photographs, at full size and in colour ----------
   Every one is 2.02:1, so a plain even grid is the right answer rather than a
   clever one. The consistency is what makes ten photographs read as a set.
   The first and last span the full width, which gives the run a lead and a
   close and leaves eight in four tidy rows between them. */
.acts {
  display: grid;
  grid-template-columns: 1fr;
  gap: clamp(1.4rem, 3vw, 2.4rem);
  margin-top: clamp(3rem, 6vw, 4.5rem);
}
.act { margin: 0; }
.act-shot {
  overflow: hidden;
  background: var(--bg2);
  border: 1px solid var(--line);
}
.act-shot img {
  display: block;
  width: 100%;
  height: auto;
  transition: transform 0.7s var(--e-out);
}
/* The captions carry .vh in the markup, so this styles nothing today. It is
   kept because taking the class off is how they come back, and a caption
   returning to no styling at all is a worse surprise than a rule that sits
   idle. */
.act figcaption {
  margin-top: 0.7rem;
  color: var(--muted);
  font-size: 0.875rem;
  line-height: 1.5;
}
@media (hover: hover) {
  .act:hover .act-shot img { transform: scale(1.035); }
}
@media (prefers-reduced-motion: reduce) {
  .act-shot img { transition: none; }
  .act:hover .act-shot img { transform: none; }
}
@media (min-width: 44rem) {
  .acts { grid-template-columns: 1fr 1fr; }
  .act:first-child,
  .act:last-child { grid-column: 1 / -1; }
}

/* ---------- About us: the company in its own words ----------
   Two short standards of work and one paragraph behind them. The rule down
   the left marks the two as quoted commitments rather than site copy; both
   take the same colour on purpose, because the .t pairs mean an EN and an ID
   copy of each line are siblings and any nth-of-type alternation would
   colour the languages, not the lines. */
.creed {
  display: grid;
  gap: clamp(1.1rem, 2.2vw, 1.7rem);
  margin: clamp(3rem, 6vw, 4.5rem) auto 0;
  max-width: min(100%, 62ch);
}
.creed-line {
  margin: 0;
  padding-left: clamp(0.9rem, 2vw, 1.2rem);
  border-left: 2px solid var(--cyan);
  font-size: clamp(1.05rem, 1.9vw, 1.25rem);
  line-height: 1.6;
}
.creed-body {
  margin: 0;
  color: var(--muted);
  font-size: 1rem;
  line-height: 1.75;
}

/* ---------- The Group & Contact ---------- */
.two, .three {
  display: grid;
  grid-template-columns: 1fr;
  gap: clamp(2.5rem, 5vw, 4rem);
  margin-top: clamp(3rem, 6vw, 4.5rem);
}
.entity-k {
  display: block;
  font-size: 0.625rem;
  font-weight: 600;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--cyan-t);
}
/* ---------- Clients ----------
   Two rows running in opposite directions rather than a grid of boxes. The
   logos sit straight on the section's ground with no tile around them, which
   is the point: a wall of framed panels reads as a table of contents, a wall
   in motion reads as a company with clients.

   Each row prints its logos twice and travels exactly -50%, so the second copy
   lands where the first began and the loop has no seam. The duplicate is
   aria-hidden: eleven clients, not twenty-two. */
.marquee {
  display: grid;
  gap: clamp(1.4rem, 3vw, 2.4rem);
  margin-top: clamp(3rem, 6vw, 4.5rem);
  overflow: hidden;
  /* the rows dissolve at both ends instead of being cut off by the edge */
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 7%, #000 93%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 7%, #000 93%, transparent);
}
.marquee-row {
  display: flex;
  width: max-content;
  /* The tint has to be painted HERE, not on .marquee. mix-blend-mode blends a
     child against its nearest isolating ancestor, and this row is one: the
     animation puts a transform on it, which creates a stacking context. With
     the colour two levels up the logos multiplied against transparency and
     their white stayed white. Keep this equal to #clients. */
  background: var(--bg2);
}
.marquee-set {
  display: flex;
  align-items: center;
  gap: clamp(2.4rem, 6vw, 5rem);
  margin: 0;
  padding: 0 clamp(1.2rem, 3vw, 2.5rem) 0 0;
  list-style: none;
}
.client { flex: none; display: flex; align-items: center; }

/* Sized by height, never by width. A tile forces every mark into one box; a
   common height is what actually makes a square seal and a wordmark five times
   wider than tall look like they belong on the same line. */
/* Sized by area, not by height. One height for every mark is the obvious
   thing and it is wrong: the eye reads area, and at a shared 44px a wordmark
   five times wider than tall covered 10,465px² against 1,812px² for a round
   seal, a 5.8x spread, which is why the compact marks looked shrunken next
   to the long ones.

   Each height is now scaled by (2.2 / aspect)^0.45. At an exponent of 0.5
   the areas match exactly, but the widest wordmarks drop to 28px and stop
   being readable; 0.45 brings the spread to 1.2x and keeps them at 29px and
   up. Selectors match on the filename, so both the visible set and its
   aria-hidden duplicate pick the same value with no class to keep in sync. */
.client img {
  display: block;
  height: calc(var(--k, 1) * clamp(26px, 3.6vw, 44px));
  width: auto;
  max-width: none;
  /* The marks arrived as JPEGs, so each carries its own white rectangle. On a
     tinted ground that reads as a panel, the very thing this section stopped
     being. multiply drops white to the backdrop and leaves the ink, so the
     logo sits on the section instead of on a card. */
  mix-blend-mode: multiply;
  filter: grayscale(1);
  opacity: 0.66;
  transition: filter 0.45s var(--e), opacity 0.45s var(--e);
}
.client:hover img { filter: none; opacity: 1; }

.client img[src$="client-vallianz.webp"] { --k: 0.667; }   /* ar 5.41 */
.client img[src$="client-gemalindo.webp"] { --k: 0.679; }   /* ar 5.19 */
.client img[src$="client-pelindo.webp"] { --k: 0.736; }   /* ar 4.35 */
.client img[src$="client-lanpan.webp"] { --k: 0.771; }   /* ar 3.92 */
.client img[src$="client-hamilton.webp"] { --k: 0.829; }   /* ar 3.33 */
.client img[src$="client-bina-buana-raya.webp"] { --k: 1.094; }   /* ar 1.8 */
.client img[src$="client-pelindo-marine-service.webp"] { --k: 1.182; }   /* ar 1.52 */
.client img[src$="client-delta-sinergi-prima.webp"] { --k: 1.387; }   /* ar 1.06 */
.client img[src$="client-marco-polo-marine.webp"] { --k: 1.411; }   /* ar 1.02 */
.client img[src$="client-asian-lift.webp"] { --k: 1.426; }   /* ar 1 */
.client img[src$="client-delta-sinergi-solusi.webp"] { --k: 1.469; }   /* ar 0.94 */

@media (prefers-reduced-motion: no-preference) {
  .marquee-a { animation: run-left 52s linear infinite; }
  .marquee-b { animation: run-right 64s linear infinite; }
  /* stopping under the cursor lets someone actually read a mark, and
     focus-within does the same for a keyboard */
  .marquee:hover .marquee-row,
  .marquee:focus-within .marquee-row { animation-play-state: paused; }
}
@keyframes run-left  { from { transform: none; }                to { transform: translateX(-50%); } }
@keyframes run-right { from { transform: translateX(-50%); }    to { transform: none; } }

/* Without the animation the rows would sit frozen with half of each off-screen,
   so reduced motion gets a wrapped, centred row instead: every logo visible,
   nothing moving. */
@media (prefers-reduced-motion: reduce) {
  .marquee-row { width: auto; flex-wrap: wrap; justify-content: center; }
  .marquee-set[aria-hidden="true"] { display: none; }
  .marquee-set { flex-wrap: wrap; justify-content: center; gap: clamp(1.6rem, 4vw, 3rem); }
}

/* ---------- The Group's two cards ----------
   A flip, and the faces are stacked in a grid rather than positioned
   absolutely. Absolute faces need the card to be given a height; grid-stacked
   ones make the card as tall as the taller face on their own, so the two
   sides can never disagree about how big the card is.

   The toggle is a checkbox, the same CSS-only pattern the language switch
   uses. No script: it takes focus and the space bar by itself, and if the
   stylesheet never arrives both faces simply render one after the other and
   every registration is still readable. */
.entity {
  perspective: 1600px;
}

.flip-inner {
  display: grid;
  transform-style: preserve-3d;
  transition: transform 0.85s var(--e-out);
}
.flip-input:checked ~ .flip-inner { transform: rotateY(180deg); }
.flip-input:focus-visible ~ .flip-inner {
  outline: 2px solid var(--cyan);
  outline-offset: 5px;
}

.flip-face {
  grid-area: 1 / 1;
  position: relative;
  display: flex;
  flex-direction: column;
  padding: clamp(1.5rem, 3vw, 2.1rem);
  /* a sheet of stock, lit from above: the ground is warmest at the top edge */
  background:
    linear-gradient(178deg, #ffffff 0%, #fbfdfe 62%, #f4f9fb 100%);
  border: 1px solid rgba(40, 58, 68, 0.1);
  border-radius: 3px;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  /* Thickness is four flat shadows stacked a pixel apart, the card's own cut
     edge, with one soft shadow underneath for the cast. Flat first, blurred
     last: reverse them and the blur washes the edge out. Offsets are on Y
     only, so rotateY leaves the edge under the card on both faces. */
  box-shadow:
    0 1px 0 0 var(--edge),
    0 2px 0 0 var(--edge),
    0 3px 0 0 var(--edge),
    0 4px 0 0 var(--edge),
    0 5px 0 0 var(--edge),
    0 6px 0 0 var(--edge-deep),
    0 20px 34px -14px rgba(40, 58, 68, 0.4);
  transition:
    box-shadow 0.45s var(--e),
    border-color 0.45s var(--e),
    translate 0.45s var(--e-out);
}
/* Both faces are pushed off the middle plane, so the card is 7px of stock
   rather than a sheet with a picture on each side. It shows while it turns. */
.flip-front { transform: translateZ(3.5px); }
.flip-back  { transform: rotateY(180deg) translateZ(3.5px); }

.entity:hover .flip-face {
  border-color: rgba(40, 58, 68, 0.18);
  translate: 0 -4px;
  box-shadow:
    0 1px 0 0 var(--edge),
    0 2px 0 0 var(--edge),
    0 3px 0 0 var(--edge),
    0 4px 0 0 var(--edge),
    0 5px 0 0 var(--edge),
    0 6px 0 0 var(--edge),
    0 7px 0 0 var(--edge),
    0 8px 0 0 var(--edge-deep),
    0 34px 52px -16px rgba(40, 58, 68, 0.46);
}
@media (prefers-reduced-motion: reduce) {
  .entity:hover .flip-face { translate: none; }
}

.flip-front { padding: 0; }
.flip-back .entity-k { color: var(--muted); }

/* The label covers its whole face, so anywhere on the card flips it; the cue
   is pushed to the bottom of that cover rather than sitting in the middle. */
.flip-hit {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: flex;
  align-items: flex-end;
  gap: 0.5em;
  padding: inherit;
  cursor: pointer;
  font-size: 0.625rem;
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--muted);
  transition: color 0.4s var(--e);
  -webkit-tap-highlight-color: transparent;
}
.entity:hover .flip-hit { color: var(--fg); }

/* The front's cue sits in the middle of the picture and carries its own
   ground, because what is behind it is a photograph nobody has supplied yet:
   a colour that works on the placeholder could vanish on the real image. A
   solid pill is legible over anything. */
.flip-front .flip-hit {
  z-index: 3;
  align-items: center;
  justify-content: center;
  padding: 0;
}
.flip-cue {
  display: inline-flex;
  align-items: center;
  gap: 0.7em;
  padding: 0.85em 1.5em;
  background: color-mix(in srgb, var(--deep) 88%, transparent);
  color: var(--bg);
  border-radius: 999px;
  box-shadow: 0 14px 28px -14px rgba(14, 24, 30, 0.6);
  backdrop-filter: blur(3px);
  transition: opacity 0.4s var(--e), translate 0.5s var(--e-out), background-color 0.4s var(--e);
}
.flip-cue i {
  width: 0.44em;
  height: 0.44em;
  border-top: 1.5px solid currentColor;
  border-right: 1.5px solid currentColor;
  rotate: 45deg;
}
.entity:hover .flip-cue { background: var(--deep); }

/* Asked for on hover, but a phone has no hover, and a card that never says
   it can be tapped is a card nobody taps. So it hides only where hovering is
   actually possible, and stays put everywhere else. */
@media (hover: hover) and (pointer: fine) {
  .flip-cue { opacity: 0; translate: 0 8px; }
  .entity:hover .flip-cue,
  .flip-input:focus-visible ~ .flip-inner .flip-cue { opacity: 1; translate: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .flip-cue { translate: none !important; }
}

/* Visually hidden, but read aloud. .flip-input is here rather than in its own
   identical block beside the cards: the checkbox that turns a card has to be
   reachable by keyboard and invisible on screen, which is the same problem
   this solves for the card's hidden name. */
.vh,
.flip-input {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
/* a chevron of two borders, pointing the way the card will turn */
.flip-hit i {
  width: 0.42em;
  height: 0.42em;
  border-top: 1.5px solid currentColor;
  border-right: 1.5px solid currentColor;
  rotate: 45deg;
  align-self: flex-end;
  margin-bottom: 0.28em;
}
/* the arrow points the way the card is about to turn */
.to-back i  { rotate: 45deg; }
.to-front i { rotate: 225deg; }

/* the rows sit above the cover so they stay selectable */
/* The front is the picture and nothing else. It fills the face edge to edge,
   so its shape is the card's shape rather than a band's. See README for what
   that means for the source files. */
.flip-shot {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: grid;
  place-items: center;
  overflow: hidden;
  background:
    radial-gradient(120% 90% at 22% 12%, rgba(0, 239, 255, 0.14), transparent 62%),
    var(--bg2);
}
/* With artwork in, that tint would show as a blue frame around the
   letterboxing. The card's own white takes over. */
.flip-shot:has(img) { background: var(--panel); }
/* Absolute, not just 100%: the slot is a centring grid, so its items size to
   their content and height: 100% has no definite box to resolve against. The
   artwork then kept its own 1500x2100 ratio and overflowed the slot by 69px,
   clipped from the top rather than fitted.

   contain, not cover. What arrived is not a photograph but a designed card
   face, a gold ornamental border around a crest, and cover would crop that
   border off at every card shape but one. contain shows the design whole,
   and the letterboxing is invisible because the artwork sits on white and so
   does the card. If a photograph ever replaces these, this wants cover back. */
.flip-shot img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}
/* out of the middle, where the cue sits. They only share the card until a
   real photograph replaces this */
.flip-shot .placeholder {
  place-self: start;
  margin: clamp(1rem, 2.4vw, 1.5rem);
  pointer-events: none;
}

.flip-back .det { position: relative; z-index: 2; pointer-events: none; }
.flip-back .det .v { pointer-events: auto; user-select: text; }
/* room at the foot of the back for the cue the cover puts there */
.flip-back .det { margin-top: 1.1rem; margin-bottom: 1.9rem; }

@media (prefers-reduced-motion: reduce) {
  .flip-inner { transition: none; }
}

/* the cards sit at the top of their row rather than stretching to match */
.two { align-items: start; }
/* the stacked edge and its cast sit below the card, so the row needs the room */
#group .two { padding-bottom: 0.5rem; row-gap: clamp(3rem, 6vw, 4.5rem); }

.contact-block h3 {
  font-family: var(--disp);
  font-weight: 400;
  font-size: clamp(1.35rem, 2.4vw, 1.75rem);
  margin-top: 0.7rem;
}
/* ---------- The cards as a hand, on a narrow screen ----------
   Two cards side by side need 52rem. Below that they are dealt as a hand:
   both in one stage, one squarely in front and the rest fanned behind it.
   Swapping throws the front card out to the right, lifts it and tucks it in
   at the back, the way a card goes to the bottom of a hand.

   Do not reach for position: sticky here. Sticky only holds inside its own
   container and .two is barely taller than one card, so the range is spent
   immediately: at 375x812 neither card pins at all, and 200px into the
   section the second covers the first and never uncovers it. Nothing in the
   arrangement below depends on scroll position, which is the point.

   Every rule here waits on #group.deck-on, which reveal.js sets only once it
   has actually dealt the hand and takes off again above the breakpoint. That
   is deliberate: absolute cards in a stage the script never finished setting
   up would sit exactly on top of one another with no way to swap them. With
   no script at all this stays the ordinary column it has always been, both
   cards fully readable. A deck that needs a script to deal it must not be
   the only way to reach half the content. */
@media (max-width: 51.9375rem) {
  #group.deck-on .two {
    display: block;
    position: relative;
    /* one card tall; the fanned cards are scaled down inside the same box */
    height: min(74svh, 620px);
    padding-bottom: 0;
    /* a drag across is the deck's, a drag down is the page's */
    touch-action: pan-y;
  }
  #group.deck-on .entity {
    position: absolute;
    inset: 0;
    /* the fan's geometry is worked out from the middle of the card */
    transform-origin: 50% 50%;
  }

  /* A card taller than the screen would put its foot out of reach. Measured
     at 375x812 the back came to 1004px against about 700px of usable height,
     so the card is capped and the row list scrolls inside it instead.
     overscroll-behavior: contain keeps that inner scroll from chaining into
     the page once it hits an end. */
  #group.deck-on .flip-inner,
  #group.deck-on .flip-face { height: 100%; }
  #group.deck-on .flip-back { overflow: hidden; }
  #group.deck-on .flip-back .det {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    /* it has to receive the touch to scroll, so tapping a row no longer turns
       the card. The cue at the foot still does */
    pointer-events: auto;
  }
  #group.deck-on .flip-shot { flex: none; }

  /* Where each card rests. 0 is the card in hand; 1 and up are fanned behind
     it, each a little smaller, a little lower and turned a little further, so
     the corner of the next one shows past the bottom right and the stack
     reads as a stack before anyone touches it.

     These are an id, two classes and an attribute, which is what it takes to
     outrank the reveal's own "html.js #group .entity.is-in { transform: none }"
     at an id and two classes. Without the margin the reveal's reset would
     flatten the fan the moment the section came into view. */
  #group.deck-on .two .entity {
    transition:
      transform 0.62s var(--e-out) 0.06s,
      opacity 0.85s var(--e-out),
      filter 0.85s var(--e-out);
  }
  #group.deck-on .two .entity[data-card] { z-index: 1; }
  #group.deck-on .two .entity[data-card="0"] {
    z-index: 3;
    transform: none;
  }
  #group.deck-on .two .entity[data-card="1"] {
    z-index: 2;
    transform: translate3d(9px, 13px, 0) rotate(2.6deg) scale(0.955);
  }
  #group.deck-on .two .entity[data-card="2"] {
    z-index: 1;
    transform: translate3d(18px, 26px, 0) rotate(5.2deg) scale(0.912);
  }

  /* The card leaving the hand is on its own path, so its transition is off
     and a keyframe carries it: out to the right and up, then back down into
     the rear of the fan. It lands on exactly the transform that data-card="1"
     holds, so dropping the class at the end changes nothing on screen.

     z-index is stepped inside the animation rather than left to the attribute,
     because the card has to stay over the deck while it is in the air and go
     behind it on the way back down. From the attribute it would move the
     instant the swap began and the card would sink through the one coming
     forward. */
  #group.deck-on .two .entity.is-dealt {
    transition: none;
    animation: deal-away 0.72s var(--e-out) both;
  }
}
@keyframes deal-away {
  0% {
    transform: none;
    z-index: 4;
  }
  26% {
    transform: translate3d(22%, -7%, 0) rotate(7deg) scale(1.03);
  }
  52% {
    transform: translate3d(46%, -10%, 0) rotate(14deg) scale(1.03);
    z-index: 4;
  }
  54% { z-index: 1; }
  100% {
    transform: translate3d(9px, 13px, 0) rotate(2.6deg) scale(0.955);
    z-index: 1;
  }
}
/* Asked for less motion, the swap still happens, just all at once.
   reveal.js clears .is-dealt on a timer as well as on animationend, so an
   animation that was switched off here cannot leave the class stuck on. */
@media (max-width: 51.9375rem) and (prefers-reduced-motion: reduce) {
  #group.deck-on .two .entity { transition: none; }
  #group.deck-on .two .entity.is-dealt { animation: none; }
}

/* ---------- The deck control ----------
   Hidden until the hand is dealt, on the same class as the deck itself, so it
   can never offer to page through cards that are sitting in a plain column. */
.deck-nav { display: none; }
#group.deck-on .deck-nav {
  display: block;
  margin-top: 1.6rem;
  text-align: center;
}
.deck-tabs {
  display: flex;
  justify-content: center;
  gap: 0.5rem;
}
.deck-tab {
  flex: 1 1 0;
  min-width: 0;
  padding: 0.72rem 0.5rem;
  border: 1px solid var(--edge);
  border-radius: 999px;
  background: transparent;
  color: var(--muted);
  font-family: inherit;
  font-size: 0.625rem;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  cursor: pointer;
  transition:
    color 0.35s var(--e),
    border-color 0.35s var(--e),
    background-color 0.35s var(--e);
}
.deck-tab:focus-visible {
  outline: 2px solid var(--cyan);
  outline-offset: 3px;
}
/* the card in hand, 11.6:1 on white */
.deck-tab.is-front {
  color: var(--fg);
  border-color: var(--edge-deep);
  background: var(--panel);
}
.deck-hint {
  margin-top: 0.8rem;
  color: var(--muted);
  font-size: 0.8125rem;
}

@media (min-width: 52rem) {
  .two { grid-template-columns: 1fr 1fr; }
  .three { grid-template-columns: repeat(3, 1fr); }
  .three .det-row { grid-template-columns: 1fr; gap: 0.35rem; }
}

/* ---------- The assurance (inverted close) ---------- */
.quote {
  background:
    radial-gradient(80% 60% at 50% 0%, rgba(160, 102, 203, 0.34), transparent 62%),
    var(--deep);
  color: var(--bg);
  padding-block: clamp(6rem, 14vh, 12rem);
}
.quote .wrap { max-width: min(92vw, 62rem); }
.qmark {
  display: block;
  font-family: var(--disp);
  font-size: 4rem;
  line-height: 0.4;
  color: var(--purple-l);
  opacity: 0.95;
  margin-bottom: 1.6rem;
}
.statement {
  margin-top: 1.6rem;
  max-width: 24ch;
  font-style: italic;
  font-size: clamp(1.6rem, 4vw, 3rem);
  line-height: 1.24;
  text-wrap: balance;
}
.qnote {
  margin: 2rem auto 0;
  max-width: 58ch;
  color: var(--dim);
  font-size: 1.0625rem;
}

/* ---------- Footer ---------- */
.site-footer {
  background: var(--deep);
  color: rgba(250, 254, 255, 0.66);
  padding-block: clamp(2.5rem, 5vw, 3.5rem);
}
/* A wrapping flex row with space-between could not hold this: three short
   runs of text and one tall image spread to the corners, baseline-aligned
   against the image, and the last line dropped to a second row on its own at
   the far left. A centred column has one axis and cannot come apart. */
.footer-col {
  display: grid;
  justify-items: center;
  gap: 0.7rem;
  text-align: center;
  font-size: 0.6875rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}
.footer-line {
  margin: 0;
  /* not ch: at 0.18em tracking a character is about 1.4ch wide, so a ch-based
     cap wrapped both lines in half. Measured, the longer line needs ~460px. */
  max-width: min(100%, 32rem);
  line-height: 1.7;
}
.site-url {
  margin-top: 0.5rem;
  color: var(--cyan);
  letter-spacing: 0.14em;
  border-bottom: 1px solid transparent;
  transition: border-color 0.4s var(--e);
}
.site-url:hover { border-bottom-color: var(--cyan); }

/* Motion. One gesture per section, slow enough to read through.
   Layer 1 runs on load, for the cold open. Layer 2 is scroll-linked and
   wrapped in @supports, so a browser without scroll-driven animation
   renders the finished state instead. */

@keyframes mask-up { from { transform: translate3d(0, 112%, 0); } to { transform: none; } }
@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes rise-in { from { opacity: 0; transform: translate3d(0, 28px, 0); } to { opacity: 1; transform: none; } }
@keyframes track-in { from { opacity: 0; letter-spacing: 0.5em; } to { opacity: 1; letter-spacing: 0.3em; } }
@keyframes flank-in { from { transform: scaleX(0); } to { transform: scaleX(1); } }
}
@keyframes caustics {
  from { transform: translate3d(-2%, -1%, 0) scale(1); }
  to { transform: translate3d(2%, 2%, 0) scale(1.09); }
}
@keyframes cue {
  0% { transform: translateY(-110%); }
  100% { transform: translateY(260%); }
}

/* the heading clips, the line inside it travels: clip-path rather than
   overflow, which would turn the heading into a scroll container */
.open h1, h2.disp { clip-path: inset(-0.45em 0 0 0); padding-bottom: 0.22em; margin-bottom: -0.22em; }
.ln { display: block; }
.eyebrow-flank::before { transform-origin: right center; }
.eyebrow-flank::after { transform-origin: left center; }

/* ---------- Layer 1: the cold open, on load ---------- */
@media (prefers-reduced-motion: no-preference) {
  .open-bg::before { animation: caustics 24s ease-in-out infinite alternate; }
  .scrollcue i { animation: cue 2.6s cubic-bezier(0.7, 0, 0.3, 1) infinite; }
  .open .eyebrow { animation: track-in 1400ms var(--e-out) 200ms both; }
  .open .eyebrow-flank::before,
  .open .eyebrow-flank::after { animation: flank-in 1200ms var(--e-out) 420ms both; }
  .open h1 .ln { animation: mask-up 1500ms var(--e-out) 260ms both; }
  .open-sub { animation: rise-in 1200ms var(--e-out) 700ms both; }
  .open-cta { animation: rise-in 1200ms var(--e-out) 860ms both; }
  .open-bg img { animation: fade-in 1600ms var(--e-out) 200ms both; }
  .scrollcue { animation: fade-in 1400ms var(--e-out) 1200ms both; }
}

/* anime.js drives the cold-open headline when it is loaded, so the CSS
   mask-up below would otherwise fight it, because a CSS animation outranks the
   inline styles anime writes. This class is only set once anime.js has
   actually been found, so without it the CSS animation still runs. */
html.anime .open h1 .ln { animation: none; }
html.anime .open h1 .ln [data-char] { will-change: transform, opacity; }

/* ---------- Layer 2: scroll-triggered ----------
   Driven by reveal.js, which puts "js" on <html> before the body paints and
   then adds .is-in to each block as it enters the viewport.

   Every hidden state below is scoped to html.js. If the script is blocked,
   fails, or JavaScript is off, none of these rules apply and the page
   renders exactly as it does with every reveal finished. The animation is
   decoration, never a precondition for reading the page.

   This replaced a pure-CSS version built on animation-timeline: view(),
   which only ran in Chrome/Edge 115+, Firefox 144+ and the newest Safari.
   Everywhere else the reveals silently did nothing. */

html.js .site-header {
  background-color: transparent;
  border-bottom-color: transparent;
  transition: background-color 0.5s var(--e), border-color 0.5s var(--e), color 0.5s var(--e);
}
/* Transparent only while it is over the cold open, and the cold open is now
   dark. Scoped to html.js: without the script the header keeps its own light
   ground and dark text, so this can never strand light type on light. */
html.js body:has(.open-bg img) .site-header:not(.stuck) { color: var(--bg); }
html.js body:has(.open-bg img) .site-header:not(.stuck) .lang-switch {
  border-color: rgba(250, 254, 255, 0.5);
}
html.js .site-header.stuck {
  background-color: color-mix(in srgb, var(--bg) 86%, transparent);
  border-bottom-color: var(--line);
}

/* ---------- 06 the organisation ---------- */
/* Three tiers, no connector lines. The source chart draws every line from the
   bottom of one box, which would put three operational posts under the
   supervisory commissioner rather than the director. See HANDOVER.md §1.5.
   Tiers state the levels the chart is unambiguous about and assert nothing
   about who reports to whom. */
.tiers {
  display: grid;
  gap: clamp(3rem, 6vw, 4.5rem);
  margin-top: clamp(3rem, 6vw, 4.5rem);
}
.tier {
  display: grid;
  gap: clamp(1.2rem, 2.6vw, 1.8rem);
  justify-items: center;
}
/* one, two or three across; the single-person tier stays portrait-width */
.people {
  display: grid;
  grid-template-columns: 1fr;
  gap: clamp(1.6rem, 3.4vw, 2.6rem);
  justify-items: center;
  /* .tier centres its items, which would shrink this to its content width and
     leave max-width and the column counts with nothing to act on */
  justify-self: stretch;
  width: 100%;
  max-width: 16rem;
  margin-inline: auto;
}
/* one portrait is 16rem wide; the rows are that, doubled, or trebled, plus
   the gaps between them, so the columns stay the same width at every tier */
.people-1 { max-width: 16rem; }
.people-2 { max-width: 36rem; }
.people-3 { max-width: 56rem; }

.person {
  display: grid;
  gap: 0.55rem;
  width: 100%;
  max-width: 16rem;
  justify-items: center;
  text-align: center;
}

/* ---------- The organisation, on a hexagon lattice ----------
   No photographs here. Six of them would have to be shot, lit and cropped
   alike or the row falls apart, and none exist. The section stands on the
   figure the brand is already made of instead: asset_bg.svg is three
   hexagonal strokes, so the hexagon is the house shape. */
.hexwrap {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  /* Without this the pattern stops dead at the section's edges, which reads
     as a crop against the identically tinted section above. Faded at both
     ends there is no line where it begins or ends. */
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 16%, #000 84%, transparent);
  mask-image: linear-gradient(to bottom, transparent, #000 16%, #000 84%, transparent);
}
/* No viewBox: SVG user units are CSS pixels, and patternUnits="userSpaceOnUse"
   pins the tile to those. A cell is therefore the same size on a phone as on a
   desktop. A fixed viewBox scaled with "slice" does not survive this section:
   at 375×1879 the scale works out at 4.08×, one 302px hexagon filling the
   screen. */
.hexfield { display: block; width: 100%; height: 100%; }
.hexfield path { fill: none; }
.hex-grid { fill: url(#hexlattice-lg); }
.hexfield pattern path {
  stroke: var(--deep);
  stroke-width: 1;
  opacity: 0.075;
}

/* One cell size cannot serve 375px and 1920px: a 222px cell is nine across a
   desktop and one and a half on a phone. Three tiles instead, each with its
   own runners cut to match. The middle step exists only to keep the ladder
   even: going straight from the small tile to the large one halved the cell
   count at the breakpoint (6.4 across to 3.5), which showed as a jump. With
   the middle step in, the two changes are 1.4x and 1.33x.
   Phones get r=40, tablets r=56, desktops r=74. */
.runners-sm, .runners-md { display: none; }
@media (max-width: 48rem) {
  .hex-grid { fill: url(#hexlattice-sm); }
  .runners-lg, .runners-md { display: none; }
  .runners-sm { display: block; }
}
@media (min-width: 48.0625rem) and (max-width: 72rem) {
  .hex-grid { fill: url(#hexlattice-md); }
  .runners-lg, .runners-sm { display: none; }
  .runners-md { display: block; }
}

/* The runners. A short dash chases the perimeter of one cell: the gap is the
   rest of the way round, so only one segment is ever lit. No two periods share
   a factor, so the field never falls into step. */
.hr {
  fill: none;
  stroke-width: 2.5;
  stroke-linecap: round;
}
.runners-lg .hr { stroke-dasharray: 54 390; }
.runners-md .hr { stroke-dasharray: 42 294; }
.runners-sm .hr { stroke-dasharray: 34 206; }
.hr1, .hr4 { stroke: var(--orange); }
.hr2, .hr6 { stroke: var(--cyan-t); }
.hr3, .hr5 { stroke: var(--purple); }
.hr7 { stroke: var(--orange); }
.hr8 { stroke: var(--purple); }

@media (prefers-reduced-motion: no-preference) {
  .hr { animation-timing-function: linear; animation-iteration-count: infinite; }
  .runners-lg .hr { animation-name: hex-run-lg; }
  .runners-md .hr { animation-name: hex-run-md; }
  .runners-sm .hr { animation-name: hex-run-sm; }
  .hr1 { animation-duration: 7s;  animation-delay: -1s;  }
  .hr2 { animation-duration: 11s; animation-delay: -4s;  }
  .hr3 { animation-duration: 9s;  animation-delay: -6s;  }
  .hr4 { animation-duration: 13s; animation-delay: -2s;  }
  .hr5 { animation-duration: 8s;  animation-delay: -5s;  }
  .hr6 { animation-duration: 12s; animation-delay: -9s;  }
  .hr7 { animation-duration: 10s; animation-delay: -3s;  }
  .hr8 { animation-duration: 14s; animation-delay: -7s;  }
}
/* one cycle is exactly one lap, and a lap is 6r */
@keyframes hex-run-lg { to { stroke-dashoffset: -444; } }
@keyframes hex-run-md { to { stroke-dashoffset: -336; } }
@keyframes hex-run-sm { to { stroke-dashoffset: -240; } }

/* Reduced motion keeps the geometry and drops the chase, so the pattern is
   still there, it just holds still. */
@media (prefers-reduced-motion: reduce) {
  .hr { stroke-dasharray: none; opacity: 0.45; }
}

/* Each person is a hexagon carrying their initials. The shape is the same one
   the lattice is built from, one node of it brought forward. */
.node {
  position: relative;
  display: grid;
  place-items: center;
  width: clamp(94px, 12vw, 122px);
  aspect-ratio: 1;
  margin-bottom: 0.65rem;
}
.node svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.node svg path {
  fill: var(--panel);
  stroke: var(--cyan-t);
  stroke-width: 2;
  transition: stroke 0.5s var(--e), fill 0.5s var(--e);
}
.node b {
  position: relative;
  font-family: var(--disp);
  font-weight: 500;
  letter-spacing: 0.06em;
  color: var(--fg);
  line-height: 1;
  font-size: clamp(1.5rem, 2.5vw, 1.9rem);
}
/* One, two or three letters share one box, so each length gets its own size.
   The two-letter case is the default, which also catches any length that has
   no rule of its own. */
.ini-1 b { font-size: clamp(2rem, 3.4vw, 2.6rem); }
.ini-2 b { font-size: clamp(1.5rem, 2.5vw, 1.9rem); }   /* also the default */
.ini-3 b { font-size: clamp(1.15rem, 2vw, 1.5rem); }

/* the tier reads in the stroke: supervisory, leadership, day to day */
.tier:nth-child(1) .node svg path { stroke: var(--purple); }
.tier:nth-child(2) .node svg path { stroke: var(--orange); }
.tier:nth-child(3) .node svg path { stroke: var(--cyan-t); }  /* --cyan is a fill colour; as a 2px line on near-white it disappears */
.tier:nth-child(1) .person-r { color: var(--purple-t); }
.tier:nth-child(3) .person-r { color: var(--cyan-t); }


.person-r {
  font-size: 0.625rem;
  font-weight: 600;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--orange-t);
}
.person-n {
  margin: 0;
  font-size: clamp(1.05rem, 2.1vw, 1.35rem);
  line-height: 1.25;
  text-wrap: balance;
}

/* The three-across row waits for 48rem. Letting it in at 40rem squeezed the
   columns to 175px, narrower than a portrait wants to be. */
@media (min-width: 40rem) {
  .people-2 { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 48rem) {
  .people-3 { grid-template-columns: repeat(3, 1fr); }
}

@media (prefers-reduced-motion: no-preference) {
  /* the shared hidden state: every block a segment can hold */
  html.js :is(.reveal, .quote) :is(.eyebrow, .lede, .panel, .copy, .note,
      .mission-item, .motto, .carousel, .stage, .entity, .creed, .act,
      .contact-block, .person, .qmark, .qnote) {
    opacity: 0;
    transform: translate3d(0, 30px, 0);
    transition:
      opacity 0.85s var(--e-out),
      transform 0.95s var(--e-out),
      filter 0.85s var(--e-out),
      clip-path 0.9s var(--e-out),
      letter-spacing 0.9s var(--e-out);
  }
  html.js :is(.reveal, .quote) :is(.eyebrow, .lede, .panel, .copy, .note,
      .mission-item, .motto, .carousel, .stage, .entity, .creed, .act,
      .contact-block, .person, .qmark, .qnote).is-in {
    opacity: 1;
    transform: none;
    filter: none;
    clip-path: none;
  }

  /* eyebrows settle out of wide tracking, their hairlines draw outward */
  html.js :is(.reveal, .quote) .eyebrow {
    transform: none;
    letter-spacing: 0.5em;
  }
  html.js :is(.reveal, .quote) .eyebrow.is-in { letter-spacing: 0.34em; }
  html.js :is(.reveal, .quote) .eyebrow-flank::before,
  html.js :is(.reveal, .quote) .eyebrow-flank::after {
    transform: scaleX(0);
    transition: transform 0.9s var(--e-out) 0.1s;
  }
  html.js :is(.reveal, .quote) .eyebrow-flank.is-in::before,
  html.js :is(.reveal, .quote) .eyebrow-flank.is-in::after { transform: scaleX(1); }

  /* headings rise out from behind their own clip */
  html.js .reveal :is(.head h2, .center > h2) .ln {
    transform: translate3d(0, 112%, 0);
    transition: transform 1s var(--e-out);
  }
  html.js .reveal :is(.head h2, .center > h2).is-in .ln { transform: none; }

  /* 01 vision & mission */
  html.js #vision .mission-item:nth-child(2) { transition-delay: 0.09s; }
  html.js #vision .mission-item:nth-child(3) { transition-delay: 0.18s; }
  html.js #vision .motto { filter: blur(9px); transform: translate3d(0, 18px, 0); }

  /* intro: the hairline draws down after the statement */
  html.js #intro .rule {
    transform: scaleY(0);
    transition: transform 0.9s var(--e-out) 0.25s;
  }
  html.js #intro .rule.is-in { transform: scaleY(1); }

  /* 02 flag & scope: panels open, copy arrives from the far side */
  html.js #flag-scope .panel { transform: scale(1.07); }
  html.js #flag-scope .feat .copy { transform: translate3d(38px, 0, 0); }
  html.js #flag-scope .feat.rev .copy { transform: translate3d(-38px, 0, 0); }
  html.js #flag-scope .note { filter: blur(9px); transform: translate3d(0, 18px, 0); }

  /* the gallery ring settles out of a slow push */
  html.js #gallery .carousel { transform: scale(1.07); transition-duration: 1.2s; }

  /* 03 the port call */
  html.js #services .head h2 .ln { transform: none; }
  html.js #services .head h2 { opacity: 0; filter: blur(9px); transition: opacity 0.9s var(--e-out), filter 0.9s var(--e-out); }
  html.js #services .head h2.is-in { opacity: 1; filter: none; }
  html.js #services .stage-k { letter-spacing: 0.5em; transition: letter-spacing 0.9s var(--e-out) 0.1s; }
  html.js #services .stage.is-in .stage-k { letter-spacing: 0.3em; }

  /* 04 coverage: the tiles come up row by row */
  html.js #coverage .head h2 .ln { transform: none; clip-path: inset(0 100% 0 0); opacity: 0.3; transition: clip-path 1s var(--e-out), opacity 1s var(--e-out); }
  html.js #coverage .head h2.is-in .ln { clip-path: inset(0 0 0 0); opacity: 1; }

  /* 05 the group: out of focus into focus */
  html.js #group .head h2 .ln { transform: none; }
  html.js #group .head h2 { opacity: 0; transform: scale(1.07); transition: opacity 0.9s var(--e-out), transform 0.9s var(--e-out); }
  html.js #group .head h2.is-in { opacity: 1; transform: none; }
  html.js #group .entity { filter: blur(9px); transform: translate3d(0, 18px, 0); }
  html.js #group .entity:nth-child(2) { transition-delay: 0.1s; }

  /* 04 the coverage map: the pins drop onto the island
     The coast draws itself from reveal.js; these are the markers landing
     after it, west to east in the order they sit in the markup. Scoped to
     html.js and hung on .is-in, so a script that never runs leaves six pins
     already in place rather than six holes in a map. */
  /* scale, not transform: transform is what hangs each name off its dot,
     and overriding it here would drop all six names back onto their
     coordinates. The standalone property composes rather than replaces. */
  html.js #coverage .portmap .pin {
    opacity: 0;
    scale: 0.4;
    transition: opacity 0.5s var(--e-out), scale 0.62s var(--e-out);
  }
  html.js #coverage .portmap.is-in .pin { opacity: 1; scale: 1; }
  html.js #coverage .portmap.is-in .pin:nth-of-type(1) { transition-delay: 0.62s; }
  html.js #coverage .portmap.is-in .pin:nth-of-type(2) { transition-delay: 0.71s; }
  html.js #coverage .portmap.is-in .pin:nth-of-type(3) { transition-delay: 0.80s; }
  html.js #coverage .portmap.is-in .pin:nth-of-type(4) { transition-delay: 0.89s; }
  html.js #coverage .portmap.is-in .pin:nth-of-type(5) { transition-delay: 0.98s; }
  html.js #coverage .portmap.is-in .pin:nth-of-type(6) { transition-delay: 1.07s; }
  html.js #coverage .portmap.is-in .pin:nth-of-type(7) { transition-delay: 1.16s; }
  html.js #coverage .portmap.is-in .pin:nth-of-type(8) { transition-delay: 1.25s; }

  /* the activity grid: the right-hand column trails the left */
  html.js #activity .act:nth-child(even) { transition-delay: 0.08s; }

  /* 06 the organisation: each tier settles in turn */
  html.js #org .person { filter: blur(9px); transform: translate3d(0, 22px, 0); }
  html.js #org .people .person:nth-child(2) { transition-delay: 0.1s; }
  html.js #org .people .person:nth-child(3) { transition-delay: 0.2s; }

  /* 07 contact */
  html.js #contact .contact-block:nth-child(2) { transition-delay: 0.09s; }
  html.js #contact .contact-block:nth-child(3) { transition-delay: 0.18s; }

  /* clearing the per-section gestures
     The rules above scope their hidden state with an id (#vision, #flag-scope,
     #group, #gallery), which makes them specificity (1,2,1). The generic
     .is-in reset is (0,4,1), and one id outranks any number of classes, so
     that reset never won for filter or transform. The blocks faded in but
     stayed blurred, scaled or offset. These resets sit at the same id level so
     they actually apply. Any new id-scoped gesture needs a partner here. */
  html.js #vision .motto.is-in,
  html.js #flag-scope .note.is-in,
  html.js #group .entity.is-in,
  html.js #org .person.is-in {
    filter: none;
    transform: none;
  }
  html.js #flag-scope .panel.is-in,
  html.js #gallery .carousel.is-in,
  html.js #flag-scope .feat .copy.is-in,
  html.js #flag-scope .feat.rev .copy.is-in {
    transform: none;
  }

  /* 08 the assurance: the statement lights word by word */
  html.js .quote .qmark { transform: scale(1.07); }
  html.js .statement span { color: var(--wash-dim); transition: color 0.55s linear; }
  html.js .statement.is-in span { color: var(--bg); }
  html.js .statement.is-in span:nth-child(1) { transition-delay: 0.05s; }
  html.js .statement.is-in span:nth-child(2) { transition-delay: 0.14s; }
  html.js .statement.is-in span:nth-child(3) { transition-delay: 0.23s; }
  html.js .statement.is-in span:nth-child(4) { transition-delay: 0.32s; }
  html.js .statement.is-in span:nth-child(5) { transition-delay: 0.41s; }
  html.js .statement.is-in span:nth-child(6) { transition-delay: 0.5s; }
  html.js .statement.is-in span:nth-child(7) { transition-delay: 0.59s; }
  html.js .statement.is-in span:nth-child(8) { transition-delay: 0.68s; }
  html.js .statement.is-in span:nth-child(9) { transition-delay: 0.77s; }
  html.js .quote .qnote { transition-delay: 0.2s; }
}

/* ---------- Reduced motion ---------- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
  .carousel { aspect-ratio: 4 / 5; width: min(100%, 22rem); perspective: none; }
  .carousel-stage { position: static; transform: none; }
  .cface {
    position: static;
    width: 100%;
    transform: none !important;
  }
  .cface-in { position: static; box-shadow: none; transform: none !important; }
  .cface:not(:first-child) { display: none; }
  html.js #gallery { height: auto; padding-block: clamp(6rem, 13vh, 11rem); }
  html.js #gallery .gallery-pin { position: static; height: auto; overflow: visible; }
  .scrollcue { display: none; }
}

/* entity crest
   PT Washa Indonesia Berlayar's roundel, set into the top of its panel. An
   <img> rather than inline markup: 859 paths would cost more than every other
   asset on the page put together, and nothing here needs per-path access. */
.crest {
  position: absolute;
  top: clamp(1.2rem, 2.6vw, 2rem);
  left: clamp(1.2rem, 2.6vw, 2rem);
  width: clamp(76px, 26%, 148px);
  height: auto;
  filter: drop-shadow(0 6px 18px rgba(40, 58, 68, 0.16));
  pointer-events: none;
  user-select: none;
}


/* eight-point frame
   Two squares, the same figure that surrounds the group mark. Drawn here
   rather than lifted from the logo file, where that frame is some seventy
   traced gold shapes with no single outline to stroke. */
.orn {
  display: block;
  width: clamp(52px, 7vmin, 74px);
  height: auto;
  margin: 0 auto clamp(1rem, 2.2vw, 1.6rem);
  fill: none;
  stroke: var(--gold);
  stroke-width: 3;
  stroke-linejoin: round;
  opacity: 0.75;
  overflow: visible;
}

