/* Basalt staging site — navigation model and shared page furniture.
 *
 * Everything visual comes out of the design system bundle. Two things are composed here
 * because the system does not ship them: the mega menus section 5.5 calls for, and a header
 * bar that puts the wordmark, the nav, Login and the CTA on one shared baseline. Both are
 * built from the system's own pieces — Mark, Button, SketchRule, SketchBox, roughLine — and
 * both belong upstream in SiteHeader.
 *
 * Brand-level changes Mike asked for (the type direction, button legibility) are not here:
 * they live in site/apply_ds_changes.py, which patches the design system where type and
 * component skins actually belong.
 */
const DS = window.BasaltDesignSystem_2748d9;

/* Section 5.5 — primary navigation. Three destinations and one CTA.
   Resources (technical briefs, demos, customer proof, insights) is held back from launch —
   Dylan, 7 Sept: "keep our initial launch claims as tight as possible … white papers,
   perspectives, blogs, that's best done over time." It returns as the material exists.
   Why Basalt was removed 15 Sept — Triq V2 walkthrough: Mike, "I'm gonna remove the Why Basalt
   page," since the home page now carries the full pitch on its own (see home.jsx's own note). */
/* Platform, Industries and Compare each open a mega menu whose first item is that section's own
   overview page — Mike, v3 review: the nav label itself used to link straight to that same page
   (Platform → platform.html, which "Platform overview" inside the menu also pointed to), so
   there were two ways to reach one destination and the top-level word did nothing the menu
   didn't already do better. The label is a hover trigger now (see HeaderBar) with no page of its
   own; "X overview" inside the menu is the actual destination. */
/* Mike, 15 Sept — Triq V2 walkthrough: "I'm gonna remove the Why Basalt page" (redundant with the
   home page, see pages.jsx's own note) and "solutions becomes industries" — split into Commercial
   (MSP first, then oil & gas, telecommunications, regulated industries, enterprise
   virtualization) and Government (tactical/DDIL, regional, enterprise), per Luigi's own list on
   that call. Federal/DDIL edge and Remote/resilient edge already existed with real content and
   are refiled under Government rather than rewritten; the rest are new placeholder destinations
   (see review.jsx's IA–YB notes) alongside MSP, which is new and real. */
/* Mike, 16 Sept review: "our mega menus do not help us instinctually understand the options that
   we have" — Platform's own menu read as one flat list of five equally-weighted items, when
   "Platform overview" is actually a different kind of thing (a destination with no depth of its
   own) from the four pages under it. Each section's `links` became `groups`: an ordered list of
   {eyebrow, links} columns, eyebrow rendered above its own column exactly the way a page's own
   `.eyebrow` class already reads (base.css).

   Second pass, same day, wide-monitor review: repeating the same eyebrow text over two adjacent
   columns ("why would you create a third column called Details? That's redundant.") read as three
   separate labeled things, not one. Mike's fix: one eyebrow per *concept*, drawn once, with a
   hand-drawn box (SketchBox, ds/_ds_bundle.js) behind however many columns that concept actually
   needs — "behind all four of them should be some sort of hand-drawn rectangle that combines
   them." A group can now carry `columns: [[...], [...]]` (several columns under one box and one
   eyebrow) instead of a single `links` array — MegaPanel below normalizes `links` to a one-column
   `columns` internally, so every group gets the same boxed treatment: Platform's Overview (1
   column) and Details (2 columns of 2) each in their own box; Industries' Overview/Government/
   Commercial each in their own box (already one column each, so only the box is new here);
   Compare's Overview and Competitors (2 columns of 3) each in their own box. MSP is spelled out as
   "Managed service providers" in this menu — "I don't know what MSP stands for." */
/* Mike, 16 Sept, HourPlus call — Dylan's own competitor-tiering framework: "in the comparison,
   we're supposed to only have three options: VMware, Nutanix, and Red Hat." VMware and Nutanix
   are what a buyer is actually migrating off of; Red Hat is "probably the top-tier alternative to
   Nutanix in our market." Proxmox, Oracle Linux, and HPE Morpheus drop out of the featured
   Competitors list (and the mega menu, the footer, the cost calculator, and the mega comparison
   table) for the same reason Dylan gave: "we're not competing with Proxmox... the audience is
   zero from our perspective" — those three still exist as real pages (see CompareProxmox etc.,
   pages.jsx), reachable from a new "you're probably also considering" callout on the three
   featured pages (Mike: "when they come to the VMware page... by the way, we already know you're
   considering these ones, go check them out"), not deleted the way Resources was held back. */
const NAV = [
  { label: 'Platform', groups: [
    { eyebrow: 'Overview', links: [
      { label: 'Platform overview', href: 'platform.html', detail: 'One control plane and one operating model, from a single node to a global footprint, and a table of contents for the three pages here.' },
    ]},
    /* Technical documentation removed for launch, 17 Sept — see build_pages.py. Down to three
       links, a single column instead of the old 2x2 (matches the Compare group's own single-
       column fix from earlier this round). */
    { eyebrow: 'Details', links: [
      { label: 'Security & trust',       href: 'platform-security.html',  detail: 'Outbound-only management, audit depth, and certification status stated precisely.' },
      { label: 'Pricing & licensing',    href: 'platform-licensing.html', detail: 'Deployment-scoped licensing, so consolidating does not create a new penalty.' },
      { label: 'Migration & evaluation', href: 'platform-migration.html', detail: 'Prove Basalt beside the incumbent before the organization commits to anything.' },
    ]},
  ]},

  /* Dylan Conner, 21 Sept, consolidated feedback email: "Please use these headings consistently
     in the navigation and on the corresponding pages" — a full replacement table for every
     industry label/subtitle, plus renaming "Regional" to "Regional & field operations." Applied
     verbatim; hrefs/slugs are unchanged since he didn't ask for those to move. */
  { label: 'Industries', groups: [
    { eyebrow: 'Overview', links: [
      { label: 'Industries overview', href: 'industries.html', detail: 'Explore Basalt for government missions and commercial infrastructure.' },
    ]},
    { eyebrow: 'Government', links: [
      { label: 'Tactical / DDIL',            href: 'industries-government-tactical-ddil.html', detail: 'Run tactical mission workloads in disconnected environments and across unreliable networks.' },
      { label: 'Regional & field operations', href: 'industries-government-regional.html',      detail: 'Manage compute across government facilities, regional offices, and field locations.' },
      { label: 'Enterprise compute',         href: 'industries-government-enterprise.html',    detail: 'Support agency workloads with centrally managed virtualization across large-scale data center environments.' },
    ]},
    { eyebrow: 'Commercial', links: [
      { label: 'Managed service providers', href: 'industries-msp.html',                          detail: 'Reduce operational overhead by managing customer environments from one platform with dedicated access and tenant isolation.' },
      { label: 'Oil & gas',                 href: 'industries-oil-gas.html',                      detail: 'Support operational workloads across remote production sites and central facilities, even where network access is unreliable.' },
      { label: 'Telecommunications',        href: 'industries-telecommunications.html',           detail: 'Manage widely distributed compute across network sites, edge locations, and data centers.' },
      { label: 'Critical Infrastructure and Medical Industries', href: 'industries-regulated.html', detail: 'Keep control of your infrastructure with governed access and auditable operations.' },
      { label: 'Enterprise virtualization', href: 'industries-enterprise-virtualization.html',    detail: 'Run business workloads on your infrastructure with streamlined management and straightforward, high-value licensing.' },
    ]},
  ]},

  { label: 'Compare', groups: [
    { eyebrow: 'Overview', links: [
      { label: 'Comparison overview', href: 'compare.html', detail: 'Where each alternative is the better answer, and where it isn’t.' },
    ]},
    { eyebrow: 'Competitors', links: [
      { label: 'VMware',  href: 'compare-vmware.html',  detail: 'The bargain changed after you had standardized. Here is what a credible alternative looks like.' },
      { label: 'Nutanix', href: 'compare-nutanix.html', detail: 'Hyperconverged at the core. Now 93% as expensive as VMware, and easier to leave.' },
      { label: 'Red Hat OpenShift Virtualization', href: 'compare-red-hat.html', detail: 'Credible and capable: built for containers first, with virtualization added on top.' },
    ]},
  ]},

];

/* One primary ask, one secondary, everywhere. The primary is the cheap one: it runs on the
   buyer's own numbers, needs no internal permission, and is useful to someone who has never
   heard of Basalt — section 3's Conversion A. The evaluation is the expensive ask and stays
   the secondary, because it spends the sponsor's credibility. */
const CTA = { label: 'Run the numbers', href: 'run-the-numbers.html' };
const CTA_SECOND = { label: 'Plan a controlled evaluation', href: 'evaluation.html' };

/* Section 5.5 — utility navigation lives in the footer so it never competes with the
   buyer's primary decision path. One source of truth: SiteFooter takes the labels, and
   the staging site resolves a label back to its destination on click. */
const FOOTER_SECTIONS = [
  { title: 'Platform', links: [
    ['Platform overview',        'platform.html'],
    ['Security & trust',         'platform-security.html'],
    ['Pricing & licensing',      'platform-licensing.html'],
    ['Migration & evaluation',   'platform-migration.html'],
  ]},
  /* Mike, 16 Sept, second pass: reverses the "split into two columns" call above — "you should
     have one column that says Industries... list your government the way you have it right now
     ... and indent them," so the estate doesn't grow to six footer columns. SiteFooter
     (ds/_ds_bundle.js) renders `links` as a flat list of strings with no sub-heading concept, so
     "Government" and "Commercial" are real links here too — to industries.html#government and
     #commercial (real ids now on IndustriesGovernment/IndustriesCommercial, site/pages.jsx) —
     rather than dead labels, and the actual destinations underneath each one get leading
     non-breaking spaces (U+00A0, since a plain leading space collapses in HTML) for a visual
     indent SiteFooter itself has no prop for.

     Mike, 17 Sept: two of those weren't enough — "it just looks like they're slightly offset...
     I would probably do two tabs" — four now, roughly two tab-widths, so the sub-items read as
     clearly nested rather than nudged. FOOTER_HREFS strips that prefix back off before using a
     label as a lookup key (see below) so the click handler still resolves the real page. */
  { title: 'Industries', links: [
    ['Government',                'industries.html#government'],
    ['    Tactical / DDIL',           'industries-government-tactical-ddil.html'],
    ['    Regional & field operations', 'industries-government-regional.html'],
    ['    Enterprise compute',        'industries-government-enterprise.html'],
    ['Commercial',                'industries.html#commercial'],
    ['    Managed service providers', 'industries-msp.html'],
    ['    Oil & gas',                 'industries-oil-gas.html'],
    ['    Telecommunications',        'industries-telecommunications.html'],
    ['    Critical Infrastructure and Medical Industries', 'industries-regulated.html'],
    ['    Enterprise virtualization', 'industries-enterprise-virtualization.html'],
  ]},
  /* Mike, 16 Sept, HourPlus call: three featured competitors now, not six — see NAV's own note
     above. Proxmox, HPE Morpheus, and Oracle Linux drop out of the footer along with the mega
     menu and the cost calculator; still real pages, just not part of the featured set a visitor
     lands on from utility navigation. "Red Hat" (not the full "Red Hat OpenShift Virtualization")
     is still the footer's own shortened label — see the wrap-and-alignment note this replaced. */
  { title: 'Compare', links: [
    ['VMware',                   'compare-vmware.html'],
    ['Nutanix',                  'compare-nutanix.html'],
    ['Red Hat',                  'compare-red-hat.html'],
  ]},
  { title: 'Company', links: [
    ['Run the numbers',          'run-the-numbers.html'],
    ['Plan a controlled evaluation', 'evaluation.html'],
    ['About Triq',               'about-triq.html'],
    ['Contact',                  'contact.html'],
    ['Documentation login',      'docs-login.html'],
    ['Customer portal',          'customer-portal.html'],
    /* Mike, 14 Sept: "combine your accessibility policy in with the privacy policy and your
       legal policy — it should all be in your privacy policy." One page (site/pages.jsx's
       PrivacyPage) instead of three since then — but the footer itself still listed all three as
       separate rows, which is what actually prompted Mike's confusion on this later review ("you
       made legal, accessibility, and privacy pages that are individual"): the content was already
       merged, the footer just never stopped presenting it as three destinations. One row now,
       "Privacy policy," to privacy.html itself — a visitor can still reach the accessibility and
       legal sections by scrolling or from that page's own contents. */
    ['Privacy policy',           'privacy.html'],
  ]},
];

const FOOTER_COLUMNS = FOOTER_SECTIONS.map(s => ({ title: s.title, links: s.links.map(l => l[0]) }));
const FOOTER_HREFS = new Map(FOOTER_SECTIONS.flatMap(s => s.links.map(([label, href]) => [label.replace(/^ +/, ''), href])));

/* ------------------------------------------------------------- drawn helpers */

/* The lockup's delimiter, reused as the menu's vertical divider.
 *
 * Mark draws the bar that separates the letter from the word as a piece of the same stone the
 * columns are cut from: a rough rectangle with a pale face, hatching clipped inside it, and a
 * pen outline over the top — `DELIM` at {w:14, h:232} against a 300-unit letter, inset 34 units
 * top and bottom, built with roughShape at wob 0.4 / jitter 0.6 and hatch at gap 8 / slope -3.2.
 *
 * Mark keeps `DELIM` and its skin table private, so the recipe is rebuilt here from the same
 * Sketch primitives with the same parameters rather than approximated. Both should be exported
 * from the design system — this is the second thing the site has had to reach past `Mark` for.
 *
 * It is drawn at absolute pixel sizes rather than scaled down from the letter's units: a
 * faithful reduction would put the bar at three pixels wide, where the face, the hatching and
 * the pen all collapse into the single gray line this replaced.
 */
const DELIM = { w: 8, inset: 5, pen: 1.9, gap: 6, seed: 71, hatchSeed: 73 };

/* Mark's own skin table. face and ink match --column-front and --border-ink exactly; the hatch
   tone sits between --pumice-400 and --pumice-500 and has no token, so it is carried literally. */
const MARK_SKIN = { face: 'var(--column-front)', hatch: '#C4CAD8', ink: 'var(--border-ink)' };

function DrawnVRule() {
  const { Sketch } = DS;
  const ref = React.useRef(null);
  const [box, setBox] = React.useState(0);
  const cid = 'dl' + React.useId().replace(/[^a-zA-Z0-9]/g, '');

  React.useLayoutEffect(() => {
    const el = ref.current;
    if (!el || typeof ResizeObserver === 'undefined') return undefined;
    const ro = new ResizeObserver(() => setBox(Math.round(el.getBoundingClientRect().height)));
    ro.observe(el);
    return () => ro.disconnect();
  }, []);

  const art = React.useMemo(() => {
    const H = box - DELIM.inset * 2;
    if (H < 10) return null;
    const half = DELIM.pen / 2;
    const rect = [[half, half], [DELIM.w - half, half], [DELIM.w - half, H - half], [half, H - half]];
    return {
      H,
      shape: Sketch.roughShape(rect, { seed: DELIM.seed, weight: DELIM.pen, wob: 0.4, jitter: 0.6, redraw: 0.15 }),
      hatch: Sketch.hatch(rect, { seed: DELIM.hatchSeed, gap: DELIM.gap, weight: 1, slope: -3.2 }),
    };
  }, [box]);

  return (
    <span ref={ref} aria-hidden="true"
      style={{ position: 'absolute', left: 0, top: 0, bottom: 0, width: DELIM.w }}>
      {art && (
        <svg width={DELIM.w} height={art.H} viewBox={'0 0 ' + DELIM.w + ' ' + art.H}
          style={{ position: 'absolute', top: DELIM.inset, left: 0, display: 'block', overflow: 'visible' }}>
          <defs><clipPath id={cid}><path d={art.shape.fill} /></clipPath></defs>
          <path className="dl-face" d={art.shape.fill} fill={MARK_SKIN.face} />
          <g clipPath={'url(#' + cid + ')'}>
            {art.hatch.map((h, i) => (
              <path key={i} className="dl-hatch" d={h.d} fill={MARK_SKIN.hatch}
                opacity={Math.min(1, h.opacity * 1.5)} />
            ))}
          </g>
          {art.shape.strokes.map((h, i) => (
            <path key={'s' + i} className="dl-ink" d={h.d} fill={MARK_SKIN.ink} opacity={h.opacity} />
          ))}
        </svg>
      )}
    </span>
  );
}

/* ------------------------------------------------------------- read-more pill */

/* Mike, 17 Sept: on the card-grid "table of contents" sections (Platform overview, Industries
   overview, Comparison overview), the whole card looked clickable but only the small "Read more"
   text link at the bottom actually was — "they would probably end up clicking on that graphic and
   then going, why can I not... it's hard to click." His own fix, explicitly not the whole-card
   option: "just make that read more button a pill... the sketched purple, sketched light purple
   box." The design system has no pill/capsule button variant to reach for (Button's three skins
   are all rectangular extruded blocks); Badge's own `accent` tone is exactly that light-lapis
   sketched chip already used elsewhere on the site (the "Staging" badge, etc.) — composing it
   inside a plain anchor gives a real, larger, obviously-clickable target without forking anything
   to invent a shape the system doesn't have. */
/* Mike, 17 Sept: "your little arrow that goes to the right, it's not really an arrow... you have
   to hand-draw your arrow a little bit clearer." The plain "→" glyph, set in Badge's own mono
   font, rendered as a bare horizontal-then-vertical stroke rather than a real chevron — a font
   fallback issue, not something a different Unicode character fixes reliably. Drawn instead, the
   same way every arrowhead on the site already is (see arrowHead(), diagrams-v2.jsx): one
   hand-drawn shaft, one hand-drawn chevron, both via Sketch's own Stroke — real geometry, not a
   character some font may or may not have a decent glyph for. */
function PillArrow() {
  const { Stroke } = DS;
  return (
    <svg width="16" height="12" viewBox="0 0 16 12" aria-hidden="true"
      style={{ display: 'inline-block', verticalAlign: 'middle', marginLeft: 2, overflow: 'visible' }}>
      <Stroke pts={[[0, 6], [10, 6]]} seed={91} weight={1.5} ink="currentColor" />
      <Stroke pts={[[6, 1.5], [11, 6], [6, 10.5]]} seed={92} weight={1.5} ink="currentColor" />
    </svg>
  );
}

function ReadMorePill({ href, children = 'Read more' }) {
  const { Badge } = DS;
  return (
    <a href={href} style={{ display: 'inline-block', textDecoration: 'none' }}>
      <Badge tone="accent">{children}<PillArrow /></Badge>
    </a>
  );
}

/* ---------------------------------------------------------------- header bar */

/* One line. The lockup's baseline is the line every other thing in the bar sits on — nav
   labels, Login, and the CTA label — which is why the bar is laid out on `align-items:
   baseline` rather than centring each group in its own box.

   The lockup itself is the design system's, not ours. Mark was redrawn on 2026-09-01: two pale
   columns standing inside a drawn lowercase b whose proportions are measured off the vendored
   Adventor bold b, extruded under one sun from the upper right. Mark now exports Mark, Lockup
   and Wordmark, all drawn in code from a fixed seed, so the hand-built lockup this file used to
   carry is gone. SiteHeader still centers its groups, which is why the bar is composed here. */

const NAV_LABEL = { font: 'var(--weight-bold) 14px/1 var(--font-body)', letterSpacing: '0.005em' };

/* Lockup renders its letter at about 89% of the height it is given — the drawn overshoot sits
   above the ascender and below the baseline — so 52 gives a 46px letter, clear of the 40px
   floor. Its name sits on y=300 of a -22..316 viewBox, so the baseline is 0.9527 of the way
   down the box and the slack below it is pulled off as negative margin to put the lockup on
   the bar's shared line. */
const LOCKUP_H = 52;
const LOCKUP_BASELINE = 300 / 338 + 22 / 338;   /* = 0.9527 */

function HomeLink({ onMouseEnter }) {
  const { Lockup } = DS;
  return (
    <a href="index.html" aria-label="Basalt home" onMouseEnter={onMouseEnter}
      style={{ borderBottom: 0, display: 'block' }}>
      {/* label={null} — the anchor already names it, so the drawing is decorative */}
      <Lockup tone="paper" height={LOCKUP_H} label={null}
        style={{ marginBottom: -Math.round(LOCKUP_H * (1 - LOCKUP_BASELINE)) }} />
    </a>
  );
}

/* Mike: plain flat bars, not a drawn line — sharp rectangles, no wobble, no hand-drawn treatment
   at all. The one icon on the site that doesn't try to look sketched.

   Mike: the two states should morph into each other, not swap instantly — so this is the same
   three rects in both states (never unmounted, so the transform/opacity transition below actually
   has something to animate between) rather than three bars for closed and two different ones for
   open. transform-box: fill-box makes each rect rotate around its own center regardless of its x/y
   position, so the math below only has to solve one thing: how far each bar's center is from the
   icon's own center (12,12). Top and bottom bars translate to that center and rotate ±45° into the
   two arms of the X; the middle bar has nowhere useful to go, so it just fades. */
function Hamburger({ open, onClick }) {
  const barTransition = { transformBox: 'fill-box', transformOrigin: 'center', transition: 'transform 0.25s ease' };
  return (
    <button type="button" className="nav-hamburger" aria-label={open ? 'Close menu' : 'Open menu'}
      aria-expanded={open} onClick={onClick}>
      <svg viewBox="0 0 24 24" width="24" height="24" style={{ display: 'block' }}>
        <rect x="2" y="3" width="20" height="4" fill="var(--lapis-600)"
          style={{ ...barTransition, transform: open ? 'translateY(7px) rotate(45deg)' : 'none' }} />
        <rect x="2" y="10" width="20" height="4" fill="var(--lapis-600)"
          style={{ transition: 'opacity 0.2s ease', opacity: open ? 0 : 1 }} />
        <rect x="2" y="17" width="20" height="4" fill="var(--lapis-600)"
          style={{ ...barTransition, transform: open ? 'translateY(-7px) rotate(-45deg)' : 'none' }} />
      </svg>
    </button>
  );
}

/* The mobile replacement for the inline nav + mega menus — a dropdown panel, not the mega panel's
   own layout, since there's no hover state to build an accordion needs a tap instead. Platform,
   Industries and Compare expand in place.

   Mike, v4 review: Login and the CTA used to be two more stacked rows, each with its own
   divider — on top of Compare's own row border, that put two lines between Compare and Login
   and a third between Login and the button. One row, side by side, closes on one divider (the
   last nav row's own bottom border) instead of three. Login also carried the same bold-black
   row styling as the nav items, reading as a second primary link; it's the header's own muted
   NAV_LABEL style here, matching what Login actually looks like everywhere else on the site. */
function MobileMenu() {
  const { Button } = DS;
  const [expanded, setExpanded] = React.useState(null);
  return (
    <div className="mobile-menu">
      {NAV.map(item => {
        if (item.groups) {
          const isOpen = expanded === item.label;
          return (
            <div key={item.label} className="mobile-menu-group">
              <button type="button" className="mobile-menu-row mobile-menu-trigger" aria-expanded={isOpen}
                onClick={() => setExpanded(isOpen ? null : item.label)}>
                {item.label}
                <span className="mobile-menu-chevron">{isOpen ? '−' : '+'}</span>
              </button>
              {/* Mike, 16 Sept review: the desktop mega menu now groups links under an eyebrow
                  (Overview / Details, Overview / Government / Commercial) rather than one flat
                  list — the tap-accordion carries the same grouping, just as a small muted label
                  above its own run of links instead of a whole separate column. A group's own
                  `columns` (Platform's Details, Compare's Competitors — desktop-only, since a
                  hand-drawn box makes sense to draw around two columns side by side but not to
                  redraw as a mobile-width single column) flattens into one run of links here —
                  mobile has no side-by-side columns to preserve, so the eyebrow prints once over
                  all of them either way. */}
              {isOpen && (
                <div className="mobile-menu-sublist">
                  {item.groups.map((g, gi) => (
                    <React.Fragment key={gi}>
                      {g.eyebrow && (
                        <div className="eyebrow" style={{ marginTop: gi > 0 ? 14 : 0, marginBottom: 2 }}>{g.eyebrow}</div>
                      )}
                      {(g.columns ? g.columns.flat() : g.links).map(l => (
                        <a key={l.href} href={l.href} className="mobile-menu-sublink">{l.label}</a>
                      ))}
                    </React.Fragment>
                  ))}
                </div>
              )}
            </div>
          );
        }
        return <a key={item.label} href={item.href} className="mobile-menu-row">{item.label}</a>;
      })}
      <div className="mobile-menu-footer">
        <Button size="md" seed={5} href={CTA.href} style={{ marginTop: 0 }}>{CTA.label}</Button>
      </div>
    </div>
  );
}

function HeaderBar({ active, open, setOpen, mobileOpen, setMobileOpen }) {
  const { Button, SketchRule } = DS;
  return (
    <header style={{ position: 'relative', zIndex: 2, display: 'flex', alignItems: 'baseline',
                     gap: 40, padding: '18px var(--gutter) 13px', background: 'var(--surface-card)' }}>
      <SketchRule seed={12} weight={1.8} ink="var(--stone-400)"
        style={{ position: 'absolute', left: 0, right: 0, bottom: -3 }} />
      {/* Mike, v3 review: hovering off a mega-menu trigger onto anything else in the bar (the
          logo, a plain link, the review tab) left the last-opened menu on screen — only the
          trigger spans and the Login/CTA cluster ever closed it. Every other hoverable thing in
          the bar needs the same onMouseEnter, not just the ones that happen to open one. */}
      <HomeLink onMouseEnter={() => setOpen(null)} />
      <nav className="nav-links" style={{ display: 'flex', alignItems: 'baseline', gap: 28 }}>
        {NAV.map(item => {
          /* Mike, 17 Sept: "when I'm on the comparison tab and I mouse over the industries tab or
             the platform tab, I can't see where my mouse is hovering over... whatever the active
             mega menu is showing, we need to fix that." The underline used to track only `active`
             (the current page), so hovering a different trigger opened its panel with no visual
             tie back to the label that opened it — the one indicator on the bar kept pointing at
             wherever you already were, not at what your mouse had open. For a hover-trigger item,
             the underline now also lights up while its own panel is the one open (`open`), not
             just when its page is the current one — the two states matter for different labels
             (Industries can be `active` with a different one currently `open`), so both are
             checked rather than one replacing the other. */
          const on = item.label === active || item.label === open;
          const labelStyle = { ...NAV_LABEL, borderBottom: 0, paddingBottom: 7, whiteSpace: 'nowrap',
                       color: on ? 'var(--text-primary)' : 'var(--text-muted)',
                       boxShadow: 'inset 0 -2px 0 ' + (on ? 'var(--lapis-600)' : 'transparent'),
                       transition: 'var(--transition-control)' };
          /* A section with a mega menu (Platform, Industries, Compare) is a hover trigger only —
             it has no page of its own to navigate to, since "X overview" inside its own menu is
             that destination. */
          if (item.groups) {
            return (
              <span key={item.label} tabIndex={0} role="button" aria-haspopup="true"
                aria-expanded={item.label === open}
                onMouseEnter={() => setOpen(item.label)}
                onFocus={() => setOpen(item.label)}
                style={{ ...labelStyle, cursor: 'default' }}>
                {item.label}
              </span>
            );
          }
          return (
            <a key={item.label} href={item.href} onMouseEnter={() => setOpen(null)} style={labelStyle}>
              {item.label}
            </a>
          );
        })}
      </nav>
      <div className="nav-actions" style={{ marginLeft: 'auto', display: 'flex', alignItems: 'baseline', gap: 18 }}
        onMouseEnter={() => setOpen(null)}>
        {/* Mike, 25 Sept call, reviewing this page live: "Would you like me to remove the login?"
            confirmed, then "Login is gone." Superseded yesterday's fix pointing this at Triq's own
            customer portal (portal.triq.tech/login) — the customer portal's own future is still
            undecided (Donnie: "I don't even know if we keep our current customer portal in its
            current incarnation"), so the nav link comes out rather than getting reskinned or
            redirected while that's unresolved. */}
        {/* Button reserves marginTop for its extrusion depth, which drops the whole block
            below the bar's line. Zeroing it centers the block on the nav text's optical
            middle — a box control aligns to that, not to the baseline — and lets the lit top
            face draw up into the bar's padding instead of pushing the bar taller. */}
        <Button size="md" seed={5} href={CTA.href} style={{ marginTop: 0 }}>{CTA.label}</Button>
      </div>
      {/* Mike: below the breakpoint where the inline nav used to just wrap onto extra lines,
          this replaces it outright rather than reflowing it. */}
      <Hamburger open={mobileOpen} onClick={() => setMobileOpen(!mobileOpen)} />
    </header>
  );
}

/* ------------------------------------------------------------------ mega menu */

/* The panel is one drawn shape, not a rectangle with a drawn line laid over it: SketchBox
   fills and inks the same wobbling outline, so the sheet's bottom edge *is* the sketch line
   and no paper can show between the two. Its top edge is tucked up behind the bar. */
/* Mike, 16 Sept review, second pass: one eyebrow per group, drawn once above a hand-drawn box
   (SketchBox) that holds however many columns that group actually needs — see the NAV comment
   above for the full "why." `g.columns` (an array of link-arrays) is the general case; a plain
   `g.links` array is normalized to a single-column `columns` here rather than duplicated as its
   own code path, so Industries' single-column Government/Commercial groups get the exact same
   box treatment as Platform's two-column Details and Compare's two-column Competitors. The outer
   grid's own track count is the sum of every group's own column count, and each group's wrapper
   spans exactly as many tracks as it has columns, so a two-column group sits exactly twice as
   wide as a one-column one instead of getting an equal-width slot it doesn't need. */
function MegaPanel({ section }) {
  const { SketchBox } = DS;
  const groups = section.groups.map(g => ({ eyebrow: g.eyebrow, columns: g.columns || [g.links] }));
  const totalTracks = groups.reduce((sum, g) => sum + g.columns.length, 0);
  return (
    <div style={{ position: 'absolute', top: '100%', left: 0, right: 0, zIndex: 1 }}>
      <SketchBox className="mega-panel" seed={41} weight={1.9} redraw={0.3}
        ink="var(--stone-400)" fill="var(--surface-raised)"
        style={{ marginTop: -18, paddingTop: 50, paddingBottom: 38,
                 paddingLeft: 'var(--gutter)', paddingRight: 'var(--gutter)' }}>
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(' + totalTracks + ', minmax(190px, 1fr))',
                      columnGap: 28, rowGap: 28, alignItems: 'start' }}>
          {groups.map((g, gi) => (
            <div key={gi} style={{ gridColumn: 'span ' + g.columns.length }}>
              {g.eyebrow && <div className="eyebrow" style={{ marginBottom: 14 }}>{g.eyebrow}</div>}
              <SketchBox seed={53 + gi * 7} weight={1.4} redraw={0.4} ink="var(--stone-300)" fill="none"
                style={{ padding: '22px 24px' }}>
                <div style={{ display: 'grid', gridTemplateColumns: 'repeat(' + g.columns.length + ', 1fr)',
                              columnGap: 32, rowGap: 28 }}>
                  {g.columns.map((colLinks, ci) => (
                    <div key={ci} style={{ display: 'grid', rowGap: 28 }}>
                      {colLinks.map(l => (
                        <a key={l.href} href={l.href} className="mega-link"
                          style={{ display: 'block', position: 'relative', paddingLeft: DELIM.w + 16, borderBottom: 0 }}>
                          <DrawnVRule />
                          <span style={{ display: 'block', font: 'var(--weight-bold) 16px/1.25 var(--font-display)',
                                         letterSpacing: 'var(--tracking-heading)', color: 'var(--text-primary)' }}>{l.label}</span>
                          <span style={{ display: 'block', font: 'var(--type-body-sm)', color: 'var(--text-muted)',
                                         marginTop: 6, maxWidth: 330 }}>{l.detail}</span>
                        </a>
                      ))}
                    </div>
                  ))}
                </div>
              </SketchBox>
            </div>
          ))}
        </div>
      </SketchBox>
    </div>
  );
}

function SiteNav({ active }) {
  const [open, setOpen] = React.useState(null);
  const [mobileOpen, setMobileOpen] = React.useState(false);
  const section = open ? NAV.find(n => n.label === open) : null;
  /* Crossing back above the breakpoint with the mobile menu open would otherwise leave it stuck
     open behind the now-visible inline nav — nothing currently listening for that resize. */
  React.useEffect(() => {
    if (!mobileOpen) return undefined;
    const mq = window.matchMedia('(min-width: 901px)');
    const onChange = () => { if (mq.matches) setMobileOpen(false); };
    mq.addEventListener('change', onChange);
    return () => mq.removeEventListener('change', onChange);
  }, [mobileOpen]);
  return (
    <div style={{ position: 'relative', zIndex: 30 }} onMouseLeave={() => setOpen(null)}>
      <HeaderBar active={active} open={open} setOpen={setOpen} mobileOpen={mobileOpen} setMobileOpen={setMobileOpen} />
      {section && <MegaPanel section={section} />}
      {mobileOpen && (
        <div style={{ position: 'absolute', top: '100%', left: 0, right: 0, zIndex: 1 }}>
          <MobileMenu />
        </div>
      )}
    </div>
  );
}

/* ---------------------------------------------------------- section divider */

/* Mike, v3 review: sections already alternate white and light-purple backgrounds full width —
   clear on their own — but the one hand-drawn rule on the site (end of the homepage hero) was a
   normal-flow element, inset by that section's own gutter padding like any other content. Next
   to a full-bleed color change, an inset line reads as a mistake, not a second kind of divider.
   This one runs edge to edge the same way the header's own rule does: absolutely positioned
   inside a zero-height, position:relative strip, rather than inside either section it sits
   between. Used between every pair of sections on a page, never above the first or below the
   last (DecisionBand's own strong color is boundary enough there). */
/* Mike, 14 Sept: two of these now sit between a pair of DARK sections in a row (each Compare
   page's own cost calculator into its "what X got right" block, both --surface-inverse) — "you've
   got two dark sections next to each other... a light gray or white line." stone-400 is tuned for
   the far more common light-to-light/light-to-dark seam and reads muddy against dark on both
   sides. `tone="inverse"` swaps in a light ink instead — pass it only where both neighboring
   sections are dark. */
function SectionDivider({ seed = 7, tone = 'paper' }) {
  const { SketchRule } = DS;
  return (
    <div style={{ position: 'relative', height: 0 }}>
      <SketchRule seed={seed} weight={1.6} ink={tone === 'inverse' ? 'rgba(237,239,245,.35)' : 'var(--stone-400)'}
        style={{ position: 'absolute', left: 0, right: 0, top: 0 }} />
    </div>
  );
}

/* ------------------------------------------------- closing decision section */

/* Section 5.5, step 8: run the numbers, then try the platform. */
/* Mike, 15 Sept — Triq V2 walkthrough: "run the numbers or run the platform... I don't like that."
   Landed on changing the OR to a THEN — Luigi: "I think at the bottom of the page it should be
   'try it out'... if someone scrolled down, moved their mouse wheel at least twice, they're
   interested... something along the lines of 'run the numbers or try it out,' I don't think it
   has to be too cute." First landed on "run the numbers, then try it out."
   Mike, 16 Sept review (first pass): swapped "it out" for "the platform" and dropped the comma —
   "run the numbers then try the platform." Same-day follow-up, on seeing it live: "you need to
   put the comma back... yes, I'm fine with that comma there." Final: "Run the numbers, then try
   the platform." Eyebrow "Two ways to start" → "Two ways to get started," Mike, 17 Sept. */
function DecisionBand() {
  const { SectionHeader, Button } = DS;
  return (
    <Reviewed id="AB">
    <section style={{ position: 'relative', padding: '84px var(--gutter)', background: 'var(--lapis-700)' }}>
      <SectionHeader align="center" tone="inverse" eyebrow="Two ways to get started"
        title="Run the numbers, then try the platform"
        lead="Run the numbers with your own infrastructure, then put Basalt beside your incumbent in a controlled environment, with a rollback story written down before anything moves." />
      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 12, justifyContent: 'center', marginTop: 30 }}>
        <Button size="lg" variant="inverse" href={CTA.href}>{CTA.label}</Button>
        <Button size="lg" variant="ghost-inverse" href={CTA_SECOND.href}>{CTA_SECOND.label}</Button>
      </div>
    </section>
    </Reviewed>
  );
}

/* SiteFooter renders its links as href="#" — it takes labels, not destinations. Rather
   than fork the component, the staging site resolves the clicked label to its page. */
/* `hideWidget`: Run the numbers (site/calculator.jsx) rebuilt itself as a permanent, always-open
   continuation of the corner widget's own dark box — showing the small collapsed one too on that
   one page would just be the same live numbers a second time, in the corner, while the page's
   own copy of them sits open above. */
function SiteFoot({ hideWidget } = {}) {
  const { SiteFooter } = DS;

  const click = (event) => {
    /* the footer lockup is a drawn SVG now, not an image */
    if (event.target.closest('svg')) { event.preventDefault(); window.location.href = 'index.html'; return; }
    const el = event.target.closest('a');
    if (!el) return;
    const href = FOOTER_HREFS.get((el.textContent || '').trim());
    if (href) { event.preventDefault(); window.location.href = href; }
  };

  return (
    /* site-footer-wrap: SiteFooter (design system) hardcodes a fixed 5-column grid with no
        responsive collapse at all — Mike, v3 review, is the "you didn't design mobile"
        complaint made concrete. The grid div carries no class of its own; site.css targets it
        by its inline grid-template-columns rather than forking SiteFooter to add one.

       The wrapping fragment: every page mounts SiteFoot exactly once (Home, Calculator,
       Placeholder, and every real page component in pages.jsx), which makes it the one true
       common denominator for anything that needs to render site-wide — the live savings widget
       (site/widget.jsx) rides along here rather than being added to each of those ~16 call sites
       individually. Fixed positioning means where SavingsWidget sits in the DOM doesn't matter. */
    <React.Fragment>
      <div className="site-footer-wrap" onClick={click}>
        <SiteFooter columns={FOOTER_COLUMNS}
          blurb="Basalt is the virtualization platform engineers wish they already had: one control plane and one operating model that run the same way from a single edge node to a global enterprise footprint." />
      </div>
      {!hideWidget && <SavingsWidget />}
    </React.Fragment>
  );
}

/* ------------------------------------------------------ placeholder page shell */

/* Every destination in section 5.5 exists so the structure can be reviewed. Most pages are
   still deliberately unbuilt: each states what it is for and what will live on it, so the
   shape of the site can be judged before any of the depth is written.

   `sourced` marks the exception: a handful of pages (Platform overview, Security & trust,
   Migration & evaluation) now carry real blocks drafted straight from the 9/10 call
   rather than generic structure — but Dylan and Donny have not looked at these specific pages
   yet, so the card says "drafted," not "written," "confirmed," or "reviewed." */
function Placeholder({ section, title, lead, purpose, blocks, active, review = {}, sourced = false }) {
  const { SectionHeader, Card, StatRow, Badge, Button } = DS;
  return (
    <div>
      <SiteNav active={active} />
      {!review.hidden && <ReviewBar page={review.page} />}
      <Reviewed id={review.intro}>
      <section style={{ position: 'relative', padding: '80px var(--gutter) 20px', background: 'var(--surface-card)' }}>
        <div style={{ display: 'flex', gap: 10, marginBottom: 20, flexWrap: 'wrap' }}>
          <Badge tone="accent">Staging</Badge>
          <Badge>{sourced ? 'Drafted from the 9/10 call' : 'Structure only'}</Badge>
          {review.hidden && <Badge>Held back from launch: links from nowhere</Badge>}
        </div>
        <SectionHeader eyebrow={section} title={title} lead={lead} />
      </section>
      </Reviewed>
      <Reviewed id={review.blocks} top={8}>
      <section style={{ position: 'relative', padding: '44px var(--gutter) 88px', background: 'var(--surface-card)' }}>
        <div className="split" style={{ display: 'grid', gridTemplateColumns: '1fr .78fr', gap: 56, alignItems: 'start' }}>
          <div>
            <h3 style={{ marginBottom: 8 }}>What this page has to do</h3>
            <p style={{ font: 'var(--type-body-lg)', color: 'var(--text-secondary)', maxWidth: 'var(--container-text)' }}>{purpose}</p>
            <StatRow style={{ marginTop: 30 }} items={blocks} />
          </div>
          {sourced ? (
            <Card eyebrow="Drafted, not yet reviewed" title="A real first draft, not placeholder text" meta="staging · structure review">
              These claims are sourced from the 9/10 call, the same material the homepage draws
              on, not invented for this page. Dylan and Donny have not looked at this specific
              page yet, so treat it the way any homepage section still needing a pass would be
              treated: read it, then say what's right and what needs to change.
              <div style={{ marginTop: 20 }}>
                <Button variant="secondary" href="index.html">Back to the home page</Button>
              </div>
            </Card>
          ) : (
            <Card eyebrow="Not yet written" title="This page is a placeholder" meta="staging · structure review">
              The home page is built. Every other destination in the recommended hierarchy exists
              as a page so the navigation, the depth, and the buyer's path can be reviewed before
              the writing starts.
              <div style={{ marginTop: 20 }}>
                <Button variant="secondary" href="index.html">Back to the home page</Button>
              </div>
            </Card>
          )}
        </div>
      </section>
      </Reviewed>
      <DecisionBand />
      <SiteFoot />
    </div>
  );
}
