/* ============================================================================
   FursaRadar — design foundation
   ============================================================================

   The brief for this file is "operational instrument". One operator opens the
   dashboard at 07:00 to scan 238 sources and ~6,000 postings and find what
   broke overnight. Everything here serves scanning speed and state legibility:
   colour carries meaning, never decoration; numbers align; the three broken
   rows in a table of twenty-five announce themselves without being read.

   The page is Arabic and RTL first. Every directional property below is
   logical (inline-start / inline-end, margin-inline, border-inline-start), so
   nothing needs an [dir] override. The two things that DO need mirroring —
   arrows and chevrons — are handled once, in the icon layer, so page authors
   never think about it.

   ----------------------------------------------------------------------------
   COMPONENT INVENTORY — this is the contract. Page templates use these; they
   do not add CSS. If something you need is missing, ask for it here rather
   than inlining a style.
   ----------------------------------------------------------------------------

   ICONS  (Jinja global, see fursaradar/app/web/icons.py)
     {{ icon("source") }}                decorative, aria-hidden
     {{ icon("x-circle", label="فشل") }} meaningful on its own
     {{ icon("edit", size="sm") }}       sm (0.95em) | md | lg (1.35em) | xl (24px)
     {{ icon("clock", class="…") }}      extra classes
     Directional glyphs (chevron-start/end/first/last, arrow-start/end,
     external-link, logout, play) mirror automatically under RTL.

   TONE  — five semantic tones. Every tone class sets four local custom
   properties (--tone, --tone-ink, --tone-bg, --tone-border) which the
   tone-aware components below read. Use the tone that matches the MEANING:
     success  something is working          warning  degraded, still working
     danger   broken, nothing is getting through
     info     neutral fact worth noticing   neutral  off / absent / unknown

   STATUS PILLS — .st
     .st                       base pill (icon + text; never colour alone)
     .st--success|warning|danger|info|neutral
     domain aliases, same five tones, named for what they mean here:
       .st--ok .st--live .st--sent            → success
       .st--partial .st--stale .st--pending   → warning
       .st--failed .st--dead .st--revoked     → danger
       .st--disabled .st--never .st--none     → neutral
       .st--new .st--queued                   → info
     .st--sm                   table-density (use inside dense cells)
     .st--solid                filled variant, for the one thing on a page
                               that must be seen first
     .st--quiet                text + dot only, no chip — for long columns
                               where 25 filled chips would be noise
     <span class="st-dot"></span>  optional leading dot inside .st--quiet

   PANELS
     .panel                    the base card (existing)
     .panel--success|warning|danger|info   tinted card with a severity bar on
                               the inline-start edge
     .panel.success/.error/.warning/.danger-zone   legacy aliases, kept
     .panel.quiet              sunken, shadowless (existing)
     .callout-head             the one-line headline inside a toned panel;
                               takes the panel's tone ink automatically
     .callout-list             the list of causes under a .callout-head

   TABLES / DENSITY
     .table-wrap               scroll container (existing)
     .table-wrap--tall         caps height so .table--sticky has something to
                               stick against
     .table--dense             tighter rows + smaller type, for long tables
     .table--sticky            sticky <thead>
     tr.is-danger / .is-warning / .is-success
                               severity stripe on the row's leading edge
     tr.is-spent               the opposite: a row that is over (expired,
                               revoked) and kept for the record, not for work.
                               Recedes. Colour only — the pill still says it.
     .num                      tabular figures (already on th/td/stat/score)
     .zero                     a zero that should recede, not shout
     .cell-main / .cell-sub    two-line cell: value over context
     .truncate                 one line + ellipsis
     .truncate-2               two lines + ellipsis
     .boilerplate              repeated text that should not compete
                               (muted, truncated)
     [data-tip="full text"]    hover/focus tooltip carrying the full string.
                               Put it on the <td> or a .tip wrapper — NEVER on
                               the truncating element itself, whose
                               overflow:hidden would clip the bubble:
                                 <td data-tip="the whole string">
                                   <span class="boilerplate">…</span></td>
     .tip                      inline wrapper to hang a data-tip on when there
                               is no cell to use

   TOOLBAR / FILTERS
     .toolbar                  horizontal bar of controls above a table
     .toolbar__group           a cluster that stays together when wrapping
     .toolbar__spacer          pushes what follows to the far edge
     .toolbar__count           the "N of M" readout
     .chip-filter              a toggleable filter chip; .is-on when active
     .filters / .source-filters / .seeker-filters   existing filter cards

   SHELL / TOP BAR
     .topbar-tools             the cluster at the far end of the top bar
                               (appearance control, and logout on the admin
                               side). Collapses to its own full-width row on a
                               phone.
     .theme-switch             the appearance control: light / auto / dark, as
                               a role="radiogroup" of three buttons. Rendered
                               by templates/_theme_switch.html — include that,
                               do not hand-roll it. Hidden until .has-js is on
                               <html> (set by theme-boot.js before first
                               paint), because it does nothing without JS.
     .theme-switch__opt        one option; styles off aria-checked, so the
                               semantics and the appearance cannot disagree.

   THEMING
     Light is the bare :root palette. Dark arrives two ways and both are
     honoured: @media (prefers-color-scheme: dark) for the OS, and
     [data-theme="dark"] on <html> for an explicit choice, which wins in both
     directions. Never define a colour only inside the media query — page
     styles read tokens and must resolve in every theme.

   INSTALL CARD — .install-card
     templates/_install_card.html; behaviour in static/pwa.js. Ships with the
     `hidden` attribute and is revealed only by script, because whether it
     applies is a browser fact (already installed? promptable? iOS?) that the
     server cannot know. Include it, do not hand-roll it:
       {% set install_permanent = false %}{% include "_install_card.html" %}
     false = dismissible (jobs page), true = permanent (settings page).
     .install-card__row / __mark / __copy / __head / __sub / __cta / __x
     .install-steps            numbered manual route
     .install-steps__glyph     inline iOS share mark

   PAGE GUIDANCE  (an operator page whose objects are not self-evident)
     .page-lede                one sentence under the <h1> saying what the page
                               is for; measure-capped at 74ch
     details.panel.guide       the folded explainer. Collapsed it is one row, so
                               the table it explains stays above the fold:
                                 <details class="panel guide">
                                   <summary>…</summary>
                                   <div class="guide__body">…</div></details>
     .guide__warn              the one line in it that warns rather than
                               defines. Pair with a tone class — it reads
                               --tone-*, so it can never drift out of step with
                               the rest of the app's warnings:
                                 <p class="guide__warn tone-warning">
     .guide__cols              auto-fit grid of explanation blocks (no
                               breakpoint: the count is a page fact)
     .guide__block             one block; its h3 takes an icon
     .guide__list              bulleted rules; <strong> lifts to full ink
     .guide__defs              status legend — a real .st pill in the <dt>, its
                               meaning in the <dd>. Render the component, never
                               a description of it.
     .section-head             an h2 plus the line saying what the section holds
     .section-head__note       that line

   PRIMARY ACTION  — one panel per page may claim to be the thing to do here
     .action-panel             takes the 3px accent edge (brand colour, which no
                               severity ever is, so it reads "start here" and
                               not "something is wrong")
     .steps                    the path in three words each, inline and numbered
     .action-form              fields + submit on one line; stacks under 560px
     .action-form__grow        the field that takes the slack
     .action-form__narrow      a short field (a number, a duration)
     .action-form__hint        the help line UNDER the row. Not a .field-hint
                               inside a label: that makes one label taller and
                               drops the submit below the inputs.

   SEEKER RESULT TUNING  (templates/seeker_settings.html)
     .tuning                   the fieldset holding the four levers that decide
                               what the seeker sees. Takes the accent edge, like
                               .action-panel, because it is the thing to do here
     .weight-list .weight      the "حتى 30 نقطة" pill beside a field name in the
                               explainer. Numbers come from SCORE_WEIGHTS, which
                               is read from scorer.py — never typed in

   JOURNEY STRIP  (templates/_journey.html)
     .journey                  the four-step map, on /join, /join/done and every
                               intake page. Include with `journey` set to the
                               current step number:
                                 {% set journey = 3 %}{% include "_journey.html" %}
     .journey__step            one step; .is-done / .is-current carry the state
     .journey__mark __label    the numbered disc and the text beside it

   LANDING PAGE  (templates/landing.html — the site root, "/")
     .landing-shell            wider main column than the seeker flow's 780px;
                               set via base_public.html's `main_class` block
     .landing-hero             two columns: copy beside the sample digest
     .radar-mark               the sweeping dish in the eyebrow. The ONLY
                               animation in the app; honours reduced-motion
     .landing-hero__lede       the sentence that has to land, measure-capped
     .landing-cta              the one button, repeated verbatim at the close.
                               Same words both times on purpose
     .digest / __frame __bar __avatar __bot __badge __bubble __line __caption
                               the signature element: the daily Telegram digest
                               as the chat it arrives in. Its text tracks
                               services/alerts.py::format_digest_ar line for
                               line — change that function, change this
     .landing-figures          live counters (<dl>); rendered only when the
                               inventory is non-empty, see the template
     .landing-why              four claims, each one a thing the pipeline does
     .landing-steps            the four-step path as a numbered <ol>. NOT
                               .steps: that one is the inline three-word strip
                               above an operator form, and these steps carry a
                               sentence each
     .landing-proof            a replica of the real .job-card, with
                               .landing-proof__tag marking it as an example
     .faq                      objections as native <details>. No script: the
                               production CSP would refuse an inline handler and
                               a question that will not open is worse than one
                               that is not asked
     .topbar-cta               the header's copy of the CTA (landing + privacy
                               only; hidden under 620px, where .landing-sticky
                               takes over)
     .landing-sticky           phone-only action bar. position:sticky — fixed
                               is broken here, see the note on the rule
     .landing-close            the closing CTA band, the only centred block
     (there is NO .landing-admin — see the note at the foot of landing.html)

   PUBLIC JOIN FORM  (templates/join.html, join_done.html, privacy.html)
     .hp-field                 the honeypot wrapper: clipped (never parked at
                               a negative offset — under RTL that scrolls the
                               page), NOT display:none, and paired with
                               aria-hidden + tabindex="-1" on the markup so no
                               keyboard or screen-reader user can land in it
     .field-hint               help sentence under an input (inside the <label>)
     .consent-block            the PDPL consent fieldset. NOT a `.seg` — that
                               control clips its input to 1×1 and shows state by
                               tinting the label, which hides the one tick a
                               person must be able to see. Pairs with
                               .consent-block__tick for the box + sentence.
     .legal                    prose measure and rhythm for the privacy notice
     .legal-todo               a clause a lawyer has not written yet. Never
                               remove one without replacing the text it marks.

   JOIN REQUEST QUEUE  (templates/join_requests.html)
     .join-card                one request; cards, not table rows, because a
                               request carries a free-text note
     .join-card__head / __name / __email / __facts / __note / __states
                               / __actions

   LAYOUT UTILITIES
     .stack                    vertical rhythm (gap via --stack-gap)
     .cluster                  horizontal wrap with gap
     .u-nowrap .u-break .u-mono .u-end
     .visually-hidden          screen-reader-only text

   ============================================================================ */

/* ── Typography ─────────────────────────────────────────────────────────────
   IBM Plex Sans Arabic, self-hosted. This app makes zero external requests and
   keeps it that way — a webfont CDN would be the first one, and would leak
   every admin page view to a third party. Arabic and Latin ship as separate
   subsets behind unicode-range: an all-Arabic page never downloads the Latin
   file, and the URL/token columns get real Latin from the same family instead
   of a fallback that does not match.

   Three weights ship, and only three: 400 (body), 600 (labels, nav, table
   heads) and 700 (headings, the brand, emphasised numbers). Weight 500 used
   to be declared here and was never once selected by a rule — `document.fonts`
   reported both 500 faces `unloaded` on every page — so the declarations and
   the two 32 KB files went with it.

   Nothing here is <link rel=preload>ed, deliberately. It used to be, for the
   two Arabic faces, and Chrome logged "preloaded but not used" on every page
   load. The reason is that the guess was wrong and could not be right: a page
   pulls six faces (three weights × two subsets) and which one the renderer
   needs first is a property of that page's content, not of the base template.
   Measured on /sources, the preloads and the stylesheet all left the preload
   scanner in the same batch at 86 ms and the CSS-discovered faces followed at
   91 ms — a 5 ms head start, on an already-open same-origin connection, in
   exchange for a claim about the critical path that was false for four of the
   six files. `font-display: swap` is what actually prevents invisible text,
   and it needs no help. */

@font-face {
  font-family: "IBM Plex Sans Arabic";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("fonts/ibm-plex-sans-arabic-arabic-400-normal.woff2") format("woff2");
  unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891, U+0898-08E1,
    U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE70-FEFF,
    U+102E0-102FB, U+10E60-10E7E, U+10EFD-10EFF, U+1EE00-1EEFF;
}

@font-face {
  font-family: "IBM Plex Sans Arabic";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("fonts/ibm-plex-sans-arabic-arabic-600-normal.woff2") format("woff2");
  unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891, U+0898-08E1,
    U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE70-FEFF,
    U+102E0-102FB, U+10E60-10E7E, U+10EFD-10EFF, U+1EE00-1EEFF;
}

@font-face {
  font-family: "IBM Plex Sans Arabic";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("fonts/ibm-plex-sans-arabic-arabic-700-normal.woff2") format("woff2");
  unicode-range: U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891, U+0898-08E1,
    U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE70-FEFF,
    U+102E0-102FB, U+10E60-10E7E, U+10EFD-10EFF, U+1EE00-1EEFF;
}

@font-face {
  font-family: "IBM Plex Sans Arabic";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("fonts/ibm-plex-sans-arabic-latin-400-normal.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "IBM Plex Sans Arabic";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("fonts/ibm-plex-sans-arabic-latin-600-normal.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "IBM Plex Sans Arabic";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("fonts/ibm-plex-sans-arabic-latin-700-normal.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ── Tokens: light ──────────────────────────────────────────────────────────
   This block and its dark twin below are the ONLY place a colour literal
   appears. Everything after them reads a token. */

:root {
  color-scheme: light;

  /* surfaces */
  --bg: #f4f6f5;
  --surface: #ffffff;
  --surface-soft: #f7f9f8;
  --surface-sunken: #eef2f0;
  --surface-hover: #f2f6f4;
  --surface-glass: rgba(255, 255, 255, 0.86);
  --row-hover: rgba(15, 118, 110, 0.04);

  /* text */
  --text: #10201a;
  --ink: var(--text);
  --muted: #5a6b63;
  --faint: #697771;
  /* --on-accent: text on an --accent-strong fill (buttons, brand mark).
     --on-tone:   text on a saturated tone fill (score badges, solid pills).
     They part company in dark mode, where the tones go light and the
     brand teal stays deep. */
  --on-accent: #ffffff;
  --on-tone: #ffffff;

  /* lines */
  --border: #dde5e1;
  --border-strong: #c3cfc9;
  --border-hover: #a9bab2;

  /* brand */
  --accent: #0f766e;
  --accent-strong: #0b5f59;
  --accent-deep: #084f4a;
  --accent-soft: #e6f3f0;
  --accent-ink: #0a534e;
  --accent-2: #2563eb;

  /* semantic tones: line colour / text-on-tint / tint / tint border */
  --success: #0f766e;
  --success-ink: #08514c;
  --success-bg: #e6f3f0;
  --success-border: #a9d6cf;

  --warning: #b45309;
  --warning-ink: #8a4708;
  --warning-bg: #fdf3e5;
  --warning-border: #eccba0;

  --danger: #b42318;
  --danger-ink: #97180f;
  --danger-bg: #fdecea;
  --danger-border: #f0b4ae;

  --info: #2563eb;
  --info-ink: #1a4bb8;
  --info-bg: #e9f0fe;
  --info-border: #b9cdf7;

  --neutral: #6b7a73;
  --neutral-ink: #47554e;
  --neutral-bg: #eef1ef;
  --neutral-border: #d3dbd7;

  /* legacy aliases kept so nothing that already reads them breaks */
  --warn: var(--warning);
  --bad: var(--danger);

  /* focus */
  --focus: #2563eb;
  --focus-ring: rgba(37, 99, 235, 0.45);
  /* Ink for text sitting ON a saturated tone fill, and the tick inside a
     checked box — both flip with the theme so white never lands on a pale
     dark-mode tone. */
  --check-fill: var(--accent-strong);
  --hairline: rgba(255, 255, 255, 0.16);

  /* type */
  --font-sans: "IBM Plex Sans Arabic", "SF Arabic", "Geeza Pro", "Noto Sans Arabic",
    "Segoe UI", Tahoma, Arial, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, "IBM Plex Mono", Menlo, Consolas, monospace;
  --fs-display: clamp(27px, 1.6vw + 21px, 34px);
  --fs-h2: 19px;
  --fs-h3: 13.5px;
  --fs-body: 15px;
  --fs-sm: 13.5px;
  --fs-meta: 12.5px;
  --fs-micro: 11.5px;
  --lh-tight: 1.2;
  --lh-snug: 1.4;
  --lh-body: 1.6;

  /* shape */
  --radius-sm: 5px;
  --radius-md: 7px;
  --radius-lg: 8px;
  --shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.05);
  --shadow-md: 0 14px 34px rgba(16, 24, 40, 0.08);
  --shadow: var(--shadow-md);
  --container: 1180px;
}

/* ── Tokens: dark ───────────────────────────────────────────────────────────
   Only the tokens are redefined; not one rule below is duplicated. The tones
   invert their relationship — the tint becomes a deep wash and the "ink"
   becomes the light end — so every tone-aware component follows without
   knowing which theme it is in. Contrast was checked in both directions:
   body text 14.6:1, muted 7.6:1, every tone ink on its own tint above 8:1.

   Dark arrives by two independent routes and the palette has to answer to
   both:

     1. the OS asks for it   → @media (prefers-color-scheme: dark)
     2. the operator asks    → [data-theme="dark"] on <html>

   Route 1 is scoped `:root:not([data-theme="light"])` so that choosing light
   on a dark-mode laptop actually gets light — without the :not(), the media
   query would keep winning and the control would only work in one direction.
   Route 2 is written last so an explicit choice also wins ties.

   The two blocks below are byte-identical in the tokens they set, and
   test_theme_control.py fails if they ever drift apart. CSS has no way to
   share one declaration list across a media boundary, so the duplication is
   structural — the test is what keeps it honest.

   Every colour has a light definition on the bare :root above, so nothing on
   this page is defined *only* inside a media query. */

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;

    --bg: #0d1512;
    --surface: #131e1a;
    --surface-soft: #182521;
    --surface-sunken: #0f1815;
    --surface-hover: #1d2b26;
    --surface-glass: rgba(19, 30, 26, 0.86);
    --row-hover: rgba(45, 212, 191, 0.06);

    --text: #e7efeb;
    --muted: #9fb2aa;
    --faint: #7f948c;
    --on-accent: #ffffff;
    --on-tone: #0d1512;

    --border: #263630;
    --border-strong: #35483f;
    --border-hover: #45594f;

    --accent: #2dd4bf;
    /* Deep enough that white text clears AA on it (5.2:1) and light
       enough that the filled button still reads as a raised object
       against the dark panel (3.3:1). */
    --accent-strong: #0d7a70;
    --accent-deep: #0a635a;
    --accent-soft: #12302c;
    --accent-ink: #6ee7d7;
    --accent-2: #7ea6ff;

    --success: #34d399;
    --success-ink: #6ee7b7;
    --success-bg: #102b25;
    --success-border: #245c4e;

    --warning: #f59e0b;
    --warning-ink: #fbbf6e;
    --warning-bg: #2e2109;
    --warning-border: #6b4c14;

    --danger: #f87171;
    --danger-ink: #fca5a5;
    --danger-bg: #331414;
    --danger-border: #6f2a2a;

    --info: #7ea6ff;
    --info-ink: #a9c4ff;
    --info-bg: #16203a;
    --info-border: #2f4478;

    --neutral: #8fa39b;
    --neutral-ink: #b3c4bd;
    --neutral-bg: #1c2723;
    --neutral-border: #35483f;

    --focus: #7ea6ff;
    --focus-ring: rgba(126, 166, 255, 0.5);
    --hairline: rgba(255, 255, 255, 0.08);

    --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
    --shadow-md: 0 14px 34px rgba(0, 0, 0, 0.45);
  }
}

:root[data-theme="dark"] {
  color-scheme: dark;

  --bg: #0d1512;
  --surface: #131e1a;
  --surface-soft: #182521;
  --surface-sunken: #0f1815;
  --surface-hover: #1d2b26;
  --surface-glass: rgba(19, 30, 26, 0.86);
  --row-hover: rgba(45, 212, 191, 0.06);

  --text: #e7efeb;
  --muted: #9fb2aa;
  --faint: #7f948c;
  --on-accent: #ffffff;
  --on-tone: #0d1512;

  --border: #263630;
  --border-strong: #35483f;
  --border-hover: #45594f;

  --accent: #2dd4bf;
  /* Deep enough that white text clears AA on it (5.2:1) and light
     enough that the filled button still reads as a raised object
     against the dark panel (3.3:1). */
  --accent-strong: #0d7a70;
  --accent-deep: #0a635a;
  --accent-soft: #12302c;
  --accent-ink: #6ee7d7;
  --accent-2: #7ea6ff;

  --success: #34d399;
  --success-ink: #6ee7b7;
  --success-bg: #102b25;
  --success-border: #245c4e;

  --warning: #f59e0b;
  --warning-ink: #fbbf6e;
  --warning-bg: #2e2109;
  --warning-border: #6b4c14;

  --danger: #f87171;
  --danger-ink: #fca5a5;
  --danger-bg: #331414;
  --danger-border: #6f2a2a;

  --info: #7ea6ff;
  --info-ink: #a9c4ff;
  --info-bg: #16203a;
  --info-border: #2f4478;

  --neutral: #8fa39b;
  --neutral-ink: #b3c4bd;
  --neutral-bg: #1c2723;
  --neutral-border: #35483f;

  --focus: #7ea6ff;
  --focus-ring: rgba(126, 166, 255, 0.5);
  --hairline: rgba(255, 255, 255, 0.08);

  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
  --shadow-md: 0 14px 34px rgba(0, 0, 0, 0.45);
}

/* ── Base ─────────────────────────────────────────────────────────────────── */

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  overflow-x: hidden;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: var(--fs-body);
  font-weight: 400;
  line-height: var(--lh-body);
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
}

/* One page-load treatment, and only one. 4px of rise over 220ms tells the eye
   the page finished; anything more would be a toy on a tool. */
@keyframes fr-enter {
  from {
    opacity: 0;
    transform: translateY(4px);
  }
}

body > main {
  animation: fr-enter 220ms ease-out both;
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    scroll-behavior: auto !important;
  }
}

.skip-link {
  position: fixed;
  inset-block-start: 8px;
  inset-inline-start: 12px;
  z-index: 1000;
  padding: 9px 14px;
  border-radius: var(--radius-md);
  background: var(--accent-strong);
  color: var(--on-accent);
  font-weight: 700;
  text-decoration: none;
  transform: translateY(-160%);
  transition: transform 120ms ease;
}

.skip-link:focus {
  transform: translateY(0);
}

a {
  color: inherit;
  text-underline-offset: 3px;
}

code,
kbd,
samp,
pre {
  font-family: var(--font-mono);
  font-size: 0.92em;
}

.shell {
  width: min(var(--container), calc(100vw - 32px));
  margin: 0 auto;
}

/* ── Type scale ─────────────────────────────────────────────────────────────
   The old scale was flat: an h1 at 36 and a section head at 21 read as the
   same rank once Arabic set them both bold. The ladder is now carried by size,
   weight AND colour together, so the rungs are unmistakable at a glance:
   page title 34/700 ink · section 19/600 ink · sub-head 13.5/700 muted ·
   body 15/400 · meta 12.5/400 muted. */

h1,
h2,
h3,
p {
  margin-top: 0;
}

h1 {
  margin-bottom: 0;
  font-size: var(--fs-display);
  font-weight: 700;
  line-height: var(--lh-tight);
  letter-spacing: 0;
  color: var(--text);
}

h2 {
  margin-bottom: 10px;
  font-size: var(--fs-h2);
  font-weight: 600;
  line-height: 1.35;
  color: var(--text);
}

h3 {
  margin: 22px 0 8px;
  font-size: var(--fs-h3);
  font-weight: 700;
  line-height: var(--lh-snug);
  color: var(--muted);
}

.eyebrow {
  margin: 0 0 6px;
  color: var(--accent-ink);
  font-size: var(--fs-micro);
  font-weight: 700;
  line-height: 1.3;
}

.meta {
  font-size: var(--fs-meta);
}

/* Numbers that sit in a column must not shuffle sideways between rows. */
th,
td,
.num,
.stat strong,
.card-stats strong,
.score,
.seg .count,
.toolbar__count,
time,
code {
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}

/* ── Icons ──────────────────────────────────────────────────────────────────
   The sprite is inlined once per page by the base template. Glyphs inherit the
   colour and optical size of the text they sit beside, so an icon inside a
   danger pill is danger-coloured without anyone saying so. */

.icon-sprite {
  position: absolute;
  width: 0;
  height: 0;
  overflow: hidden;
}

.icon {
  flex: none;
  inline-size: 1.15em;
  block-size: 1.15em;
  vertical-align: -0.2em;
  overflow: visible;
}

.icon--sm {
  inline-size: 0.95em;
  block-size: 0.95em;
  vertical-align: -0.14em;
}

.icon--lg {
  inline-size: 1.35em;
  block-size: 1.35em;
  vertical-align: -0.26em;
}

.icon--xl {
  inline-size: 24px;
  block-size: 24px;
  vertical-align: -5px;
}

/* Chirality, handled once. A "next" chevron points left in Arabic; leaving
   this to page authors is how half a pagination bar ends up pointing the wrong
   way. icons.py tags the directional glyphs, this flips them. */
[dir="rtl"] .icon--flip {
  transform: scaleX(-1);
}

/* ── Tone ───────────────────────────────────────────────────────────────────
   A tone class sets four local properties and nothing else. Every component
   that can be toned reads --tone-ink / --tone-bg / --tone-border, so one class
   on a wrapper colours the whole thing correctly in both themes. */

.st--success,
.st--ok,
.st--live,
.st--sent,
.panel--success,
.panel.success,
.tone-success {
  --tone: var(--success);
  --tone-ink: var(--success-ink);
  --tone-bg: var(--success-bg);
  --tone-border: var(--success-border);
}

.st--warning,
.st--partial,
.st--stale,
.st--pending,
.panel--warning,
.panel.warning,
.tone-warning {
  --tone: var(--warning);
  --tone-ink: var(--warning-ink);
  --tone-bg: var(--warning-bg);
  --tone-border: var(--warning-border);
}

.st--danger,
.st--failed,
.st--dead,
.st--revoked,
.panel--danger,
.panel.error,
.panel.danger-zone,
.tone-danger {
  --tone: var(--danger);
  --tone-ink: var(--danger-ink);
  --tone-bg: var(--danger-bg);
  --tone-border: var(--danger-border);
}

.st--info,
.st--new,
.st--queued,
.panel--info,
.tone-info {
  --tone: var(--info);
  --tone-ink: var(--info-ink);
  --tone-bg: var(--info-bg);
  --tone-border: var(--info-border);
}

.st--neutral,
.st--disabled,
.st--never,
.st--none,
.tone-neutral {
  --tone: var(--neutral);
  --tone-ink: var(--neutral-ink);
  --tone-bg: var(--neutral-bg);
  --tone-border: var(--neutral-border);
}

/* ── Status pill ────────────────────────────────────────────────────────────
   The single highest-value component here. Source health, link status, match
   tier and invite state all render through it. Three signals carry the state —
   shape, colour and a word — so it survives greyscale, colour blindness and a
   screen reader, and none of them is load-bearing alone. */

/* The untoned default is expressed as a var() FALLBACK, never as an assignment
   on .st. `.st { --tone: … }` would sit later in this file than `.st--danger`
   at identical specificity and quietly win, painting every pill on the site
   neutral grey — which is precisely the failure this component exists to
   prevent. */
.st {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  max-inline-size: 100%;
  padding: 3px 9px;
  border: 1px solid var(--tone-border, var(--neutral-border));
  border-radius: 999px;
  background: var(--tone-bg, var(--neutral-bg));
  color: var(--tone-ink, var(--neutral-ink));
  font-size: var(--fs-meta);
  font-weight: 600;
  line-height: 1.45;
  white-space: nowrap;
  vertical-align: middle;
}

.st .icon {
  color: var(--tone, var(--neutral));
}

/* Dense tables: 25 rows of full-size chips is a wall. This is the same signal
   at table weight. */
.st--sm {
  gap: 4px;
  padding: 1px 7px;
  font-size: var(--fs-micro);
}

/* For the one status on a page that has to be seen before anything else. */
.st--solid {
  border-color: var(--tone, var(--neutral));
  background: var(--tone, var(--neutral));
  color: var(--on-tone);
}

.st--solid .icon {
  color: inherit;
}

/* No chip at all — a dot and a word. Use down a long column, where the chip
   background itself becomes the noise. */
.st--quiet {
  padding: 0;
  border-color: transparent;
  background: none;
  color: var(--tone-ink, var(--neutral-ink));
  font-weight: 600;
}

.st-dot {
  flex: none;
  inline-size: 8px;
  block-size: 8px;
  border-radius: 50%;
  background: var(--tone, var(--neutral));
  box-shadow: 0 0 0 2px var(--tone-bg, var(--neutral-bg));
}

/* An unknown state is not a good state: give it a hollow dot so "never
   fetched" never reads as "fine". */
.st--never .st-dot,
.st--none .st-dot,
.st--disabled .st-dot {
  background: transparent;
  box-shadow: inset 0 0 0 1.5px var(--tone, var(--neutral));
}

.st__label {
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ── Shell: top bar and navigation ──────────────────────────────────────── */

.topbar {
  min-height: 70px;
  margin-top: 14px;
  padding: 10px 14px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface-glass);
  box-shadow: var(--shadow-sm);
  backdrop-filter: blur(14px);
}

.brand,
.nav {
  display: flex;
  align-items: center;
}

.brand {
  gap: 10px;
  text-decoration: none;
  font-weight: 700;
  letter-spacing: 0;
}

.brand-mark {
  display: inline-grid;
  place-items: center;
  width: 36px;
  height: 36px;
  border-radius: var(--radius-md);
  background: var(--accent-strong);
  color: var(--on-accent);
  font-size: 13px;
  box-shadow: inset 0 0 0 1px var(--hairline);
}

.nav {
  gap: 4px;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
  padding: 4px;
  box-shadow: var(--shadow-sm);
}

.nav a {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  text-decoration: none;
  padding: 8px 11px;
  border-radius: var(--radius-md);
  color: var(--muted);
  font-size: var(--fs-sm);
  font-weight: 600;
  white-space: nowrap;
}

.nav a .icon {
  color: var(--faint);
}

.nav a:hover {
  background: var(--surface-hover);
  color: var(--text);
}

.nav a:hover .icon {
  color: var(--muted);
}

.nav a.active,
.nav a[aria-current="page"] {
  background: var(--accent-soft);
  color: var(--accent-ink);
  font-weight: 700;
}

.nav a.active .icon,
.nav a[aria-current="page"] .icon {
  color: var(--accent);
}

/* Right end of the top bar: the appearance control, plus the logout form on
   the admin side. A cluster rather than two loose children so the bar keeps
   three columns on desktop and collapses as one unit on a phone. */
.topbar-tools {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* ── Appearance control ─────────────────────────────────────────────────────
   Three states, not two. A two-state toggle has no way to say "follow the
   device", so anyone whose laptop flips at sunset would be stranded in
   whichever half they last picked — and 19:00 Riyadh is exactly when this
   dashboard gets opened. So: light / auto / dark, as a real radio group.

   Shape and semantics agree. It is three <button>s in a role="radiogroup"
   with aria-checked, which is the pattern for "pick one of three", driven by
   arrow keys and a roving tabindex in theme.js. A single cycling button would
   have been smaller, but a screen-reader user cannot see what the next press
   would do, and nobody can jump straight to the state they want.

   Reading order is light → auto → dark, which under RTL puts light on the
   right: the palette gets lighter toward the start of the line.

   Hidden until theme.js can drive it. The pre-paint snippet in <head> sets
   .has-js before the first frame, so this is a cascade decision, not a
   post-load mutation — the control never appears late and never shifts the
   bar. */
.theme-switch {
  display: none;
}

.has-js .theme-switch {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 3px;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--shadow-sm);
}

/* Overrides the 44px global button floor on purpose: this sits inside a 70px
   bar beside 38px controls, and 32px square keeps the whole tap target inside
   the bar. It is still a 32px target with 3px of container padding around it,
   which clears the 24px minimum, and on a phone it grows to 40px below. */
.theme-switch__opt {
  min-height: 32px;
  width: 32px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-md);
  background: transparent;
  box-shadow: none;
  color: var(--faint);
}

.theme-switch__opt .icon {
  color: inherit;
}

.theme-switch__opt:hover {
  background: var(--surface-hover);
  border: 0;
  box-shadow: none;
  transform: none;
  color: var(--text);
}

.theme-switch__opt:hover .icon {
  color: inherit;
}

/* The checked state is carried by aria-checked, so the style reads it from
   there — the attribute and the appearance cannot drift apart. */
.theme-switch__opt[aria-checked="true"] {
  background: var(--accent-soft);
  color: var(--accent-ink);
}

.theme-switch__opt[aria-checked="true"]:hover {
  background: var(--accent-soft);
  color: var(--accent-ink);
}


.page-head,
.section-head,
.panel-head,
.match-title-line {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 20px;
}

.page-head {
  margin: 30px 0 20px;
  padding-bottom: 2px;
}

/* ── Buttons ────────────────────────────────────────────────────────────── */

.actions,
.row-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}

button,
.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 44px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
  color: var(--text);
  padding: 9px 13px;
  font: inherit;
  font-size: var(--fs-sm);
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
  transition:
    background-color 160ms ease,
    border-color 160ms ease,
    color 160ms ease,
    box-shadow 160ms ease,
    transform 160ms ease;
}

button .icon,
.button .icon {
  color: var(--muted);
}

button:hover,
.button:hover {
  border-color: var(--border-hover);
  box-shadow: var(--shadow-sm);
  transform: translateY(-1px);
}

button:hover .icon,
.button:hover .icon {
  color: var(--text);
}

button:disabled,
.button[aria-disabled="true"] {
  cursor: not-allowed;
  opacity: 0.64;
  transform: none;
}

.primary {
  background: var(--accent-strong);
  border-color: var(--accent-strong);
  color: var(--on-accent);
}

.primary .icon,
.primary:hover .icon {
  color: inherit;
}

.primary:hover {
  background: var(--accent-deep);
  border-color: var(--accent-deep);
}

.danger-action {
  border-color: var(--danger-border);
  color: var(--danger-ink);
}

.danger-action .icon {
  color: var(--danger);
}

.danger-action:hover {
  border-color: var(--danger);
  background: var(--danger-bg);
}

/* ── Stat tiles ─────────────────────────────────────────────────────────── */

.stats-grid,
.profile-grid,
.detail-grid {
  display: grid;
  gap: 14px;
}

.stats-grid {
  grid-template-columns: repeat(4, minmax(0, 1fr));
  margin-bottom: 28px;
}

.stat,
.panel,
.empty,
.match-row,
.table-wrap,
.filters {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
}

.stat {
  padding: 18px;
}

.stat > span,
.stat__label {
  display: flex;
  align-items: center;
  gap: 6px;
  color: var(--muted);
  font-size: var(--fs-meta);
  font-weight: 600;
}

.stat__label .icon,
.stat > span .icon {
  color: var(--faint);
}

.meta {
  color: var(--muted);
}

.stat strong {
  display: block;
  margin-top: 6px;
  font-size: 30px;
  font-weight: 600;
  line-height: 1.05;
  color: var(--text);
}

/* A metric that is zero is information, but it is not news. Dropping it to the
   faint token (still AA against the surface) keeps a row of "0 0 0 0" from
   shouting as loudly as the one column that actually moved. */
.zero,
.stat strong.zero,
.num.zero {
  color: var(--faint);
  font-weight: 400;
}

.section {
  margin: 24px 0;
}

.section-head {
  align-items: center;
  margin-bottom: 12px;
}

.section-head a {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  color: var(--accent-ink);
  font-size: var(--fs-sm);
  font-weight: 600;
  text-decoration: none;
}

.section-head a .icon {
  color: var(--accent);
}

/* ── Panels and callouts ────────────────────────────────────────────────── */

.panel,
.empty {
  padding: 22px;
}

.panel {
  position: relative;
}

.panel-head h2,
.section-head h2 {
  margin-bottom: 0;
}

.empty {
  color: var(--muted);
  background: var(--surface-soft);
}

.panel.quiet {
  background: var(--surface-soft);
  box-shadow: none;
}

/* A toned panel says how bad it is before a word of it is read: a tinted
   ground, and a severity bar on the leading edge (inline-start, so it lands on
   the right in Arabic without an override). */
.panel--success,
.panel--warning,
.panel--danger,
.panel--info,
.panel.success,
.panel.warning,
.panel.error,
.panel.danger-zone {
  border-color: var(--tone-border);
  border-inline-start: 3px solid var(--tone);
  background: var(--tone-bg);
  color: var(--tone-ink);
}

.panel--success .meta,
.panel--warning .meta,
.panel--danger .meta,
.panel--info .meta,
.panel.success .meta,
.panel.warning .meta,
.panel.error .meta {
  color: inherit;
}

/* The headline of a toned panel. Carries the panel's own tone, so a total
   outage cannot render in the same colour as "all clear" — which is exactly
   what used to happen on the dashboard's alert-readiness block. */
.callout-head {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin: 0;
  color: var(--tone-ink, var(--text));
  font-size: var(--fs-body);
  font-weight: 700;
  line-height: var(--lh-snug);
}

.callout-head .icon {
  margin-block-start: 0.15em;
  color: var(--tone, var(--muted));
}

.callout-list {
  margin: 10px 0 0;
  padding-inline-start: 20px;
  color: var(--tone-ink, var(--text));
  font-size: var(--fs-sm);
  font-weight: 400;
}

.callout-list li + li {
  margin-top: 5px;
}

/* Existing inline warning text. Left on the warning tone deliberately: it is
   the "this is degraded" voice. Anything that BLOCKS belongs in a
   .panel--danger, not here. */
.risks {
  margin: 10px 0 0;
  color: var(--warning-ink);
  font-weight: 600;
}

.activation-panel,
.status-line {
  margin-top: 14px;
}

/* ── Match rows and scores ──────────────────────────────────────────────── */

.match-list {
  display: grid;
  gap: 12px;
}

.match-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(170px, auto);
  gap: 20px;
  padding: 20px;
  overflow: hidden;
}

.job-title {
  font-size: 17px;
  font-weight: 700;
  text-decoration: none;
  color: var(--text);
  overflow-wrap: anywhere;
}

.job-title:hover {
  color: var(--accent-ink);
}

.score {
  display: inline-grid;
  place-items: center;
  min-width: 52px;
  min-height: 36px;
  padding: 5px 10px;
  border-radius: var(--radius-md);
  color: var(--on-tone);
  font-weight: 700;
  font-size: var(--fs-sm);
  text-align: center;
  box-shadow: var(--shadow-sm);
}

.score-excellent {
  background: var(--accent);
}

.score-good {
  background: var(--accent-2);
  color: var(--on-tone);
}

.score-maybe {
  background: var(--warning);
  color: var(--on-tone);
}

.score-weak {
  background: var(--danger);
  color: var(--on-tone);
}

.chips {
  display: flex;
  flex-wrap: wrap;
  gap: 7px;
  margin: 12px 0;
}

.chips span,
.status {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--surface-soft);
  color: var(--muted);
  padding: 3px 9px;
  font-size: var(--fs-meta);
  font-weight: 600;
  line-height: 1.45;
}

.chips.quiet span {
  background: transparent;
  color: var(--faint);
}

.reason-list {
  margin-bottom: 0;
  padding-inline-start: 20px;
  color: var(--muted);
  font-size: var(--fs-sm);
}

.row-actions {
  align-content: flex-start;
  justify-content: flex-end;
  max-width: 340px;
}

.profile-grid {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.detail-grid {
  grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
}

/* ── Tables ─────────────────────────────────────────────────────────────── */

.table-wrap {
  max-width: 100%;
  overflow: auto;
  overscroll-behavior-inline: contain;
  -webkit-overflow-scrolling: touch;
}

/* A sticky header needs a scroll box with a ceiling to stick against. */
.table-wrap--tall {
  max-block-size: min(70vh, 720px);
}

.table-scroll-hint {
  display: none;
  margin: 0 0 8px;
  color: var(--muted);
  font-size: var(--fs-meta);
}

.panel:has(table) {
  overflow-x: auto;
}

/* Narrow 2-3 column tables (analytics panels): the global 720px table
   min-width overflows their grid column, and in RTL the overflow hides the
   leading digits of the numbers column (2063 renders as "063"). */
.table-wrap.table-compact table {
  min-width: 100%;
}

/* Quiet one-line status notes on the dashboard (finished cycle/rematch):
   informational history, not something demanding attention. */
.update-note {
  margin: 0 0 10px;
  font-size: var(--fs-meta);
}

/* Person-card counters as one line instead of a nested 4-box stats grid. */
.card-stats {
  margin: 10px 0 4px;
  color: var(--muted);
  font-size: var(--fs-sm);
}

.card-stats strong {
  color: var(--text);
  font-weight: 600;
}

table {
  width: 100%;
  border-collapse: collapse;
  min-width: 720px;
}

th,
td {
  padding: 12px 14px;
  border-bottom: 1px solid var(--border);
  text-align: start;
  vertical-align: top;
}

th {
  background: var(--surface-soft);
  color: var(--muted);
  font-size: var(--fs-meta);
  font-weight: 600;
}

tbody tr:hover {
  background: var(--row-hover);
}

td span {
  display: block;
  color: var(--muted);
  font-size: var(--fs-sm);
}

/* Long inventories — sources, jobs, invites. Rows get shorter and the type
   gets smaller, so more of the table is on screen at once and the eye travels
   less to compare two rows. */
.table--dense th,
.table--dense td {
  padding: 7px 10px;
  font-size: var(--fs-sm);
  line-height: 1.45;
}

.table--dense th {
  font-size: var(--fs-micro);
}

.table--dense td span,
.table--dense .cell-sub {
  font-size: var(--fs-micro);
}

/* The column headings stay put while the body scrolls, so column 8 of 14 is
   still identifiable 60 rows down. */
.table--sticky thead th {
  position: sticky;
  inset-block-start: 0;
  z-index: 2;
  background: var(--surface-soft);
  box-shadow: inset 0 -1px 0 var(--border-strong);
}

/* The three broken rows among twenty-five. A stripe on the row's leading edge
   is visible in peripheral vision, which is what "scan a table" actually
   means — and it is a second channel on top of the status pill's colour. */
tr.is-danger > td:first-child,
tr.is-warning > td:first-child,
tr.is-success > td:first-child {
  border-inline-start: 3px solid var(--tone, var(--border));
}

tr.is-danger {
  --tone: var(--danger);
}

tr.is-warning {
  --tone: var(--warning);
}

tr.is-success {
  --tone: var(--success);
}

/* The opposite signal to the three above: a row that is OVER — an expired or
   revoked invite, kept for the record and not for work. It recedes rather
   than announcing itself, so the rows that can still be acted on are what the
   eye lands on. Colour only; the status pill beside it still carries the word,
   because two thirds of a long ledger fading out must never be the only thing
   that says so. */
tr.is-spent > td {
  color: var(--faint);
}

/* Two lines in one cell: the value, then the context that qualifies it. */
.cell-main {
  display: block;
  color: var(--text);
  font-weight: 600;
}

.cell-sub {
  display: block;
  margin-top: 1px;
  color: var(--muted);
  font-size: var(--fs-meta);
  font-weight: 400;
}

.num {
  white-space: nowrap;
}

/* ── Truncation and tooltips ────────────────────────────────────────────────
   Half the width of a source table is the same boilerplate URL prefix repeated
   twenty-five times. Truncating it gives the columns that differ room to
   breathe; data-tip hands the full string back on hover or keyboard focus, so
   nothing is actually lost. */

.truncate {
  display: block;
  max-inline-size: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.truncate-2 {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.boilerplate {
  display: block;
  max-inline-size: 28ch;
  overflow: hidden;
  color: var(--muted);
  font-size: var(--fs-meta);
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* IMPORTANT: data-tip must NOT sit on the element that truncates. The ellipsis
   needs overflow:hidden, and overflow:hidden clips this pseudo-element too. Put
   it on the <td> (or a .tip wrapper) and truncate the span inside:
       <td data-tip="the whole string"><span class="boilerplate">…</span></td> */
[data-tip] {
  position: relative;
}

.tip {
  position: relative;
  display: inline-block;
  max-inline-size: 100%;
  vertical-align: top;
}

[data-tip]::after {
  content: attr(data-tip);
  position: absolute;
  inset-block-start: calc(100% + 6px);
  inset-inline-start: 0;
  z-index: 40;
  max-inline-size: 46ch;
  padding: 6px 9px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-md);
  background: var(--surface);
  color: var(--text);
  box-shadow: var(--shadow-md);
  font-size: var(--fs-meta);
  font-weight: 400;
  line-height: 1.45;
  white-space: normal;
  overflow-wrap: anywhere;
  opacity: 0;
  visibility: hidden;
  transition: opacity 120ms ease;
}

[data-tip]:hover::after,
[data-tip]:focus-visible::after {
  opacity: 1;
  visibility: visible;
}

/* ── Toolbar ────────────────────────────────────────────────────────────────
   The bar that sits above a table: search, filters, the count, the actions.
   One row on a desktop, wrapping into groups that stay together on a phone. */

.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 10px;
  padding: 10px 12px;
  margin-bottom: 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--shadow-sm);
}

.toolbar__group {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  min-width: 0;
}

.toolbar__spacer {
  flex: 1 1 auto;
}

.toolbar__count {
  color: var(--muted);
  font-size: var(--fs-meta);
  font-weight: 600;
  white-space: nowrap;
}

.toolbar label {
  gap: 4px;
  font-size: var(--fs-micro);
}

.toolbar button,
.toolbar .button,
.toolbar select,
.toolbar input {
  min-height: 38px;
  padding-block: 6px;
  font-size: var(--fs-sm);
}

.chip-filter {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  min-height: 32px;
  padding: 4px 11px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  color: var(--muted);
  font-size: var(--fs-meta);
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
}

.chip-filter:hover {
  border-color: var(--border-hover);
  background: var(--surface-hover);
  color: var(--text);
  transform: none;
  box-shadow: none;
}

.chip-filter.is-on,
.chip-filter[aria-pressed="true"] {
  border-color: var(--accent);
  background: var(--accent-soft);
  color: var(--accent-ink);
}

.chip-filter .icon {
  color: currentcolor;
}

/* ── Layout utilities ───────────────────────────────────────────────────── */

.stack {
  display: grid;
  gap: var(--stack-gap, 12px);
}

.cluster {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--cluster-gap, 8px);
}

.u-nowrap {
  white-space: nowrap;
}

.u-break {
  overflow-wrap: anywhere;
  word-break: break-word;
}

.u-mono {
  font-family: var(--font-mono);
  font-size: 0.92em;
}

.u-end {
  margin-inline-start: auto;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ── Invite link tools ──────────────────────────────────────────────────── */

.copy-link {
  width: 100%;
  min-width: 0;
}

.invite-link-tools {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 8px;
  align-items: start;
  width: 100%;
  min-width: 0;
}

.invite-share-button {
  white-space: nowrap;
}

.invite-share-status {
  grid-column: 1 / -1;
  min-height: 20px;
  color: var(--muted);
  font-size: var(--fs-meta);
}

/* ── Page lede, section heads, guidance ─────────────────────────────────────
   A page whose objects are not self-evident (invites: a one-off link and a
   seeker's permanent portal are the same row type in the database and two
   completely different things to a person) has to say so on the page. These
   are the pieces that do the saying. */

/* One sentence under an <h1>: what this page is for, in the operator's words.
   Measure-capped, because a full-width line at --container is unreadable. */
.page-lede {
  max-inline-size: 74ch;
  margin: 8px 0 0;
  color: var(--muted);
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
}

/* The heading of one section inside a panel, plus the line that says what the
   section contains. Baseline-aligned so the note reads as a continuation of
   the heading, not as a second heading. */
.section-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 12px;
  margin-bottom: 12px;
}

.section-head h2 {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
}

.section-head__note {
  min-width: 0;
  margin: 0;
  color: var(--muted);
  font-size: var(--fs-meta);
  line-height: var(--lh-snug);
}

/* The folded explainer. Collapsed it is one 44px row, so the table it explains
   stays where the operator expects it; a wall of prose above the fold would be
   read once and resented daily. */
.guide__body {
  display: grid;
  gap: 16px;
}

/* The one line in the panel that is a warning rather than a definition: the
   token in the URL is the whole of the access. Reads its colours from the tone
   the element also carries (.tone-warning), so it cannot drift out of step
   with every other warning in the app. */
.guide__warn {
  display: flex;
  align-items: flex-start;
  gap: 9px;
  margin: 0;
  padding: 11px 13px;
  border: 1px solid var(--tone-border);
  border-inline-start: 3px solid var(--tone);
  border-radius: var(--radius-md);
  background: var(--tone-bg);
  color: var(--tone-ink);
  font-size: var(--fs-sm);
  line-height: var(--lh-snug);
}

.guide__warn .icon {
  flex: none;
  margin-block-start: 0.1em;
  color: var(--tone);
}

/* auto-fit rather than a breakpoint: the number of explanation blocks is a
   property of the page, not of the viewport. */
.guide__cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 18px 24px;
  align-items: start;
}

.guide__block {
  min-width: 0;
}

.guide__block h3 {
  display: flex;
  align-items: center;
  gap: 7px;
  margin: 0 0 6px;
  color: var(--text);
  font-size: var(--fs-sm);
}

.guide__block p {
  margin: 0;
  color: var(--muted);
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
}

.guide__list {
  margin: 0;
  padding-inline-start: 18px;
  color: var(--muted);
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
}

.guide__list li + li {
  margin-top: 6px;
}

.guide__list strong {
  color: var(--text);
}

/* The status legend: the real pill next to what it means. Rendering the
   component itself rather than describing it is the point — the operator maps
   colour to word to meaning once, and the legend cannot go stale when a tone
   changes. */
.guide__defs {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  gap: 7px 12px;
  align-items: baseline;
  margin: 0;
}

.guide__defs dt,
.guide__defs dd {
  margin: 0;
}

.guide__defs dd {
  color: var(--muted);
  font-size: var(--fs-sm);
  line-height: var(--lh-snug);
}

/* ── Primary action card ────────────────────────────────────────────────────
   One panel per page may claim to be THE thing to do here. It takes the accent
   edge — the same 3px inline-start bar the toned panels use for severity, in
   the brand colour, which no severity ever is — so it reads as "start here"
   and not as "something is wrong". */
.action-panel {
  border-inline-start: 3px solid var(--accent);
}

.action-panel > h2 {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* The path, in three words each. Numbered because the order is the whole
   point, and inline because as a stacked list it would outweigh the form it
   introduces. */
.steps {
  display: flex;
  flex-wrap: wrap;
  gap: 7px 18px;
  margin: 0 0 14px;
  padding: 0;
  list-style: none;
  counter-reset: step;
}

.steps li {
  display: flex;
  align-items: center;
  gap: 7px;
  color: var(--muted);
  font-size: var(--fs-meta);
  font-weight: 600;
}

.steps li::before {
  counter-increment: step;
  content: counter(step);
  display: grid;
  place-items: center;
  flex: none;
  inline-size: 20px;
  block-size: 20px;
  border-radius: 50%;
  background: var(--accent-soft);
  color: var(--accent-ink);
  font-size: var(--fs-micro);
  font-weight: 700;
}

/* Fields and their submit on one line at desktop: the whole action is a single
   visual gesture rather than a stack the eye has to walk down. */
.action-form {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  gap: 10px 12px;
  margin: 0;
}

.action-form__grow {
  flex: 1 1 260px;
  min-width: 0;
}

.action-form__narrow {
  flex: 0 1 190px;
  min-width: 0;
}

.action-form > button {
  flex: none;
  min-height: 44px;
}

/* The help text is one line under the whole row, not a .field-hint inside each
   label. With `align-items: end` a hint inside a label makes that label taller
   than the others and drops the submit button a hint's height below the inputs
   it belongs to -- the row stops reading as one gesture. */
.action-form__hint {
  flex: 1 1 100%;
  margin: 2px 0 0;
  color: var(--muted);
}

/* ── Filter cards ───────────────────────────────────────────────────────── */

.filters {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 12px;
  align-items: end;
  padding: 16px;
  margin-bottom: 16px;
}

.filters > .additional-filters {
  grid-column: 1 / -1;
}

.filter-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 12px;
}

/* ── Disclosure (details/summary) ───────────────────────────────────────── */

details > summary {
  min-height: 44px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  color: var(--text);
  font-size: var(--fs-sm);
  font-weight: 700;
  cursor: pointer;
  list-style: none;
  user-select: none;
}

details > summary::-webkit-details-marker {
  display: none;
}

/* A caret, not a "+". Physical right/bottom borders on purpose: rotating that
   corner 45deg yields a down-pointing chevron in BOTH directions, whereas the
   logical properties would flip it into a meaningless sideways tick under
   RTL — "open downward" has no reading direction. */
details > summary::after {
  content: "";
  flex: none;
  margin-inline-start: auto;
  margin-block-end: 4px;
  inline-size: 8px;
  block-size: 8px;
  border-right: 1.6px solid var(--accent);
  border-bottom: 1.6px solid var(--accent);
  transform: rotate(45deg);
  transition: transform 140ms ease;
}

details[open] > summary::after {
  margin-block-end: 0;
  margin-block-start: 4px;
  transform: rotate(225deg);
}

details.panel > summary {
  margin: -4px 0;
}

details.panel[open] > summary {
  margin-bottom: 14px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--border);
}

.additional-filters,
.filter-drawer,
.match-diagnostics,
.secondary-actions {
  min-width: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface-soft);
}

.additional-filters,
.filter-drawer {
  padding: 4px 12px;
}

.additional-filters[open],
.filter-drawer[open] {
  padding-bottom: 12px;
}

.additional-filters > .detail-grid,
.filter-drawer__body {
  margin-top: 8px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
}

.additional-filters > .detail-grid {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.advanced-tools,
.dashboard-diagnostics,
.manual-send {
  margin-block: 16px;
}

.match-diagnostics {
  margin-top: 12px;
  padding: 2px 10px 10px;
}

.secondary-actions {
  width: 100%;
  padding: 2px 8px 8px;
}

.operator-tools,
.portal-management,
.inline-editor {
  min-width: 0;
  padding: 3px 10px 9px;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface-soft);
}

.operator-tools[open] > summary,
.portal-management[open] > summary,
.inline-editor[open] > summary {
  margin-bottom: 9px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--border);
}

/* In-panel edit form (profile data, search plan): folded away by default so
   the read-only summary above it stays the first thing an operator sees. */
.inline-editor {
  margin-top: 16px;
}

/* Email alerts sit beside the Telegram settings, visibly a separate channel
   so nobody reads the two forms as one save. */
.email-alerts {
  margin-top: 20px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
}

.portal-management {
  margin-top: 10px;
}

/* ── Source health table ────────────────────────────────────────────────── */

.source-health-table {
  min-width: 1620px;
}

/* Actions live in one narrow column; the edit form gets a row of its own so the
   table does not grow another eight columns of inputs. The invites table reuses
   the same shape for the same reason. */
.source-actions,
.invite-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  align-items: center;
  min-width: 130px;
}

.source-actions form,
.invite-actions form {
  margin: 0;
}

.source-edit-row > td,
.invite-edit-row > td {
  padding-top: 0;
  background: var(--surface-soft);
}

.source-edit-form,
.invite-edit-form {
  max-width: 980px;
  margin-bottom: 8px;
}

.source-edit-form label:has(textarea) {
  grid-column: 1 / -1;
}

.inline-check {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 8px;
}

.source-health-table th:first-child,
.source-health-table td:first-child {
  min-width: 150px;
}

.source-health-table th:nth-child(8),
.source-health-table td:nth-child(8) {
  min-width: 230px;
}

.source-health-table th:nth-child(6),
.source-health-table td:nth-child(6) {
  min-width: 125px;
}

.secondary-actions form + form {
  margin-top: 8px;
}

/* ── Form controls ──────────────────────────────────────────────────────── */

label {
  display: grid;
  gap: 6px;
  color: var(--muted);
  font-size: var(--fs-meta);
  font-weight: 700;
}

select,
textarea,
input {
  width: 100%;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
  color: var(--text);
  padding: 10px 11px;
  font: inherit;
  font-size: var(--fs-sm);
  min-height: 44px;
  transition:
    border-color 160ms ease,
    box-shadow 160ms ease,
    background-color 160ms ease;
}

textarea {
  resize: vertical;
}

select:hover,
textarea:hover,
input:hover {
  border-color: var(--border-hover);
}

select:focus,
textarea:focus,
input:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
  outline: none;
}

[aria-invalid="true"] {
  border-color: var(--danger);
  box-shadow: 0 0 0 3px var(--danger-bg);
}

input[type="checkbox"] {
  min-height: auto;
  width: auto;
  accent-color: var(--accent-strong);
}

.check-label {
  grid-template-columns: auto 1fr;
  align-items: center;
  color: var(--ink);
}

/* The no-JS half of a destructive-action guard (templates/_confirm.html).
   Nobody normally sees this: forms.js removes it on load and the confirm()
   dialog takes over. It is on screen only when that file was blocked or
   failed, which is precisely when the operator needs to read what the button
   is about to destroy before the browser will let them submit. */
.confirm-gate {
  display: flex;
  gap: 8px;
  align-items: flex-start;
  margin: 8px 0;
  padding: 8px 10px;
  border: 1px solid var(--danger-border);
  border-radius: 8px;
  font-size: 0.85rem;
  line-height: 1.5;
  text-align: start;
  color: var(--ink);
}

.confirm-gate input[type="checkbox"] {
  margin-top: 3px;
  flex: none;
}

.feedback-form {
  display: grid;
  gap: 12px;
  margin-top: 24px;
}

.panel > form:not(.feedback-form),
.profile-wizard,
.stack-form {
  display: grid;
  gap: 18px;
}

.recommendation {
  color: var(--accent-ink);
  font-size: 17px;
  font-weight: 700;
  text-transform: capitalize;
}

/* Global a11y: visible keyboard focus everywhere. */
*:focus-visible {
  outline: 3px solid var(--focus-ring);
  outline-offset: 2px;
}

/* ============================================================
   Job-seeker public experience (RTL / mobile-first)
   ============================================================ */

/* Minimal public header: brand on one side, token links on the other */
.topbar--public {
  min-height: 64px;
}

.topbar--public a:not(.brand) {
  text-decoration: none;
  padding: 8px 12px;
  border-radius: var(--radius-md);
  color: var(--muted);
  font-weight: 600;
  font-size: var(--fs-sm);
}

.topbar--public a:not(.brand):hover {
  background: var(--surface-hover);
  color: var(--text);
}

/* Narrow centered reading column for the whole seeker flow */
.seeker-shell {
  width: min(780px, calc(100vw - 28px));
  margin-inline: auto;
}

/* Card-like filter bar (mirrors .filters look) */
.seeker-filters {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px 14px;
  align-items: end;
  padding: 16px;
  margin: 0 0 18px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
}

/* Search, view tabs and the submit row take the full width on top/bottom */
.seeker-filters > label:first-child,
.seeker-filters label:has([name="q"]),
.seeker-filters input[name="q"],
.seeker-filters .seg.tabs,
.seeker-filters .filter-actions {
  grid-column: 1 / -1;
}

.filter-primary {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 8px;
}

.seeker-filters > .filter-drawer {
  grid-column: 1 / -1;
}

.filter-drawer__body {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px;
  align-items: end;
}

.filter-drawer__body .active-filter-summary,
.filter-drawer__body .filter-actions {
  grid-column: 1 / -1;
}

.active-filter-summary {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin: 0;
  padding: 9px 11px;
  border-radius: var(--radius-md);
  background: var(--accent-soft);
  color: var(--accent-ink);
  font-size: var(--fs-meta);
}

.active-filter-summary span + span::before {
  content: "·";
  margin-inline-end: 6px;
}

.seeker-filters .filter-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 8px;
}

/* Segmented control (work_type / sort) and view tabs */
.seg {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
  padding: 4px;
}

fieldset.seg {
  min-inline-size: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  padding: 12px;
}

fieldset.seg legend,
fieldset.seg .meta {
  width: 100%;
}

fieldset.seg legend {
  padding: 0 4px;
  color: var(--muted);
  font-size: var(--fs-meta);
  font-weight: 700;
}

.seg.tabs {
  display: flex;
  flex-wrap: wrap;
}

.seg a,
.seg label,
.seg button {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin: 0;
  min-height: auto;
  padding: 8px 12px;
  border: 0;
  border-radius: var(--radius-md);
  background: transparent;
  color: var(--muted);
  font-size: var(--fs-sm);
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
}

.seg a:hover,
.seg label:hover,
.seg button:hover {
  background: var(--surface-hover);
  border-color: transparent;
  box-shadow: none;
  transform: none;
}

/* Visually hide the underlying radio while keeping it accessible */
.seg input {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  border: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

.seg .active,
.seg a.active,
.seg input:checked + *,
.seg label:has(input:checked) {
  background: var(--accent-soft);
  color: var(--accent-ink);
  font-weight: 700;
}

/* A small count chip the tabs can carry */
.seg .count {
  font-size: var(--fs-micro);
  font-weight: 700;
  opacity: 0.75;
}

/* Job card */
.job-card {
  display: grid;
  gap: 12px;
  padding: 18px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
  scroll-margin-top: 18px;
}

.job-card + .job-card {
  margin-top: 16px;
}

.job-card__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
}

.job-card__title {
  margin: 0;
  font-size: 18px;
  font-weight: 700;
  line-height: 1.3;
  overflow-wrap: anywhere;
}

.job-card__meta {
  margin: 0;
  color: var(--muted);
  font-size: var(--fs-sm);
  overflow-wrap: anywhere;
}

.job-card__badges {
  display: flex;
  flex-wrap: wrap;
  gap: 7px;
}

.job-card__reasons {
  display: grid;
  gap: 6px;
  margin: 2px 0 0;
  padding-inline-start: 18px;
  color: var(--muted);
  font-size: var(--fs-sm);
}

.job-card__actions {
  display: grid;
  gap: 8px;
  padding-top: 4px;
}

.job-card__secondary-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 7px;
}

.job-card__secondary-actions form {
  flex: 0 1 auto;
}

.button.compact,
button.compact {
  min-height: 44px;
  padding: 7px 10px;
  font-size: var(--fs-meta);
  font-weight: 600;
}

.job-card__unavailable {
  margin: 0;
  padding: 11px 13px;
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius-md);
  background: var(--surface-soft);
  color: var(--muted);
  text-align: center;
  font-weight: 600;
}

/* Forms inside the action row stay compact (no global form grid here) */
.job-card__actions form {
  margin: 0;
}

/* Pills / badges */
.badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--surface-soft);
  color: var(--muted);
  padding: 3px 9px;
  font-size: var(--fs-meta);
  font-weight: 600;
  line-height: 1.45;
}

.badge--new {
  border-color: var(--accent);
  background: var(--accent);
  color: var(--on-tone);
}

.badge--official {
  border-color: var(--accent-2);
  background: var(--accent-2);
  color: var(--on-tone);
}

.badge--fit-excellent {
  border-color: var(--accent);
  background: var(--accent);
  color: var(--on-tone);
}

.badge--fit-good {
  border-color: var(--accent-2);
  background: var(--accent-2);
  color: var(--on-tone);
}

.badge--fit-maybe {
  border-color: var(--warning);
  background: var(--warning);
  color: var(--on-tone);
}

/* A search-plan field an admin pinned by hand: it no longer auto-updates, and
   a silently pinned field is a trap, so it is marked everywhere it appears. */
.badge--pinned {
  background: var(--warning-bg);
  border-color: var(--warning-border);
  color: var(--warning-ink);
  font-size: var(--fs-micro);
}

/* Link our checker could not verify (usually a 403 anti-bot response, not a
   dead posting): the job stays visible, with an honest caveat. */
.badge--unverified {
  background: var(--warning-bg);
  border-color: var(--warning-border);
  color: var(--warning-ink);
}

/* Secondary "قد تناسبك" list: clearly subordinate to the primary results. */
.suggested-section {
  margin-top: 32px;
  padding-top: 20px;
  border-top: 1px solid var(--border);
}

.suggested-head h2 {
  font-size: var(--fs-h2);
}

.suggested-head .meta {
  margin-top: 2px;
}

.suggested-section .job-card {
  background: var(--surface-soft);
}

/* Neutral pill that reflects feedback_status text */
.state-pill {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  border-radius: 999px;
  border: 1px solid var(--info-border);
  background: var(--info-bg);
  color: var(--info-ink);
  padding: 3px 9px;
  font-size: var(--fs-meta);
  font-weight: 600;
}

/* Primary apply CTA: prominent, grows to fill the action row */
.apply-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 1 1 auto;
  min-height: 48px;
  font-size: var(--fs-body);
  font-weight: 700;
  min-width: 150px;
}

/* Long URLs (e.g. Telegram bot link) never overflow the column */
.link-wrap {
  word-break: break-all;
  overflow-wrap: anywhere;
}

/* Vertical form layout for intake/settings without a global form{} rule */
.stack-form {
  display: grid;
  gap: 18px;
}

/* ── Responsive ─────────────────────────────────────────────────────────── */

@media (max-width: 1100px) {
  .filters {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 860px) {
  .topbar,
  .page-head,
  .match-row,
  .detail-grid {
    grid-template-columns: 1fr;
    display: grid;
  }

  .profile-grid,
  .filters {
    grid-template-columns: 1fr;
  }

  .stats-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .nav {
    width: 100%;
    overflow-x: auto;
    overscroll-behavior-inline: contain;
    scrollbar-width: thin;
  }

  .actions,
  .section-head,
  .panel-head,
  .match-title-line {
    align-items: stretch;
  }

  .actions > *,
  .actions form,
  .actions button,
  .actions .button,
  .row-actions > *,
  .row-actions form,
  .row-actions button,
  .row-actions .button {
    width: 100%;
  }

  .row-actions {
    justify-content: flex-start;
    max-width: none;
  }

  .table-scroll-hint {
    display: block;
  }

  .toolbar__spacer {
    display: none;
  }

  .source-filters {
    grid-template-columns: 1fr;
  }
}

@media (max-width: 560px) {
  .shell {
    width: min(100% - 24px, var(--container));
  }

  .topbar {
    margin-top: 10px;
    padding: 10px;
  }

  .topbar .nav {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 4px;
    overflow: visible;
  }

  .topbar--public .nav {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .topbar .nav a {
    padding-inline: 6px;
    justify-content: center;
    text-align: center;
  }

  /* Six nav items across three columns of a phone: the label is the thing that
     has to survive, so the glyph steps aside rather than squeezing it. */
  .topbar .nav a .icon {
    display: none;
  }

  /* The bar is stacked by now, so the tools get their own full-width row.
     The control grows to 40px squares — a comfortable thumb target — and
     still leaves the logout button the rest of the row. */
  .topbar-tools {
    width: 100%;
  }

  .has-js .theme-switch {
    padding: 4px;
  }

  .theme-switch__opt {
    min-height: 40px;
    width: 40px;
  }

  .dashboard-actions {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    width: 100%;
  }

  .dashboard-actions > form:first-child,
  .dashboard-actions > a.button,
  .dashboard-actions > .operator-tools {
    grid-column: 1 / -1;
  }

  /* Two up, not one: these tiles are a label and a number, and stacking them
     full-width pushed the actual content of the dashboard a whole screen down
     on a phone. */
  .stats-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 10px;
  }

  .stat {
    padding: 14px;
  }

  .stat strong {
    font-size: 25px;
  }

  .panel,
  .empty,
  .match-row,
  .job-card,
  .seeker-filters {
    padding: 16px;
  }

  .job-card__head {
    display: grid;
  }

  .match-title-line {
    display: grid;
  }

  .match-title-line .score {
    justify-self: start;
  }

  .apply-btn {
    width: 100%;
  }

  .invite-link-tools {
    grid-template-columns: 1fr;
  }

  /* One field per line, and a full-width submit: the three-across action row
     is a desktop affordance, and half-width controls on a 390px phone are
     worse than a stack. */
  .action-form > label,
  .action-form > button {
    flex-basis: 100%;
    width: 100%;
  }

  .guide__defs {
    grid-template-columns: 1fr;
    gap: 2px 0;
  }

  .guide__defs dd {
    margin-bottom: 8px;
  }

  .invite-share-button,
  .invite-share-status {
    grid-column: 1;
    width: 100%;
  }

  .seeker-filters {
    grid-template-columns: 1fr;
  }

  .filter-primary,
  .filter-drawer__body,
  .additional-filters > .detail-grid {
    grid-template-columns: 1fr;
  }

  .filter-primary .primary,
  .filter-drawer__body .seg {
    width: 100%;
  }

  .job-card__secondary-actions {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    width: 100%;
    gap: 6px;
  }

  .job-card__secondary-actions form,
  .job-card__secondary-actions button {
    width: 100%;
    min-width: 0;
  }

  .seg.tabs {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    overflow: visible;
  }

  .seg.tabs a {
    min-width: 0;
    text-align: center;
  }

  table {
    min-width: 560px;
  }

  .toolbar {
    padding: 10px;
  }

  .toolbar__group {
    width: 100%;
  }

  /* A tooltip anchored to the inline-start edge runs off a narrow screen. */
  [data-tip]::after {
    max-inline-size: min(46ch, calc(100vw - 48px));
  }
}

/* ── Admin login ───────────────────────────────────────────────────────────
   Its own page shell: no top bar, because there is nowhere to navigate to
   until you are in. */
.auth-body {
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 24px 16px;
  background:
    radial-gradient(1100px 520px at 82% -12%, var(--accent-soft), transparent 62%),
    var(--bg);
}

.auth-shell {
  width: min(420px, 100%);
}

.auth-card {
  display: flex;
  flex-direction: column;
  padding: 28px 26px 22px;
  border: 1px solid var(--border);
  border-radius: 14px;
  background: var(--surface);
  box-shadow: var(--shadow-md);
}

.auth-brand {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 18px;
}

.auth-brand-name {
  font-weight: 700;
}

.auth-title {
  margin: 0 0 6px;
  font-size: 23px;
  font-weight: 700;
  line-height: 1.25;
}

.auth-sub {
  margin: 0 0 20px;
  color: var(--muted);
  font-size: var(--fs-sm);
}

.auth-label {
  margin-bottom: 6px;
  font-size: var(--fs-meta);
  font-weight: 700;
  color: var(--muted);
}

.auth-input {
  min-height: 46px;
  margin-bottom: 16px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
  color: var(--text);
  font: inherit;
}

.auth-input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: var(--accent);
}

.auth-submit {
  margin-top: 4px;
  width: 100%;
  justify-content: center;
}

/* Failure is stated in one line, in Arabic, above the fields it concerns —
   not as a browser dialog the visitor cannot read. */
.auth-error {
  margin: 0 0 16px;
  padding: 10px 12px;
  border: 1px solid var(--danger-border);
  border-radius: var(--radius-md);
  background: var(--danger-bg);
  color: var(--danger-ink);
  font-size: var(--fs-sm);
  font-weight: 600;
}

.auth-note {
  margin: 0 0 16px;
  padding: 10px 12px;
  border: 1px solid var(--warning-border);
  border-radius: var(--radius-md);
  background: var(--warning-bg);
  color: var(--warning-ink);
  font-size: var(--fs-meta);
}

.auth-note code {
  font-size: var(--fs-micro);
}

.auth-foot {
  margin: 14px 0 0;
  color: var(--muted);
  font-size: var(--fs-micro);
  text-align: center;
}

.topbar-logout {
  margin: 0;
}

.topbar-logout-btn {
  min-height: 38px;
  padding: 6px 12px;
  font-size: var(--fs-meta);
  font-weight: 600;
  color: var(--muted);
}

.topbar-logout-btn:hover {
  color: var(--text);
}

@media (max-width: 560px) {
  .auth-card {
    padding: 22px 18px 18px;
  }

  .topbar-logout {
    flex: 1;
  }

  .topbar-logout-btn {
    width: 100%;
  }
}

/* ── CV upload ─────────────────────────────────────────────────────────────
   The seeker's single most important action. The native control renders as an
   untranslated "Choose File / No file chosen" button with no feedback after a
   pick, so the real input is kept (for keyboard, screen readers and no-JS) and
   visually replaced by the drop area below. */
.file-field {
  display: block;
}

.file-field__label {
  display: block;
  margin-bottom: 6px;
  font-weight: 700;
}

.file-field__drop {
  display: grid;
  justify-items: center;
  gap: 4px;
  padding: 22px 16px;
  border: 1.5px dashed var(--border-strong);
  border-radius: var(--radius-lg);
  background: var(--surface-soft);
  text-align: center;
  transition:
    border-color 160ms ease,
    background-color 160ms ease;
}

.file-field__icon {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: var(--accent-soft);
  color: var(--accent-ink);
  font-weight: 700;
}

.file-field__cta {
  font-weight: 700;
  color: var(--accent-ink);
}

.file-field__hint {
  color: var(--muted);
  font-size: var(--fs-meta);
  overflow-wrap: anywhere;
}

.file-field input[type="file"] {
  /* Clipped, not display:none — a hidden input is skipped by keyboard focus
     and by some screen readers, and `required` on a display:none field makes
     the browser refuse to submit with an error it cannot point at. */
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.file-field input[type="file"]:focus-visible + .file-field__drop,
.file-field:hover .file-field__drop {
  border-color: var(--accent);
  background: var(--accent-soft);
}

.file-field.has-file .file-field__drop {
  border-style: solid;
  border-color: var(--accent);
  background: var(--accent-soft);
}

.file-field.has-file .file-field__hint {
  color: var(--text);
  font-weight: 600;
}

.file-field.is-dragging .file-field__drop {
  border-color: var(--accent);
  background: var(--accent-soft);
}

/* ── Sources filters ────────────────────────────────────────────────────── */
.source-filters {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr) auto auto;
  align-items: end;
  gap: 10px;
  padding: 14px;
  margin-bottom: 12px;
}

.source-count {
  margin: 0 0 10px;
  font-size: var(--fs-meta);
}

.pager {
  margin-top: 14px;
  align-items: center;
}

/* Work-type chips read as three tinted pills whether or not they are selected,
   so nothing tells the seeker they are toggles — and every one of them starts
   checked. A box that fills in when checked makes the state legible. */
fieldset.seg label {
  border: 1px solid var(--border);
  background: var(--surface);
}

fieldset.seg label::before {
  content: "";
  flex: none;
  width: 17px;
  height: 17px;
  border: 1.5px solid var(--border-hover);
  border-radius: 5px;
  background: var(--surface);
  transition:
    background-color 140ms ease,
    border-color 140ms ease;
}

fieldset.seg label:has(input:checked) {
  border-color: var(--accent);
}

fieldset.seg label:has(input:checked)::before {
  border-color: var(--check-fill);
  background:
    var(--check-fill)
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23fff' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 8.5l3.2 3.2L13 5'/%3E%3C/svg%3E")
    center / 12px 12px no-repeat;
}

fieldset.seg label:focus-within {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* ── Install card ───────────────────────────────────────────────────────────
   "Add FursaRadar to your home screen", on the seeker's own pages. Composed
   from .panel.quiet + .button.primary + the icon layer; the only new things
   here are the layout of the row, the numbered steps, and the dismiss target.

   It ships with the `hidden` attribute and pwa.js removes it. Three of the
   elements below therefore need an explicit [hidden] rule: .button is
   display:inline-flex and <ol> is display:block, and a display declaration
   beats the hidden attribute's UA style. Without these the card would flash
   on every load, including for people who already installed. */
.install-card[hidden],
.install-card__cta[hidden],
.install-how[hidden] {
  display: none;
}

.install-card {
  /* Room for the dismiss target, which is absolutely placed so that the copy
     and the button can wrap freely without it joining the wrap. */
  padding-inline-end: 2.75rem;
}

.install-card__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
}

.install-card__mark {
  display: inline-flex;
  flex: 0 0 auto;
  align-items: center;
  justify-content: center;
  inline-size: 36px;
  block-size: 36px;
  border-radius: var(--radius-md);
  background: var(--accent-soft);
  color: var(--accent);
}

.install-card__mark .icon {
  color: var(--accent);
}

/* 12rem basis: above ~390px the button sits on the same line; below it the
   button wraps to its own row instead of squeezing the headline to shreds. */
.install-card__copy {
  flex: 1 1 12rem;
  min-inline-size: 0;
}

.install-card__head {
  margin: 0;
  font-size: var(--fs-body);
  font-weight: 700;
  line-height: var(--lh-snug);
}

.install-card__sub {
  margin: 2px 0 0;
  color: var(--muted);
  font-size: var(--fs-sm);
  line-height: var(--lh-snug);
}

.install-card__cta {
  flex: 0 0 auto;
}

.install-card__x {
  position: absolute;
  inset-block-start: 8px;
  inset-inline-end: 8px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* 36px: a comfortable thumb target that still reads as secondary. */
  inline-size: 36px;
  block-size: 36px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-md);
  background: none;
  color: var(--muted);
  cursor: pointer;
}

.install-card__x:hover {
  background: var(--surface-sunken);
  color: var(--text);
  box-shadow: none;
}

.install-card__x:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* The manual route, for browsers with no install API to call. Collapsed by
   default: on a 390px phone the three open steps made the card 234px tall,
   which is a quarter of the viewport spent on something the seeker did not
   come for. Closed, the whole card costs about one row of the job list, and
   the steps are one tap away for the person who wants them. */
.install-how > summary {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-block-start: 8px;
  color: var(--accent-ink);
  font-size: var(--fs-sm);
  font-weight: 600;
  cursor: pointer;
}

.install-how > summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius-sm);
}

.install-steps {
  margin: 8px 0 0;
  padding-inline-start: 1.25rem;
  color: var(--text);
  font-size: var(--fs-sm);
  line-height: 1.75;
}

.install-steps li + li {
  margin-block-start: 2px;
}

/* The iOS share glyph sits inline in the middle of a sentence, so it is sized
   in em and baseline-nudged to sit with the text rather than on top of it. */
.install-steps__glyph {
  display: inline-flex;
  vertical-align: -0.24em;
  inline-size: 1.15em;
  block-size: 1.15em;
  color: var(--accent);
}

.install-steps__glyph svg {
  inline-size: 100%;
  block-size: 100%;
}

@media (max-width: 480px) {
  /* On a phone the card must cost about one row of the job list, not a screen.
     The button goes full width once it has wrapped, so it is still an easy
     target without adding a third line. */
  .install-card__cta {
    flex: 1 0 100%;
  }
}

/* ── Join request: the public form ──────────────────────────────────────── */

/* The honeypot. Deliberately NOT `display: none`: the crudest bots skip
   display:none inputs, and the ones worth catching read the computed box. This
   keeps the field in the layout tree and merely hides it, which still takes it
   away from every sighted user. `aria-hidden` + `tabindex="-1"` on the markup
   take it away from screen readers and the tab order too, so a keyboard or
   screen-reader user can never land in it by accident and get their real
   submission silently dropped.

   Clipped in place, NOT parked at `inset-inline-start: -9999px`. That was the
   first version and it was measured wrong: the page is RTL, so the logical
   offset resolves to `right: -9999px`, which is the *scrollable* direction --
   /join came out 10,376 CSS pixels wide with a horizontal scrollbar under every
   phone. Clipping (the same technique .visually-hidden already uses here)
   removes the box from the painted output without ever touching the scroll
   width. */
.hp-field {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* A sentence under a field explaining what happens to what you typed. Reads as
   help text, not as a second label, so it drops the label's bold weight. */
.field-hint {
  color: var(--muted);
  font-size: var(--fs-meta);
  font-weight: 400;
  line-height: 1.6;
}

/* The PDPL consent box. A standalone component rather than a `.seg` fieldset,
   because `.seg` is the segmented-pill control and clips its <input> to 1×1 —
   correct when the pill IS the affordance, fatal when the tick is. The person
   has to be able to see, before and after clicking, whether they consented. */
.consent-block {
  min-inline-size: 0;
  margin: 0;
  padding: 12px 14px 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
}

.consent-block legend {
  padding: 0 4px;
  color: var(--muted);
  font-size: var(--fs-meta);
  font-weight: 700;
}

/* Consent is a paragraph, not a word, so the box aligns to the first line
   instead of floating in the middle of three. The whole sentence is inside the
   <label>, so the tap target is the paragraph, not just the 18px box. */
.consent-block__tick {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: start;
  gap: 10px;
  color: var(--ink);
  font-size: var(--fs-sm);
  font-weight: 400;
  line-height: 1.7;
  cursor: pointer;
}

.consent-block__tick input[type="checkbox"] {
  inline-size: 18px;
  block-size: 18px;
  min-block-size: 0;
  /* Nudged down to sit on the first line's baseline rather than its box top. */
  margin-block-start: 3px;
}

.consent-block .meta {
  margin-block: 10px 0;
}

/* ── Legal / notice prose ───────────────────────────────────────────────── */

/* The privacy page is the one place in this app that is read top to bottom
   rather than scanned, so it gets real prose measure and rhythm. */
.legal h2 {
  margin-block: 24px 8px;
  font-size: var(--fs-h2);
}

.legal h2:first-child {
  margin-block-start: 0;
}

.legal p {
  margin-block: 0 12px;
  line-height: 1.8;
  max-inline-size: 68ch;
}

.legal ul {
  margin-block: 0 12px;
  padding-inline-start: 1.25rem;
  line-height: 1.8;
  max-inline-size: 68ch;
}

.legal li + li {
  margin-block-start: 4px;
}

/* Every unfinished clause a lawyer still has to write, marked so it cannot be
   mistaken for settled text — including in print and in forced-colours mode,
   where a background tint alone would vanish. */
.legal-todo,
mark.legal-todo {
  background: var(--warning-bg);
  color: var(--warning-ink);
  border: 1px dashed var(--warning);
  border-radius: var(--radius-sm);
  padding: 1px 6px;
  font-weight: 600;
}

/* ── Join request: the admin queue ──────────────────────────────────────── */

/* One card per request rather than a table row: a request carries a free-text
   note and up to three status pills, and both of those turn a dense table into
   a horizontal scroll on the phone the owner actually triages from. */
.join-card {
  display: grid;
  gap: 12px;
}

.join-card__head {
  display: flex;
  flex-wrap: wrap;
  align-items: start;
  justify-content: space-between;
  gap: 8px 12px;
}

.join-card__name {
  margin: 0;
  font-size: var(--fs-h2);
}

.join-card__email {
  margin: 2px 0 0;
}

/* Label-over-value pairs that reflow from four columns to one. auto-fit keeps
   the four facts on one line on a laptop and stacks them on a phone without a
   breakpoint. */
.join-card__facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 10px 16px;
  margin: 0;
}

.join-card__facts dt {
  color: var(--muted);
  font-size: var(--fs-meta);
  font-weight: 600;
}

.join-card__facts dd {
  margin: 2px 0 0;
  color: var(--text);
  font-size: var(--fs-sm);
}

/* The applicant's own words. Set apart with a leading rule so it never reads as
   something the system wrote. */
.join-card__note {
  margin: 0;
  padding-inline-start: 12px;
  border-inline-start: 3px solid var(--border);
  color: var(--text);
  font-size: var(--fs-sm);
  line-height: 1.7;
  white-space: pre-line;
}

.join-card__states {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}

.join-card__actions {
  gap: 8px;
}


/* ── Seeker result tuning (templates/seeker_settings.html) ───────────────────
   The controls that decide what the seeker sees, grouped and set apart from the
   contact/alert fields around them. A <fieldset> and not a .panel: these belong
   to the form they sit in, and a panel inside a panel reads as a second form
   with its own save button. */
.tuning {
  min-inline-size: 0;
  margin: 4px 0;
  padding: 14px 16px 16px;
  border: 1px solid var(--border);
  border-inline-start: 3px solid var(--accent);
  border-radius: var(--radius-lg);
  background: var(--surface-soft);
}

.tuning legend {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 0 6px;
  color: var(--accent-ink);
  font-size: var(--fs-meta);
  font-weight: 700;
}

.tuning > label {
  display: block;
}

.tuning > label + label,
.tuning > .field-hint {
  margin-block-start: 12px;
}

/* The weight beside each field in the "what changes your results" panel. Right
   after the field name, not in a second column: the number is the argument for
   fixing that field, and a table would separate the two. */
.weight-list .weight {
  display: inline-block;
  margin-inline-start: 6px;
  padding: 1px 8px;
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--accent-ink);
  font-size: var(--fs-micro);
  font-weight: 700;
  white-space: nowrap;
}

.weight-list .weight.tone-danger {
  background: var(--danger-bg);
  color: var(--danger-ink);
}

.weight-list li + li {
  margin-block-start: 7px;
}


/* ── The journey strip (templates/_journey.html) ─────────────────────────────
   Shown on /join, /join/done and every intake page. Horizontal at desktop with
   a connecting rule, stacked on a phone where four labels in a row would each
   wrap to three lines. The rule is drawn with a pseudo-element on the list
   rather than a border on each step, so it cannot break where a step wraps. */
.journey {
  margin: 0 0 20px;
  padding: 14px 16px;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
}

.journey ol {
  position: relative;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 10px;
  margin: 0;
  padding: 0;
  list-style: none;
}

/* The line every mark sits on. Inset by half a mark at each end so it starts
   and stops under the first and last discs instead of running off the edge. */
.journey ol::before {
  content: "";
  position: absolute;
  inset-block-start: 12px;
  inset-inline: 12.5%;
  block-size: 2px;
  background: var(--border);
}

.journey__step {
  position: relative;
  display: grid;
  justify-items: center;
  gap: 7px;
  text-align: center;
}

.journey__mark {
  display: grid;
  place-items: center;
  inline-size: 26px;
  block-size: 26px;
  border: 2px solid var(--border);
  border-radius: 50%;
  background: var(--surface);
  color: var(--faint);
  font-size: var(--fs-meta);
  font-weight: 700;
}

.journey__label {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  color: var(--faint);
  font-size: var(--fs-meta);
  font-weight: 600;
  line-height: 1.5;
}

.journey__step.is-done .journey__mark {
  border-color: var(--success);
  background: var(--success);
  color: var(--on-tone);
}

.journey__step.is-done .journey__label {
  color: var(--muted);
}

/* The current step is the only one carrying full ink and the accent ring: at a
   glance the question "where am I" is answered before any label is read. */
.journey__step.is-current .journey__mark {
  border-color: var(--accent);
  background: var(--accent-soft);
  color: var(--accent-ink);
  box-shadow: 0 0 0 4px var(--accent-soft);
}

.journey__step.is-current .journey__label {
  color: var(--text);
  font-weight: 700;
}

@media (max-width: 620px) {
  .journey ol {
    grid-template-columns: minmax(0, 1fr);
    gap: 12px;
  }

  /* The connector turns vertical, and the steps become a left-marked list:
     four centred labels stacked in a column read as four unrelated headings. */
  .journey ol::before {
    inset-block: 12px;
    /* 12px from the inline start — which is the RIGHT edge under RTL, where the
       marks are. Written as `calc(100% - 13px)` it resolved to `right:
       calc(100% - 13px)` and drew the connector down the far left of the card,
       joining nothing. */
    inset-inline: 12px auto;
    inline-size: 2px;
    block-size: auto;
  }

  .journey__step {
    grid-template-columns: auto 1fr;
    justify-items: start;
    gap: 10px;
    direction: rtl;
    text-align: start;
  }
}


/* ── Landing page (the site root) ────────────────────────────────────────────
   Everything below is scoped to templates/landing.html. It is the only surface
   in the app addressed to someone who does not yet know what FursaRadar is, and
   the only one that is a layout rather than a document — so it gets a wider
   shell and one animated element, and nothing else in the app changes.

   The design brief is the product itself: a radar that hands you a message.
   The signature is therefore the message, rendered as the chat it arrives in,
   beside the headline instead of below the explanation. Everything around it
   is deliberately the app's ordinary furniture — the same .panel, the same
   .st pills, the same .job-card — because the visitor's next screen is the
   real product and it must not look like a different one. */

.landing-shell {
  width: min(1040px, calc(100vw - 32px));
  margin-inline: auto;
}

.landing-hero {
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
  align-items: center;
  gap: 30px 40px;
  margin: 6px 0 34px;
  padding: 34px 30px 30px;
  border: 1px solid var(--accent-soft);
  border-radius: 14px;
  background:
    radial-gradient(105% 130% at 100% 0%, var(--accent-soft), transparent 58%),
    var(--surface);
  box-shadow: var(--shadow-sm);
}

.landing-hero__copy {
  min-width: 0;
}

.landing-hero .eyebrow {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0;
  color: var(--accent-ink);
}

/* The one animated element on the page, and the only one anywhere in the app.
   It earns its place by being the subject: a sweep going round a dish. Slow
   (7s) and low-contrast so it reads as ambient rather than as a spinner
   claiming something is loading. */
.radar-mark {
  position: relative;
  display: inline-grid;
  place-items: center;
  inline-size: 18px;
  block-size: 18px;
  border: 1px solid var(--accent);
  border-radius: 50%;
  overflow: hidden;
}

.radar-mark::before {
  content: "";
  position: absolute;
  inset: -25%;
  background: conic-gradient(from 0deg, var(--accent) 0deg, transparent 70deg);
  opacity: 0.55;
  animation: radar-sweep 7s linear infinite;
}

.radar-mark::after {
  content: "";
  position: absolute;
  inline-size: 4px;
  block-size: 4px;
  border-radius: 50%;
  background: var(--accent);
}

@keyframes radar-sweep {
  to {
    transform: rotate(360deg);
  }
}

@media (prefers-reduced-motion: reduce) {
  .radar-mark::before {
    animation: none;
  }
}

.landing-hero h1 {
  margin: 10px 0 0;
  font-size: clamp(30px, 4.4vw, 44px);
  font-weight: 700;
  line-height: 1.24;
  letter-spacing: -0.015em;
}

/* Measure-capped well under the column: this is the sentence that has to land,
   and at full width it would be read as a paragraph and skipped. */
.landing-hero__lede {
  max-inline-size: 48ch;
  margin: 14px 0 0;
  color: var(--muted);
  font-size: clamp(15px, 1.4vw, 16.5px);
  line-height: 1.85;
}

.landing-hero__cta {
  margin-block-start: 22px;
}

.landing-cta {
  padding: 12px 22px;
  font-size: var(--fs-body);
  font-weight: 700;
}

.landing-hero__reassure {
  margin: 12px 0 0;
}

/* ── The sample digest ──────────────────────────────────────────────────────
   A chat surface, not a phone. A device frame would add a bezel, a notch and a
   status bar — three pieces of furniture that say "mockup" and none that say
   what the product does. The bubble is the product. */
.digest {
  min-width: 0;
  margin: 0;
}

.digest__frame {
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--surface-sunken);
  box-shadow: var(--shadow-sm);
  overflow: hidden;
}

.digest__bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 9px 12px;
  border-block-end: 1px solid var(--border);
  background: var(--surface);
}

.digest__avatar {
  display: grid;
  place-items: center;
  inline-size: 24px;
  block-size: 24px;
  border-radius: 50%;
  background: var(--accent-strong);
  color: var(--on-accent);
}

.digest__bot {
  color: var(--text);
  font-size: var(--fs-meta);
  font-weight: 700;
}

/* Says "مثال" inside the frame, where the eye already is. A caption alone is
   read after the thing it captions, which is one beat too late for a screenshot
   of a message that is not real. */
.digest__badge {
  margin-inline-start: auto;
  padding: 2px 8px;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  color: var(--faint);
  font-size: var(--fs-micro);
  font-weight: 700;
}

.digest__bubble {
  margin: 12px;
  padding: 12px 14px;
  border-start-start-radius: 4px;
  border-start-end-radius: 12px;
  border-end-start-radius: 12px;
  border-end-end-radius: 12px;
  background: var(--surface);
  border: 1px solid var(--border);
}

.digest__line {
  display: flex;
  align-items: baseline;
  gap: 6px;
  margin: 0;
  color: var(--text);
  font-size: 13px;
  line-height: 1.85;
}

/* Fixed-width so every line's text starts on the same vertical rule, the way
   it does in a real message, and so the blank slots on continuation lines
   indent under their glyph instead of jumping back to the margin. */
.digest__glyph {
  flex: none;
  inline-size: 15px;
  font-size: 12px;
  text-align: center;
}

.digest__line--head {
  font-weight: 700;
}

.digest__line--job {
  margin-block-start: 10px;
  font-weight: 700;
}

.digest__line--score {
  color: var(--accent-ink);
  font-weight: 700;
}

.digest__line--label {
  margin-block-start: 4px;
}

.digest__line--reason {
  color: var(--muted);
  padding-inline-start: 12px;
}

.digest__line--url {
  color: var(--accent);
  font-size: var(--fs-meta);
  word-break: break-all;
}

.digest__line--foot {
  margin-block-start: 10px;
  padding-block-start: 8px;
  border-block-start: 1px solid var(--border);
  color: var(--faint);
  font-size: var(--fs-micro);
  line-height: 1.7;
}

.digest__caption {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-block-start: 10px;
  color: var(--muted);
  font-size: var(--fs-meta);
}

/* auto-fit, not a fixed count: the third figure is a constant and the first two
   come from the database, so a deployment missing one must not leave a hole. */
.landing-figures {
  grid-column: 1 / -1;
  display: grid;
  /* Sized so all three fit one row on a 360px phone: at 150px it fitted two and
     orphaned the third, and three short numbers in a row read as one fact about
     the service rather than as a list that ran out. */
  grid-template-columns: repeat(auto-fit, minmax(96px, 1fr));
  gap: 12px 14px;
  margin: 0;
  padding-block-start: 22px;
  border-block-start: 1px solid var(--border);
}

.landing-figures dt {
  display: flex;
  align-items: center;
  gap: 6px;
  color: var(--muted);
  font-size: var(--fs-meta);
  font-weight: 600;
}

.landing-figures dd {
  margin: 4px 0 0;
  color: var(--text);
  font-size: 24px;
  font-weight: 700;
}

/* ── Sections ───────────────────────────────────────────────────────────────
   One heading treatment for the whole page, distinct from .section-head (which
   belongs to the operator's tables) but built from the same tokens. */
.landing-shell .section {
  margin-block-start: 40px;
}

.landing-h2 {
  margin: 0 0 4px;
  font-size: clamp(20px, 2.4vw, 25px);
  letter-spacing: -0.01em;
}

.landing-note {
  max-inline-size: 78ch;
  margin: 10px 0 0;
  line-height: 1.85;
}

/* Two columns at desktop, deliberately not auto-fit: four items in a 1040px
   row come out as three and an orphan, and the orphan reads as a mistake. */
.landing-why {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 14px;
  margin-block-start: 18px;
}

@media (max-width: 620px) {
  .landing-why {
    grid-template-columns: minmax(0, 1fr);
  }
}

.landing-why__item {
  padding: 16px 18px;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
}

.landing-why__item h3 {
  display: flex;
  align-items: center;
  gap: 7px;
  margin: 0;
  font-size: var(--fs-body);
}

.landing-why__item h3 .icon {
  color: var(--accent);
}

.landing-why__item p {
  margin: 6px 0 0;
  color: var(--muted);
  font-size: var(--fs-meta);
  line-height: 1.75;
}

/* The four steps. Counter-numbered rather than list-marker-numbered so the
   digit can be a filled disc; the <ol> stays an <ol> and each item keeps a real
   heading, so the sequence survives for a screen reader too. */
.landing-steps {
  counter-reset: landing-step;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(232px, 1fr));
  gap: 18px 22px;
  margin: 18px 0 0;
  padding: 0;
  list-style: none;
}

.landing-steps li {
  position: relative;
  padding-inline-start: 36px;
}

.landing-steps li::before {
  counter-increment: landing-step;
  content: counter(landing-step);
  position: absolute;
  inset-inline-start: 0;
  inset-block-start: 0;
  display: grid;
  place-items: center;
  inline-size: 26px;
  block-size: 26px;
  border-radius: 50%;
  background: var(--accent-soft);
  color: var(--accent-ink);
  font-size: var(--fs-meta);
  font-weight: 700;
}

.landing-steps h3 {
  display: flex;
  align-items: center;
  gap: 7px;
  margin: 2px 0 0;
  font-size: var(--fs-body);
}

.landing-steps p {
  margin: 6px 0 0;
  color: var(--muted);
  font-size: var(--fs-meta);
  line-height: 1.75;
}

/* The job card replica. .job-card carries its own border and padding, so this
   only positions the label that keeps it honest. */
.landing-proof {
  position: relative;
  margin-block-start: 16px;
  max-inline-size: 560px;
}

/* The replica's apply button keeps its natural width. .job-card__actions
   stretches it edge to edge inside the real card, where it is the one thing to
   do; here a full-width bar reads as an invitation the example cannot honour. */
.landing-proof .job-card__actions {
  justify-content: start;
}

.landing-proof .button {
  inline-size: auto;
}

.landing-proof__tag {
  position: absolute;
  inset-block-start: -9px;
  inset-inline-end: 14px;
  z-index: 1;
  padding: 2px 9px;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  background: var(--bg);
  color: var(--faint);
  font-size: var(--fs-micro);
  font-weight: 700;
}

/* ── FAQ ────────────────────────────────────────────────────────────────────
   details.panel already carries the app's disclosure styling; this only sets
   the rhythm between them and the answer's measure. */
.faq {
  display: grid;
  gap: 10px;
  margin-block-start: 18px;
}

.faq details.panel {
  padding: 0;
}

.faq summary {
  padding: 14px 16px;
  font-size: var(--fs-body);
  font-weight: 700;
}

.faq p {
  max-inline-size: 74ch;
  margin: 10px 16px 16px;
  color: var(--muted);
  font-size: var(--fs-meta);
  line-height: 1.85;
}

/* ── Close ──────────────────────────────────────────────────────────────────
   Centred, and the only centred block on the page: it is the one moment the
   reader is meant to stop scanning and act. */
.landing-close {
  margin: 34px 0 8px;
  padding: 30px 24px;
  border: 1px solid var(--accent-soft);
  border-radius: 14px;
  background:
    radial-gradient(90% 140% at 50% 0%, var(--accent-soft), transparent 62%),
    var(--surface);
  text-align: center;
}

.landing-close h2 {
  margin: 0;
  font-size: clamp(20px, 2.4vw, 26px);
}

.landing-close p {
  margin: 8px 0 20px;
  color: var(--muted);
  font-size: var(--fs-body);
}

.landing-close__fine {
  margin: 18px 0 0;
  color: var(--faint);
  font-size: var(--fs-meta);
}

/* The header CTA. Compact, and gone under 620px where the sticky bar at the
   foot of the viewport takes over — two copies of the same button on one small
   screen is a nag, not a convenience. */
.topbar-cta {
  padding: 7px 14px;
  font-size: var(--fs-meta);
  font-weight: 700;
}

/* The phone-only action bar.
   STICKY, NOT FIXED, and that is not a preference. `body > main` runs
   `fr-enter` with fill mode `both`, so it keeps a transform forever once the
   entrance finishes — and a transformed ancestor becomes the containing block
   for every `position: fixed` descendant. A fixed bar here anchored itself to
   the foot of the *page* instead of the viewport and was measured 3377px below
   the fold. Sticky is unaffected by that, and as the last child in the flow it
   pins to the bottom of the screen for the whole scroll and then settles under
   the final section. */
.landing-sticky {
  display: none;
  position: sticky;
  inset-block-end: 0;
  z-index: 20;
  /* Cancels .landing-shell's own gutter so the bar reaches both screen edges. */
  margin-inline: -12px;
  padding: 10px 14px calc(10px + env(safe-area-inset-bottom));
  border-block-start: 1px solid var(--border);
  background: var(--surface-glass);
  backdrop-filter: blur(8px);
}

.landing-sticky .button {
  inline-size: 100%;
  justify-content: center;
  padding: 12px 18px;
  font-size: var(--fs-body);
  font-weight: 700;
}

/* One orchestrated entrance, not scattered effects: the page's own reading
   order, arriving in it. Opacity and transform only — both composited, so the
   first paint is not delayed by layout work. */
@media (prefers-reduced-motion: no-preference) {
  .landing-hero__copy > *,
  .digest {
    animation: landing-rise 0.5s cubic-bezier(0.2, 0.7, 0.3, 1) backwards;
  }

  .landing-hero__copy > *:nth-child(1) { animation-delay: 0.02s; }
  .landing-hero__copy > *:nth-child(2) { animation-delay: 0.08s; }
  .landing-hero__copy > *:nth-child(3) { animation-delay: 0.14s; }
  .landing-hero__copy > *:nth-child(4) { animation-delay: 0.20s; }
  .digest { animation-delay: 0.26s; }
}

@keyframes landing-rise {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
}

@media (max-width: 860px) {
  .landing-hero {
    grid-template-columns: minmax(0, 1fr);
    gap: 26px;
  }

  /* The message moves under the copy, never away: on a phone it is still the
     fastest explanation of the product on the page. */
  .digest {
    order: 2;
  }

  .landing-figures {
    order: 3;
  }
}

@media (max-width: 620px) {
  .topbar-cta {
    display: none;
  }

  .landing-sticky {
    display: block;
  }

  /* No bottom padding needed: a sticky bar occupies its own space in the flow,
     so nothing can end up underneath it. */
  .landing-close {
    margin-block-end: 20px;
  }
}

@media (max-width: 560px) {
  .landing-shell {
    width: min(1040px, calc(100vw - 24px));
  }

  .landing-hero {
    padding: 26px 18px 22px;
  }

  .landing-close {
    padding: 26px 18px;
  }
}
