/* ===========================================================================
   OfferGodHK — stylesheet
   ---------------------------------------------------------------------------
   Plain by design. ONE colour carries the whole site:

     red    the HKSAR government red. Accent only: CTAs, the rule under the
            masthead, badges, step numbers. Never a background wash.

   Everything else is neutral, and deliberately matches csb.gov.hk: a white
   ground, grey bands, near-black text. The brand navy has left the stylesheet
   entirely -- see the token block below for where it still lives.

   History, so nobody "restores" a look on purpose:
     v1 was 檔案綠 × 琥珀金 per the design report (§2.2), which argued archive
     green was unclaimed in this niche and navy was the civil-service default.
     v2 (2026-09-14) moved to the logo's navy + amber + a warm cream ground.
     v3 (2026-09-14) dropped the cream, dropped amber, and went navy +
     government red.
     v4 (2026-09-14, current) is the CSB retheme, on the operator's
     instruction. White ground, #e9e9e9 bands, #212121 and #575757 text, red
     as the only colour. The palette was MEASURED off csb.gov.hk rather than
     guessed; the samples are in the token block.

   NOTE ON POSITIONING -- READ THIS BEFORE THE NEXT RETHEME. This palette is no
   longer merely *reminiscent* of government material. On the operator's
   explicit instruction it now copies the Civil Service Bureau's own portal
   surfaces, and the grade cards carry real department crests. The footer on
   every page still states the platform is independent and unconnected to the
   HKSAR government, and that disclaimer is now doing more work than it was --
   keep it prominent, and do not let it drift into fine print. Two lines were
   held back and should stay held back: no government wordmark, and no HKSAR
   regional emblem (Cap. 2602). The four CSB grades carry drawn marks for
   exactly that reason -- see the head of make_logos.py.

   This file is hand-written and is the main design lever. Edit it directly.
   `build.py` generates only the HTML; it never overwrites this file.

   Content-Security-Policy note: the site ships `style-src 'self'` with no
   `unsafe-inline`. That means NO inline `style="..."` attributes anywhere in
   the HTML. If you need a one-off tweak, add a class here instead — loosening
   the CSP is the wrong fix.
   =========================================================================== */

/* --- Design tokens -------------------------------------------------------- */

:root {
  /* Brand navy. NO LONGER USED BY THIS STYLESHEET -- the CSB retheme took
     every surface and every piece of text off it. It survives in exactly two
     places, and both are artwork files rather than CSS: the four drawn grade
     marks (assets/logos/jre.svg, cre.svg, ao.svg, eo2.svg -- make_logos.py
     paints them #0d2b4d) and the platform logo itself. The tokens are kept so
     there is one place to look if the brand colour is ever wanted back; the
     `a` rule below says how to put it on the links in one line.
     --navy-700 is sampled straight out of the platform logo's wordmark
     (assets/logos/platform.png, the "offer"/"hk" lettering = #133e6b).
     --navy-900 and --navy-100 are the darker and lighter steps built off it.
     The logo's other colours are tan/gold #a29078 ("god"), slate #6a7a89 (the
     hand) and a teal skyline; the slate is now --control-line. */
  --navy-900: #0d2b4d;
  --navy-700: #133e6b;
  --navy-100: #e7edf5;

  /* Accent: the HKSAR government red.
     PROVENANCE. There is no legislated screen value to copy. The Regional Flag
     and Regional Emblem Ordinance names no hex at all -- it defines the red
     only as identical to the PRC national flag red, deferring to a physical
     swatch. So this had to be sampled, and there are two government reds in
     circulation that are NOT the same colour:

       #ee1c25  the flag and emblem red. Sampled from the Protocol Division's
                own natflags.png (95.5% of its opaque pixels are exactly this).
                It is the "true" red -- and it is unusable here, because white
                text on it measures 4.35:1 and every button on this site is
                white on red. Do not "restore" it without solving that first.
       #e50020  the red in GovHK's own logo, images/emblem/GovHK_with_Emblem_
                PD.svg. Government-published, a direct screen sample rather
                than a print conversion, and white on it is 4.82:1 -- which is
                why this one ships. Both were sampled by hand; neither is a
                converted Pantone number, and an earlier #e4002b (PMS 185 C,
                from an Administration Wing reply calling PMS 185 "a good
                colour reference") was dropped in favour of the sampled value.

     If exactness ever matters, re-sample from protocol.gov.hk rather than
     trusting any number written here. */
  --accent: #e50020;
  --accent-soft: #fde8ec;   /* badge/pill ground -- never used for text */
  --accent-deep: #b00020;   /* red TEXT on light backgrounds -- 7.3:1 */
  --on-accent: #ffffff;     /* white on red; this red is dark enough for it */

  /* Surfaces. THE CSB SYSTEM: a plain white ground, with grey bands carrying
     the hero, the interior page headers and the footer.
     MEASURED, NOT GUESSED. csb.gov.hk was rendered and row-sampled on
     2026-09-14: masthead #ffffff (88-94% of pixels), nav band #e9e9e9 (93%),
     dropdown panels #e9e9e9 (95%), footer #e9e9e9 (100%), text #212121 and
     #575757. The only chromatic element on the whole site is the regional
     emblem's red, at 0.15% of pixels. A green-pixel search returned 0.00%
     greenish pixels -- the mint rgb(197,244,224) that getComputedStyle reports
     on .headerMenu and .footerArea is never actually painted, so do not
     "restore" it on the strength of that reading. */
  --paper: #ffffff;
  --surface: #ffffff;
  --band: #e9e9e9;          /* hero, interior page header, footer, table heads */

  /* Text, from the same measurement. --ink-soft is CSB's own secondary grey:
     7.23:1 on white and 5.95:1 on the band, so it clears 4.5 on both surfaces
     and is safe for the small English sub-lines that need it most. */
  --ink: #212121;
  --ink-soft: #575757;
  --on-band: #212121;       /* text on --band; 13.26:1 */

  /* Lines. --line and --line-strong are dividers on WHITE only. A line that
     crosses the grey band cannot use them -- #e2e6ea on #e9e9e9 is a 2-point
     difference and reads as no line at all. That is what --edge is for.
     A line marking the edge of a CONTROL needs 3:1 (WCAG 1.4.11), which is
     what --control-line is for; it comes off the logo's slate and clears 3:1
     on the band as well as on white, so it is the one line colour that works
     on either surface. */
  --line: #e2e6ea;
  --line-strong: #cbd2d9;
  --edge: #9e9e9e;          /* a divider that has to read on EITHER surface:
                               2.68:1 on white, 2.21:1 on the band. It is a
                               separator, not a control boundary -- anything
                               whose edge has to clear 3:1 uses --control-line
                               below instead. */
  --control-line: #6a7a89;

  /* Focus ring: near-black, and NOT the accent. A red ring is invisible around
     a red button -- the ring sits 2px off a fill of its own colour.
     Since the CSB retheme EVERY surface this site paints is light (white or
     #e9e9e9), so one dark ring works everywhere: 16.10:1 on white, 13.26:1 on
     the band. That is why the per-surface override list that used to sit under
     :focus-visible is gone. It existed only because four blocks were navy, and
     it was a silent failure waiting to happen -- a white ring on a light band
     is invisible, and focus rings only ever show under keyboard navigation. */
  --focus: #212121;

  /* Rhythm */
  --radius: 10px;
  --radius-sm: 6px;
  --radius-pill: 999px;

  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;
  --space-7: 3rem;
  --space-8: 4rem;

  --container: 1120px;
  /* Shadow tint tracks --ink (#212121), not the old #1f2937. */
  --shadow-sm: 0 1px 2px rgba(33, 33, 33, 0.06);
  --shadow-md: 0 4px 12px rgba(33, 33, 33, 0.08);
  --shadow-lg: 0 12px 28px rgba(33, 33, 33, 0.12);

  /* Bilingual pairing: Chinese leads, English follows one step smaller and
     muted. Kept as tokens so the ratio stays consistent everywhere. */
  --font-cjk: "Microsoft JhengHei", "PingFang HK", "Noto Sans CJK TC",
    "Heiti TC", sans-serif;
  --font-latin: "Segoe UI", system-ui, -apple-system, "Helvetica Neue", Arial,
    sans-serif;
}

/* --- Base ----------------------------------------------------------------- */

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* Type scale anchor. Every font-size in this file is in rem, and so is every
     --space-* token, so this one value scales the whole page proportionally --
     type and rhythm together, like a small zoom-out. 15px is ~6% under the
     16px browser default. Change this single number to resize the site; the
     px used in the media queries below are deliberately NOT rem, so the
     breakpoints stay exactly where they are. */
  font-size: 15px;
}

/* The header is sticky, so it sits on top of the page for the whole scroll.
   Without this, any in-page jump (the 職系一覽 nav link, a #grades link, a
   shared anchor) lands with the target's first line hidden underneath it.
   The value is the header height plus a little breathing room. */
[id] {
  scroll-margin-top: 84px;
}

body {
  margin: 0;
  font-family: var(--font-cjk), var(--font-latin);
  font-size: 1rem;          /* relative, so body copy follows the root size
                               above. An absolute 16px here would have pinned
                               body text while everything around it shrank. */
  line-height: 1.65;
  color: var(--ink);
  background-color: var(--paper);
  /* Chinese text has no word breaks; without this a long unbroken string
     (a URL, a grade code) pushes the layout wider than the viewport. */
  overflow-wrap: break-word;
}

img {
  max-width: 100%;
  height: auto;
  display: block;
}

/* Links are near-black and underlined, which is what CSB does -- it carries no
   link colour at all, so colour is not available to distinguish a link from the
   text around it. The UNDERLINE is therefore doing that job, which makes it
   load-bearing: never add `text-decoration: none` to a link sitting in body
   copy. Nav links and cards still drop it, because their shape already marks
   them out as targets.
   To bring the OfferGod navy back as the link colour, swap --ink below for
   --navy-700. That is the one-line reversal. */
a {
  color: var(--ink);
  text-decoration-thickness: 1px;
  text-underline-offset: 2px;
}

a:hover {
  color: #000000;
  text-decoration-thickness: 2px;
}

/* A single visible focus treatment for every interactive element. */
:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

h1,
h2,
h3,
h4 {
  line-height: 1.3;
  margin: 0 0 var(--space-3);
  font-weight: 600;
}

h1 {
  font-size: clamp(1.75rem, 1.2rem + 2.2vw, 2.5rem);
}

h2 {
  font-size: clamp(1.35rem, 1.1rem + 1vw, 1.75rem);
}

h3 {
  font-size: 1.125rem;
}

p {
  margin: 0 0 var(--space-4);
}

ul,
ol {
  margin: 0 0 var(--space-4);
  padding-left: 1.35em;
}

li {
  margin-bottom: var(--space-2);
}

hr {
  border: 0;
  border-top: 1px solid var(--line);
  margin: var(--space-6) 0;
}

/* The `hidden` attribute must beat any author `display` declaration: the UA
   stylesheet's `[hidden] { display: none }` loses to a class rule, so without
   this an element that sets `display` would ignore its own `hidden`. */
[hidden] {
  display: none !important;
}

/* --- Layout --------------------------------------------------------------- */

.container {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--space-5);
}

.section {
  padding-block: var(--space-8);
}

.section--tight {
  padding-block: var(--space-6);
}

.section--paper {
  background-color: var(--surface);
}

.section-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-6);
}

.section-head h2 {
  margin: 0;
}

.section-head p {
  margin: 0;
  color: var(--ink-soft);
  font-size: 0.9375rem;
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  background: var(--accent);
  color: var(--on-accent);
  padding: var(--space-3) var(--space-4);
  font-weight: 600;
  border-radius: 0 0 var(--radius) 0;
}

.skip-link:focus {
  left: 0;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* --- Bilingual text ------------------------------------------------------- */

/* English sits one step smaller and muted, matching the convention used
   across every OfferGodHK Telegram asset. */
.t-en {
  display: block;
  font-family: var(--font-latin);
  font-size: 0.8125rem;
  line-height: 1.45;
  color: var(--ink-soft);
  font-weight: 400;
}

.t-en--lg {
  font-size: 0.9375rem;
}

/* --- Language options (繁 / 簡 / EN) --------------------------------------
   build.py pairs every run of Chinese with a Simplified twin in .t-sc, and
   script.js puts the reader's choice on <html>. These rules are the other half
   of that contract. The class names must match data-lang in lang_switch().

   DEFAULT (no class, and the no-JavaScript case) is the site's normal
   bilingual presentation -- Chinese with the English beneath it. 繁 and 簡 swap
   the SCRIPT and deliberately KEEP the English, because .t-en sometimes carries
   text that has no Chinese twin at all (the brand subtitle in the masthead, for
   one). Hiding it there would lose content rather than translate it. Only EN
   hides the Chinese outright. */
.t-sc {
  display: none;
}

/* 簡 swaps the script rather than removing one: hide the Traditional twin and
   put the Simplified one in its place. Without this line the base rule above
   wins and 簡 renders NOTHING -- the page goes blank. */
html.lang-sc .t-sc {
  display: inline;
}

html.lang-sc .t-tc,
html.lang-en .t-tc,
html.lang-en .t-sc {
  display: none;
}

/* In English mode the English IS the content, not a subtitle under it, so it
   takes the size, colour and weight of whatever it sits in. font-family is NOT
   inherited here: this text is Latin, so --font-latin is still the right face. */
html.lang-en .t-en {
  display: inline;
  font-size: inherit;
  line-height: inherit;
  color: inherit;
  font-weight: inherit;
}

/* .t-en--lg is for h1s; in English mode the h1 itself already sets the size. */
html.lang-en .t-en--lg {
  font-size: inherit;
}

/* --- English on single-line controls -------------------------------------
   `.t-en` is display:block by default because almost everywhere it is a
   subtitle sitting under the Chinese. The nav bar and the masthead button are
   nowrap controls, so a second line there would double their height. Suppress
   the subtitle in 繁/簡 and show the label in EN, where it is the only text
   left once `.t-tc` and `.t-sc` are hidden. */
.primary-nav .t-en,
.header-actions .t-en,
.grade-card-cta .t-en,
.ladder-here .t-en { display: none; }

html.lang-en .primary-nav .t-en,
html.lang-en .header-actions .t-en,
html.lang-en .grade-card-cta .t-en,
html.lang-en .ladder-here .t-en { display: inline; }

/* Latin tokens that belong to the Chinese sentence and have no English twin --
   the "16" and "Telegram" in the hero line. build.py's language_spans only
   wraps runs of CJK, so a bare Latin token inside a Chinese line survives into
   English mode and collides with the English beside it, rendering
   "16 Telegram 16 Telegram groups for Hong Kong civil service candidates."
   Hidden in EN only; both Chinese modes still need them. */
html.lang-en .zh-only { display: none; }

/* NOT USED BY ANY PAGE. The name dates from when "dark" meant the navy header,
   which no longer exists. Kept so a stale reference lands on the right grey
   rather than falling through to full-strength body ink. */
.t-en--on-dark {
  color: var(--ink-soft);
}

/* --- Header --------------------------------------------------------------- */

/* White masthead, as CSB's is. The red rule under it is the ONLY thing
   separating the header from the page now, so it is structural rather than
   decorative -- do not thin it, and do not drop it for a grey hairline. */
.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background-color: var(--surface);
  color: var(--ink);
  border-bottom: 3px solid var(--accent);
}

.header-inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-4);
  min-height: 68px;
  padding-block: var(--space-3);
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  color: var(--ink);
  text-decoration: none;
  font-weight: 600;
  font-size: 1.0625rem;
  margin-right: auto;
}

.brand:hover {
  color: var(--ink);
}

.brand img {
  width: 40px;
  height: 40px;
  /* contain, not the default `fill`: an <img> given both a width and a height
     stretches to that box unless told otherwise, so any logo file that is not
     square would come out distorted with nothing to warn you. */
  object-fit: contain;
  border-radius: var(--radius-sm);
  background: var(--surface);
}

.brand-text {
  display: flex;
  flex-direction: column;
  line-height: 1.2;
}

.brand-text .t-en {
  font-size: 0.6875rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-soft);
}

.primary-nav ul {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3) var(--space-5);
  margin: 0;
  padding: 0;
  list-style: none;
}

.primary-nav li {
  margin: 0;
}

.primary-nav a {
  color: var(--ink);
  text-decoration: none;
  font-size: 0.9375rem;
  white-space: nowrap;
  padding-block: var(--space-1);
  border-bottom: 2px solid transparent;
}

.primary-nav a:hover,
.primary-nav a[aria-current="page"] {
  color: var(--ink);
  border-bottom-color: var(--accent);
}

.header-actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

/* --- Language switcher ----------------------------------------------------
   A three-way segmented control: 繁 / 簡 / EN. Buttons rather than links,
   because there is no per-language URL to navigate to -- the choice is a class
   on <html>, applied by script.js and remembered in localStorage.

   --control-line rather than --line-strong for the border: the control is a UI
   component, so it needs 3:1 against its ground, and --line-strong is a hair
   over 1:1 (it is a divider, not a control edge). */
.lang-switch {
  display: inline-flex;
  border: 1px solid var(--control-line);
  border-radius: 3px;
  overflow: hidden;
}

.lang-switch button {
  font: inherit;
  font-size: 0.75rem;
  line-height: 1;
  padding: 0.4rem 0.5rem;
  /* WCAG 2.5.8 wants a 24x24 CSS px pointer target. Measured here the box was
     ~26x23 -- the glyph width cleared it but the vertical axis did not, on the
     control that appears on every page and is the most used after the nav.
     A 28px floor clears it with room to spare and does not change the header
     height, because the switcher was already the tallest thing in that row. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 28px;
  min-height: 28px;
  border: 0;
  background: transparent;
  color: var(--ink-soft);
  cursor: pointer;
  appearance: none;
}

/* Hairlines BETWEEN segments only -- a border on each button would double up
   against the container's own edge and read as 2px on the ends. */
.lang-switch button + button {
  border-left: 1px solid var(--control-line);
}

.lang-switch button:hover {
  background: var(--band);
  color: var(--ink);
}

/* 繁/簡/EN are three letters, not a sentence -- no room for a tick, so the
   filled segment IS the indicator. Paired with aria-pressed for screen
   readers, which cannot see the fill. */
.lang-switch button[aria-pressed="true"] {
  background: var(--accent);
  color: var(--on-accent);
}

.lang-switch button:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: -2px;
}

/* --- Buttons -------------------------------------------------------------- */

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: 0.6rem 1.1rem;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  font-family: inherit;
  font-size: 0.9375rem;
  font-weight: 600;
  line-height: 1.35;
  text-decoration: none;
  cursor: pointer;
  transition: background-color 0.15s ease, border-color 0.15s ease,
    transform 0.15s ease;
}

/* The primary action is red with white on it -- 4.82:1, which is exactly why
   this red ships and the flag red does not (4.35:1; see the token block).
   THE HOVER USED TO GO AMBER. It was #fbbf24, a leftover from v2, and it put
   white text on that light ground at 1.67:1 -- the label very nearly vanished
   whenever a pointer crossed it. No gate caught it because the contrast check
   only ever measured resting states. Hover darkens now, which is the direction
   this palette has room to move in. */
.button-primary {
  background-color: var(--accent);
  color: var(--on-accent);
  border-color: var(--accent);
}

.button-primary:hover {
  background-color: var(--accent-deep);
  border-color: var(--accent-deep);
  color: var(--on-accent);
}

/* NOT USED BY ANY PAGE -- grep before you trust it. It was built for the navy
   hero, where a transparent button with a light edge made sense. On a light
   band the equivalent is a transparent button with a dark edge; the hover fill
   is a step darker than --band so it still reads when the button sits on one. */
.button-ghost {
  background-color: transparent;
  color: var(--ink);
  border-color: var(--control-line);
}

.button-ghost:hover {
  background-color: #dcdcdc;
  color: var(--ink);
  border-color: var(--ink);
}

.button-outline {
  background-color: transparent;
  color: var(--ink);
  border-color: var(--control-line);   /* the border IS the button's only edge
                                          marker, so it needs 3:1 -- see the
                                          --control-line note above. */
}

.button-outline:hover {
  background-color: var(--band);
  color: var(--ink);
  border-color: var(--ink);
}

.button-sm {
  padding: 0.4rem 0.8rem;
  font-size: 0.875rem;
}

.button-block {
  display: flex;
  width: 100%;
}

.button-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  align-items: center;
}

.button-row--center {
  justify-content: center;
}

/* --- Hero ----------------------------------------------------------------- */

/* Flat grey band -- the CSB nav band, #e9e9e9 -- and no gradient wash. The two
   radial gradients that used to sit here read as decoration on a page whose
   whole job is to hand the reader a list of grades and a search box. */
.hero {
  background-color: var(--band);
  color: var(--on-band);
  padding-block: var(--space-7) var(--space-6);
}

.hero h1 {
  color: var(--ink);
  margin-bottom: var(--space-3);
}

.hero-sub {
  font-size: 1.0625rem;
  color: var(--ink-soft);
  max-width: 46ch;
  margin-bottom: var(--space-6);
}

.hero-sub .t-en {
  margin-top: var(--space-2);
}

/* Search: an input plus a live client-side filter. Deliberately NOT a <form>
   — the site collects nothing (no email, no forms, no message boards), and a
   static site cannot search server-side anyway. */
.search {
  max-width: 620px;
  margin-bottom: var(--space-6);
}

.search-field {
  position: relative;
  display: flex;
  align-items: center;
}

.search-icon {
  position: absolute;
  left: 0.85rem;
  width: 18px;
  height: 18px;
  color: var(--ink-soft);
  pointer-events: none;
}

.search input {
  width: 100%;
  font-family: inherit;
  font-size: 1rem;
  color: var(--ink);
  background-color: var(--surface);
  border: 2px solid transparent;
  border-radius: var(--radius);
  padding: 0.75rem 0.9rem 0.75rem 2.6rem;
  box-shadow: var(--shadow-md);
}

.search input::placeholder {
  color: var(--ink-soft);
}

.search-hint {
  margin: var(--space-2) 0 0;
  font-size: 0.8125rem;
  color: var(--ink-soft);
}

/* Grade chips: the homepage lays out all 16 rather than a "popular 6" shortcut
   — the 16 groups ARE the product, so they are not hidden behind a database. */
.chips {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--space-3);
  margin: 0;
  padding: 0;
  list-style: none;
}

.chips li {
  margin: 0;
}

/* NOT USED BY ANY PAGE -- grep before you trust it. On the grey band a chip is
   a white patch with a real edge, which is how CSB draws the panels that hang
   off its nav. */
.chip {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: 0.55rem 0.7rem;
  border-radius: var(--radius-sm);
  background-color: var(--surface);
  border: 1px solid var(--control-line);   /* a chip is a LINK, so its edge is
                                              a control boundary and needs 3:1
                                              -- --edge is only a divider */
  color: var(--ink);
  text-decoration: none;
  font-size: 0.875rem;
  line-height: 1.3;
}

.chip:hover {
  background-color: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
}

.chip-emoji {
  flex: none;
  font-size: 1rem;
}

.chip-label {
  min-width: 0;
}

.chip-name {
  display: block;
  font-weight: 600;
}

.chip-code {
  display: block;
  font-family: var(--font-latin);
  font-size: 0.6875rem;
  color: var(--ink-soft);
  letter-spacing: 0.02em;
}

/* On hover the chip fills red, so the code cannot stay --ink-soft: that grey
   on the accent is far under 4.5:1. */
.chip:hover .chip-code {
  color: var(--on-accent);
}

/* --- Cards ---------------------------------------------------------------- */

.card-grid {
  display: grid;
  gap: var(--space-4);
}

.card-grid--4 {
  grid-template-columns: repeat(4, minmax(0, 1fr));
}

.card-grid--3 {
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

.card-grid--2 {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

/* Grade card (§2.5): the grade's own mark on the LEFT, name and link stacked
   to its right. The mark is what identifies the post at a glance, so it sits
   beside the name rather than above it -- a column stack pushed the name down
   and made sixteen cards read as sixteen identical blocks.
   The whole card is a link target because the Telegram group is the product
   and the site is only the shop window. */
.grade-card {
  display: flex;
  flex-direction: row;
  align-items: stretch;   /* NOT flex-start: the body has to fill the card so
                             .grade-card-cta's `margin-top: auto` can pin the
                             links to one baseline across the row. */
  gap: var(--space-3);
  padding: var(--space-4);
  background-color: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  text-decoration: none;
  color: var(--ink);
  transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.grade-card:hover {
  border-color: var(--accent);
  color: var(--ink);
}

.grade-card-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  flex: 1;
  min-width: 0;   /* lets long names wrap instead of forcing the card wider */
}

.grade-card-logo {
  width: 56px;
  height: 56px;
  flex: none;
  border-radius: var(--radius-sm);
  background-color: var(--surface);
  object-fit: contain;
}

.grade-card-name {
  font-size: 1rem;
  font-weight: 600;
  line-height: 1.35;
  margin: 0;
}

.grade-card-name .t-en {
  margin-top: 1px;
}

.grade-card-cta {
  margin-top: auto;
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--accent-deep);
  display: inline-flex;
  align-items: center;
  gap: 0.3em;
}

/* Job card (§2.5): badge → department → position → salary → dates, the field
   order borrowed from CSRADAR's information architecture while the visual
   treatment stays ours. */
.job-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-4);
  background-color: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
}

.badge {
  align-self: flex-start;
  display: inline-block;
  padding: 0.15rem 0.55rem;
  border-radius: var(--radius-pill);
  background-color: var(--accent-soft);
  color: var(--accent-deep);
  font-size: 0.75rem;
  font-weight: 600;
}

.job-card-dept {
  font-size: 0.8125rem;
  color: var(--ink-soft);
  margin: 0;
}

.job-card-title {
  font-size: 1.0625rem;
  font-weight: 600;
  margin: 0;
  line-height: 1.4;
}

.job-card-pay {
  font-size: 0.875rem;
  margin: 0;
  color: var(--ink);
}

.job-card-pay strong {
  font-weight: 700;
}

.job-card-dates {
  font-size: 0.75rem;
  color: var(--ink-soft);
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem 0.75rem;
}

/* The amber dot marks the deadline, so the eye lands on it first. */
.job-card-deadline::before {
  content: "";
  display: inline-block;
  width: 6px;
  height: 6px;
  margin-right: 0.35em;
  border-radius: 50%;
  background-color: var(--accent);
  vertical-align: middle;
}

/* Price card: bundle name, the price as the focal figure, a one-line note and
   an order button. Written for the /pricing page, which was dropped before it
   ever shipped, so it sat unused until the bundles moved onto /services/ on
   2026-09-14 — it was never dead on purpose. The figure is --accent-deep red
   on white (7.3:1), NOT the amber its original comment claimed; amber left the
   palette in the CSB retheme. */
.price-card {
  position: relative;
  display: flex;
  flex-direction: column;
  padding: var(--space-5);
  background-color: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow-sm);
}

.price-card--feature {
  border-color: var(--accent);
  border-width: 2px;
  box-shadow: var(--shadow-md);
}

.price-card-tag {
  position: absolute;
  top: calc(-1 * var(--space-3));
  right: var(--space-4);
  background-color: var(--accent);
  color: var(--on-accent);
  font-size: 0.75rem;
  font-weight: 700;
  padding: 0.2rem 0.6rem;
  border-radius: var(--radius-pill);
}

.price-card h3 {
  margin-bottom: var(--space-1);
}

.price-card-stages {
  font-size: 0.8125rem;
  color: var(--ink-soft);
  margin-bottom: var(--space-4);
}

.price-amount {
  font-family: var(--font-latin);
  font-size: 2.25rem;
  font-weight: 700;
  line-height: 1;
  color: var(--accent-deep);
  margin-bottom: var(--space-1);
}

.price-amount small {
  font-size: 0.9375rem;
  font-weight: 600;
}

.price-note {
  font-size: 0.8125rem;
  color: var(--ink-soft);
  margin-bottom: var(--space-4);
}

.price-features {
  list-style: none;
  padding: 0;
  margin: 0 0 var(--space-5);
  font-size: 0.875rem;
}

.price-features li {
  position: relative;
  padding-left: 1.4em;
  margin-bottom: var(--space-2);
}

.price-features li::before {
  content: "✓";
  position: absolute;
  left: 0;
  color: var(--ink);
  font-weight: 700;
}

.price-card .button {
  margin-top: auto;
}

/* --- Price table ---------------------------------------------------------- */

.table-scroll {
  overflow-x: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background-color: var(--surface);
  /* A wide fee table genuinely has to scroll on a phone. Windows draws a full
     height scrollbar inside the box for that, which reads as the page being
     broken rather than the table being scrollable. Thinning it keeps the
     affordance and drops the alarm. */
  scrollbar-width: thin;
}

.table-scroll::-webkit-scrollbar {
  height: 8px;
}

.table-scroll::-webkit-scrollbar-track {
  background: transparent;
}

.table-scroll::-webkit-scrollbar-thumb {
  background-color: var(--line-strong);
  border-radius: var(--radius-sm);
}

table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.9375rem;
}

caption {
  text-align: left;
  padding: var(--space-4) var(--space-4) 0;
  color: var(--ink-soft);
  font-size: 0.875rem;
}

th,
td {
  text-align: left;
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--line);
}

thead th {
  background-color: var(--band);
  color: var(--on-band);
  font-size: 0.8125rem;
  font-weight: 600;
  white-space: nowrap;
}

tbody th {
  font-weight: 600;
}

tbody tr:last-child th,
tbody tr:last-child td {
  border-bottom: 0;
}

.num {
  font-family: var(--font-latin);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Group heading INSIDE the services table (筆試 / 面試 / 體能及模擬 / 審查 /
   套裝). A real row rather than four separate tables, so the fee columns stay
   aligned down the whole catalogue and the numbers can be compared at a
   glance — which is the entire point of putting them in a table. */
.table-group th {
  background-color: var(--paper);
  color: var(--ink);
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.02em;
  padding-top: var(--space-5);
  border-bottom: 1px solid var(--line-strong);
}

/* The wrapper scrolls sideways when the table is too wide, so it has to be
   reachable without a pointer -- WCAG 2.1.1. script.js strips the tabindex
   again whenever the table actually fits, so a table that does not scroll
   does not leave an empty tab stop behind. */
.table-scroll:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* --- Pay and progression -------------------------------------------------- */

/* The pay-baseline warning at the top of /pay/. It sits ABOVE the tables
   rather than beside them on purpose: a reader who skips it will find a live
   advertisement quoting a figure ~2% lower and conclude this site is wrong. */
.notice {
  border-left: 4px solid var(--accent);
}

.notice h2 {
  font-size: 1.125rem;
  margin-bottom: var(--space-4);
}

.notice ul {
  margin: 0;
  padding-left: 1.1em;
  color: var(--ink-soft);
}

.notice li + li {
  margin-top: var(--space-3);
}

/* JRE and CRE are examinations, not grades. Muting the row keeps the table
   accounting for all sixteen entries without implying they have a pay scale
   whose figures simply failed to load. */
.pay-exam th,
.pay-exam td {
  color: var(--ink-soft);
  font-weight: 400;
}

.pay-grid {
  display: grid;
  gap: var(--space-5);
  /* `minmax(min(320px, 100%), 1fr)` rather than a bare `minmax(320px, 1fr)`.
     A fixed 320px track minimum cannot shrink below itself, so once the
     container got narrower than 320px + padding -- a 320px phone, where the
     container inner width is viewport minus 30px -- the track overflowed the
     page. The min() lets the track collapse to the container instead. */
  grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr));
}

.pay-card {
  background-color: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: var(--space-5);
  /* Without this a long rank name pushes the grid track wider than the
     viewport instead of wrapping -- the classic grid-blowout that puts a
     horizontal scrollbar on the whole page. */
  min-width: 0;
}

/* `thead th` is nowrap globally, so the big fee tables keep their columns
   aligned down the page. Inside a pay card the entry-qualification table is
   only ~294px wide, and that nowrap forced its min-content to 296px -- two
   pixels wider than the card. Two pixels is all it takes for the browser to
   draw a horizontal scrollbar inside a table that otherwise fits, which is a
   scrollbar appearing in the middle of the page for no visible reason.
   Letting these particular headers wrap removes the min-width that caused it. */
.pay-card thead th {
  white-space: normal;
}

.pay-card h3 {
  font-size: 1.0625rem;
  margin-bottom: var(--space-2);
}

.pay-scale {
  color: var(--ink-soft);
  font-size: 0.875rem;
  margin-bottom: var(--space-4);
}

/* The promotion ladder. A grid, not a flex row, so the pay column stays
   aligned down the card -- comparing rungs at a glance is the entire point. */
.ladder {
  list-style: none;
  margin: 0 0 var(--space-4);
  padding: 0;
  border-left: 2px solid var(--line);
}

.ladder-step {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 0 var(--space-4);
  padding: var(--space-3) var(--space-3) var(--space-3) var(--space-4);
  position: relative;
  border-bottom: 1px solid var(--line);
}

.ladder-step:last-child {
  border-bottom: 0;
}

/* The dot on the rail. It straddles the .ladder border-left rather than
   sitting inside it, so the rungs read as beads on one thread. */
.ladder-step::before {
  content: "";
  position: absolute;
  left: -5px;
  top: calc(var(--space-3) + 0.4em);
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background-color: var(--line-strong);
}

/* The rung the reader would actually ENTER on. Marked with the accent dot and
   a pill rather than a background wash -- red is an accent on this site and
   never a fill. */
.ladder-step--entry::before {
  background-color: var(--accent);
}

.ladder-rank {
  grid-column: 1;
  font-weight: 600;
}

.ladder-band {
  grid-column: 1;
  color: var(--ink-soft);
  font-size: 0.8125rem;
}

.ladder-pay {
  grid-column: 2;
  grid-row: 1 / span 2;
  align-self: center;
  font-family: var(--font-latin);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.ladder-here {
  display: inline-block;
  margin-left: var(--space-2);
  padding: 0 var(--space-2);
  border-radius: var(--radius-sm);
  background-color: var(--accent);
  color: var(--on-accent);
  font-size: 0.6875rem;
  font-weight: 600;
  vertical-align: middle;
}

/* A correction worth stating -- that CSD has no "Senior Officer" rank, say.
   A footnote in tone, not a warning. */
.pay-note {
  margin-top: var(--space-4);
  padding-top: var(--space-3);
  border-top: 1px solid var(--line);
  color: var(--ink-soft);
  font-size: 0.875rem;
}

.allowance-list {
  margin: 0;
  padding-left: 1.1em;
}

.allowance-list li + li {
  margin-top: var(--space-3);
}

/* --- Activity / news lists ------------------------------------------------ */

.list-plain {
  list-style: none;
  margin: 0;
  padding: 0;
}

/* "This week's popular grades" — four rows, replacing CSRADAR's realtime
   browsing ticker. At low traffic a realtime list reads as an empty room;
   a weekly ranking never does. */
.rank-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--line);
  text-decoration: none;
  color: var(--ink);
}

.rank-row:last-child {
  border-bottom: 0;
}

/* .rank-row and .rank-num are NOT USED BY ANY PAGE -- the "this week's popular
   grades" list was never built. Kept, and converted, so that nothing sitting in
   this file still assumes a navy ground it no longer has. */
.rank-row:hover .rank-name {
  color: var(--accent-deep);
}

.rank-num {
  flex: none;
  width: 26px;
  height: 26px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background-color: var(--band);
  color: var(--ink);
  font-family: var(--font-latin);
  font-size: 0.8125rem;
  font-weight: 700;
}

.rank-row:first-child .rank-num {
  background-color: var(--accent);
  color: var(--on-accent);
}

.rank-name {
  font-weight: 600;
  font-size: 0.9375rem;
}

/* Current-affairs preview: headline, source, and one line on the exam angle. */
.news-item {
  padding: var(--space-4) 0;
  border-bottom: 1px solid var(--line);
}

.news-item:last-child {
  border-bottom: 0;
}

.news-item h3 {
  font-size: 1rem;
  margin-bottom: var(--space-1);
}

.news-meta {
  font-size: 0.75rem;
  color: var(--ink-soft);
  margin-bottom: var(--space-2);
}

.news-angle {
  font-size: 0.875rem;
  color: var(--ink);
  margin: 0;
}

/* --- Split layout (homepage lower half) ----------------------------------- */

.split {
  display: grid;
  grid-template-columns: minmax(0, 2.4fr) minmax(0, 1fr);
  gap: var(--space-6);
  align-items: start;
}

.aside-panel {
  background-color: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: var(--space-5);
}

.aside-panel h2 {
  font-size: 1.0625rem;
  margin-bottom: var(--space-2);
}

/* --- Panel / callout ------------------------------------------------------ */

.panel {
  background-color: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: var(--space-5);
}

.callout {
  border-left: 4px solid var(--accent);
  background-color: var(--accent-soft);
  border-radius: var(--radius-sm);
  padding: var(--space-4) var(--space-5);
  margin-bottom: var(--space-5);
}

.callout p:last-child {
  margin-bottom: 0;
}

.callout--info {
  border-left-color: var(--ink);
  background-color: var(--band);
}

/* --- Page header (interior pages) ----------------------------------------- */

/* The grey band again, carrying the same red rule as the masthead. Together
   the two bracket every page: white header, red rule, grey band, white
   content, grey band, grey footer. */
.page-header {
  background-color: var(--band);
  color: var(--on-band);
  padding-block: var(--space-7) var(--space-6);
  border-bottom: 3px solid var(--accent);
}

/* The grade's own mark sits BESIDE the title, not above it: on a page named
   after one grade, the mark is the fastest confirmation the reader landed on
   the right one. */
.page-header-main {
  display: flex;
  align-items: flex-start;
  gap: var(--space-5);
}

.page-header-mark {
  width: 80px;
  height: 80px;
  flex: none;
  border-radius: var(--radius-sm);
  background-color: var(--surface);
}

.page-header-text {
  min-width: 0;
}

.page-header h1 {
  color: var(--ink);
  margin-bottom: var(--space-3);
}

.page-header .lead {
  font-size: 1.0625rem;
  color: var(--ink-soft);
  max-width: 62ch;
  margin: 0;
}

.page-header .lead .t-en {
  margin-top: var(--space-2);
}

.breadcrumb {
  font-size: 0.8125rem;
  margin-bottom: var(--space-4);
}

.breadcrumb ol {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  list-style: none;
  margin: 0;
  padding: 0;
}

.breadcrumb li {
  margin: 0;
  color: var(--ink-soft);
}

.breadcrumb li + li::before {
  content: "/";
  margin-right: var(--space-2);
  color: var(--ink-soft);
}

.breadcrumb a {
  color: var(--ink);
}

/* --- Stage list (grade pages) --------------------------------------------- */

.stages {
  list-style: none;
  margin: 0;
  padding: 0;
  counter-reset: stage;
}

.stage {
  position: relative;
  counter-increment: stage;
  padding: 0 0 var(--space-5) var(--space-7);
  border-left: 2px solid var(--line);
  margin-left: 15px;
}

.stage:last-child {
  border-left-color: transparent;
  padding-bottom: 0;
}

.stage::before {
  content: counter(stage);
  position: absolute;
  left: -16px;
  top: 0;
  width: 30px;
  height: 30px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background-color: var(--accent);
  color: var(--on-accent);
  font-family: var(--font-latin);
  font-size: 0.8125rem;
  font-weight: 700;
}

.stage-name {
  font-size: 1.0625rem;
  font-weight: 600;
  margin: 0 0 var(--space-1);
}

.stage-body {
  font-size: 0.9375rem;
  color: var(--ink-soft);
  margin: 0;
}

/* --- Footer --------------------------------------------------------------- */

/* The grey band again, closing the page the way the hero opens it. The footer
   carries the independence disclaimer, which the CSB retheme has made the
   single most load-bearing sentence on the site -- see the NOTE ON POSITIONING
   at the head of this file. */
.site-footer {
  background-color: var(--band);
  color: var(--ink-soft);
  padding-block: var(--space-7) var(--space-5);
  margin-top: var(--space-8);
}

.footer-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.6fr) repeat(3, minmax(0, 1fr));
  gap: var(--space-6);
  margin-bottom: var(--space-6);
}

.footer-brand {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  color: var(--ink);
  text-decoration: none;
  font-weight: 600;
  margin-bottom: var(--space-3);
}

.footer-brand img {
  width: 36px;
  height: 36px;
  object-fit: contain;   /* see .brand img -- same stretch trap */
  border-radius: var(--radius-sm);
  background: var(--surface);
}

.footer-about {
  font-size: 0.875rem;
  color: var(--ink-soft);
  max-width: 34ch;
  margin: 0;
}

/* These were --navy-100 on navy, then briefly the amber accent before that.
   Both are gone. The headings are LABELS, not brand moments -- they take full
   ink so they still structure the footer, and the red stays where it works:
   fills on light ground, and the rule under the masthead. */
.footer-heading {
  font-size: 0.8125rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink);
  margin-bottom: var(--space-3);
}

.footer-links {
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: 0.875rem;
}

.footer-links li {
  margin-bottom: var(--space-2);
}

.footer-links a {
  color: var(--ink);
  text-decoration: none;
}

.footer-links a:hover {
  color: #000000;
  text-decoration: underline;
}

/* The independence disclaimer is mandatory on every page. It is deliberately
   the most legible thing in the footer -- not fine print -- because the whole
   positioning rests on not being mistaken for a government channel. That was
   true when this was a merely government-adjacent palette; it is more true
   now, because the surfaces below it are the CSB's own. */
.footer-disclaimer {
  border-top: 1px solid var(--edge);
  border-bottom: 1px solid var(--edge);
  padding-block: var(--space-4);
  margin-bottom: var(--space-5);
  font-size: 0.875rem;
  color: var(--ink);
}

.footer-disclaimer strong {
  color: #000000;
}

.footer-disclaimer .t-en {
  color: var(--ink-soft);
  margin-top: var(--space-1);
}

.footer-meta {
  font-size: 0.8125rem;
  color: var(--ink-soft);
}

.footer-meta p {
  margin: 0 0 var(--space-2);
}

.footer-meta a {
  color: var(--ink);
}

/* --- Grade job summary ---------------------------------------------------- */

/* The one-line "what this job actually is", under the grade name on every
   card. This is NOT .job-card-dept, which says what the GROUP covers — a
   visitor who has never heard of "CSDO" needs this one first.
   No line clamp: the grid stretches every card in a row to the same height and
   .grade-card-cta already has `margin-top: auto`, so the CTAs stay aligned
   without truncating anyone's job description. */
.grade-card-summary {
  font-size: 0.8125rem;
  line-height: 1.55;
  color: var(--ink);
  margin: 0;
}

.grade-card-summary .t-en {
  margin-top: 2px;
}

.grade-about p:last-child {
  margin-bottom: 0;
}

/* --- Channel cards (channels & services) ---------------------------------- */

/* A Telegram channel or bot. The whole card is an outbound t.me link — the
   site never embeds a channel, it only points at one. */
.channel-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-5);
  background-color: var(--paper);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  text-decoration: none;
  color: var(--ink);
  transition: border-color 0.15s ease, box-shadow 0.15s ease,
    transform 0.15s ease;
}

.channel-card:hover {
  border-color: var(--accent);
  box-shadow: var(--shadow-lg);
  transform: translateY(-2px);
  color: var(--ink);
}

.channel-card-icon {
  font-size: 1.5rem;
  line-height: 1;
}

/* Marks a channel as belonging to THIS network. 「政府職位資訊頻道」 on its own
   can be skimmed as a government channel; the label removes that reading. Kept
   small and quiet so it labels without competing with the channel name. */
.channel-card-owner {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--ink-soft);
  margin: 0;
}

.channel-card-owner .t-en {
  font-weight: 400;
  color: var(--ink-soft);
}

.channel-card-name {
  font-size: 1.0625rem;
  font-weight: 600;
  line-height: 1.35;
  margin: 0;
}

.channel-card-name .t-en {
  margin-top: 1px;
}

.channel-card-desc {
  font-size: 0.875rem;
  color: var(--ink-soft);
  margin: 0;
}

.channel-card-desc .t-en {
  margin-top: var(--space-1);
}

.channel-card-cta {
  margin-top: auto;
  padding-top: var(--space-2);
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--accent-deep);
}

/* --- Services panel ------------------------------------------------------- */

.services-panel {
  margin-top: var(--space-6);
}

.services-panel-title {
  font-size: 1.125rem;
  margin-bottom: var(--space-2);
}

.services-panel-title .t-en {
  margin-top: 2px;
}

.services-panel-lead {
  font-size: 0.9375rem;
  color: var(--ink-soft);
  max-width: 68ch;
  margin-bottom: var(--space-4);
}

/* Sub-heading inside a services panel. The rule under it separates the free
   tier from the paid tier, which is the one distinction that matters here. */
.services-subhead {
  font-size: 1rem;
  font-weight: 600;
  color: var(--ink);
  margin: var(--space-5) 0 var(--space-3);
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--line);
}

/* .service-list / .service-item were a two-column list of the four bundles.
   RETIRED 2026-09-14: the bundles became .price-card (below), which carries a
   price, and the rest of the catalogue became the services table. Nothing
   renders these any more — if a future page wants the old list back it is in
   git history, not here. */

/* --- Responsive ----------------------------------------------------------- */

@media (max-width: 960px) {
  .card-grid--4 {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .card-grid--3 {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .split {
    grid-template-columns: minmax(0, 1fr);
  }

  .footer-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .chips {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 720px) {
  .header-inner {
    min-height: 60px;
  }

  .primary-nav {
    order: 3;
    flex-basis: 100%;
  }

  .primary-nav ul {
    gap: var(--space-2) var(--space-4);
  }

  .card-grid--4,
  .card-grid--3,
  .card-grid--2 {
    grid-template-columns: minmax(0, 1fr);
  }

  .footer-grid {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-5);
  }

  .section {
    padding-block: var(--space-6);
  }

  .service-list {
    grid-template-columns: minmax(0, 1fr);
  }
}

@media (max-width: 420px) {
  .chips {
    grid-template-columns: minmax(0, 1fr);
  }

  .container {
    padding-inline: var(--space-4);
  }
}

/* --- Stacked tables -------------------------------------------------------
   Measured at a 360px viewport, the tables' min-content widths are 519px (pay
   overview) and 498px (fee table) against a 330px content box. That floor is
   set by the dollar ranges and the two-language column headers, not by slack,
   so no amount of wrapping brings a four-column bilingual table down to a
   phone -- the choice is sideways scrolling or a different shape.

   Scrolling is what shipped, and it read as a broken page. The wrapper is as
   tall as the whole table -- 2333px on /pay/ -- so its scrollbar painted at
   the BOTTOM of that box, hundreds of pixels below the row being read, and
   readers saw a bar floating in the middle of the page. That is the reported
   bug, and it is a layout consequence, not a scrollbar-styling one.

   Below 600px every cell becomes its own labelled line instead. Nothing is
   hidden, no bar is drawn, and the min-content floor stops mattering. The
   labels come from data-label-* via attr() -- which is why build.py's lab()
   writes three attributes instead of the usual .t-tc/.t-sc span pair:
   language_spans() rewrites text nodes and can never reach an attribute, so
   the trilingual switch has to be done here, in CSS. */

@media (max-width: 600px) {
  .table-scroll {
    overflow-x: visible;
    border: 0;
    background: none;
  }

  .table-scroll table,
  .table-scroll caption,
  .table-scroll tbody,
  .table-scroll tr,
  .table-scroll th,
  .table-scroll td {
    display: block;
  }

  /* Redundant once every cell carries its own visible label. */
  .table-scroll thead {
    display: none;
  }

  .table-scroll caption {
    padding: 0 0 var(--space-4);
    text-align: left;
  }

  .table-scroll tr {
    margin-bottom: var(--space-3);
    padding: var(--space-4);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background-color: var(--surface);
  }

  .table-scroll th,
  .table-scroll td {
    padding: 0;
    border: 0;
  }

  /* The row heading is the card's title. */
  .table-scroll tbody th[scope="row"] {
    margin-bottom: var(--space-2);
    padding-bottom: var(--space-2);
    border-bottom: 1px solid var(--line);
    font-size: 1rem;
  }

  /* Label above value, both block-level.
     NOT flex. A cell here holds three elements, not one value: the 繁 span, the
     簡 span and the English twin, and `.t-en` is display:block by default. As
     flex items they all land on ONE line that cannot break, which pushed
     /services/ 123px past the viewport at 360px -- trading a scrollbar inside
     the table for a scrollbar on the whole page, a strictly worse bug. Block
     layout lets the spans wrap the way they do everywhere else on the site. */
  .table-scroll td {
    display: block;
    padding-block: var(--space-1);
  }

  .table-scroll td::before {
    content: attr(data-label-tc);
    display: block;
    color: var(--ink-soft);
    font-size: 0.8125rem;
    font-weight: 600;
  }

  html.lang-sc .table-scroll td::before {
    content: attr(data-label-sc);
  }

  html.lang-en .table-scroll td::before {
    content: attr(data-label-en);
  }

  /* The amounts no longer need to hold one line; letting them wrap removes
     the last thing that could set a min-content floor here. */
  .table-scroll .num {
    white-space: normal;
  }

  /* The in-table group bands (筆試 / 面試 / 套裝) are headings, not cards. */
  .table-scroll tr.table-group {
    margin-bottom: var(--space-2);
    padding: 0;
    border: 0;
    background: none;
  }

  .table-scroll tr.table-group th {
    padding-bottom: var(--space-1);
    background: none;
  }
}

/* --- Motion preference ---------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* --- Print ---------------------------------------------------------------- */

@media print {
  .site-header,
  .search,
  .button {
    display: none;
  }

  body {
    background: #fff;
  }
}
