
/* Hallmark · genre: warm-utilitarian · macrostructure: shared task card · design-system: design.md · designed-as-app
 * pre-emit critique: P5 H5 E5 S5 R5 V4 · contrast: pass (40–41) · mobile: pass (34,49–57)
 */
/**
 * Metric-matched fallback faces for Mitr/Sarabun (Lighthouse CLS pass,
 * 2026-09-09; revised same day — an earlier cut used font-display:optional
 * instead, which was rejected: Mitr/Sarabun are the brand faces (design.md
 * § 3) and optional means a reader who misses the ~100ms block window on a
 * slow connection never gets them, for the rest of that page view). Google
 * Fonts still ships display:swap (views.ts GOOGLE_FONTS_URL) — swap always
 * shows the real face once it arrives — but swap repaints whatever the
 * fallback already laid out, and a generic fallback's box (line height,
 * average character width) differs from Mitr/Sarabun's enough to reflow
 * the page. These two @font-face rules give the *fallback* Mitr/Sarabun's
 * own box, via the four CSS Fonts Module 4 metric overrides, so the swap
 * repaints glyphs without moving anything around them.
 *
 * Numbers were computed the capsize/fontaine way, with fontkit: for the
 * real webfont file and the local fallback, read unitsPerEm/ascent/descent/
 * lineGap, and average the advance width over the 46 Thai consonants
 * (ก–ฮ, U+0E01–U+0E2E) rather than every glyph in the file — a fallback
 * with a large multi-script glyph repertoire (a macOS system font commonly
 * covers dozens of scripts in one file) skews a whole-file average far past
 * anything a Thai reader's line of text looks like; a first pass that
 * averaged every glyph produced a 280%+ ascent-override, which is what
 * happens when the two files are not actually being compared on the same
 * text. Then, per character width normalised to each font's own em (raw
 * advance widths are not comparable across files with a different
 * unitsPerEm — Thonburi's is 2560, Mitr/Sarabun's is 1000):
 *   webEm      = webfont.avgWidth  / webfont.unitsPerEm
 *   fallbackEm = fallback.avgWidth / fallback.unitsPerEm
 *   size-adjust        = webEm / fallbackEm
 *   ascent-override     = (webfont.ascent  / webfont.unitsPerEm) / size-adjust
 *   descent-override    = (webfont.descent / webfont.unitsPerEm) / size-adjust
 *   line-gap-override   = (webfont.lineGap / webfont.unitsPerEm) / size-adjust
 * measured against the Thai-subset files this page actually loads (Mitr
 * weight 700 — every heading and the wordmark; Sarabun weight 400 — body
 * text) and, as the local-font reference, macOS's bundled Thonburi
 * (Regular for the Sarabun pairing, Bold for Mitr's) — the one local()
 * candidate below this repo could actually fetch, load and measure, and
 * then verify end-to-end with a real headless-Chrome Lighthouse run on this
 * machine (CLS on /หางาน dropped from a reproducible ~0.17 with the
 * whole-glyph-average numbers to 0 across five runs with these). "Noto Sans
 * Thai" and "Leelawadee UI" go first in the local() list because they are
 * the more likely match on Android/ChromeOS and Windows respectively, but
 * their own metrics were not available to measure in this environment —
 * Thonburi's numbers are the closest verified proxy for them, not a
 * verification that they are exactly right too. Re-run apps/api's font
 * metrics script (see the PR that added this comment) if Mitr or Sarabun's
 * published files ever change, or when a Windows/Android box is available
 * to measure Leelawadee UI/Noto Sans Thai directly instead of assuming.
 */
@font-face{
  font-family:"Mitr Fallback";
  src:local("Noto Sans Thai"),local("Leelawadee UI"),local("Thonburi"),local("Noto Sans");
  font-weight:700;
  ascent-override:109.82%;
  descent-override:40.11%;
  line-gap-override:0%;
  size-adjust:104.72%;
}
@font-face{
  font-family:"Sarabun Fallback";
  src:local("Noto Sans Thai"),local("Leelawadee UI"),local("Thonburi"),local("Noto Sans");
  font-weight:400;
  ascent-override:107.24%;
  descent-override:23.30%;
  line-gap-override:0%;
  size-adjust:99.59%;
}
:root{
  --color-paper:#FBF6EC;
  --color-paper-2:#F3EBDB;
  --color-paper-3:#E8DECB;
  --color-card:#FFFDF7;
  --color-ink:#1E1A16;
  --color-ink-2:#4A413A;
  --color-muted:#6B6055;
  --color-rule:#D9CDB8;
  --color-rule-2:#E6DCCB;
  /* 2026-09-06 (Astra proposal, founder-approved) — the SSR page had its own
     hand-written tomato (#B4452C and a lighter hover/deep pair), so the public
     /j/:id primary was a different colour from the SPA and the app. These are
     design.md § 2's generated values for oklch(50% .15 33) and its states;
     regenerate them with 'bun run design:tokens', never by eye. */
  --color-primary:#A73923;
  --color-primary-ink:#FCF5E7;
  --color-primary-hover:#9A2B16;
  --color-primary-deep:#892312;
  --color-primary-tint:#FEE0D8;
  --color-accent:#D9603F;
  --color-accent-tint:#F3EBDB;
  --color-tag-ink:#1E1A16;
  --color-focus:#2F5FD6;
  --status-pending:#4F5661;
  --status-pending-bg:#E7EAED;
  --status-contacted:#005187;
  --status-contacted-bg:#D8EDFC;
  --status-hired:#005725;
  --status-hired-bg:#D7F4DC;
  --status-completed:#01421A;
  --status-completed-bg:#D7F4DC;
  --status-rejected:#8C2E23;
  --status-rejected-bg:#FFE4DF;
  --status-withdrawn:#4F5661;
  --status-withdrawn-bg:#E7EAED;
  --font-ui:"Sarabun","Sarabun Fallback","Noto Sans Myanmar","Noto Sans",system-ui,sans-serif;
  --font-display:"Mitr","Mitr Fallback","Noto Sans Myanmar","Sarabun",sans-serif;
  --font-numeral:"Sarabun","Sarabun Fallback","Noto Sans Myanmar",sans-serif;
  --font-mono:"IBM Plex Mono",ui-monospace,monospace;
  --text-sm:.875rem;
  --text-xl:1.5rem;
  --radius-chip:10px;
  --radius-input:12px;
  --radius-tag:10px;
  --radius-card:18px;
  --radius-doc:8px;
  --radius-pill:999px;
  --shadow-card:0 1px 0 var(--color-rule-2),0 12px 28px -18px rgba(30,26,22,.35);
  --shadow-doc:6px 6px 0 var(--color-rule-2);
  --shadow-sheet:0 -4px 24px rgba(30,26,22,.16);
  --layout-read:640px;
  --layout-task:480px;
  font-family:var(--font-ui);
  color:var(--color-ink);
  background:var(--color-paper);
  font-synthesis:none;
}
*{box-sizing:border-box}
html,body{overflow-x:clip}
html{font-size:16px}
body{margin:0;min-width:320px;min-height:100vh;background:var(--color-paper);line-height:1.55}
:lang(my){line-height:1.65}
h1,h2{font-family:var(--font-display);font-style:normal;font-weight:700}
a{color:inherit}
button,input,select,textarea{font:inherit}
button,a,input,select,textarea,summary{-webkit-tap-highlight-color:transparent}
img{display:block;max-width:100%}
.skip-link{position:fixed;z-index:99;left:1rem;top:-5rem;min-height:48px;padding:.75rem 1rem;display:flex;align-items:center;background:var(--color-card);border:2px solid var(--color-primary);border-radius:var(--radius-input)}
.skip-link:focus{top:1rem}
.site-header{height:72px;display:flex;align-items:center;justify-content:space-between;gap:1rem;padding:0 max(1rem,env(safe-area-inset-left));max-width:760px;margin:auto}
.wordmark{display:flex;align-items:center;gap:.5rem;min-height:48px;text-decoration:none;color:var(--color-primary);font-size:var(--text-xl);font-family:var(--font-display);line-height:1.4;white-space:nowrap}
.wordmark-glyph{width:24px;height:24px;flex:none;border-radius:var(--radius-chip);background:var(--color-primary)}
.wordmark strong{font-weight:700}
.language-menu{position:relative}
.language-menu summary{min-width:96px;min-height:48px;padding:0 .75rem;display:flex;align-items:center;justify-content:center;gap:.5rem;border:1px solid var(--color-muted);border-radius:var(--radius-input);background:var(--color-card);cursor:pointer;list-style:none}
.language-menu summary::-webkit-details-marker{display:none}
.language-options{position:absolute;right:0;z-index:10;top:54px;width:160px;padding:.35rem;background:var(--color-card);border-radius:var(--radius-card);box-shadow:var(--shadow-card)}
.language-options a{display:flex;align-items:center;min-height:48px;padding:0 .75rem;border-radius:var(--radius-chip);text-decoration:none}
.language-options a[aria-current=true]{background:var(--color-primary-tint);color:var(--color-primary-deep)}
.page{width:min(100%,760px);margin:auto;padding:1rem 1rem 7rem}
.job-page{max-width:calc(var(--layout-read) + 2rem)}
.job-hero{width:100%;background:var(--color-paper-2)}
.hero-image{width:100%;max-height:300px;aspect-ratio:16/9;object-fit:cover}
.fact-note{margin:.25rem 0 0;font-size:1rem;font-weight:400;line-height:1.6;color:var(--color-ink-2);overflow-wrap:anywhere}
.status-open{background:var(--status-hired-bg);color:var(--status-hired)}
.status-full{background:var(--status-completed-bg);color:var(--status-completed)}
.status-start_passed{background:var(--status-pending-bg,var(--color-paper-2));color:var(--color-ink-2)}
.status-expired,.status-missing,.status-closed{background:var(--status-withdrawn-bg);color:var(--status-withdrawn)}
/* Founder 2026-09-03 — pay-timing chip. Quiet paper by default; same_day gets
   the accent tint (mirrors .pay-timing-chip--accent in apps/web app.css). */
.status-pay-timing{background:var(--color-paper-2);color:var(--color-ink-2)}
.status-pay-timing-accent{background:var(--color-accent-tint);color:var(--color-ink)}
.text-action{display:inline-flex;align-items:center;min-height:48px;margin-top:.35rem;color:var(--color-primary);font-weight:700}
h2{margin:0 0 .6rem;font-size:1.125rem;line-height:1.4}
:lang(my) h2{line-height:1.5}
.verified{font-size:.875rem;background:var(--status-hired-bg);color:var(--status-hired);padding:.25rem .6rem;border-radius:var(--radius-pill);font-weight:700}
.primary{min-height:56px;padding:.75rem 1.25rem;border:0;border-radius:var(--radius-input);background:var(--color-primary);color:var(--color-primary-ink);text-decoration:none;font-weight:700;font-size:1.125rem;display:flex;align-items:center;justify-content:center;text-align:center;cursor:pointer;line-height:1.35}
.primary:active{background:var(--color-primary-deep);transform:translateY(1px)}
.primary:focus-visible,.text-action:focus-visible,.back-link:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible,summary:focus-visible,a:focus-visible,.star-options input:focus-visible+svg,.tag-options input:focus-visible+span{outline:2px solid var(--color-focus);outline-offset:2px}
.primary:disabled{background:var(--color-paper-3);color:var(--color-muted);cursor:wait}
.desktop-action{display:none}
/* Human review #28 — "รับงานนี้" was smaller here than the SPA's. margin:auto
   on a grid item is an auto margin: it absorbs the free space, which cancels
   the default justify-self:stretch, so the button shrank to the width of its
   own label and sat centred. width + a horizontal-only auto margin keeps the
   centring AND the full width, at the SPA's 56px height (.primary). */
/* Astra UX audit #9 (founder-approved 2026-09-16) — the bar is the reading
   panel's width (--layout-read), and the button used to fill it: ~608px on
   a 1280 desktop, past the 480px cap § 11.2 puts on a task-shaped control.
   min(100%, --layout-task) keeps the phone full-width and centres a
   480px button in the bar on desktop. */
.sticky-action .primary{width:min(100%,var(--layout-task));margin:0 auto}
.detail-trust-line{width:min(100%,var(--layout-read));margin:0 auto;color:var(--color-ink-2);font-size:1rem;font-weight:500;line-height:1.55;text-align:center;overflow-wrap:anywhere}
/* Human review #28 — a tall empty gap between the card and the footer. The
   clearance under the job page exists for exactly one reason: the fixed
   .sticky-action bar covers the bottom of the viewport, so the page must be
   able to scroll that far past its last line. It was a hand-picked 10rem,
   which matched no real bar height; --sticky-action-h is measured from the
   real element by the inline script below and re-measured on resize,
   orientation change and webfont swap.

   Founder 2026-09-15 — the bar no longer carries the "ก่อนไปทำงาน" notice or
   the two trust lines (they are page content now), so its height no longer
   depends on a notice that may be open or closed. Its pieces, top to bottom:
   .75rem padding, the 56px primary, and a bottom padding that is .75rem OR
   the phone's gesture-bar inset, whichever is larger. That is
   --sticky-action-chrome. The only other thing that can move it is the label
   wrapping to a second line in Burmese (.75rem + 2 x 1.125rem/1.35 + .75rem
   inside the button = 74px), which is --sticky-action-max, the no-JS bound.
   The viewport-proportional cap that used to bound the open notice inside the
   bar is deleted with the notice.

   Luna review 2026-09-15 — the bound MUST carry the safe-area inset, because
   the bar's own bottom padding does: a fixed 6.25rem clearance on a phone
   with a 34px gesture bar left the footer's last links under the bar again.
   The inset reaches both through --safe-bottom, so the bar and the clearance
   cannot drift apart. --safe-bottom defaults to 0px and only becomes env()
   inside @supports (same pattern as apps/web/src/styles/app.css): a WebView
   that predates env() drops a bare env() DECLARATION, but inside a custom
   property the token survives parsing and poisons every property that
   substitutes it — here, the bar's padding and the page's clearance.
   The FALLBACK (no JS, or the window before the deferred script runs)
   is now exact rather than a bound, so the no-JS page is structurally
   non-overlapping with no :has selector and no script.

   The clearance is on <body> (.job-page-active, set by renderJobPage), not on
   <main>: the bar covers the bottom of the VIEWPORT, so the last thing on the
   page — the footer — is what must be able to scroll clear of it. On <main>
   it opened a dead band between the card and the footer AND still let the
   footer's last links sit under the bar. */
:root{--safe-bottom:0px;--sticky-action-pad-bottom:max(.75rem,var(--safe-bottom));--sticky-action-chrome:calc(.75rem + 56px + var(--sticky-action-pad-bottom));--sticky-action-max:calc(.75rem + 74px + var(--sticky-action-pad-bottom))}
@supports (padding-bottom: env(safe-area-inset-bottom)){
:root{--safe-bottom:env(safe-area-inset-bottom, 0px)}
}
.job-page-active{padding-bottom:calc(var(--sticky-action-h, var(--sticky-action-max)) + 1rem)}
.state-card .primary{margin-top:1.5rem}
footer{display:flex;flex-direction:column;align-items:center;padding:2.5rem 1rem 3rem;color:var(--color-muted);font-size:.875rem;text-align:center}
footer strong{color:var(--color-ink);font-size:1rem;font-family:var(--font-display)}
/* NM-036 — the age gate's three selects. One row on a phone, because a worker
   should see day/month/year as one question; each select keeps the 56px form
   height, so every target clears 48dp. */
.birth-date{border:0;padding:0;margin:0;min-width:0}
.birth-date legend{font-weight:500;font-size:.875rem;color:var(--color-ink-2);margin-bottom:.4rem;padding:0}
@media(min-width:414px){
.page{padding-left:1.5rem;padding-right:1.5rem}
.job-page,.record-page{max-width:calc(var(--layout-read) + 3rem)}
}
@media(min-width:700px){
.site-header{height:88px}
.page{padding-top:1.5rem}
.job-page-active{padding-bottom:calc(var(--sticky-action-h, var(--sticky-action-max)) + 1rem)}
}
@media(hover:hover) and (pointer:fine){
.primary:hover{background:var(--color-primary-hover)}
.language-options a:hover{background:var(--color-primary-tint);color:var(--color-primary-deep)}
}
.check-row{grid-template-columns:auto 1fr;align-items:start;gap:.75rem!important;font-weight:400!important}
.check-row input[type=checkbox]{width:28px!important;min-height:28px!important;margin-top:.15rem;accent-color:var(--color-primary)}
@media(prefers-reduced-motion:reduce){
*{animation-duration:.01ms!important;animation-iteration-count:1!important;scroll-behavior:auto!important}
}
@media(forced-colors:active){
.status-chip,.verified,.notice,.record-stamp{border:1px solid currentColor}
.primary{border:2px solid ButtonText}
}

/* NM-060 privacy policy page. A plain reading column: the point is that
   someone can actually read it, not that it looks designed. Astra public-SSR
   pass (2026-09-06): bumped body text to 18px (was the 16px default) — a
   legal document is exactly the surface where "larger reading text" pays
   for itself, and a TOC + back link that a reader can actually tap. */
.prose-page{max-width:68ch;margin:0 auto;padding:0 16px 48px;font-size:18px}
.prose-page h2{font-family:var(--font-display);font-size:22px;margin:28px 0 8px;line-height:1.35}
.prose-page h3{font-family:var(--font-display);font-size:18px;margin:22px 0 6px}
.prose-page h4{font-size:16px;margin:18px 0 4px}
.prose-page p{margin:0 0 12px;line-height:1.75}
.prose-page ul{margin:0 0 14px;padding-left:20px}
.prose-page li{margin-bottom:6px;line-height:1.75}
.prose-page strong{font-weight:700}
/* The effective-date line — always the first paragraph of the document —
   reads as a caption above the policy, not as body text. */
.legal-effective-date{font-size:15px;font-weight:600;color:var(--color-ink-2);margin:0 0 20px}
/* Native contents list, built from the document's own h2s (legal.ts). A
   muted card so it reads as navigation, not as the first section of the
   policy itself. */
.legal-toc{margin:0 0 24px;padding:14px 16px;background:var(--color-paper-2);border-radius:12px}
.legal-toc h2{font-family:var(--font-display);font-size:15px;margin:0 0 8px;color:var(--color-muted)}
.legal-toc ol{margin:0;padding-left:20px;list-style:decimal}
.legal-toc li{margin-bottom:4px}
.legal-toc a{display:inline-flex;align-items:center;min-height:48px;color:var(--color-primary-deep);text-decoration:underline}
/* A back link that survives the locale switch (renderPolicyPage passes its
   own locale into pathWithLocale) — a real 48px tap target, not a small
   inline link at the bottom of a long document. */
.legal-back{margin:28px 0 0}
.legal-back a{display:inline-flex;align-items:center;min-height:48px;padding:0 4px;color:var(--color-primary-deep);font-weight:700;text-decoration:none}
