/* THE MASTHEAD TELEMETRY STRIP — Hadrian composition (REFS-industrial-web.md #2).
   Hairline-boxed mono cells, permanent, in the nav. The loudest cheap signal that this
   company instruments things. Values are LAST-SYNC stamps, never invented live figures. */
.tele{border-bottom:1px solid var(--hairline);background:rgba(11,21,32,.9)}

/* THE STRIP MUST NOT HIDE ITS OWN FACTS. Measured 2026-08-22 (file://, HeadlessChrome
   151, webfonts loaded) at 390px: .tele .wrap scrollWidth 1124 vs clientWidth 390 —
   FOUR of the five facts sat off-screen, and `scrollbar-width:none` plus a
   `::-webkit-scrollbar{display:none}` rule deleted the only affordance that anything
   was there. Worse: Chrome makes a keyboard-scrollable container a tab stop, so the
   SECOND thing a keyboard user reached — straight after the skip link — was this DIV,
   AX role `generic`, whose computed name was a 120-character run-on of all five facts
   with nothing to say it scrolled. It persisted to 1200px (4px still hidden), NOT to
   1080px as first reported; only at 1240px+ does the row fit.

   Wrapping closes BOTH halves at once, which is why it beats restoring a scrollbar:
   nothing is off-screen, so the element stops being a scroll container, so it stops
   being a tab stop, so no unnamed stop is left to need a name and a role. The two
   suppression declarations are DELETED rather than overridden — left in place they
   would keep a hidden scroller one CSS edit away.

   `flex-wrap` is UNCONDITIONAL on purpose: it is a no-op at widths where the row
   already fits, so correctness never depends on guessing a breakpoint right.
   `overflow-x` returns to visible so a future over-long cell fails LOUDLY instead of
   silently hiding itself again. The leading tick is unconditional for the same reason:
   once the row wraps, `span+span`'s border-left orphans at every row start (measured —
   an indented hairline left dangling beneath row 1), so every span carries the rule and
   each row opens on it. The media query tunes DENSITY only; if it ever fails to match,
   the strip is still correct, merely taller. */
.tele .wrap{display:flex;gap:0;align-items:stretch;flex-wrap:wrap;overflow-x:visible}
/* THE CELLS FILL THEIR ROW, 2026-08-27. The wrap above is the safety valve and stays; what
   was missing is what happens to a row AFTER it wraps. Measured across all 25 pages at
   1101/1280/1440/1920: the cells stopped short of the wrap's right rail on EVERY page —
   36 to 347px of unbordered blank on the fifteen that fit in one row, and 783 to 916px of
   blank beside a single orphaned cell on the nine that wrap. 12,046px of it site-wide at
   1440. The strip's own bottom border runs the full width, so that blank is BOUNDED: on
   the nine it reads as a cell whose contents failed to arrive, which is the same shape as
   the empty `.flow` cell cured in proof.css today.

   `flex:1 1 auto` grows each cell into its own row's slack. `flex-basis` stays `auto`, so a
   cell never shrinks below its content and NOTHING wraps internally that did not already —
   the measured heights are identical to before at all four widths (mean 42.2/50.3, max
   62/74), which is the evidence for that claim. Blank goes to 0 on every page, and the
   orphaned cell grows from 196-329px to the full 1112px, so it becomes a closing row
   rather than a fragment with a hole beside it.

   `flex:1 1 0` WAS MEASURED AND REJECTED, and only the screenshots caught it. On paper it
   won everything: 0 wrapping pages at every width (against 9-13), 0 blank, +4px mean
   height. Equal basis makes every cell the same width, so every cell wraps INTERNALLY —
   and what that does to a two-part fact is not visible in a row count. On index.html it
   set "FIGURES DATED" against "2026-05-" / "25", breaking a date across a line, and split
   the label from its value in all five cells with the two vertically misaligned. It made
   the fifteen good pages worse to fix the nine. Rows were never what a reader sees; the
   bounded empty box was.

   CONTENT-INDEPENDENT ON PURPOSE. The density query further down this file (9px padding,
   12px type) is a number measured against the strings of the day, and nine pages have
   since outgrown it — the same rot the `white-space` valve above documents about its own
   deleted 359px threshold. This rule has no number in it: a longer fact string wraps its
   own cell and never pushes a neighbour onto a second row. */
.tele .wrap>span{flex:1 1 auto;min-width:0}
.tele span{font-family:var(--f-mono);font-size:12.5px;letter-spacing:.14em;text-transform:uppercase;
  color:var(--ink-ghost);padding:9px 18px;white-space:nowrap;display:flex;align-items:center;gap:8px;
  border-left:1px solid var(--hairline)}
@media (max-width:1300px){.tele span{padding-top:6px;padding-bottom:6px}}
/* Safety valve for the `overflow-x:visible` above: the widest single cell can be wider
   than the content box, and visible overflow means it SPILLS rather than hides. `normal`
   only breaks a line that cannot fit, so wherever the cell fits this changes nothing. It
   exists so the loud-failure choice above degrades into a wrapped cell instead of a
   horizontal page scroll.

   THE THRESHOLD IS GONE, AND ITS ABSENCE IS THE FIX. This shipped as
   `@media (max-width:359px)`, a number derived from the widest cell AT THE TIME
   ("PLANT / MINSAWI, KUALA KANGSAR", 263px, spilling at 280px). contact.html then grew a
   longer one — "OFFICES CYBERJAYA · IPOH · COIMBATORE", 378px — and sailed straight over
   a ceiling set for a shorter string. Measured on the local tree with scripting OFF:
   contact.html scrolled the DOCUMENT sideways by 38px at 360, 23px at 375 and 8px at 390.
   Only with JS off, because tele-collapse.js closes the strip on a narrow screen and the
   markup ships it open — so the one reader who cannot get the collapse got the overflow.
   A threshold measured against content rots the moment the content changes; no threshold
   cannot. Both candidates were measured across 13 widths x 5 pages x JS on/off:

     as shipped          contact.html +38 / +23 / +8 at 360 / 375 / 390 (no-JS)
     valve raised to 430 no page scroll anywhere
     no query at all     no page scroll anywhere — and byte-identical to the row above at
                         EVERY width on all five pages, desktop strip height 32/38/38 px at
                         1280/1440/1920, unchanged from as-shipped

   The two fixes are indistinguishable in outcome, so take the one with no number in it.
   Cost is one extra line on one cell at 360-412px (strip 180 -> 200px). */
.tele span{white-space:normal}
.tele b{color:var(--ink-subtle);font-weight:500}
.tele .live{color:var(--signal-gold)}

/* backdrop-filter removed 2026-08-22. DESIGN.md bans glassmorphism BY NAME in both §5
   and §8 — "neon glow for a company that sells steel" is the same sentence — and it was
   shipping on the sticky masthead of all six pages with no check looking for it. The
   ground was already 94% opaque, so the blur was doing almost nothing visually; the
   remaining translucency matches .tele, which has always been rgba with no blur.
   Enforced now by the EFFECT rule in qa/motion-lint.py. */
header.site{position:sticky;top:0;z-index:100;background:rgba(11,21,32,.97);
  border-bottom:1px solid var(--hairline)}
/* THE STICKY HEADER WAS DEAD ON EVERY LIVE PAGE, AND ONLY ON THE LIVE ONES.
   A position:sticky box can only travel inside its PARENT's box. In the static tree that
   parent is <body> (11070px tall on the home page) so the header pins at top:0 and stays.
   Shopify renders the same header through {% section 'site-header' %}, which wraps it in a
   shrink-to-fit <div id="shopify-section-site-header"> whose height is the header's own
   84px — so the header had 0px of travel and scrolled away like static content.
   Measured 2026-08-26, 1440x900, with the static tree as the control:
     static  /index.html            parent BODY   11070px tall  rect.top @scrollY 1800 =     0  STUCK
     live    /                      parent DIV       84px tall  rect.top @scrollY 1800 = -1762  GONE
     live    /pages/smart-mill      parent DIV       84px tall  rect.top @scrollY 1800 = -1762  GONE
   Promoting the wrapper to the sticky element restores the travel; the inner header stays
   sticky-at-0 inside it, which is a no-op. The rule is INERT in the static tree — no such
   element exists there — so one stylesheet keeps serving both, which is the point. */
#shopify-section-site-header{position:sticky;top:0;z-index:100}
header.site .bar{display:flex;align-items:center;justify-content:space-between;gap:24px;
  padding:15px 0}
/* The logo is the client's registered mark — it must NEVER be scaled non-uniformly.
   `.logo` inherited flex-shrink:1, so it absorbed the header bar's overflow: measured
   15.1% horizontally squashed at 1440/1600/1920, 17.3% at 1100, 36.4% at 1000 and 69.2%
   at 901 (rendered aspect 3.798 against a natural 12.333), with object-fit:fill squashing
   rather than letterboxing. `flex:none` existed but only inside the <=900px queries.
   Base rule now, so no viewport can distort the mark; the header gives elsewhere. */
header.site .logo{flex:none}
header.site .logo img{height:22px;width:auto;object-fit:contain;flex:none}
/* 26px was never affordable: at its true 12.333 aspect that is a 320.7px wordmark, and the
   bar only ever had ~272px for it — which is exactly why it rendered squashed. 22px gives
   271.3px undistorted, the width the squashed version was occupying anyway. */
nav.main{display:flex;gap:26px;align-items:center}
/* `nav.main a` (0,1,1) out-specifies `.btn-primary` (0,1,0), so a CTA placed inside the
   nav inherited --ink-muted and rendered light-on-gold — 1.48:1. Scope it to plain links. */
nav.main a:not(.btn){font-size:14.5px;color:var(--ink-muted);letter-spacing:-0.0017em;
  transition:color var(--d2) var(--e-quad)}
nav.main a.btn-primary,nav.main a.btn-primary:hover,nav.main a.btn-primary:focus{color:#14202C}
@media (hover:hover) and (pointer:fine){nav.main a:not(.btn):hover{color:var(--ink)}}
/* TWO current-states, both site-wide, and only one of them was styled site-wide.
   `page` marks the link whose href IS this URL. `true` marks the SECTION this page sits
   under when the page itself is not in the nav — smart-mill.html:112 marks "Products",
   privacy.html marks "Company", ms/kilang-pintar.html marks "Produk". That second rule was
   written in proof.css, a page-family sheet, and none of those three pages loads it: they
   load smart-mill.css or demo.css. Measured 2026-08-26 at 1440px across all 25 pages —
   three links carrying aria-current computed rgb(169,182,196) (--ink-muted) instead of gold,
   so on those pages the header showed no "you are here" mark at all. The header is shared
   furniture; its states belong in the shared sheet. */
nav.main a[aria-current="page"],nav.main a[aria-current="true"]{color:var(--signal-gold)}
.lang{display:flex;gap:2px;align-items:center;font-family:var(--f-mono);font-size:13px;
  letter-spacing:.1em;text-transform:uppercase}
.lang a{padding:5px 8px;color:var(--ink-ghost);border-radius:2px}
.lang a[aria-current="true"]{color:var(--signal-gold)}
@media (hover:hover) and (pointer:fine){.lang a:hover{color:var(--ink)}}
/* padding:12px 16px (was 9px 13px) -- at font-size:11px/line-height:1.6 the old box
   measured ~37.6px tall, under the 44px touch-target guideline; the blind mobile-critic
   gauntlet flagged this as "worth auditing" beside the EN/BM toggle 2026-08-23. This is
   the only nav control that must open the whole drawer, so it gets the full 44px. */
.navtoggle{display:none;background:none;border:1px solid var(--hairline-strong);border-radius:2px;
  padding:12px 16px;min-height:44px;color:var(--ink);font-family:var(--f-mono);font-size:13px;
  letter-spacing:.12em;text-transform:uppercase;cursor:pointer}
@media (max-width:900px){
  nav.main{display:none}
  nav.main.open{display:flex;position:absolute;left:0;right:0;top:100%;flex-direction:column;
    align-items:flex-start;gap:0;background:var(--mill-navy);border-bottom:1px solid var(--hairline);
    padding:8px var(--gut) 20px}
  nav.main.open a{padding:13px 0;width:100%;border-bottom:1px solid var(--hairline);font-size:16px}
  .navtoggle{display:block}
  header.site .bar{position:relative}
}
footer .cols{display:grid;gap:36px;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));
  padding-bottom:40px;border-bottom:1px solid var(--hairline)}
footer .ftitle{font-family:var(--f-mono);font-size:13px;letter-spacing:.18em;text-transform:uppercase;
  color:var(--ink-ghost);margin:0 0 14px;font-weight:600}
footer a{color:var(--ink-muted);display:block;padding:4px 0;font-size:14.5px}
@media (hover:hover) and (pointer:fine){footer a:hover{color:var(--ink)}}
footer .legal{padding-top:26px;display:flex;flex-wrap:wrap;gap:18px;justify-content:space-between;
  font-family:var(--f-mono);font-size:13px;letter-spacing:.05em;color:var(--ink-ghost)}


/* ═══════════════════════════════════════════════════════════════════════════
   CONSOLIDATED SHELL CORRECTIONS — one definition, in the file that owns the shell.

   These lived PAGE-SCOPED in home.css (Lane A) and smart-mill.css (Lane B), each lane
   patching the shared masthead in its own stylesheet so it would not edit a parallel
   lane underneath it. That was the right call while the lanes ran; it is the wrong
   resting place. The drift it sets up is not hypothetical — it had ALREADY happened
   before this consolidation, measured 2026-08-22:

     * proof.css carries its own third copy of `main[tabindex="-1"]:focus{outline:none}`
       but has NEITHER the anchor fix NOR the footer-grid fix.
     * demo.css and privacy have none of the five at all.
     * The two "identical" copies were NOT identical, and the difference was a live bug
       (see ANCHOR LANDING below).

   Cascade note: page stylesheets load after this file, so any surviving page-scoped
   copy still wins. Every value here is identical to the copy it replaces, so the
   duplicates are inert until deleted — consolidating first and deleting second cannot
   change rendering in between.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── STICKY-HEADER HEIGHT — the only input to anchor landing ──
   Measured per breakpoint on both lanes rather than assumed: 65px below 901, 80px
   from 901 (the mid-width lockup shrinks the CTA), 84px from 1360. It had been a flat
   58px, which is what put anchor jumps 8-12px UNDER the header. The sub-901 value is
   65 and not 64 on purpose — a 1px shortfall is exactly what tucks a cap-line beneath
   a hairline. */
:root{--hdr:65px}
@media (min-width:901px){:root{--hdr:80px}}
@media (min-width:1360px){:root{--hdr:84px}}

/* ── ANCHOR LANDING ──
   THE ONE REAL DIFFERENCE BETWEEN THE TWO LANES, and it was a defect, not noise.
   Lane A scoped this to `[id]` — every anchor target on the page. Lane B scoped it to
   `.st` and `.pair .st` — its station blocks only. Measured on smart-mill.html before
   consolidation, scrollIntoView() clearance of target-top minus sticky-header-bottom:

       #stations   -64px @390   -80px @1000   -84px @1440      (i.e. fully hidden)
       #rail-h     -64px @390

   A negative clearance the exact height of the header means the target sits entirely
   behind it. Lane A's `[id]` is therefore the correct version and is the one adopted;
   it fixes smart-mill's non-station anchors as a side effect, and gives demo, privacy,
   grading and proof — which had no rule at all — correct landings for free.
   The +8px is --lift-2: `.rev` translates its target by that much and the browser
   computes the anchor scroll before the transform settles. */
[id]{scroll-margin-top:calc(var(--hdr) + var(--lift-2) + 24px)}

/* ── SKIP LINK TARGET ──
   `tabindex="-1"` on <main> (in the HTML, present on all six pages) is what makes the
   skip link actually move focus — without it Enter sets location.hash and
   activeElement stays BODY, so the bypass-blocks affordance does nothing. Focus is
   moved for keyboard and screen-reader continuity; a focus ring around a page-sized
   element is noise, so it is suppressed on the -1 target only. main does not become a
   tab stop. */
main[tabindex="-1"]:focus{outline:none}

/* ── FOOTER COLUMNS ──
   `footer .cols` above is repeat(auto-fit,minmax(210px,1fr)) with exactly FOUR items.
   Four cells need 4*210 + 3*36 = 948px and do not fit under ~1280 — but three do, so
   900-1279 rendered 3 cells for 4 items and stranded "Contact" alone on a second row.
   Two even rows instead. Same auto-fit-vs-fixed-item-count trap as .chain, .stations
   and .statband: with a KNOWN item count, auto-fit is the wrong tool. */
@media (min-width:640px) and (max-width:1279px){
  footer .cols{grid-template-columns:repeat(2,minmax(0,1fr))}
}

/* ── MASTHEAD FIT ──
   (a) >=1360: without nowrap, "Smart Mill", "AI Grading" and "Talk to an engineer"
       each broke onto two lines. Measured at 1440: logo 289 + nav 526 + lang 60 +
       CTA 215 + 4x24 gap = 1186px against 1112px of content width. Nowrap alone is
       not enough — the lockup must shed ~74px, so 901-1359 tightens.
   (b) <=900: that same nowrap then drives the masthead PAST the viewport — measured
       at 390px, documentElement.scrollWidth 504 against clientWidth 390, with the
       Menu button at x=442, off-screen. Below 900 the drawer already carries the nav,
       so the bar keeps only logo, language pair and toggle. The header CTA is not
       lost: it moves into the drawer, and the same "Book a site assessment" closes
       every page and sits in the footer Contact column. */
nav.main a,.lang a,header.site .btn{white-space:nowrap}
@media (min-width:901px) and (max-width:1359px){
  header.site .logo img{height:22px}
  nav.main{gap:19px}
  nav.main a{font-size:14px}
  header.site .bar{gap:18px}
  header.site .btn{padding:14px 18px;font-size:13px;letter-spacing:.07em}
}
/* The 901px shrink above was tuned against the ENGLISH label. Bahasa Malaysia's header CTA
   ("Hubungi jurutera kami") is 220.9px against English's shorter run, and it pushed the bar
   20.7px past the viewport across 901-922px — measured, on 9 Malay pages, js-on AND js-off.
   Half this site's audience reads the longer language, so the tight band is sized to IT.
   Trimmed here rather than by moving the 900/901 drawer breakpoint, because that pair is
   shared by six stylesheets for LAYOUT and desyncing them would trade one bug for a class. */
/* MEASURED: with the logo drawn at its true 12.333 aspect, the desktop bar needs 1092px (EN)
   and 1208px (BM) to hold logo + nav + CTA. Below that something must give, and the mark is not
   a candidate — it is the client's registered logo. Trimming padding recovered ~28px against a
   190px BM deficit, so it could not work. The header CTA goes instead, which is exactly what the
   design already does at <=900px (`bar > .btn{display:none}`), and it is the one element here
   that is duplicated: every page carries a hero CTA and the footer carries another. Nav stays
   inline so laptop users keep real navigation. */
@media (min-width:901px) and (max-width:1219px){
  header.site .bar > .btn{display:none}
  header.site .bar{gap:18px}
  /* the true-aspect mark is ~48px wider than the squashed one it replaces; measured deficit was
     27px at 928 and 16px at 940, so the nav gives back exactly that and no more */
  /* `nav.main a:not(.btn)` (0,2,1) out-specifies a bare `nav.main a` (0,1,1) — a first attempt
     here used the bare selector and computed to 14.5px, i.e. changed nothing. Measured deficit
     at 901 BM: children 271+506+70 = 847 + 36 gap = 883 in an 811 box, 72px short. */
  nav.main{gap:10px}
  nav.main a:not(.btn){font-size:12.5px}
  /* THE LAPTOP BAND HAD NO CTA AT ALL. `header.site .bar > .btn` is hidden here and the
     in-nav .navcta was only ever un-hidden at <=900 (drawer) and under scripting:none — so
     between 901 and 1219px the masthead carried five links and no way to book. Measured at
     1100 after the nav became the journey rather than the catalogue: logo 151 + nav 205 +
     lang 70 + two 18px gaps = 462 inside a 990px bar. The button costs ~180px and there are
     528 to spend. The old fix for this band was to DELETE the current-page link to buy room;
     with five items instead of seven that trade is no longer needed, so the nav stays whole. */
  nav.main .navcta{display:inline-flex;padding:9px 16px;font-size:12.5px}
}
.navcta{display:none}
@media (max-width:900px){
  header.site .bar{gap:14px}
  header.site .logo{flex:none}
  header.site .logo img{height:20px;width:auto}
  header.site .bar > .btn{display:none}   /* direct child only — not the drawer CTA */
  nav.main.open a.navcta{display:inline-flex;width:auto;border-bottom:0;
    padding:15px 24px;margin-top:16px;font-size:13px}
}
@media (max-width:560px){
  header.site .bar{gap:12px}
  header.site .logo img{height:18px}
  .lang a{padding:5px 6px}
}
/* the wordmark is an 11:1 lockup, so height is the only lever left on a narrow phone */
/* 16px x 12.333 = 197px, which overran by 8px at 361-374 once the mark stopped squashing.
   15px cleared 6 of those 8; the last 2px comes off the bar gap rather than the mark. */
@media (max-width:400px){header.site .logo img{height:15px}header.site .bar{gap:9px}}
@media (max-width:359px){header.site .bar{gap:8px}header.site .logo img{height:14px}
  .lang a{padding:5px 4px}}

/* ── NO-JS FALLBACK ──
   The reveal class must never be why a page hides its own content, and the drawer must
   never be why a page becomes unnavigable. This is the same rule set the per-page
   `<noscript><style>` blocks carry, expressed so the SHELL owns it and a new page
   inherits it. Critically it un-hides nav.main only AFTER letting the bar wrap: un-hide
   without the wrap and the row is logo + 7 links + lang on one nowrap line — measured
   at 503px against a 390 client, a 113px overflow. The nav takes a row of its own.
   The per-page <noscript> blocks are deliberately NOT removed here: they are the
   fallback for engines without `scripting` media-query support, and they sit in page
   <head>s. Retiring them is a separate, reversible decision. */
@media (scripting:none){
  .rev{opacity:1;transform:none}
  @media (max-width:900px){
    header.site .bar{flex-wrap:wrap}
    header.site .logo{flex:none}
    nav.main{display:flex;flex-wrap:wrap;gap:14px 20px;padding:4px 0 12px;
      flex:1 0 100%;min-width:0;order:3}
    nav.main .navcta{display:inline-flex}
    .navtoggle{display:none}
  }
}


/* NARROW-VIEWPORT LANG TOGGLE. The 2026-08-24 type lift raised every mono label below 13px to a
   readable floor -- the right call, since 58% of body text was under 14px and the hero stamp ran
   183 characters at 10.5px. But .lang went 11px -> 13px and at 320px the header row stopped
   fitting: e2e-responsive-ink measured 3 overflows, all the string "BM" pushing 10.5px past the
   viewport on ms/kenali-jurutera.html, privacy.html and one sibling.
   "EN" and "BM" are two capitals. They do not need the body floor to be legible, and the floor
   exists for RUNNING TEXT, not for a two-glyph switch. Scoped to <=360px so every other viewport
   keeps the lift. Re-measured after: 0 overflows across 2016 measurements. */
@media (max-width:360px){
  .lang{font-size:11.5px}
  /* AND the real cause, which was not font-size at all. Measured at 320px: the bar is a flex row
     280px wide holding logo 173 + navtoggle 71 + lang 50 = 294px of children plus 16px of gaps.
     .lang therefore started at x=280 and ran to 330 -- past both the bar and the viewport. The
     type lift only made a pre-existing squeeze visible.
     Two fixes measured. Wrapping the bar clears it (0 overflow) but grows the header 75px -> 109px,
     spending 34px of vertical on the smallest screens we have. Trimming the wordmark to 150px
     clears it with the header unchanged at 75px, and 150px is more proportionate anyway -- at
     173px the mark was 54% of a 320px screen. */
  header.site .logo img{width:150px;height:auto}
}

/* THE CLIENT'S ACTUAL LOGO, SOMEWHERE IT CAN BE READ.
   Until now the whole site carried exactly one variant — the reversed, descriptor-less
   mark, 20 pages, one file — and the registered identity (two-tone triangle, wordmark,
   the descriptor line, the trademark sign) appeared nowhere. The header is too tight for
   the descriptor; the footer is not. Capped by width so the descriptor stays legible
   without the mark dominating the legal line beneath it. */
/* ── THE APP ROW ──
   MyPalm ships on both stores and airei.com.my carries both badges in the footer of all
   88 of its URLs. This build carried them on two pages, so a mill manager reading the
   Smart Mill page could not find the app AIREI sells him. One row, above the mark, laid
   out inline so it reads as a pair of links rather than a fifth column. */
footer .fapps{padding-top:26px;margin-top:4px;border-top:1px solid var(--hairline);
  display:flex;flex-wrap:wrap;align-items:baseline;gap:8px 22px}
footer .fapps .ftitle{margin:0}
footer .fapps a{display:inline-block;padding:0}
@media (max-width:520px){ footer .fapps{gap:6px 18px} }

footer .fmark{padding-top:30px;margin-top:8px;border-top:1px solid var(--hairline)}
footer .fmark img{width:min(360px,74vw);height:auto;display:block;opacity:.94}
@media (max-width:520px){ footer .fmark img{width:min(280px,86vw)} }

/* TAP TARGETS — the controls, on touch only.
   Measured at 390px: footer nav links were 350x31, the EN/BM switch 30x31 — each one or two
   pixels under the 32px floor, which is the most annoying kind of miss because it looks fine
   and fails anyway. Raised on `(pointer:coarse)` only, so a mouse-driven desktop keeps its
   tighter rhythm.
   Links inside a sentence are deliberately NOT padded. WCAG 2.5.8 exempts them, and giving
   an inline link a 32px box either overlaps the line above or forces the paragraph's
   leading open — it would trade a real reading problem for a theoretical tapping one.

   FLOOR RAISED 32 -> 44 (2026-08-27). 32 cleared WCAG 2.5.8 (AA wants 24) but left every
   control short of the 44 that WCAG 2.5.5 and Apple's HIG ask. Measured at 390/414 in a
   REAL mobile context — is_mobile + has_touch; a default Playwright context reports
   pointer:fine at 390px, which makes this whole block INERT and every reading through it
   false: logo 103x38 · .lang a 38x38 · footer .cols a 350x41 · .legal a 65-224x35 ·
   .fapps a 81-95x34 · nav.main a 38, on all eight pages sampled. Each is a flex box that
   centres its own content, so the hit box grows and no text moves (header 75px -> 75px).
   `.fapps a` needs its own line: it carries display:inline-block;padding:0 at a higher
   specificity than `footer a`, so the generic rule never reached it. */
@media (pointer:coarse){
  footer a{padding:12px 0;min-height:44px;display:flex;align-items:center}
  footer .legal a{min-height:44px;display:inline-flex;align-items:center;padding:11px 0}
  footer .fapps a{min-height:44px;display:inline-flex;align-items:center}
  .lang a{min-width:44px;min-height:44px;display:inline-flex;align-items:center;justify-content:center}
  header.site .logo{min-height:44px;display:inline-flex;align-items:center}
  .navtoggle{min-width:44px;min-height:44px}
  nav.main a{min-height:44px;display:inline-flex;align-items:center}
  a.skip{min-height:44px;display:inline-flex;align-items:center;padding:0 14px}

  /* DISCLOSURE TOGGLES measured 27-31px — including the "How this was measured" toggles
     that open the evidence. min-height ONLY: these summaries are variously list-item,
     grid (.rd>summary) and flex (.mp-fold>summary) with their own markers and padding, so
     display or padding here would break three layouts to fix one number. */
  summary{min-height:44px}
}

/* ── TELE STRIP AS A DISCLOSURE (mobile) ──────────────────────────────────────────
   The strip's founding rule holds: it must not hide its own facts. The wrap that
   enforced it cost 161px at 390px — five rows, 19% of the viewport, standing between a
   first-time visitor and the headline, with the page's own call to action pushed
   147-455px below the fold. A native <details> keeps the rule and pays a fraction of the
   price: every fact stays in the DOM, the affordance is VISIBLE (unlike the scrollbar
   that was deleted), it is a keyboard stop with a real role and name for free, and no
   JavaScript is involved so it works with scripting off.

   DESKTOP IS UNCHANGED. At 1101px and above the details is forced open and the marker
   removed, so the strip renders exactly as before — one row of hairline-boxed cells. */
.tele-d>summary{display:flex;align-items:center;gap:12px;cursor:pointer;
  font-family:var(--f-mono);font-size:12.5px;letter-spacing:.14em;text-transform:uppercase;
  color:var(--ink-ghost);padding:9px 18px;list-style:none;min-height:40px}
.tele-d>summary::-webkit-details-marker{display:none}
/* display:block, NOT flex — and that is the whole rule, not a preference.
   `text-overflow:ellipsis` truncates the INLINE content of the box it is set on. A flex
   container has no inline content: its children are flex items, so the ellipsis had nothing
   to attach to and `overflow:hidden` cut the line dead at the box edge with no mark of any
   kind. Measured 2026-08-26 at 390px CSS px (is_mobile + has_touch, so the mobile queries
   actually match): 15 of the 25 pages truncate here, and what the reader sees is
     products.html   "RANGE FIVE PRODUCTS · FO"                 (122px hidden, cut mid-word)
     ms/produk.html  "RANGKAIAN LIMA PRODUK ·"                  (186px hidden, ends on a
                                                                 separator with nothing after it)
     ms/hubungi.html "PEJABAT CYBERJAYA · IPOH · COIMBAT…"      (140px hidden)
   Still clipped at 414px on ten of them.
   (The first two strings now read FOUR/EMPAT rather than FIVE/LIMA. Left as measured
   rather than edited — a dated observation is a
   record, not a caption. The truncation is unaffected: "FOUR" is the same four
   characters as "FIVE", and "EMPAT PRODUK" is one character longer than "LIMA PRODUK",
   so both still clip here and the rule below is still what fixes them.) As a block the same three declarations do what
   they were written to do. `gap:8px` goes with the flex context; the markup already carries
   a literal space between the lead word and its <b>, which at 12.5px mono with .14em
   tracking measures ~9px — the gap this was buying. */
.tele-d>summary .tele-lead{display:block;min-width:0;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.tele-d>summary .tele-lead b{color:var(--ink-subtle);font-weight:500}
.tele-d>summary .tele-more{margin-left:auto;flex:none;color:var(--airei-blue-lift);
  display:inline-flex;align-items:center;gap:6px}
.tele-d>summary .tele-more::after{content:"";width:6px;height:6px;border-right:1px solid currentColor;
  border-bottom:1px solid currentColor;transform:rotate(45deg) translate(-1px,-1px);transition:transform .2s}
.tele-d[open]>summary .tele-more::after{transform:rotate(-135deg) translate(-2px,-2px)}
.tele-d[open]>summary{border-bottom:1px solid var(--hairline)}
.tele-d>summary:focus-visible{outline:2px solid var(--signal-gold);outline-offset:-2px}
/* The open panel repeats the lead cell, so the summary hides itself rather than showing
   the same fact twice. */
.tele-d[open]>summary .tele-lead{display:none}
.tele-d[open]>summary::before{content:"Page facts";color:var(--ink-ghost)}
@media (min-width:1101px){
  .tele-d>summary{display:none}
  .tele-d[open]>summary{display:none}
  .tele-d>.wrap{display:flex}
}

/* Footer lockup: 360px put the descriptor line at roughly 5px of tracked-out caps —
   present but not readable. 420px carries it comfortably; measured legible at 3x and
   at 2x. Still capped by viewport width so it never dominates a phone footer. */
footer .fmark img{width:min(420px,80vw)}

/* The strip's five cells measure 1271px against a 1240px wrap at 1440 — thirty-one pixels
   over — no: measured properly, the wrap's CONTENT box is 1240 minus 64px of padding each
   side = 1112px, against 1166px of cells. Comparing cells to the BORDER box was the mistake
   and it hid a 54px overflow behind a number that looked like it fitted. Nine pixels of
   padding a side plus half a point of type recovers it with room to spare. The wrap itself stays
   unconditional, exactly as the rule above requires: if a future cell pushes it over again
   it wraps rather than hides, and the page is merely taller. */
@media (min-width:1101px){ .tele span{padding-left:9px;padding-right:9px;font-size:12px} }

/* The disclosure chevron rotates when the fact strip opens. Nothing depends on the rotation
   — the open panel is the signal — so it simply stops. */
@media (prefers-reduced-motion:reduce){
  .tele-d>summary .tele-more::after{transition:none}
}
