/*
Theme Name: Kerja
Theme URI: https://kerja0.com
Author: BizJaya
Author URI: https://bizjaya.com
Description: Purpose-built theme for kerja0.com — a Malaysian job board. Requires the Kerja Core plugin for listing, category and proximity data.
Version: 0.2.5
Requires at least: 6.0
Tested up to: 7.1
Requires PHP: 8.2
License: GPL-2.0-or-later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Text Domain: kerja
*/

/* ============================================================ tokens

   Every colour is defined HERE, at the top level, and never only inside a media
   query — a colour whose sole definition sits in a dark-only branch renders
   unstyled in the other theme. Dark is an override of these, not a parallel set.
*/

:root {
    --kj-bg:            #ffffff;
    --kj-bg-soft:       #f6f7f9;
    --kj-bg-sunk:       #eef0f4;
    --kj-surface:       #ffffff;
    --kj-border:        #e2e5ea;
    --kj-border-strong: #cbd1da;

    --kj-text:          #14181f;
    --kj-text-soft:     #5a6472;
    --kj-text-faint:    #8a93a1;
    --kj-text-on-brand: #ffffff;

    --kj-brand:         #0082ca;
    --kj-brand-hover:   #0069a6;
    --kj-brand-soft:    #e3f4fd;
    --kj-brand-ink:     #0069a6;
    --kj-brand-ink-hov: #00517f;

       /* ⚠ THE INK IS A SEPARATE TOKEN FROM THE BRAND, AND ONLY ON LIGHT GROUNDS.
          The brand is vivid because the operator picked it that way, and a vivid colour
          fills a shape beautifully and reads terribly as 13px text on white: the orange
          measures 2.80:1 there, the cyan 4.16:1. So --kj-brand paints FILLS - the badge,
          the buttons, the active chip, the filter header - and --kj-brand-ink paints
          TEXT on a light ground: links, prices, distances, the wordmark's dot.

          ⚠ AND THE PAIR IS REDEFINED IN THE DARK BLOCKS TOO, where it deliberately
          COLLAPSES: on a dark ground the vivid colour is already 8:1, so ink == brand.
          An ink defined only in :root would leave every link in dark mode painted a deep
          colour on a dark ground - the "colour defined in one branch only" defect, which
          is invisible until somebody opens the other theme. */
    --kj-accent:        #f0a202;
    --kj-accent-soft:   #fdf3e0;
    --kj-accent-ink:    #7a5300;
    --kj-accent-border: #f0d9a8;

    /* ⚠ THE LOGOTYPE'S COLOUR, AND IT IS DELIBERATELY NOT THEMED. The mark is a
       `data:` URI, which cannot read a custom property, so its colours are fixed at the
       hex in functions.php — and the wordmark sitting beside it is part of the same
       object. A themed dot would take one colour in light and another in dark while the
       badge it sits against never moved, which reads as a mistake because it is one.
       css-check asserts this token against that hex.

       ⚠ IT IS ALSO THE ONE PLACE THE VIVID COLOUR IS ALLOWED TO PAINT TEXT. Everywhere
       else --kj-brand-ink does that, because the vivid colour measures under 3:1 on
       white. WCAG exempts logotypes from contrast for exactly this reason: a brand name
       is recognised, not read. */
    --kj-mark:         #0082ca;

    /* The site header is `position: sticky`, so anything else that sticks has to clear
       it: 66px of content plus its 1px bottom border. */
    --kj-header-h:     67px;

    --kj-tone-1:        #0082ca;
    --kj-tone-2:        #2f6db5;
    --kj-tone-3:        #8a4fbd;
    --kj-tone-4:        #b5622f;
    --kj-tone-5:        #2f8fb5;
    --kj-tone-6:        #55708a;

    /* ⚠ THESE THREE ARE DEFINED ONCE AND NEVER OVERRIDDEN FOR DARK, and each has a reason
       — they are not an oversight to be "completed" later.

       A pin sits on OpenStreetMap tiles, which are light whatever the page is, so its ring
       and its drop shadow follow the TILES, not the theme. The veil is a translucent white
       laid over an already-brand-coloured chip to lift its count badge; it is a lightening
       of whatever is underneath rather than a colour of its own, which is exactly why it
       reads correctly in both themes and would break if it were themed. */
    --kj-pin-ring:      #ffffff;
    --kj-pin-shadow:    rgba(0, 0, 0, .4);
    /* The dim behind the small-screen filter drawer. Declared here, with the other values
       that do not vary by theme, because a scrim that inverted would stop dimming the page
       and start lighting it. */
    --kj-scrim:          rgba(0, 0, 0, .42);
    --kj-veil:          rgba(255, 255, 255, .22);

    --kj-danger:        #b3261e;
    --kj-danger-soft:   #fdeceb;

    --kj-radius:        10px;
    --kj-radius-lg:     16px;
    /* Form controls are softer than cards: 10px on a 32px-tall input barely reads
       as rounded at all. Its own token so a card and a field can disagree. */
    --kj-radius-field:  12px;
    --kj-shadow:        0 1px 2px rgba(20, 24, 31, .06), 0 4px 16px rgba(20, 24, 31, .06);
    --kj-shadow-lift:   0 2px 6px rgba(20, 24, 31, .08), 0 12px 32px rgba(20, 24, 31, .10);
    --kj-glow:       rgba(0, 130, 202, .16);

    /* ⚠ THE FEATURED OUTLINE, AS A TOKEN, BECAUSE THE TWO SITES DISAGREE ON IT.
       One rule in both stylesheets; the value is the per-site decision, and it lives
       here with the other per-site colours. Declared ONCE - it resolves lazily, so a
       value written in terms of another token follows that token into dark. */
    --kj-featured-line: var(--kj-brand);

    --kj-max:           1200px;
    --kj-gap:           20px;

    --kj-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue",
               Arial, "Noto Sans", sans-serif;
}

/* ============================================================ the layer scale

   ⚠ EVERY z-index ON THIS SITE IS ONE OF THE NAMES BELOW. A number typed inline is how a
   stack stops being arguable-about: the next person raises theirs to 9999 to win, the one
   after that to 99999, and no file any longer says what is supposed to sit above what.

   ⚠ AND z-index IS RELATIVE TO A STACKING CONTEXT, NOT TO THE PAGE — which is the whole
   reason this section exists. `.kj-header` is `position: sticky` WITH a z-index, so it
   opens a context of its own, and every z-index inside it orders the header's own children
   against each other and against NOTHING ELSE. Against the rest of the page the header
   competes as one flat box at --kj-z-header.

   Measured 2026-08-28, which is what that costs in practice: the category mega menu was
   written at `z-index: 60` and was painted over by both the map and the /near/ filter bar.
   60 was never in either comparison. Leaflet numbers its own panes, controls and corners
   100 → 1000 (`grep z-index assets/leaflet/leaflet.css`) and NOTHING contained them, so
   they stacked against the root at up to 1000; the header brought 40 to that fight and the
   filter bar brought a tie at 40, which a later sibling wins. Raising the menu's own 60 —
   the obvious fix, and the one that gets tried first — changes nothing at all.

   So: the map is CONTAINED (see .kj-map) rather than out-numbered, and the three things
   that stack against the page are named here in the order they must paint.

   These do not vary by theme, so they are declared once, outside the three colour blocks.
*/
:root {
    --kj-z-map:    10;   /* the map, and every Leaflet layer sealed inside it */
    --kj-z-pane:   15;   /* a toolbar sticking INSIDE a scrolling pane. Above the rows it
                            scrolls over, and deliberately BELOW the chrome: it sits later
                            in the DOM than the search bar, so at an equal number it would
                            paint over the bar and clip the very picker panel that opens
                            downward out of it */
    --kj-z-chrome: 20;   /* page furniture that must clear the map: the /near/ filter bar
                            that sticks below the header, and the search field, which opens
                            a suggest panel over a map on the front page */
    --kj-z-header: 30;   /* the site header — and therefore the mega menu it contains */

    /* ⚠ ABOVE THE HEADER, AND THAT IS THE WHOLE POINT. The small-screen filter drawer is
       the first thing in this stylesheet that must cover the site header rather than sit
       under it: a drawer opening BEHIND the header looks like a drawer that failed to
       open. The scrim sits one step below the drawer and one above everything else, so it
       dims the page without dimming the panel it belongs to. */
    /* ⚠ ABOVE WORDPRESS'S ADMIN BAR, WHICH IS 99999 AND IS NOT OURS TO RENUMBER.
       Measured logged in: #wpadminbar is z-index 99999 and 32-46px tall, and
       elementFromPoint(20,16) over an open drawer returned "ab-icon" - the bar owned the
       strip holding the drawer's own heading and close button.

       Stepping over it rather than offsetting beneath it, for two reasons. Offsetting has
       to know whether the bar is `fixed` or `absolute`, which WordPress switches at 782px -
       a fourth breakpoint in a stylesheet whose whole point is that it has three. And a
       drawer with a scrim IS a modal: modals cover chrome, the admin bar is WordPress's
       furniture rather than the site's, and the visitor this site exists for never sees it.

       The scrim covering the bar is deliberate too - otherwise a click at the top of a
       dimmed page opens an admin menu instead of closing the panel. */
    --kj-z-scrim:  100000;
    --kj-z-drawer: 100001;

    /* ⚠ LOCAL, AND DELIBERATELY NOT ON THE SCALE ABOVE. The mega menu and the suggest
       panel are each sealed inside a parent that already stacks (the header, the hero,
       the /near/ bar), so this number only ever orders a panel against its own siblings.
       One token serves both because the two are never siblings of each other. */
    /* Set by site.js to the scrollbar width at the moment the filter drawer locks
       the body, and read back by `.kj-noscroll` as padding so the content does not
       jump. Declared here as well as defaulted in the var() because a token that
       exists only in a fallback is indistinguishable from a typo - to the gate, and
       to the next reader. */
    --kj-sbw: 0px;

    --kj-z-pop:    1;

    /* ⚠ ONE STEP OVER A POP, AND IT HAS EXACTLY ONE JOB. A card's title anchor is
       stretched over the whole card so the pointer cursor is honest; anything inside that
       card which is ITSELF interactive - the View button, a tag link - would otherwise be
       covered by that overlay and stop responding. It is local in the same sense --kj-z-pop
       is: it only ever orders two elements inside one card against each other. */
    --kj-z-above:  2;
}

/* ⚠ THE DARK TOKENS APPEAR TWICE, AND THE DUPLICATION IS DELIBERATE.

   The theme has THREE states, not two: an explicit choice stamps data-theme="dark" or
   data-theme="light" on <html>, and the default "system" setting stamps nothing at all —
   only prefers-color-scheme separates light from dark there. That needs two selectors:

     @media (prefers-color-scheme: dark) :root:not([data-theme="light"])
         system is dark AND the visitor has not explicitly chosen light

     :root[data-theme="dark"]
         the visitor chose dark, whatever the system says

   The obvious de-duplication is CSS light-dark(), which needs ONE definition per token.
   It is rejected: it is Baseline 2024, and on an older Android WebView the whole custom
   property is invalid — which does not degrade to the light theme, it degrades to an
   UNSTYLED PAGE. On a Malaysian consumer site with a long device tail that trade is not
   worth one saved copy.

   ⚠ THEY ARE ONE FACT IN TWO PLACES. Edit both, or the toggle and the system default
   disagree and nobody notices until someone flips it. */

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        --kj-bg:            #0f1216;
        --kj-bg-soft:       #151a20;
        --kj-bg-sunk:       #1b212a;
        --kj-surface:       #171d24;
        --kj-border:        #262e39;
        --kj-border-strong: #37414f;
        --kj-text:          #eef1f5;
        --kj-text-soft:     #a8b2c0;
        --kj-text-faint:    #7b8595;
        --kj-brand:         #41b6e6;
        --kj-brand-hover:   #7bcbee;
        --kj-brand-soft:    #0d2a3a;
        --kj-brand-ink:     #41b6e6;
        --kj-brand-ink-hov: #7bcbee;
        --kj-accent:        #f5b731;
        --kj-accent-soft:   #322612;
        --kj-accent-ink:    #f5b731;
        --kj-accent-border: #4a3a12;
        --kj-danger:        #f2837c;
        --kj-danger-soft:   #331a19;
        --kj-shadow:        0 1px 2px rgba(0, 0, 0, .5), 0 4px 16px rgba(0, 0, 0, .4);
        --kj-shadow-lift:   0 2px 6px rgba(0, 0, 0, .55), 0 12px 32px rgba(0, 0, 0, .45);
        --kj-glow:       rgba(65, 182, 230, .22);
    }
}

:root[data-theme="dark"] {
        --kj-bg:            #0f1216;
        --kj-bg-soft:       #151a20;
        --kj-bg-sunk:       #1b212a;
        --kj-surface:       #171d24;
        --kj-border:        #262e39;
        --kj-border-strong: #37414f;
        --kj-text:          #eef1f5;
        --kj-text-soft:     #a8b2c0;
        --kj-text-faint:    #7b8595;
        --kj-brand:         #41b6e6;
        --kj-brand-hover:   #7bcbee;
        --kj-brand-soft:    #0d2a3a;
        --kj-brand-ink:     #41b6e6;
        --kj-brand-ink-hov: #7bcbee;
        --kj-accent:        #f5b731;
        --kj-accent-soft:   #322612;
        --kj-accent-ink:    #f5b731;
        --kj-accent-border: #4a3a12;
        --kj-danger:        #f2837c;
        --kj-danger-soft:   #331a19;
        --kj-shadow:        0 1px 2px rgba(0, 0, 0, .5), 0 4px 16px rgba(0, 0, 0, .4);
        --kj-shadow-lift:   0 2px 6px rgba(0, 0, 0, .55), 0 12px 32px rgba(0, 0, 0, .45);
        --kj-glow:       rgba(65, 182, 230, .22);
}

/* ============================================================ reset + base */

*, *::before, *::after { box-sizing: border-box; }

html {
    -webkit-text-size-adjust: 100%;

}

/* ⚠ THE GUTTER IS RESERVED ONLY WHILE THE DRAWER IS LOCKED - not always, which is what it
   used to be. Always-on cost 15px of dead strip down the right of EVERY page: measured at an
   820px viewport, clientWidth 820 and body 805, with the header, footer and tab bar all
   stopping 15px short of the window. That is the gap the operator could see and could not
   place (2026-09-01).

   It cannot simply be deleted, and the original comment explains why: opening the drawer sets
   `overflow: hidden` on the body, which REMOVES the scrollbar and widens the layout viewport
   by ~15px - enough to take an 890px page out of `max-width: 900px`, so the drawer's own
   matchMedia stops matching and the script closes the panel it just opened.

   Reserving the gutter at exactly the moment the scrollbar goes keeps the width constant,
   which is the whole requirement, and costs nothing the rest of the time. The `settling`
   guard in site.js is a real two-frame guard now rather than setTimeout(0) as well - belt and
   braces, because this failure mode is invisible until somebody reports it. */
/* ⚠ NOT `scrollbar-gutter`, WHICH WAS MEASURED DOING NOTHING HERE. The gutter is only
   honoured on a scroll container that can show scrollbars, and `overflow: hidden`
   propagated from the body is not one - clientWidth still went 865 -> 880 on open with
   the rule in place. Padding equal to the scrollbar that just vanished is what actually
   holds the content still; --kj-sbw is measured and set by site.js at that moment.

   ⚠ THE VIEWPORT STILL WIDENS, and nothing in CSS can stop that - so the matchMedia
   guard in site.js is what keeps the drawer from closing itself, and it is a real
   two-frame guard now rather than setTimeout(0). This rule is the cosmetics; that one
   is the correctness. */

body {
    margin: 0;
    font-family: var(--kj-font);
    font-size: 16px;
    line-height: 1.55;
    color: var(--kj-text);
    background: var(--kj-bg-soft);
    -webkit-font-smoothing: antialiased;
}

img, svg { max-width: 100%; height: auto; display: block; }

a { color: var(--kj-brand-ink); text-decoration: none; }
a:hover { color: var(--kj-brand-ink-hov); text-decoration: underline; }

h1, h2, h3, h4 { margin: 0 0 .5em; line-height: 1.22; font-weight: 700; letter-spacing: -.015em; }
h1 { font-size: clamp(1.75rem, 1.2rem + 2vw, 2.5rem); }
h2 { font-size: clamp(1.3rem, 1.05rem + 1vw, 1.65rem); }
h3 { font-size: 1.08rem; }
p  { margin: 0 0 1em; }

.kj-wrap { width: 100%; max-width: var(--kj-max); margin: 0 auto; padding: 0 20px; }

.screen-reader-text {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip: rect(1px, 1px, 1px, 1px); white-space: nowrap;
}

/* ============================================================ buttons */

.kj-btn {
    display: inline-flex; align-items: center; gap: 8px;
    padding: 11px 18px; border: 1px solid transparent; border-radius: var(--kj-radius);
    font: inherit; font-weight: 600; line-height: 1;
    background: var(--kj-brand); color: var(--kj-text-on-brand);
    cursor: pointer; transition: background .15s ease, transform .05s ease;
}
.kj-btn:hover { background: var(--kj-brand-hover); color: var(--kj-text-on-brand); text-decoration: none; }
.kj-btn:active { transform: translateY(1px); }

.kj-btn--ghost {
    background: transparent; color: var(--kj-text); border-color: var(--kj-border-strong);
}
.kj-btn--ghost:hover { background: var(--kj-bg-sunk); color: var(--kj-text); }

/* ============================================================ site header */

.kj-header {
    background: var(--kj-surface);
    border-bottom: 1px solid var(--kj-border);
    position: sticky; top: 0; z-index: var(--kj-z-header);
}
.kj-header__inner {
    display: flex; align-items: center; gap: 24px;
    min-height: 66px;
}
.kj-brand {
    display: inline-flex; align-items: baseline; gap: 2px;
    font-size: 1.32rem; font-weight: 800; letter-spacing: -.03em;
    color: var(--kj-text);
}
.kj-brand:hover { text-decoration: none; color: var(--kj-text); }
.kj-brand__dot { color: var(--kj-mark); }

.kj-nav { margin-left: auto; }
/* ⚠ CHILD SELECTOR, NOT DESCENDANT. `.kj-nav ul` also matches every <ul> inside the
   category mega-menu that lives in this nav, so the menu's columns were laid out as
   horizontal flex rows and the labels overlapped into an unreadable pile. A descendant
   selector on a container that later grows a nested list is one of the easiest CSS bugs
   to ship and one of the hardest to see coming. */
/* ⚠ OUTLINE BUTTONS, AND `white-space: nowrap` IS THE POINT. These were bare links in a
   flex row, so at any width the row could not fit them "Add your business" broke across two
   lines mid-phrase and the header grew a second row. A button has an outline that makes the
   wrap visible as damage rather than as a layout, and nowrap stops it happening at all:
   below md the whole nav moves to the bottom bar instead of compressing. */
.kj-nav > ul { display: flex; gap: 10px; margin: 0; padding: 0; list-style: none; align-items: center; }
.kj-nav > ul > li { display: flex; }
.kj-nav a,
.kj-nav__toggle {
    display: inline-flex; align-items: center; gap: 6px;
    padding: 7px 13px; border-radius: 999px;
    border: 1px solid var(--kj-border);
    background: transparent;
    color: var(--kj-text-soft); font-weight: 500; font-size: .93rem;
    white-space: nowrap;
}
.kj-nav a:hover,
.kj-nav__toggle:hover,
.kj-nav__toggle[aria-expanded="true"] {
    color: var(--kj-text); border-color: var(--kj-border-strong);
    background: var(--kj-bg-sunk); text-decoration: none;
}
.kj-nav .current-menu-item > a { color: var(--kj-text); border-color: var(--kj-border-strong); }

.kj-header__cta { display: flex; gap: 10px; align-items: center; }

/* ============================================================ hero + search */

.kj-hero {
    background: linear-gradient(180deg, var(--kj-brand-soft) 0%, var(--kj-bg-soft) 100%);
    border-bottom: 1px solid var(--kj-border);
    padding: 56px 0 44px;
    text-align: center;
}
.kj-hero h1 { margin-bottom: 10px; }
.kj-hero__sub { color: var(--kj-text-soft); margin: 0 auto 28px; max-width: 56ch; }

.kj-search {
    display: flex; gap: 8px; flex-wrap: wrap;
    max-width: 760px; margin: 0 auto;
    background: var(--kj-surface);
    border: 1px solid var(--kj-border);
    border-radius: var(--kj-radius-lg);
    padding: 8px;
    box-shadow: var(--kj-shadow);
}
.kj-search__field { flex: 1 1 220px; display: flex; align-items: center; gap: 8px; padding: 0 10px; }
.kj-search__field + .kj-search__field { border-left: 1px solid var(--kj-border); }
.kj-search input[type="search"], .kj-search input[type="text"] {
    width: 100%; border: 0; outline: 0; background: transparent;
    font: inherit; color: var(--kj-text); padding: 12px 0;
}
.kj-search input::placeholder { color: var(--kj-text-faint); }
.kj-search .kj-btn { flex: 0 0 auto; }

/* ============================================================ breakpoints

   ⚠ THREE WIDTHS, AND ONLY THREE. Operator ruling Q1-A, 2026-08-31. This file previously
   carried eleven distinct max-widths - 560, 620, 640, 720, 780, 820, 860, 900, 980, 1040,
   1200 - each added for one component without reference to the others. Nothing was wrong
   with any single one; the scatter was the defect, because it meant "responsive" was
   eleven unrelated decisions and every fix began by guessing which of them applied.

       sm   640    one column, stacked fields, tightest type
       md   900    the nav becomes a bottom bar and the sidebar becomes a drawer
       lg  1200    the widest multi-column layouts give up a column

   ⚠ THESE CANNOT BE CUSTOM PROPERTIES. A media query is evaluated before the cascade, so
   `@media (max-width: var(--kj-md))` never matches anything - it fails silently and the
   rule simply never applies, which is the worst way for a breakpoint to be wrong. The
   numbers are literals on purpose and this comment is the single place they are defined.

   ⚠ AND md IS THE ONE THAT MATTERS. Above it the site is a two-column product page with a
   frozen sidebar; below it the navigation and the filters both change form. A component
   that needs to move at some other width is a component that has not decided which of
   those two things it is. */

@media (max-width: 640px) {
    .kj-search__field + .kj-search__field { border-left: 0; border-top: 1px solid var(--kj-border); }
    .kj-search .kj-btn { width: 100%; justify-content: center; }
}

/* ============================================================ sections */

.kj-main { padding: 40px 0 64px; }
.kj-section + .kj-section { margin-top: 44px; }
.kj-section__head { display: flex; align-items: baseline; gap: 14px; margin-bottom: 18px; }
.kj-section__head h2 { margin: 0; }
.kj-section__more { margin-left: auto; font-size: .92rem; font-weight: 600; }

/* ============================================================ category chips */

.kj-chips { display: flex; flex-wrap: wrap; gap: 10px; padding: 0; margin: 0; list-style: none; }
.kj-chip {
    display: inline-flex; align-items: center; gap: 8px;
    padding: 9px 14px; border-radius: 999px;
    border: 1px solid var(--kj-border); background: var(--kj-surface);
    color: var(--kj-text); font-size: .93rem; font-weight: 500;
}
.kj-chip:hover { border-color: var(--kj-brand); color: var(--kj-brand-ink); text-decoration: none; }
.kj-chip.is-active { background: var(--kj-brand); border-color: var(--kj-brand); color: var(--kj-text-on-brand); }

/* ============================================================ listing grid */

.kj-grid {
    display: grid; gap: var(--kj-gap);
    grid-template-columns: repeat(3, minmax(0, 1fr));
}


/* ============================================================ the submission form
   The three-step "Add a Listing" page. Added 2026-08-31.

   ⚠ EVERY COLOUR HERE IS A --kj- TOKEN AND EVERY RADIUS IS --kj-radius-field, so this block
   is correct in both themes without a single value being restated. A hex in here would be
   right in light and wrong in dark, and nobody switches themes to check a form. */

/* ── the wizard rail ─────────────────────────────────────────────────────────────────── */

/* ⚠ A RAIL, NOT A LIST. The steps rendered as "1. 1 What it is" because an <ol> draws its
   own marker beside the number we draw ourselves - two numbers, no progress. `list-style:
   none` removes theirs; the rail below is what makes the remaining one read as a position in
   a sequence rather than as a bullet. */
.kj-steps {
    display: flex;
    align-items: flex-start;
    gap: 0;
    list-style: none;
    margin: 0 0 26px;
    padding: 0;
}

.kj-steps__dot {
    position: relative;
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    padding: 0 4px;
    background: none;
    color: var(--kj-text-faint);
    font-size: .82rem;
    text-align: center;
}

/* ⚠ THE RAIL IS DRAWN BY THE STEPS, NOT UNDER THEM. A single line behind the row would run
   past the first and last circles and out to the edges of the container. Each step draws the
   segment to its LEFT, and the first draws none - so the rail starts and ends exactly on a
   circle at any width.

   ⚠ AND IT STOPS SHORT OF BOTH CIRCLES RATHER THAN PASSING BEHIND THEM. It used to run
   centre-to-centre and rely on the circle's own background to mask the overlap - which it did
   not, so the line crossed the left half of every number. Ending the segment 20px from each
   centre (a 16px radius plus a 4px gap) means there is nothing to mask and no paint order to
   depend on. */
.kj-steps__dot::before {
    content: '';
    position: absolute;
    top: 15px;
    right: calc(50% + 20px);
    left: calc(-50% + 20px);
    height: 2px;
    background: var(--kj-border-strong);
}

.kj-steps__dot:first-child::before { display: none; }
.kj-steps__dot.is-at::before,
.kj-steps__dot.is-done::before { background: var(--kj-brand); }

/* No z-index: the rail is a ::before on the same element and this span comes later in DOM
   order, so painting order already puts the circle on top. Its opaque background is what
   masks the segment running underneath. */
.kj-steps__n {
    position: relative;
    flex: 0 0 auto;
    width: 32px;
    height: 32px;
    border-radius: 50%;
    display: grid;
    place-items: center;
    background: var(--kj-surface);
    border: 2px solid var(--kj-border-strong);
    color: var(--kj-text-faint);
    font-size: .9rem;
    font-weight: 700;
}

.kj-steps__l { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }

/* ⚠ THE LINK TAKES OVER THE COLUMN LAYOUT THAT WAS ON THE <li>. Wrapping the number and
   the label in an anchor inserts a level between the flex container and its two items, so
   without this they stop stacking and the rail's ::before - which stays on the <li>, where
   its 50% offsets still mean the whole step - would be measured against a different box.

   The anchor is added by script (submit.js), so these rules simply do not match until it
   runs, and the inert markup keeps the layout it always had. */
.kj-steps__go {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    width: 100%;
    min-width: 0;
    color: inherit;
    text-decoration: none;
}

/* A step you have not reached is still a link, and still says so on hover - clicking it
   runs the same validation Continue does, so the answer is the browser's own message
   rather than a control that quietly ignores the click. */
.kj-steps__go:hover .kj-steps__n { border-color: var(--kj-brand); }
.kj-steps__go:focus-visible { outline: 2px solid var(--kj-brand); outline-offset: 3px; border-radius: var(--kj-radius); }

/* ⚠ THREE STATES, THREE FILLS - not three shades of one. On step two the row read as three
   identical grey circles, so the control was decoration: it said there were three steps and
   refused to say which one you were on. A filled circle for where you ARE, a darker filled
   one for where you have BEEN, and an outline for what is ahead is legible at a glance and
   survives being printed, screenshotted or looked at by somebody who cannot separate the two
   brand tones. */
.kj-steps__dot.is-at { color: var(--kj-text); font-weight: 600; }
.kj-steps__dot.is-at .kj-steps__n {
    background: var(--kj-brand);
    border-color: var(--kj-brand);
    color: var(--kj-text-on-brand);
    box-shadow: 0 0 0 4px var(--kj-brand-soft);
}

/* ⚠ DONE IS A TICK, NOT A DARKER ORANGE. Measured in the browser: --kj-brand-ink and
   --kj-brand resolve to the SAME value in the dark palette, so "where you have been" and
   "where you are" painted identically and the rail was back to saying nothing. A tick is a
   different SHAPE, which no palette can flatten and no colour-blind viewer can miss. */
.kj-steps__dot.is-done { color: var(--kj-text-soft); }
.kj-steps__dot.is-done .kj-steps__n {
    background: var(--kj-brand-ink);
    border-color: var(--kj-brand-ink);
    color: transparent;
}

.kj-steps__dot.is-done .kj-steps__n::after {
    content: '\2713';
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
    color: var(--kj-text-on-brand);
    font-size: 1rem;
    line-height: 1;
}

/* ⚠ THE LABELS GO AT THE NARROWEST WIDTHS, THE CIRCLES DO NOT. Three truncated words are
   worse than none: the rail alone still says "step two of three", which is the part that
   matters on a phone. */
@media (max-width: 640px) {
    .kj-steps__l { display: none; }
}

/* ⚠ BACK SITS APART FROM CONTINUE. Two buttons side by side at the same weight get the
   wrong one pressed; pushing the forward action to the end matches every wizard anyone has
   used and puts the destructive-feeling one out of the thumb's path. */
.kj-step__foot { justify-content: flex-end; }
.kj-step__foot [data-kj-prev] { margin-right: auto; }

/* ── the combobox ────────────────────────────────────────────────────────────────────── */

/* ⚠ THE CARET SHARES THE BOX WITH THE CHIPS. It has to grow to fill what the chips leave
   and shrink to nothing rather than wrap onto its own line, which is what `flex: 1 1 60px`
   with `min-width: 0` buys. A fixed width would push the last chip out of the box. */
/* ⚠ `.kj-field .kj-pick2__type`, NOT `.kj-pick2__type`. The shared control style added for the
   softer inputs matches `.kj-field input` - (0,1,1) with its element selector - and beats a
   lone class (0,1,0). So the caret inside this box was given its own border, background,
   radius and 10px of padding: a bordered box inside the bordered box holding the chips, which
   is the double outline the operator photographed. Two classes (0,2,0) wins.

   The same trap as the admin grid's control heights, one layer down and in our own CSS this
   time rather than WordPress's. */
.kj-field .kj-pick2__type,
.kj-pick2__type {
    flex: 1 1 60px;
    min-width: 0;
    width: auto;
    border: 0;
    outline: 0;
    padding: 4px 2px;
    background: transparent;
    color: var(--kj-text);
    font: inherit;
    font-size: .95rem;
}

.kj-pick2__type::placeholder { color: var(--kj-text-faint); }

/* Disabled means the cap is reached, not that the control is broken - so it keeps its
   cursor and simply stops accepting text. */
.kj-pick2__type[disabled] { cursor: pointer; }

/* ⚠ ONE RING, NOT THREE. The box drew a 1px border and a 2px outline one pixel outside it,
   and the caret inside drew a third - `.kj-field input:focus` is (0,2,1) and beat the two
   classes I had given the caret. Measured: border 1px orange, outline 2px darker orange at
   1px offset, plus an identical outline on the input. Offset zero makes the border and the
   outline one contiguous 3px ring in one colour, and the caret is told explicitly not to draw
   its own. */
.kj-pick2__box:focus-within {
    border-color: var(--kj-brand-ink);
    outline: 2px solid var(--kj-brand-ink);
    outline-offset: 0;
}

.kj-field .kj-pick2__type:focus,
.kj-pick2__type:focus { outline: 0; box-shadow: none; }

.kj-step { border: 0; margin: 0; padding: 0; min-width: 0; }
.kj-step[hidden] { display: none; }

.kj-step__legend {
    padding: 0;
    margin: 0 0 14px;
    font-size: 1.15rem;
    font-weight: 700;
    color: var(--kj-text);
}

.kj-step__foot { display: flex; gap: 10px; align-items: center; margin: 18px 0 0; }

.kj-field-note { margin: 6px 0 0; font-size: .84rem; color: var(--kj-text-faint); }

/* ── the tree picker ─────────────────────────────────────────────────────────────────── */

/* ⚠ THE NATIVE <select> IS HIDDEN, NOT REMOVED, and this class is what hides it. It is
   still the field that submits - see submit.js. Removing it would make the form depend on a
   script for the site's most important conversion. */
.kj-pick2__native { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }

.kj-pick2 { position: relative; }

.kj-pick2__box {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 5px;
    width: 100%;
    box-sizing: border-box;
    min-height: 44px;
    padding: 6px 10px;
    cursor: pointer;
    color: var(--kj-text);
    background: var(--kj-bg);
    border: 1px solid var(--kj-border-strong);
    border-radius: var(--kj-radius-field);
}

.kj-pick2__box.is-set { border-color: var(--kj-brand); }
.kj-pick2__box:focus-visible { outline: 2px solid var(--kj-brand-ink); outline-offset: 1px; }
.kj-pick2__ph { color: var(--kj-text-faint); }

.kj-chip {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    max-width: 100%;
    padding: 3px 5px 3px 10px;
    border-radius: 999px;
    background: var(--kj-brand-soft);
    border: 1px solid var(--kj-brand);
    font-size: .84rem;
    line-height: 1.4;
}

.kj-chip__label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.kj-chip__x {
    flex: 0 0 auto;
    width: 18px;
    height: 18px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: none;
    color: var(--kj-text-soft);
    font-size: 1rem;
    line-height: 1;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

.kj-chip__x:hover, .kj-chip__x:focus-visible { background: var(--kj-brand); color: var(--kj-text-on-brand); }

.kj-pick2__panel {
    position: absolute;
    z-index: var(--kj-z-pop);
    top: calc(100% + 4px);
    left: 0;
    right: 0;
    background: var(--kj-surface);
    border: 1px solid var(--kj-border-strong);
    border-radius: var(--kj-radius-lg);
    box-shadow: var(--kj-shadow-lift);
    padding: 8px;
}

/* ⚠ THE PANEL HEAD IS LIVE NOW. `.kj-pick2__head` and `.kj-pick2__search` were both
   written for a search box that never shipped, so this file styled two elements nothing
   produced. The head carries the order switch; `__search` still had no producer and is
   gone rather than left as a third selector for somebody to puzzle over. */
.kj-pick2__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    margin-bottom: 6px;
}

.kj-pick2__headlbl { font-size: .78rem; color: var(--kj-text-faint); }

/* The same segmented shape as the Grid/List switch in the results toolbar, and for the
   same reason: the chosen side is FILLED, so which order you are in is readable at a
   glance rather than something you have to hover to discover. */
.kj-pick2__sort {
    display: inline-flex;
    flex: 0 0 auto;
    border: 1px solid var(--kj-border-strong);
    border-radius: var(--kj-radius);
    overflow: hidden;
}

.kj-pick2__sortbtn {
    display: grid;
    place-items: center;
    width: 30px;
    height: 26px;
    padding: 0;
    border: 0;
    background: transparent;
    color: var(--kj-text-soft);
    cursor: pointer;
}

.kj-pick2__sortbtn + .kj-pick2__sortbtn { border-left: 1px solid var(--kj-border-strong); }
.kj-pick2__sortbtn:hover { color: var(--kj-brand-ink); }
.kj-pick2__sortbtn.is-on { background: var(--kj-brand); color: var(--kj-text-on-brand); }
/* Inset, because the button sits flush inside a 1px bordered group and an outset ring
   would be clipped by the `overflow: hidden` that rounds the group's corners. */
.kj-pick2__sortbtn:focus-visible { outline: 2px solid var(--kj-brand); outline-offset: -2px; }

.kj-pick2__cnt {
    margin-left: auto;
    padding-left: 8px;
    font-style: normal;
    font-size: .78rem;
    color: var(--kj-text-faint);
}

/* ⚠ THE ONLY FIXED HEIGHT IN THIS BLOCK, AND IT IS ON A POPUP. Two hundred districts would
   otherwise run off the bottom of a phone with no way to reach the buttons underneath. */
.kj-pick2__body { max-height: 300px; overflow-y: auto; }

.kj-pick2__row { display: flex; align-items: center; gap: 4px; }

.kj-pick2__twisty {
    flex: 0 0 auto;
    width: 24px;
    height: 30px;
    border: 0;
    background: none;
    color: var(--kj-text-faint);
    cursor: pointer;
    font-size: 1rem;
}

.kj-pick2__twisty.is-leaf { cursor: default; }

.kj-pick2__opt {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 6px 8px;
    border: 0;
    border-radius: var(--kj-radius);
    background: none;
    color: var(--kj-text);
    font: inherit;
    font-size: .9rem;
    text-align: left;
    cursor: pointer;
}

.kj-pick2__opt:hover { background: var(--kj-bg-sunk); }
.kj-pick2__row.is-picked .kj-pick2__opt { color: var(--kj-brand-ink); font-weight: 600; }

/* ⚠ A FULL PICKER DISABLES WHAT IT CANNOT TAKE RATHER THAN REFUSING THE CLICK. Silently
   ignoring a tap on the fourth category reads as a broken control; a dimmed row reads as a
   limit, and the note under the box says what the limit is. */
.kj-pick2__opt.is-full { opacity: .4; cursor: not-allowed; }

/* The readable brand, not the vivid one: this is INK on the page ground, and the vivid
   token is a fill. css-check enforces the difference and caught this exact line. */
.kj-pick2__tick { flex: 0 0 auto; width: 14px; color: var(--kj-brand-ink); }
/* `min-width: 0` so a long name yields to the count beside it. Without it a nowrap span
   in a flex row refuses to shrink below its text and pushes the number out of the box. */
.kj-pick2__name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.kj-pick2__branch {
    flex: 1 1 auto;
    display: flex;
    align-items: center;
    padding: 6px 8px;
    font-size: .9rem;
    color: var(--kj-text-faint);
}
.kj-pick2__empty { padding: 10px; margin: 0; font-size: .86rem; color: var(--kj-text-faint); }
.kj-pick2__note { margin: 6px 0 0; font-size: .8rem; color: var(--kj-text-faint); }

/* ── confirming the address ──────────────────────────────────────────────────────────── */

.kj-verify {
    background: var(--kj-surface);
    border: 1px solid var(--kj-border);
    border-radius: var(--kj-radius-lg);
    padding: 24px;
    margin: 0 0 24px;
}

.kj-verify h2 { margin: 0 0 8px; }

/* ⚠ WIDE AND SPACED, BECAUSE IT IS SIX DIGITS READ OFF A PHONE. A code typed into an
   ordinary text field is the step people get wrong most, and the fix is to make the field
   look like the thing it is asking for. */
.kj-verify__code {
    max-width: 220px;
    font-size: 1.4rem;
    letter-spacing: .3em;
    text-align: center;
}

.kj-verify__again { margin-top: 14px; }

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

.kj-card {
    background: var(--kj-surface); border: 1px solid var(--kj-border);
    border-radius: var(--kj-radius-lg); overflow: hidden;
    display: flex; flex-direction: column;
    box-shadow: var(--kj-shadow);
    transition: box-shadow .18s ease, transform .18s ease;
}
.kj-card__media { aspect-ratio: 16 / 10; background: var(--kj-bg-sunk); }
.kj-card__body { padding: 16px 16px 18px; display: flex; flex-direction: column; gap: 6px; flex: 1; }
.kj-card__title { margin: 0; font-size: 1.02rem; }
.kj-card__meta { color: var(--kj-text-soft); font-size: .88rem; margin: 0; }
.kj-card__foot {
    margin-top: auto; padding-top: 12px; display: flex; align-items: center; gap: 10px;
    color: var(--kj-text-faint); font-size: .84rem;
}

.kj-badge {
    display: inline-flex; align-items: center; gap: 5px;
    padding: 3px 9px; border-radius: 999px;
    font-size: .76rem; font-weight: 700; letter-spacing: .02em; text-transform: uppercase;
}
.kj-badge--featured { background: var(--kj-accent-soft); color: var(--kj-accent-ink); }
.kj-badge--approx   { background: var(--kj-bg-sunk); color: var(--kj-text-soft); }

/* ⚠ VERIFIED READS AS TRUST, FEATURED READS AS PAID, so they must not share a colour.
   Verified takes the brand tint; featured keeps the accent. A visitor who cannot tell them
   apart learns that the badge means "spent money", which is the one thing a verification
   mark must never mean. */
.kj-badge--verified { background: var(--kj-brand-soft); color: var(--kj-brand-ink); }

/* The asterisk on a required label. Its own class so it is never a bare "*" a screen
   reader announces out of context — the markup carries aria-hidden. */
.kj-req { color: var(--kj-danger); margin-left: 2px; }
.kj-field__hint { display: block; margin-top: 4px; font-size: .82rem; color: var(--kj-text-faint); }

/* ============================================================ empty state */

.kj-empty {
    background: var(--kj-surface); border: 1px dashed var(--kj-border-strong);
    border-radius: var(--kj-radius-lg);
    padding: 48px 28px; text-align: center; color: var(--kj-text-soft);
}
.kj-empty h3 { color: var(--kj-text); margin-bottom: 6px; }
.kj-empty p { margin: 0 auto; max-width: 52ch; }
.kj-empty__note {
    display: inline-block; margin-top: 18px; padding: 8px 14px;
    background: var(--kj-bg-sunk); border-radius: var(--kj-radius);
    font-size: .86rem; color: var(--kj-text-faint);
}

/* ============================================================ map slot */

/* ⚠ THE MAP IS SEALED, NOT OUT-NUMBERED — `position` + `z-index` here open a stacking
   context, which is the entire fix for the header overlap. Leaflet numbers its own panes,
   controls and corners from 100 up to 1000 and cannot be asked not to; inside a context of
   our own, all of that is re-scoped to compete only with other Leaflet layers, and the map
   meets the rest of the page as ONE box at --kj-z-map. Delete either declaration and the
   corner controls climb back over the header at 1000 — which is how this shipped.

   This rule was TWO rules with the same selector, 130 lines apart, disagreeing about
   height (380px, then 420px). Consolidated to the union, which is byte-for-byte what the
   cascade already computed — but a reader can now change the height and have it work. */
.kj-map {
    position: relative; z-index: var(--kj-z-map);
    height: 420px; border-radius: var(--kj-radius-lg); overflow: hidden;
    border: 1px solid var(--kj-border); background: var(--kj-bg-sunk);
    display: flex; align-items: center; justify-content: center;
    color: var(--kj-text-faint); font-size: .92rem; text-align: center; padding: 20px;
}

/* ============================================================ article */

.kj-article {
    background: var(--kj-surface); border: 1px solid var(--kj-border);
    border-radius: var(--kj-radius-lg); padding: 32px; box-shadow: var(--kj-shadow);
}
.kj-article + .kj-article { margin-top: var(--kj-gap); }
.kj-article__meta { color: var(--kj-text-faint); font-size: .88rem; margin-bottom: 14px; }

/* ============================================================ footer */

.kj-footer {
    background: var(--kj-surface); border-top: 1px solid var(--kj-border);
    padding: 40px 0 28px; color: var(--kj-text-soft); font-size: .92rem;
}
.kj-footer__cols { display: flex; flex-wrap: wrap; gap: 40px; margin-bottom: 28px; }
.kj-footer__about { flex: 1 1 280px; }
.kj-footer__about p { margin: 10px 0 0; max-width: 46ch; }
.kj-footer ul { list-style: none; margin: 0; padding: 0; }
.kj-footer li + li { margin-top: 8px; }
.kj-footer a { color: var(--kj-text-soft); }
.kj-footer a:hover { color: var(--kj-brand-ink); }
.kj-footer__legal {
    border-top: 1px solid var(--kj-border); padding-top: 18px;
    display: flex; flex-wrap: wrap; gap: 12px; align-items: center;
    font-size: .86rem; color: var(--kj-text-faint);
}
.kj-footer__legal .kj-brand { font-size: 1rem; }

/* ⚠ THE CREDIT SITS AT THE FAR END, NOT NEXT TO THE COPYRIGHT. `margin-left: auto`
   in a flex row pushes it right on a wide footer and lets it wrap underneath on a
   narrow one, with no second rule and no breakpoint. */
.kj-footer__by { margin-left: auto; }

/* Both links inherit the row's muted colour rather than the brand link colour - a
   copyright line is not a call to action, and two coloured links down there compete
   with the navigation columns above them. They underline on hover, so they are still
   discoverable as links. */
.kj-footer__legal a { color: inherit; text-decoration: none; }
.kj-footer__legal a:hover { color: var(--kj-brand-ink); text-decoration: underline; }

/* ============================================================ listings, added with the
   real data. Every colour comes from the tokens at the top of this file — a literal here
   is correct in exactly one theme and a glaring island in the other. */

.kj-hero__stat { margin: 14px 0 0; color: var(--kj-text-soft); font-size: .95rem; }
.kj-hero__stat strong { color: var(--kj-text); }

.kj-locate {
    margin-left: 12px; padding: 5px 12px; border-radius: 999px; cursor: pointer;
    background: transparent; border: 1px solid var(--kj-border-strong);
    color: var(--kj-text-soft); font: inherit; font-size: .88rem;
}
.kj-locate:hover { border-color: var(--kj-brand); color: var(--kj-brand-ink); }
.kj-locate[disabled] { opacity: .6; cursor: progress; }
.kj-locate.is-denied { border-color: var(--kj-danger); color: var(--kj-danger); }

.kj-chip__n {
    display: inline-block; margin-left: 6px; padding: 0 6px; border-radius: 999px;
    background: var(--kj-bg-sunk); color: var(--kj-text-faint); font-size: .78rem;
}
.kj-chip.is-active .kj-chip__n { background: var(--kj-veil); color: var(--kj-text-on-brand); }

.kj-section__note { color: var(--kj-text-faint); font-size: .88rem; }

/* A card with no photograph draws a monogram. A blank grey box reads as a broken image. */
.kj-card__media--none { display: grid; place-items: center; }
.kj-card__media--none span { font-size: 2.6rem; font-weight: 700; color: var(--kj-text-on-brand); opacity: .9; }
/* The six monogram tones. Tokens rather than literals for one reason: they are the last
   colours in this file that a theme change would not reach, and "it is only decorative"
   is how the first exception always starts. They are deliberately the SAME in both
   themes — saturated grounds under white text read correctly on either. */
.kj-tone-1 { background: var(--kj-tone-1); }
.kj-tone-2 { background: var(--kj-tone-2); }
.kj-tone-3 { background: var(--kj-tone-3); }
.kj-tone-4 { background: var(--kj-tone-4); }
.kj-tone-5 { background: var(--kj-tone-5); }
.kj-tone-6 { background: var(--kj-tone-6); }

.kj-card__price { margin: 2px 0 0; font-weight: 650; color: var(--kj-text); }
.kj-card__rating { margin: 0; font-size: .84rem; color: var(--kj-text-soft); }
.kj-stars { color: var(--kj-accent); letter-spacing: 1px; }
.kj-card__dist {
    color: var(--kj-brand-ink); font-weight: 600; font-size: .82rem;
}


.kj-btn {
    display: inline-block; padding: 9px 18px; border-radius: var(--kj-radius);
    background: var(--kj-brand); color: var(--kj-text-on-brand); font-weight: 600;
    border: 1px solid var(--kj-brand);
}
.kj-btn:hover { background: var(--kj-brand-hover); border-color: var(--kj-brand-hover); text-decoration: none; }
.kj-btn--ghost { background: transparent; color: var(--kj-text); border-color: var(--kj-border-strong); }
.kj-btn--ghost:hover { background: var(--kj-bg-soft); color: var(--kj-text); border-color: var(--kj-brand); }

.kj-crumbs { margin: 18px 0 8px; color: var(--kj-text-faint); font-size: .88rem; display: flex; gap: 8px; }

/* ---------------------------------------------------------- listing detail */

.kj-listing__grid { display: grid; grid-template-columns: minmax(0, 1fr) 330px; gap: 28px; align-items: start; }
@media (max-width: 900px) { .kj-listing__grid { grid-template-columns: 1fr; } }

.kj-listing__head h1 { margin: 6px 0 8px; font-size: 1.9rem; }
.kj-listing__meta { display: flex; flex-wrap: wrap; gap: 12px; color: var(--kj-text-soft); margin: 0 0 10px; font-size: .92rem; }
.kj-listing__meta span:not(:last-child)::after { content: ' ·'; color: var(--kj-text-faint); }
.kj-listing__price { font-size: 1.35rem; font-weight: 700; margin: 0 0 6px; }
.kj-listing__summary { font-size: 1.05rem; color: var(--kj-text-soft); }
.kj-listing__section { margin-top: 28px; }
.kj-listing__section h2 { font-size: 1.15rem; margin: 0 0 10px; }

.kj-panel {
    background: var(--kj-surface); border: 1px solid var(--kj-border);
    border-radius: var(--kj-radius-lg); padding: 18px 18px 20px; margin-bottom: 18px;
}
.kj-panel h2 { font-size: 1.05rem; margin: 0 0 12px; }
.kj-panel h3 { font-size: .92rem; margin: 16px 0 4px; color: var(--kj-text-soft); }

.kj-kv { display: flex; justify-content: space-between; gap: 12px; margin: 0 0 8px; font-size: .93rem; }
.kj-kv span { color: var(--kj-text-faint); }
.kj-address { font-style: normal; color: var(--kj-text-soft); font-size: .93rem; line-height: 1.5; }
.kj-text-soft { color: var(--kj-text-soft); }

.kj-note { padding: 10px 14px; border-radius: var(--kj-radius); font-size: .9rem; margin: 0 0 12px; }
/* ⚠ NO MEDIA QUERY HERE ANY MORE. A rule whose dark variant lives in
   @media (prefers-color-scheme: dark) is invisible to the toggle — flip to dark on a
   light system and this one panel stays amber-on-cream inside a dark page. Both
   values are tokens now, so it follows whichever way the theme was decided. */
.kj-note--warn { background: var(--kj-accent-soft); color: var(--kj-accent-ink);
                 border: 1px solid var(--kj-accent-border); }

/* ---------------------------------------------------------- map pins

   Two shapes, because Q10-B says an approximate position must be visibly different — an
   exact pin is a filled dot, an approximate one is a hollow ring. Drawn in CSS rather
   than shipped as images: no files, and they inherit the theme's own colours. */

.leaflet-container { background: var(--kj-bg-sunk); font: inherit; }

.kj-pin__dot { display: block; width: 100%; height: 100%; border-radius: 50%; box-sizing: border-box; }
.kj-pin.is-exact  .kj-pin__dot { background: var(--kj-brand); border: 3px solid var(--kj-pin-ring); box-shadow: 0 1px 4px var(--kj-pin-shadow); }
.kj-pin.is-approx .kj-pin__dot { background: transparent; border: 3px dashed var(--kj-text-soft); opacity: .95; }
.kj-pin.is-featured .kj-pin__dot { background: var(--kj-accent); }

.kj-pop__cate { color: var(--kj-text-soft); font-size: .86rem; }
.kj-pop__approx { color: var(--kj-text-faint); font-size: .82rem; }

/* ---------------------------------------------------------- forms */

.kj-narrow { max-width: 760px; }
.kj-lede { color: var(--kj-text-soft); font-size: 1.05rem; margin: 0 0 20px; }

.kj-form { background: var(--kj-surface); border: 1px solid var(--kj-border);
           border-radius: var(--kj-radius-lg); padding: 22px 22px 8px; }
.kj-field { margin: 0 0 16px; display: block; }
.kj-field label { display: block; font-weight: 600; font-size: .92rem; margin-bottom: 5px; }
/* ⚠ ONE CONTROL STYLE, AND THE PADDING IS THE ONLY THING THAT VARIES. Four rules used to
   restate this same border, radius, ground and ink - and a fifth control, the price Min/Max
   pair, restated none of it and fell through to the browser's own default box. The paddings
   below stay per-rule because a toolbar control is meant to be smaller than a form field;
   everything they had in common is here.

   ⚠ `width` IS DELIBERATELY ABSENT. `.kj-ctl__sort select` sits inline in the control bar
   and has never carried one, so hoisting `width: 100%` in here would stretch it across the
   toolbar. Each rule keeps the width it already had. */
.kj-field input, .kj-field select, .kj-field textarea,
.kj-near__field input[type="search"], .kj-near__field input[type="text"], .kj-near__field select,
.kj-ctl__sort select, .kj-filters select, .kj-filters input[type="number"],
.kj-facet__pair input, .kj-filters__pair input {
    box-sizing: border-box; font: inherit;
    color: var(--kj-text); background: var(--kj-bg);
    border: 1px solid var(--kj-border-strong); border-radius: var(--kj-radius-field);
}

.kj-field input, .kj-field select, .kj-field textarea { width: 100%; padding: 10px 12px; }
.kj-field input:focus, .kj-field select:focus, .kj-field textarea:focus {
    outline: 2px solid var(--kj-brand-ink); outline-offset: 1px; border-color: var(--kj-brand-ink);
}
.kj-field-row { display: grid; grid-template-columns: 1fr 1fr; gap: 0 16px; }
@media (max-width: 640px) { .kj-field-row { grid-template-columns: 1fr; } }
.kj-req { color: var(--kj-danger); }
.kj-form__foot { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin: 6px 0 18px; }

/* ⚠ The honeypot. Hidden from sight AND from assistive technology — a screen-reader user
   who fills it in would be rejected as a bot, which is the failure this pattern is famous
   for. Not display:none: some bots skip those and fill everything else. */
.kj-hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

.kj-note--ok  { background: var(--kj-brand-soft); color: var(--kj-brand-ink); border: 1px solid var(--kj-brand); }
.kj-note--err { background: var(--kj-danger-soft); color: var(--kj-danger); border: 1px solid var(--kj-danger); }

/* ============================================================ theme toggle, menu,
   suggestions — added with the switchable theme. */

.kj-theme {
    width: 34px; height: 34px; padding: 0; cursor: pointer; flex: 0 0 auto;
    display: grid; place-items: center;
    background: transparent; border: 1px solid var(--kj-border-strong);
    border-radius: 999px; color: var(--kj-text-soft);
}
.kj-theme:hover { border-color: var(--kj-brand); color: var(--kj-brand-ink); }
/* A PAINT DROP - operator request 2026-09-01. The old control was a CSS-drawn ring; at
   15px with no label it read as a grey dot, and nobody guesses that a dot changes theme.

   The three states are told apart by how FULL the drop is, which needs no translating:
   empty = light, full = dark, half = follow the system. Two paths sharing one `d`, so the
   outline never shifts when the fill changes - a single path swapping fill for stroke
   would move by half a stroke width on every click. */
.kj-theme__icon { width: 17px; height: 17px; display: block; }
.kj-theme__icon svg { width: 100%; height: 100%; display: block; overflow: visible; }

.kj-theme__line { fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linejoin: round; }
.kj-theme__fill { fill: currentColor; }

/* ⚠ clip-path ON THE FILL, NOT opacity OR A SECOND SHAPE. Clipping keeps the fill exactly
   inside the outline at every state, so the half state is a true vertical waterline rather
   than a smaller drop floating inside a bigger one. */
.kj-theme[data-state="light"]  .kj-theme__fill { clip-path: inset(100% 0 0 0); }
.kj-theme[data-state="system"] .kj-theme__fill { clip-path: inset(50% 0 0 0); }
.kj-theme[data-state="dark"]   .kj-theme__fill { clip-path: none; }

/* ⚠ AND A DEFAULT FOR THE UNSET ATTRIBUTE. The script writes data-state on load, but the
   first paint happens before it runs; without this the drop renders solid for a frame on
   every navigation, which is a flash of the wrong answer. */
.kj-theme:not([data-state]) .kj-theme__fill { clip-path: inset(50% 0 0 0); }

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

.kj-nav__has-menu { position: relative; }
.kj-nav__toggle {
    background: transparent; border: 0; padding: 0; cursor: pointer; font: inherit;
    color: var(--kj-text-soft); font-weight: 500; font-size: .95rem;
    display: inline-flex; align-items: center; gap: 6px;
}
.kj-nav__toggle:hover, .kj-nav__toggle[aria-expanded="true"] { color: var(--kj-text); }
.kj-nav__caret {
    width: 0; height: 0; border-left: 4px solid transparent; border-right: 4px solid transparent;
    border-top: 5px solid currentColor; opacity: .7;
}

.kj-mega {
    position: absolute; top: calc(100% + 16px); left: 50%; transform: translateX(-50%);
    z-index: var(--kj-z-pop); width: min(1080px, 94vw);
    background: var(--kj-surface); border: 1px solid var(--kj-border);
    border-radius: var(--kj-radius-lg); box-shadow: var(--kj-shadow-lift); padding: 24px 26px;
}
.kj-mega__inner {
    display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 22px 30px;
}
/* One step only: the mega panel is a nav dropdown, and the nav is gone below md. */
@media (max-width: 1200px) { .kj-mega__inner { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

.kj-mega__head {
    display: flex; align-items: baseline; justify-content: space-between; gap: 8px;
    font-weight: 700; color: var(--kj-text); font-size: .92rem;
    padding-bottom: 6px; margin-bottom: 7px; border-bottom: 1px solid var(--kj-border);
}
.kj-mega__head:hover { color: var(--kj-brand-ink); text-decoration: none; }
.kj-mega__n { font-weight: 600; font-size: .76rem; color: var(--kj-text-faint); }

/* ⚠ display:block IS LOAD-BEARING, not a reset for tidiness. It undoes the flex the nav
   would otherwise impose — see the note on `.kj-nav > ul` above. */
.kj-mega__col ul { display: block; list-style: none; margin: 0; padding: 0; }
.kj-mega__col li { margin: 0; }
.kj-mega__col a {
    display: flex; justify-content: space-between; gap: 10px; align-items: baseline;
    color: var(--kj-text-soft); font-size: .855rem; line-height: 1.45;
    padding: 2px 6px; margin: 0 -6px; border-radius: 5px;
}
.kj-mega__col a:hover { color: var(--kj-brand-ink); background: var(--kj-bg-sunk); text-decoration: none; }
.kj-mega__col a em { font-style: normal; color: var(--kj-text-faint); font-size: .76rem; }
.kj-mega__more { font-weight: 600; color: var(--kj-brand-ink) !important; }

.kj-mega__foot {
    margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--kj-border);
    display: flex; gap: 18px; flex-wrap: wrap; font-size: .86rem;
}

/* ---------------------------------------------------------- search suggestions */

/* ⚠ THE FIELD CARRIES THE z-index, NOT THE PANEL. The suggest panel is absolutely
   positioned against this element, so the field is what actually competes with the page —
   and front-page.php renders a map below this search bar. Left at `position: relative`
   alone (z-index auto, no stacking context) the panel's own number went into the root
   comparison and lost to Leaflet. Seated here, the panel's number is local and the whole
   field clears the map by construction, on every template, without either one knowing
   the other exists. */
.kj-search__field { position: relative; z-index: var(--kj-z-chrome); }

.kj-suggest {
    position: absolute; top: calc(100% + 8px); left: 0; right: 0; z-index: var(--kj-z-pop);
    background: var(--kj-surface); border: 1px solid var(--kj-border);
    border-radius: var(--kj-radius); box-shadow: var(--kj-shadow-lift);
    padding: 6px; max-height: 380px; overflow-y: auto; text-align: left;
}
.kj-suggest__label {
    font-size: .72rem; text-transform: uppercase; letter-spacing: .07em;
    color: var(--kj-text-faint); padding: 8px 10px 4px;
}
.kj-suggest__item {
    display: flex; justify-content: space-between; align-items: baseline; gap: 12px;
    padding: 8px 10px; border-radius: 6px; color: var(--kj-text);
}
.kj-suggest__item em { color: var(--kj-text-faint); font-style: normal; font-size: .82rem; }
.kj-suggest__item:hover, .kj-suggest__item.is-active {
    background: var(--kj-bg-sunk); text-decoration: none; color: var(--kj-text);
}

/* ---------------------------------------------------------- scope pickers

   ⚠ THE HOST CARRIES THE LAYER, NOT THE PANEL — the same rule as .kj-search__field above,
   and it is here for the same reason: the panel is positioned against this element, so
   this element is what competes with the map and the sticky bars. A number on the panel
   alone goes into the ROOT comparison and loses to Leaflet, which is the original defect
   in a new place. */
.kj-pick { position: relative; z-index: var(--kj-z-chrome); }
.kj-pick input[type="text"], .kj-pick input[type="search"] { width: 100%; }

.kj-pick__panel {
    position: absolute; top: calc(100% + 6px); left: 0; right: 0; z-index: var(--kj-z-pop);
    min-width: 240px; max-height: 340px; overflow-y: auto; padding: 6px;
    background: var(--kj-surface); border: 1px solid var(--kj-border);
    border-radius: var(--kj-radius); box-shadow: var(--kj-shadow-lift); text-align: left;
}
.kj-pick__label {
    font-size: .72rem; text-transform: uppercase; letter-spacing: .07em;
    color: var(--kj-text-faint); padding: 8px 10px 4px;
}
.kj-pick__item {
    display: flex; justify-content: space-between; align-items: baseline; gap: 12px;
    padding: 7px 10px; border-radius: 6px; color: var(--kj-text); font-size: .92rem;
}
.kj-pick__item em { color: var(--kj-text-faint); font-style: normal; font-size: .82rem; }
.kj-pick__item b { color: var(--kj-text-faint); font-weight: 500; font-size: .8rem; }
.kj-pick__item:hover, .kj-pick__item.is-active {
    background: var(--kj-bg-sunk); text-decoration: none; color: var(--kj-text);
}
.kj-pick__item.is-on { color: var(--kj-brand-ink); font-weight: 600; }

/* The way out of a scope. Set apart from the options because it is not one of them —
   it is the row that undoes the thing every other row does. */
.kj-pick__item--clear {
    color: var(--kj-text-soft); border-bottom: 1px solid var(--kj-border);
    border-radius: 6px 6px 0 0; margin-bottom: 4px;
}
.kj-pick__none { padding: 10px; color: var(--kj-text-faint); font-size: .88rem; }

/* ⚠ CHROME REPAINTS AN AUTOFILLED INPUT WITH ITS OWN BACKGROUND, AND `background:
   transparent` DOES NOT STOP IT. That is the pale box over the search field in dark mode —
   not a theme bug, a UA style with higher precedence than anything a stylesheet can set
   normally. The inset box-shadow is the only thing that covers it, and the absurd
   transition delay is the standard trick for the brief flash before it applies. */
.kj-search input:-webkit-autofill,
.kj-search input:-webkit-autofill:hover,
.kj-search input:-webkit-autofill:focus,
.kj-form input:-webkit-autofill,
.kj-form input:-webkit-autofill:focus {
    -webkit-text-fill-color: var(--kj-text);
    -webkit-box-shadow: 0 0 0 1000px var(--kj-surface) inset;
    box-shadow: 0 0 0 1000px var(--kj-surface) inset;
    caret-color: var(--kj-text);
    transition: background-color 100000s ease-in-out 0s;
}

/* Tell the UA which palette its own widgets — scrollbars, form controls, the caret —
   should use. Without it a dark page still gets light scrollbars and a light dropdown. */
:root { color-scheme: light; }
:root[data-theme="dark"] { color-scheme: dark; }
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { color-scheme: dark; } }

/* ============================================================ /near/ — the split view */

.kj-near { max-width: none; }

/* ⚠ THE CHROME ABOVE THE SPLIT IS MEASURED ONCE, HERE, AND NOWHERE ELSE. `.kj-near__split`
   fills the viewport minus everything above it, so that number and the padding that
   produces it are one fact — and it used to be two: a hard `calc(100vh - 132px)` sitting
   130 lines away from the paddings it was derived from. Compacting the bar (operator,
   2026-08-28) changed the paddings; a literal would have left the map overhanging the fold
   by exactly the amount saved, which reads as the compaction having done nothing. */
:root { --kj-near-chrome: 116px; }

.kj-near__bar {
    display: flex; gap: 10px; align-items: flex-end; flex-wrap: wrap;
    padding: 14px 22px; background: var(--kj-surface);
    border-bottom: 1px solid var(--kj-border); position: sticky; top: 0; z-index: var(--kj-z-chrome);
}
.kj-near__field { flex: 1 1 170px; min-width: 0; }
.kj-near__field--cate { flex: 1 1 210px; }
.kj-near__field--radius { flex: 0 0 200px; }
.kj-near__field input[type="search"], .kj-near__field input[type="text"], .kj-near__field select {
    width: 100%; padding: 9px 11px;
}
.kj-near__field--radius label {
    display: block; font-size: .82rem; color: var(--kj-text-soft); margin-bottom: 2px;
}
.kj-near__field--radius output { font-weight: 700; color: var(--kj-text); }
.kj-near__field--radius input[type="range"] { width: 100%; accent-color: var(--kj-brand); }

/* ⚠ THE SPLIT IS A GRID WITH ITS OWN HEIGHT, NOT TWO FLOATED COLUMNS. The map has to fill
   the viewport and stay put while the list scrolls beside it; a document-flow map either
   collapses to zero height or scrolls away, and Leaflet on a zero-height container renders
   a grey box that looks exactly like a broken tile server. */
.kj-near__split {
    display: grid; grid-template-columns: minmax(360px, 42%) 1fr;
    height: calc(100vh - var(--kj-near-chrome)); min-height: 480px;
}
.kj-near__list { overflow-y: auto; padding: 0 18px 40px; }
.kj-near__map { position: relative; border-left: 1px solid var(--kj-border); }
.kj-near__map .kj-map { height: 100%; border: 0; border-radius: 0; }
.kj-map--empty { display: grid; place-items: center; color: var(--kj-text-faint); background: var(--kj-bg-sunk); }

@media (max-width: 900px) {
    /* On a phone the map goes ABOVE the list at a fixed height rather than beside it.
       Two panes on a 390px screen gives one useless column and one unusable one. */
    .kj-near__split { grid-template-columns: 1fr; height: auto; }
    .kj-near__map { height: 320px; border-left: 0; border-bottom: 1px solid var(--kj-border); order: -1; }
    .kj-near__list { max-height: none; }
}

/* ⚠ THE RESULTS COLUMN'S OWN TOOLBAR, AND IT STICKS. The sort and the grid/list switch
   moved in here (operator ruling 2026-08-28) so the band above can collapse to one line.
   A sort control that scrolls out of view after four results is worse than one that never
   moved, so this row is `sticky` against the pane it lives in and paints its own ground —
   without a background the rows scroll THROUGH it. */
.kj-near__count {
    display: flex; align-items: center; justify-content: space-between; gap: 12px;
    flex-wrap: wrap; position: sticky; top: 0; z-index: var(--kj-z-pane);
    padding: 10px 0 8px; margin: 0 -18px 4px; padding-left: 18px; padding-right: 18px;
    background: var(--kj-bg); border-bottom: 1px solid var(--kj-border);
    color: var(--kj-text-soft); font-size: .9rem;
}
.kj-near__count-text { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; min-width: 0; }
.kj-near__count strong { color: var(--kj-text); font-size: 1.05rem; }
.kj-near__hint { color: var(--kj-text-faint); font-size: .84rem; }

.kj-ctl__inline { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; }

/* ---------------------------------------------------------- compact row */

.kj-rows { display: flex; flex-direction: column; gap: 8px; }

.kj-row {
    display: grid; grid-template-columns: 44px 1fr auto; gap: 12px; align-items: center;
    padding: 11px 12px; background: var(--kj-surface);
    border: 1px solid var(--kj-border); border-radius: var(--kj-radius);
}

.kj-row__mark {
    width: 44px; height: 44px; border-radius: 9px; display: grid; place-items: center;
    color: var(--kj-pin-ring); font-weight: 700; font-size: 1.2rem;
}
.kj-row__body { min-width: 0; }
.kj-row__title { margin: 0 0 2px; font-size: .97rem; line-height: 1.3; }
.kj-row__title a { color: var(--kj-text); }
.kj-row__title a:hover { color: var(--kj-brand-ink); text-decoration: none; }
.kj-row__meta { margin: 0; font-size: .82rem; color: var(--kj-text-soft); display: flex; gap: 8px; flex-wrap: wrap; }
.kj-row__meta span:not(:last-child)::after { content: ' ·'; color: var(--kj-text-faint); }
.kj-row__tags { margin: 3px 0 0; display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
.kj-row__tags:empty { display: none; }
.kj-row__price { font-weight: 650; font-size: .86rem; }
.kj-row__end { text-align: right; display: flex; flex-direction: column; gap: 4px; align-items: flex-end; }
.kj-row__dist { color: var(--kj-brand-ink); font-weight: 700; font-size: .86rem; white-space: nowrap; }
.kj-row__call {
    font-size: .78rem; padding: 3px 9px; border-radius: 999px;
    border: 1px solid var(--kj-border-strong); color: var(--kj-text-soft);
}
.kj-row__call:hover { border-color: var(--kj-brand); color: var(--kj-brand-ink); text-decoration: none; }

/* ============================================================ /find/ — the shop layout

   ⚠ A SIDEBAR BESIDE A GRID, AND THE SIDEBAR COLLAPSES FIRST. On a phone the facets go
   ABOVE the results in a disclosure rather than beside them: 260px of filters on a 390px
   screen leaves 130px of products, which is not a product page. Source order already puts
   the filters first, so the mobile stack needs no re-ordering — it just stops being two
   columns. */

.kj-shop { display: grid; grid-template-columns: 260px minmax(0, 1fr); gap: 24px; align-items: start; }
.kj-shop__main { min-width: 0; }

.kj-facets {
    background: var(--kj-surface); border: 1px solid var(--kj-border);
    border-radius: var(--kj-radius-lg); overflow: hidden;

    /* ⚠ DECLARED HERE, AND ITS VALUE IS THE NO-JS DEFAULT. site.js overwrites this same
       property inline on this same element once it has measured; until then - and for ever,
       if scripting is off - the sidebar would freeze under the header, which is the right
       answer whenever it fits. Declaring it rather than writing `var(--x, calc(...))` at
       the point of use is this stylesheet's own rule: a fallback you cannot delete is the
       tell that the token does not exist. */
    --kj-facets-top: calc(var(--kj-header-h) + 12px);
}

/* ⚠ NO CAP, NO INNER SCROLL, AND STILL FROZEN - operator ruling 2026-09-01: *"the filter
   column here should be full height (it should evolve with its content), so no scrollbar
   even for the category section too, and it should be frozen too"*.

   Those three are not independent, and the conflict is worth stating because it is the
   reason the old rule capped the height. `position: sticky; top: N` on a box TALLER than
   the viewport pins the box the moment its top reaches N - and its bottom, which is where
   Apply lives, then can never be scrolled into view again. Previously the cap plus an
   inner scroll bought reachability at the price of the scrollbar the operator is now
   rejecting.

   ⚠ THE WAY OUT IS THAT `top` MAY BE NEGATIVE. Set `top = viewportH - sidebarH - gap`
   and sticky engages only once the sidebar's BOTTOM has arrived at the bottom of the
   viewport - so the visitor scrolls the whole sidebar past, reading all of it, and it
   freezes there with Apply in view. When the sidebar is shorter than the viewport that
   expression is larger than the header offset, so it freezes at the top as before. One
   formula, both cases: `min(headerOffset, viewportH - sidebarH - gap)`.

   ⚠ WHICH NEEDS A MEASUREMENT, SO IT IS AN ENHANCEMENT AND FREEZING IS OPT-IN. CSS
   cannot know the sidebar's height, so the class and the custom property are both written
   by site.js. Without scripting the sidebar is a perfectly ordinary column that scrolls
   with the page - everything reachable, nothing frozen. That is the right way round: a
   sticky sidebar whose bottom is unreachable is broken, an unfrozen one is merely plainer. */
.kj-facets.is-frozen {
    position: sticky;
    top: var(--kj-facets-top);
}
.kj-facets__head {
    display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
    padding: 10px 16px; background: var(--kj-brand); color: var(--kj-text-on-brand);

    /* The title stays put while the facets scroll under it, so the "Clear all" escape
       hatch is reachable from anywhere in a long sidebar. */
    position: sticky; top: 0; z-index: var(--kj-z-pop);
}
.kj-facets__head h2 { margin: 0; font-size: 1rem; color: var(--kj-text-on-brand); }
/* ⚠ SCOPED TO THE HEAD, AND THE SPECIFICITY IS THE POINT. The lockup carries both
   `kj-brand` and `kj-facets__brand`, and `.kj-brand { display: inline-flex }` is declared
   LATER in this file - equal weight, so it won and the logo showed at every width. Winning
   on specificity rather than position is right here: this element is defined by living in
   that head, unlike a responsive override, which belongs at the end of the file. */
.kj-facets__head .kj-facets__brand { display: none; }
.kj-facets__clear { color: var(--kj-text-on-brand); font-size: .82rem; text-decoration: underline; }
.kj-facets__clear:hover { color: var(--kj-text-on-brand); }

/* ---------------------------------------------------------- the facet accordion

   ⚠ <details>/<summary>, NOT A DIV AND A CLICK HANDLER. The disclosure is keyboard
   operable, announced as expanded or collapsed by a screen reader, and togglable with no
   JavaScript at all — so a visitor whose script fails gets a working sidebar rather than
   a column of headings that do nothing. The JS only REMEMBERS the state. */
.kj-facet { border-top: 1px solid var(--kj-border); }
.kj-facets__form > .kj-facet:first-of-type { border-top: 0; }

.kj-facet > summary {
    display: flex; align-items: center; gap: 8px;
    padding: 8px 16px; cursor: pointer; list-style: none;
    -webkit-user-select: none; user-select: none;
}
.kj-facet > summary::-webkit-details-marker { display: none; }
.kj-facet > summary:hover { background: var(--kj-bg-sunk); }
.kj-facet > summary:focus-visible { outline: 2px solid var(--kj-brand-ink); outline-offset: -2px; }

/* A chevron drawn from two borders — no glyph, so it cannot be missing from a font, and
   no image, so it takes its colour from the theme like everything else. */
.kj-facet > summary::after {
    content: ''; margin-left: auto; flex: 0 0 auto;
    width: 6px; height: 6px;
    border-right: 2px solid var(--kj-text-faint);
    border-bottom: 2px solid var(--kj-text-faint);
    transform: translateY(-2px) rotate(45deg);
    transition: transform .15s ease;
}
.kj-facet[open] > summary::after { transform: translateY(1px) rotate(-135deg); }

@media (prefers-reduced-motion: reduce) {
    .kj-facet > summary::after { transition: none; }
}

.kj-facet h3 {
    margin: 0; font-size: .8rem; letter-spacing: .04em; text-transform: uppercase;
    color: var(--kj-text-soft);
}
.kj-facet__body { padding: 0 16px 10px; }

.kj-facet .kj-check {
    display: flex; align-items: center; gap: 8px;
    padding: 2px 0; margin: 0; font-size: .88rem; line-height: 1.35;
}
.kj-facet .kj-check span { flex: 1 1 auto; min-width: 0; }
.kj-facet .kj-check em { font-style: normal; font-size: .78rem; color: var(--kj-text-faint); }

/* ⚠ DIMMED, NOT HIDDEN. A facet with no results still tells a visitor the axis exists and
   that they have filtered it away — removing it makes the sidebar change shape on every
   click, which reads as the page breaking. It stays clickable so it can be un-filtered. */
.kj-facet .kj-check.is-empty span,
.kj-facet .kj-check.is-empty em { opacity: .45; }

/* ---------------------------------------------------------- the price range

   ⚠ TWO `type=range` INPUTS ON ONE TRACK. HTML has no dual-thumb control, so both knobs
   are absolutely positioned over the same 100% strip with their own tracks hidden and
   `pointer-events: none` on the input, restored on the thumb alone - otherwise the upper
   input's full-width box swallows every click meant for the lower one and only one knob
   ever moves. */

.kj-range { position: relative; height: 30px; margin: 2px 0 10px; }
.kj-range__track {
    position: absolute; inset: 13px 0 auto; height: 4px; border-radius: 999px;
    background: var(--kj-border-strong);
}
.kj-range__fill {
    position: absolute; top: 0; bottom: 0; border-radius: 999px;
    background: var(--kj-brand); left: 0; right: 0;
}
.kj-range__knob {
    position: absolute; inset: 0; width: 100%; margin: 0;
    -webkit-appearance: none; appearance: none;
    background: transparent; pointer-events: none;
}
.kj-range__knob::-webkit-slider-runnable-track { background: transparent; border: 0; height: 30px; }
.kj-range__knob::-moz-range-track { background: transparent; border: 0; height: 30px; }
.kj-range__knob::-webkit-slider-thumb {
    -webkit-appearance: none; appearance: none; pointer-events: auto;
    width: 18px; height: 18px; border-radius: 50%; cursor: grab;
    background: var(--kj-surface); border: 2px solid var(--kj-brand);
    box-shadow: var(--kj-shadow); margin-top: 6px;
}
.kj-range__knob::-moz-range-thumb {
    pointer-events: auto; width: 18px; height: 18px; border-radius: 50%; cursor: grab;
    background: var(--kj-surface); border: 2px solid var(--kj-brand); box-shadow: var(--kj-shadow);
}
.kj-range__knob:focus-visible::-webkit-slider-thumb { outline: 2px solid var(--kj-brand-ink); outline-offset: 2px; }

.kj-facet__pair { display: flex; gap: 8px; }
.kj-facet__pair input { width: 100%; min-width: 0; padding: 8px 11px; }
.kj-facet__note, .kj-facet__none { margin: 6px 0 0; font-size: .78rem; color: var(--kj-text-faint); }

.kj-radius { display: block; font-size: .86rem; color: var(--kj-text-soft); margin-bottom: 2px; }
.kj-radius output { font-weight: 700; color: var(--kj-brand-ink); }
.kj-facet input[type="range"] { width: 100%; accent-color: var(--kj-brand); }

.kj-facet--links ul { list-style: none; margin: 0; padding: 0; }
.kj-facet--links a { display: flex; gap: 8px; padding: 2px 0; font-size: .88rem; line-height: 1.35; }
.kj-facet--links a span { flex: 1 1 auto; }
.kj-facet--links a em { font-style: normal; font-size: .78rem; color: var(--kj-text-faint); }

.kj-facets__apply { width: calc(100% - 32px); margin: 4px 16px 14px; justify-content: center; }

/* ============================================================ the control bar
   One component, both surfaces. */

.kj-ctl {
    display: flex; align-items: center; justify-content: space-between; gap: 14px;
    flex-wrap: wrap; padding: 12px 0; margin-bottom: 4px;
    border-bottom: 1px solid var(--kj-border);
}
.kj-near .kj-ctl { padding: 10px 22px; border-bottom: 1px solid var(--kj-border); margin: 0; }

/* The compacted page band: heading, count and the two controls that stayed, on ONE line. */
.kj-ctl--tight { padding: 7px 22px; gap: 12px; }
.kj-ctl--tight .kj-ctl__count { align-items: baseline; gap: 12px; flex-wrap: wrap; min-width: 0; }
.kj-ctl__h1 {
    font-size: 1.15rem; line-height: 1.2; margin: 0; font-weight: 700; color: var(--kj-text);
}
.kj-ctl--tight .kj-ctl__count strong { font-weight: 500; color: var(--kj-text-soft); font-size: .9rem; }

.kj-ctl__count { display: flex; align-items: baseline; gap: 10px; }
.kj-ctl__count strong { font-size: 1.05rem; }
.kj-ctl__clear { font-size: .84rem; }
.kj-ctl__right { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }

.kj-ctl__sort select, .kj-filters select, .kj-filters input[type="number"] {
    padding: 7px 10px; font-size: .88rem;
}

.kj-ctl__layout { display: inline-flex; border: 1px solid var(--kj-border-strong); border-radius: var(--kj-radius); overflow: hidden; }
.kj-ctl__lay {
    display: grid; place-items: center; width: 34px; height: 32px;
    color: var(--kj-text-soft); background: var(--kj-bg);
}
.kj-ctl__lay + .kj-ctl__lay { border-left: 1px solid var(--kj-border-strong); }
.kj-ctl__lay:hover { color: var(--kj-text); text-decoration: none; }
.kj-ctl__lay.is-on { background: var(--kj-brand); color: var(--kj-text-on-brand); }

/* The two icons are drawn in CSS: four squares and three bars. No files, and they inherit
   the button's colour in both themes and in the active state. */
.kj-ico { display: block; width: 14px; height: 14px; }
.kj-ico--grid {
    background:
        linear-gradient(currentColor, currentColor) 0 0 / 6px 6px no-repeat,
        linear-gradient(currentColor, currentColor) 8px 0 / 6px 6px no-repeat,
        linear-gradient(currentColor, currentColor) 0 8px / 6px 6px no-repeat,
        linear-gradient(currentColor, currentColor) 8px 8px / 6px 6px no-repeat;
}
.kj-ico--row {
    background:
        linear-gradient(currentColor, currentColor) 0 1px / 14px 2px no-repeat,
        linear-gradient(currentColor, currentColor) 0 6px / 14px 2px no-repeat,
        linear-gradient(currentColor, currentColor) 0 11px / 14px 2px no-repeat;
}

/* The order switch's two icons, drawn the same way for the same reason: no files, and
   they inherit the button's colour in both themes and in the filled state. A to Z is
   three bars getting shorter; popularity is a bar chart getting taller. */
.kj-ico--az {
    background:
        linear-gradient(currentColor, currentColor) 0 1px / 14px 2px no-repeat,
        linear-gradient(currentColor, currentColor) 0 6px / 10px 2px no-repeat,
        linear-gradient(currentColor, currentColor) 0 11px / 6px 2px no-repeat;
}

/* ⚠ THE TWO ORDER ICONS ARE DELIBERATELY SIMILAR, and that is the operator's call after
   seeing both. They were briefly an SVG trophy against these bars; the bars read as one
   family and the trophy did not. If they ever need separating again, separate them by
   DIRECTION and weight rather than by swapping one for a different kind of drawing. */
.kj-ico--pop {
    background:
        linear-gradient(currentColor, currentColor) 0 8px / 3px 6px no-repeat,
        linear-gradient(currentColor, currentColor) 5px 4px / 3px 10px no-repeat,
        linear-gradient(currentColor, currentColor) 10px 0 / 3px 14px no-repeat;
}

.kj-ctl__swap, .kj-ctl__filter {
    font: inherit; font-size: .88rem; padding: 7px 13px; cursor: pointer;
    border: 1px solid var(--kj-border-strong); border-radius: var(--kj-radius);
    background: var(--kj-bg); color: var(--kj-text-soft);
    display: inline-flex; align-items: center; gap: 7px;
}
.kj-ctl__swap:hover, .kj-ctl__filter:hover { border-color: var(--kj-brand); color: var(--kj-brand-ink); text-decoration: none; }
.kj-ctl__filter[aria-expanded="true"] { border-color: var(--kj-brand); color: var(--kj-brand-ink); }
.kj-ctl__n {
    background: var(--kj-brand); color: var(--kj-text-on-brand); border-radius: 999px;
    min-width: 18px; height: 18px; display: grid; place-items: center; font-size: .72rem; font-weight: 700;
}

/* ---------------------------------------------------------- the filter drawer */

.kj-filters {
    background: var(--kj-surface); border: 1px solid var(--kj-border);
    border-radius: var(--kj-radius-lg); padding: 20px 22px 16px; margin: 12px 0 18px;
}
.kj-near .kj-filters { margin: 0 22px 12px; }
.kj-filters__grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 22px 28px; }
@media (max-width: 640px) { .kj-filters__grid { grid-template-columns: 1fr; } }
.kj-filters__set h4 { margin: 0 0 8px; font-size: .88rem; }
.kj-filters__set select { width: 100%; }
.kj-filters__pair { display: flex; gap: 8px; }
.kj-filters__pair input { width: 100%; padding: 7px 10px; font-size: .88rem; }
.kj-filters__note { margin: 6px 0 0; font-size: .78rem; color: var(--kj-text-faint); }
.kj-filters__foot { display: flex; gap: 10px; margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--kj-border); }

.kj-check { display: flex; align-items: center; gap: 8px; font-size: .88rem; color: var(--kj-text-soft); margin-bottom: 4px; cursor: pointer; }
.kj-check:hover { color: var(--kj-text); }

/* ⚠ A FIELDSET, NOT A `.kj-field`, AND THE SPECIFICITY IS THE REASON. `.kj-field label`
   is (0,1,1) and sets `display: block`; a `<label class="kj-check">` inside one loses to
   it at (0,1,0), the flex row collapses and the dot stops sitting beside its text. It is
   the same trap the caret hit, recorded twice further up this file. A fieldset also tells
   a screen reader that these options are one question, which a <p> never did.

   ⚠ AND THE RESET IS REQUIRED, not tidiness: there is no global fieldset or legend rule
   in this stylesheet, so without `border: 0` the browser draws its own 2px groove around
   the group and insets the legend into it. */
.kj-radios { border: 0; margin: 0 0 16px; padding: 0; min-width: 0; }
.kj-radios__legend { padding: 0; margin-bottom: 5px; font-weight: 600; font-size: .92rem; }
.kj-radios .kj-check { margin: 0 0 2px; padding: 2px 0; font-weight: 400; font-size: .9rem; }
.kj-radios .kj-check span { flex: 1 1 auto; min-width: 0; }

.kj-chips--sub { margin: 4px 0 18px; }

/* A grid inside the /near/ left pane is narrower than the page grid, so it gets its own
   column count rather than inheriting one sized for the full width. */
.kj-grid--pane { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
@media (max-width: 1200px) { .kj-grid--pane { grid-template-columns: 1fr; } }
.kj-rows--wide .kj-row { padding: 14px; }

.screen-reader-text {
    position: absolute; width: 1px; height: 1px; overflow: hidden;
    clip: rect(1px, 1px, 1px, 1px); white-space: nowrap;
}

/* ---------------------------------------------------------- job badges

   ⚠ EVERY ONE OF THESE IS A TOKEN, NOT A HEX. A job board wants more badge colours than
   a directory did - employment type, remote, experience - and each new one is an
   invitation to reach for a literal. There is no "small enough" exception: a hardcoded
   colour is correct in exactly one theme and glaring in the other. */
.kj-badge--type   { background: var(--kj-brand-soft); color: var(--kj-brand-ink); }
.kj-badge--remote { background: var(--kj-accent-soft); color: var(--kj-accent-ink); }
.kj-badge--exp    { background: var(--kj-bg-sunk); color: var(--kj-text-soft); }

.kj-row__meta strong { color: var(--kj-text); font-weight: 600; }
.kj-card__emp { color: var(--kj-text-soft); font-size: .88rem; margin: 2px 0 4px; }

/* ---------------------------------------------------------- the slim homepage hero

   ⚠ THE HOMEPAGE IS THE JOB LIST, so the hero is a search box with a sentence over it
   and nothing else. Every row of chrome above the first listing answers a question the
   visitor did not ask - they came to see what is open. The directory this theme was
   ported from wanted the opposite shape, and inheriting it was the defect. */
.kj-hero--slim { padding: 28px 0 24px; }
.kj-hero--slim h1 { font-size: 1.9rem; margin: 0 0 6px; }
/* ⚠ KEEP THE `auto`. The base rule centres a 56ch box with `margin: 0 auto`; restating
   the margin here to change the bottom gap dropped it, so the box went hard left while the
   text inside stayed centred - which reads as a centred paragraph nudged off to one side
   rather than as an obviously broken rule. */
.kj-hero--slim .kj-hero__sub { margin: 0 auto 16px; font-size: .98rem; }
.kj-hero--slim .kj-hero__stat { margin-top: 12px; }

@media (max-width: 640px) {
    .kj-hero--slim { padding: 20px 0 16px; }
    .kj-hero--slim h1 { font-size: 1.5rem; }
}

/* ---------------------------------------------------------- the brand lockup

   ⚠ THE BADGE AND THE WORDMARK ARE ONE LINK, laid out here rather than in two places.
   The header and the footer both render `kerja_brand_lockup()`, so there is one logo on
   the site and not two that happen to agree today.

   ⚠ THE MARK'S OWN COLOURS ARE IN THE SVG, NOT HERE, and that is forced: a favicon is a
   data: URI and cannot read a custom property. The hex is declared once in PHP and
   css-check asserts it against --kj-brand, so the tab icon and the buttons cannot drift
   apart without failing a gate. */
.kj-brand { display: inline-flex; align-items: center; gap: 9px; }
.kj-brand__mark { display: block; flex: 0 0 auto; border-radius: 9px; }
.kj-brand__word { display: inline-block; line-height: 1; }

/* ⚠ THE WORDMARK USED TO BE HIDDEN AT 560px and no longer is. That rule existed because
   the header ran out of room; below md the nav AND the "For employers" CTA both move into
   the bottom bar, leaving a brand and a theme toggle. The wordmark it was sacrificing now
   fits at every width this site is used at. Deleted rather than migrated - Q1-A, Q2-A. */

/* ============================================================ the "Near me" switch
 *
 * ⚠ THE SWITCH IS THE ZERO ON THE DISTANCE SLIDER. The range starts at 1 km, not
 * 0, because a 0 km radius matches nothing and a slider that offers it offers a setting
 * whose only outcome is an empty page. "Off" is a state a control can show; a zero on a
 * track is a position a thumb happens to be in, and the two do not read the same.
 *
 * ⚠ AND THE TRACK IS PAINTED FROM THE SAME TOKENS AS EVERYTHING ELSE. A switch is
 * the easiest place in a stylesheet to reach for a literal grey, and a literal grey is
 * correct in exactly one theme.
 */
.kj-near__head {
    display: flex; align-items: center; justify-content: space-between;
    gap: 10px; margin-bottom: 6px;
}

.kj-switch { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; }

/* Off-screen rather than display:none - a hidden input is not focusable, so the switch
   would be unreachable by keyboard and invisible to a screen reader. */
.kj-switch input {
    position: absolute; width: 1px; height: 1px; margin: -1px;
    padding: 0; border: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
}

.kj-switch__track {
    position: relative; flex: 0 0 auto; width: 34px; height: 19px;
    background: var(--kj-bg-sunk); border: 1px solid var(--kj-border);
    border-radius: 999px; transition: background .15s ease, border-color .15s ease;
}

.kj-switch__track::after {
    content: ''; position: absolute; top: 2px; left: 2px;
    width: 13px; height: 13px; border-radius: 50%;
    background: var(--kj-text-faint);
    transition: transform .15s ease, background .15s ease;
}

.kj-switch input:checked + .kj-switch__track {
    background: var(--kj-brand); border-color: var(--kj-brand);
}

.kj-switch input:checked + .kj-switch__track::after {
    transform: translateX(15px); background: var(--kj-surface);
}

/* The focus ring goes on the TRACK, because the input it belongs to is off-screen. Without
   this the control is keyboard-operable and gives no sign of where the focus is. */
.kj-switch input:focus-visible + .kj-switch__track {
    outline: 2px solid var(--kj-brand); outline-offset: 2px;
}

.kj-switch__text { font-size: .88rem; font-weight: 600; color: var(--kj-text-soft); }
.kj-switch input:checked ~ .kj-switch__text { color: var(--kj-brand-ink); }

/* The locate button as an icon: a square target, not a shrunken text button. 32px is at the
   small end of what a thumb hits reliably, and it sits beside a switch that is easier to
   hit - so the icon is the one that needs the room, not the label. */
.kj-locate--icon {
    display: inline-flex; align-items: center; justify-content: center;
    width: 32px; height: 32px; padding: 0; flex: 0 0 auto;
    border-radius: var(--kj-radius); line-height: 0;
}

.kj-locate--icon svg { display: block; }
.kj-locate--icon[aria-busy="true"] { opacity: .55; cursor: progress; }

/* ⚠ THE SLIDER STAYS VISIBLE WHEN IT IS OFF, AND THAT IS DELIBERATE. Hiding it
 * would make the switch reveal a control the visitor has not seen yet, so they cannot tell
 * what turning it on is going to do. Dimmed-but-present says "this is what you get" — and
 * `disabled` on the input is what actually stops it being submitted. — see parts/facets.php.
 */
.kj-facet input[type="range"][disabled] { opacity: .45; cursor: not-allowed; }
.kj-facet input[type="range"][disabled] + * { opacity: .45; }
.kj-radius:has(+ input[type="range"][disabled]) { opacity: .55; }

/* ============================================================ facet radios + checkboxes
 *
 * ⚠ DRAWN HERE RATHER THAN LEFT TO `accent-color`, AND THE REASON IS MEASURABLE.
 * Chrome paints an accent-coloured radio as a filled disc and then chooses the INNER DOT's
 * colour from the accent's own luminance. dekat.co's #f97316 and kerja0.com's #0082ca sit on
 * opposite sides of that threshold, so the identical rule produced a crisp blue control on
 * one site and a muddy orange blob on the other - reported from a screenshot, 2026-08-29.
 *
 * The threshold is undocumented, differs between engines, and has changed between Chrome
 * versions. A brand colour picked to satisfy it would be a brand colour picked by a browser.
 * So: a ring and a dot, both from the brand token, on the surface colour. Identical on both
 * sites by construction, and it cannot drift when someone tunes a hex.
 *
 * ⚠ `appearance: none` REMOVES THE FOCUS RING TOO, which is the trap in this
 * technique - the control stays keyboard-operable and stops LOOKING it, so nobody notices.
 * :focus-visible puts it back explicitly.
 */
/* ⚠ AND THE TREE'S BOXES ARE ON THIS LIST TOO, WHICH THEY WERE NOT. The category tree
 * shipped with `accent-color: var(--kj-brand)` on its own inputs - the exact thing the
 * paragraph above exists to forbid - and walked into the exact defect it describes:
 * measured at 3x, an orange box with a BLACK tick (operator, 2026-09-01). A rule written
 * down in a comment does not reach the next control that needs it; a selector does. */
.kj-check input[type="radio"],
.kj-check input[type="checkbox"],
.kj-tree__label input[type="checkbox"] {
    appearance: none;
    -webkit-appearance: none;
    flex: 0 0 auto;
    width: 15px;
    height: 15px;
    margin: 0;
    padding: 0;
    display: inline-grid;
    place-content: center;
    border: 1.5px solid var(--kj-border-strong);
    border-radius: 50%;
    background: var(--kj-surface);
    cursor: pointer;
    transition: border-color .12s ease, background-color .12s ease;
}

.kj-check input[type="checkbox"],
.kj-tree__label input[type="checkbox"] { border-radius: 4px; }

/* The radio's mark is a dot, and it is the brand colour on the surface colour - the ring
   stays open, so there is nothing for it to contrast against but the card behind it. */
.kj-check input[type="radio"]::before {
    content: '';
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--kj-brand);
    transform: scale(0);
    transition: transform .12s ease;
}

/* ⚠ A CHECKBOX FILLS, SO ITS MARK SITS ON THE BRAND AND MUST BE THE ON-BRAND INK. That is
   what the operator asked for and it is also the only self-consistent answer: every other
   place in this stylesheet where something is painted ON the brand - the buttons, the
   sidebar header, the current page in the pager - takes --kj-text-on-brand for its ink.
   A tick is not an exception to that.

   ⚠ DRAWN WITH clip-path, NOT AN IMAGE OR A BORDER TRICK. A background-image would need a
   colour baked into a data URI, which is a hardcoded colour wearing a disguise and would be
   wrong in one of the two themes; the rotated-border trick cannot be centred reliably at
   this size. A polygon takes the token like anything else. */
.kj-check input[type="checkbox"]::before,
.kj-tree__label input[type="checkbox"]::before {
    content: '';
    width: 9px;
    height: 9px;
    background: var(--kj-text-on-brand);
    clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0, 43% 62%);
    transform: scale(0);
    transition: transform .12s ease;
}

.kj-check input:checked { border-color: var(--kj-brand); }
.kj-check input:checked::before { transform: scale(1); }

.kj-check input[type="checkbox"]:checked,
.kj-tree__label input[type="checkbox"]:checked {
    background: var(--kj-brand);
    border-color: var(--kj-brand);
}
.kj-tree__label input[type="checkbox"]:checked::before { transform: scale(1); }

/* ⚠ COVERED-BY-THE-PARENT IS A DASH, NOT A TICK, AND NOT THE BRAND. Ticking a root marks
   its children `indeterminate` because the query already matches the whole subtree - but
   the visitor did not choose those children, and painting them in the brand with the same
   mark as a real choice would say they did. A muted dash says "included, by the row above". */
.kj-tree__label input[type="checkbox"]:indeterminate {
    background: var(--kj-bg-sunk);
    border-color: var(--kj-border-strong);
}
.kj-tree__label input[type="checkbox"]:indeterminate::before {
    width: 8px;
    height: 2px;
    border-radius: 1px;
    background: var(--kj-text-faint);
    clip-path: none;
    transform: scale(1);
}

.kj-check:hover input:not(:checked) { border-color: var(--kj-brand); }
.kj-tree__label:hover input:not(:checked):not(:indeterminate) { border-color: var(--kj-brand); }

/* ⚠ `appearance: none` TOOK THE FOCUS RING WITH IT - the trap in this whole technique,
   because the control stays keyboard-operable and merely stops looking it. */
.kj-check input:focus-visible,
.kj-tree__label input:focus-visible {
    outline: 2px solid var(--kj-brand);
    outline-offset: 2px;
}

/* A row whose count is zero is dimmed, and its control has to dim with it - otherwise the
   control is the brightest thing in a row the visitor is being told is a dead end. */
.kj-check.is-empty input { opacity: .45; }

/* ============================================================ the small-screen shell

   ⚠ THIS BLOCK LIVES AT THE END OF THE FILE, AND THE POSITION IS LOAD-BEARING. It first
   sat above `.kj-facets`'s own declaration, and a media query adds NO specificity - so the
   later, unconditional `position: sticky` beat the drawer's `position: fixed` while the
   drawer's `transform: translateX(-100%)` still applied. The sidebar kept its full height
   in the grid and was painted one viewport to the left: a hole in the page where the
   filters used to be, and the results pushed down past it.

   Responsive overrides go after what they override. Inside this block the unconditional
   rules come FIRST for the same reason - `.kj-facets__close { display: none }` sitting
   after the md query is what kept the drawer's own close button hidden at every width.

   Operator rulings Q2-A and Q4-C, 2026-08-31.

       md 900   the nav becomes a bottom bar, the sidebar becomes a drawer */

/* ---- unconditional: the furniture exists at every width, hidden until md ---- */

.kj-tabbar { display: none; }

/* Set while the drawer is open, so a touch that misses the panel does not scroll the
   results underneath it. */
body.kj-noscroll { overflow: hidden; padding-right: var(--kj-sbw, 0px); }

.kj-facets__close {
    display: none;
    align-items: center; justify-content: center;
    width: 32px; height: 32px; margin-left: auto;
    border: 1px solid var(--kj-border); border-radius: 8px;
    background: transparent; color: var(--kj-text-soft);
    font-size: 1.25rem; line-height: 1; cursor: pointer;
}
.kj-facets__close:hover { color: var(--kj-text); border-color: var(--kj-border-strong); }

.kj-scrim {
    display: none;
    position: fixed; inset: 0; z-index: var(--kj-z-scrim);
    background: var(--kj-scrim);
    opacity: 0; visibility: hidden; transition: opacity .18s ease, visibility .18s ease;
}
.kj-scrim.is-on { opacity: 1; visibility: visible; }

/* The header's filter handle. Hidden at every width until the script confirms this page
   has filters to open, and hidden again above md where the sidebar is simply on the page. */
.kj-header__burger { display: none; }
.kj-header__burger svg { width: 20px; height: 20px; }

/* The drawer's handle only reads as a handle at the width where it opens one. */
.kj-ctl__burger { display: none; width: 17px; height: 17px; flex: 0 0 auto; }

/* ---- active filter chips: the drawer's state, readable with the drawer shut ----

   ⚠ THE DRAWER HIDES WHICH FILTERS ARE ON, AND THIS IS THE COST Q4-C PAYS FOR. An empty
   result set behind a closed drawer cannot be explained by anything on screen. These chips
   are the STATE; the drawer is the EDITOR. Each is a plain link that drops its own
   parameter and keeps the rest, so it needs no script and works with the drawer shut. */

.kj-active { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin: 0 0 14px; }
.kj-active:empty { display: none; }
.kj-active__label { font-size: .8rem; color: var(--kj-text-faint); margin-right: 2px; }
.kj-active__chip {
    display: inline-flex; align-items: center; gap: 7px;
    padding: 4px 8px 4px 11px; border-radius: 999px;
    border: 1px solid var(--kj-border-strong); background: var(--kj-surface);
    color: var(--kj-text); font-size: .82rem; font-weight: 500;
}
.kj-active__chip:hover { border-color: var(--kj-brand); text-decoration: none; }
.kj-active__x { font-size: 1rem; line-height: 1; color: var(--kj-text-faint); }
.kj-active__chip:hover .kj-active__x { color: var(--kj-brand-ink); }
.kj-active__clear { font-size: .8rem; font-weight: 600; }

/* ---- md and below ---- */

@media (max-width: 900px) {

    /* ⚠ ONE NAVIGATION, NOT TWO. The primary nav and the header CTA both move into the
       bottom bar, and there is deliberately no hamburger beside it: a bottom bar plus an
       overflow menu is two navigations competing for one job, and the second is always the
       one nobody finds. */
    .kj-nav,
    .kj-header__cta .kj-btn { display: none; }

    .kj-tabbar {
        display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
        position: fixed; inset: auto 0 0 0; z-index: var(--kj-z-header);
        background: var(--kj-surface); border-top: 1px solid var(--kj-border);
        padding-bottom: env(safe-area-inset-bottom, 0);
    }
    .kj-tabbar__item {
        display: flex; flex-direction: column; align-items: center; justify-content: center;
        gap: 3px; padding: 8px 4px 7px; min-height: 54px;
        color: var(--kj-text-soft); font-size: .69rem; font-weight: 600;
        text-align: center; line-height: 1.15;
    }
    .kj-tabbar__item:hover { text-decoration: none; color: var(--kj-text); }
    .kj-tabbar__item.is-on { color: var(--kj-brand-ink); }
    .kj-tabbar__item svg { width: 21px; height: 21px; flex: 0 0 auto; }
    .kj-tabbar__label {
        display: block; max-width: 100%;
        overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    }

    /* ⚠ THE BAR IS `fixed`, SO THE PAGE OWES IT PADDING. Without this the last listing
       sits under the bar permanently - reachable by scrolling somewhere the scroll cannot
       go. */
    body { padding-bottom: 62px; }

    .kj-shop { grid-template-columns: 1fr; gap: 14px; }

    /* ⚠ THE SIDEBAR IS A DRAWER HERE, NOT A PANEL STACKED ABOVE THE RESULTS. Stacked, it
       put a full screen of controls in front of the first card.

       Off-canvas rather than `display: none`, so the panel keeps its layout and its scroll
       position and a control inside it can still be focused when validation fails. */
    .kj-facets {
        position: fixed; inset: 0 auto 0 0; z-index: var(--kj-z-drawer);
        width: min(88vw, 340px); height: 100%; max-height: none;
        margin: 0; border-radius: 0; border-top: 0; border-bottom: 0; border-left: 0;
        transform: translateX(-100%); transition: transform .18s ease;
        display: flex; flex-direction: column; overflow-y: auto;
        overscroll-behavior: contain;
        box-shadow: var(--kj-shadow-lift);
    }
    /* ⚠ AND THE FROZEN CLASS HAS TO BE BEATEN EXPLICITLY. `.kj-facets.is-frozen` is
       (0,2,0); this block's `.kj-facets` is (0,1,0), and a media query adds NO
       specificity - so without this line a sidebar frozen on a wide screen would keep
       `position: sticky` after a resize into the drawer and the drawer would not open.
       site.js also drops the class below md; this is the half that does not depend on it. */
    .kj-facets.is-frozen { position: fixed; top: auto; }

    .kj-facets.is-open { transform: translateX(0); }

    /* The head stays put while the facets scroll under it - a drawer you have to scroll
       back up to close is a drawer with a hidden exit. */
    /* ⚠ REPAINT THE INK WITH THE GROUND. This changed `background` to --surface and left
       `color` at --text-on-brand, so the "Filter" heading was white on white: present,
       measurable, invisible. A colour defined in only one branch of a pair is the defect,
       every time. */
    .kj-facets__head {
        position: sticky; top: 0; z-index: var(--kj-z-pop);
        background: var(--kj-surface); color: var(--kj-text);
        border-bottom: 1px solid var(--kj-border);
        align-items: center;
    }
    .kj-facets__head h2 { display: none; }
    .kj-facets__head .kj-facets__clear { color: var(--kj-brand-ink); }

    /* The lockup the operator asked for: a panel that covers the page should say which
       site it belongs to. */
    .kj-facets__head .kj-facets__brand { display: inline-flex; align-items: center; gap: 8px; }

    /* ⚠ THE RIGHT-HAND GROUP LOSES ITS PUSH WHEN THE NAV GOES. `.kj-nav { margin-left:
       auto }` is the only thing moving this group across the header, so hiding the nav
       drops the toggle against the wordmark. The header needs to say so itself. */
    /* ⚠ ORDER MATTERS MORE THAN POSITION HERE. The burger is first in the header's
       source, so it sits left of the wordmark - where a phone user's thumb expects it and
       where the pattern the operator pointed at puts it. */
    .kj-header__burger[data-kj-has-filters] {
        display: inline-flex; align-items: center; justify-content: center;
        width: 38px; height: 38px; margin-right: 2px;
        border: 1px solid var(--kj-border); border-radius: 10px;
        background: transparent; color: var(--kj-text); cursor: pointer;
    }
    .kj-header__burger:hover { border-color: var(--kj-border-strong); }

    .kj-header__cta { margin-left: auto; }

    .kj-facets__close { display: inline-flex; }
    .kj-scrim { display: block; }

    /* The drawer's handle. Above md the sidebar is simply there and needs none. */
    .kj-ctl__filter { display: inline-flex; }
    .kj-ctl__burger { display: inline-block; }
}

@media (prefers-reduced-motion: reduce) {
    .kj-facets, .kj-scrim { transition: none; }
}

/* ── the account control, header and footer ──────────────────────────────────────────────
   ⚠ ONE BLOCK FOR BOTH PLACEMENTS. `data-where` is on the element so a future difference
   has somewhere to hang, but there is no difference today and inventing one would be two
   things to keep in step for no reason. */
.kj-acct {
    display: inline-flex;
    align-items: center;
    gap: 8px;
}

.kj-acct--out {
    color: var(--kj-text-soft);
    text-decoration: none;
    font-weight: 600;
    font-size: 14px;
    white-space: nowrap;
}

.kj-acct--out:hover,
.kj-acct__me:hover { color: var(--kj-brand-ink); }

.kj-acct__me {
    color: var(--kj-text);
    text-decoration: none;
    font-weight: 600;
    font-size: 14px;
    max-width: 16ch;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.kj-acct__out { display: inline; margin: 0; }

/* ⚠ A BUTTON THAT LOOKS LIKE A LINK, BECAUSE IT HAS TO BE A BUTTON. Sign-out changes
   state, so it POSTs with a nonce rather than being an <a> - a GET that ends a session is a
   CSRF away from being ended by anybody's <img> tag. The styling is what makes that
   invisible to the person, which is the point. */
.kj-acct__btn {
    border: 0;
    background: none;
    padding: 0;
    cursor: pointer;
    font: inherit;
    font-size: 13px;
    color: var(--kj-text-soft);
    text-decoration: underline;
}

.kj-acct__btn:hover { color: var(--kj-brand-ink); }

/* ── the member area ─────────────────────────────────────────────────────────────────── */
.kj-account__roles { display: flex; gap: 6px; flex-wrap: wrap; margin: 0 0 20px; }

.kj-account__empty { color: var(--kj-text-soft); }

.kj-account__list { list-style: none; margin: 0 0 28px; padding: 0; }

/* ⚠ GRID, NOT A TABLE, AND NOT FLOAT. Three columns whose widths are decided by the
   longest title rather than by a number this file guesses - and it collapses to one column
   under 560px without a media query per element. */
.kj-account__row {
    display: grid;
    grid-template-columns: 1fr auto auto;
    align-items: baseline;
    gap: 12px;
    padding: 10px 0;
    border-bottom: 1px solid var(--kj-border);
}

.kj-account__row:last-child { border-bottom: 0; }

.kj-account__title { font-weight: 600; min-width: 0; }
.kj-account__title a { text-decoration: none; }
.kj-account__title a:hover { text-decoration: underline; }

/* ⚠ A STATUS IS A WORD, NOT A COLOUR ALONE. Somebody who cannot tell the two greys apart
   still reads "Waiting for review" - the tint is a second channel, never the only one. */
.kj-account__sts {
    font-size: 12px;
    padding: 2px 8px;
    border-radius: 999px;
    background: var(--kj-bg-soft);
    color: var(--kj-text-soft);
    white-space: nowrap;
}

.kj-account__when { font-size: 12px; color: var(--kj-text-soft); white-space: nowrap; }

.kj-account__out { margin-top: 28px; }

/* 640, not 560: the scale is 640 / 900 / 1200 and a fourth breakpoint for one
   rule is a scale nobody can hold in their head. */
@media (max-width: 640px) {
    .kj-account__row { grid-template-columns: 1fr; gap: 4px; }
}

/* ============================================================ the A-Z index

   /categories/ and /locations/ — a SITEMAP, not a directory (operator, 2026-09-01).
   Every node once, alphabetically, with its subtree count. The visual job is scanning:
   a reader is looking for one name in two hundred, so the list is dense, multi-column,
   and the count sits right-aligned where the eye can run down it. */

.kj-az__jump {
    display: flex; flex-wrap: wrap; gap: 4px;
    margin: 0 0 28px; padding: 10px 12px;
    background: var(--kj-surface);
    border: 1px solid var(--kj-border); border-radius: 12px;

    /* ⚠ STICKY, BECAUSE THE PAGE IS LONG. 215 locations is several screens; a jump strip
       that scrolls away is one a reader has to scroll back up to use, which is the same as
       not having one. It sits under the header, whose own z-index it must not beat. */
    /* --*-z-pane is the scale's name for "a toolbar sticking inside a scrolling
       pane" - which is what this is. A bare 3 sits outside the scale entirely, so
       the next person to add a layer cannot tell what it has to clear. */
    position: sticky; top: 62px; z-index: var(--kj-z-pane);
}
.kj-az__jump a,
.kj-az__jump span {
    display: grid; place-items: center;
    min-width: 30px; height: 30px; padding: 0 6px;
    border-radius: 8px; font-weight: 600; font-size: .9rem; text-decoration: none;
}
.kj-az__jump a { color: var(--kj-brand-ink); }
.kj-az__jump a:hover { background: var(--kj-bg-soft); text-decoration: none; }

/* ⚠ A LETTER WITH NOTHING UNDER IT IS SHOWN, NOT HIDDEN, and is not a link. The strip then
   keeps one shape between the two pages and as the catalogue grows, so the eye does not
   have to re-find it — and an anchor to an empty section is a link that does nothing. */
.kj-az__jump span { color: var(--kj-text-soft); opacity: .38; }

.kj-az__group { margin: 0 0 30px; scroll-margin-top: 110px; }

.kj-az__letter {
    margin: 0 0 10px; font-size: 1.05rem; color: var(--kj-text-soft);
    border-bottom: 1px solid var(--kj-border); padding-bottom: 6px;
}

/* ⚠ COLUMNS, NOT A GRID. `columns` fills top-to-bottom then wraps, so the names stay in
   reading order down each column; a grid would order them across, and an A-Z list read
   left-to-right is one nobody can scan. */
.kj-az__list {
    list-style: none; margin: 0; padding: 0;
    columns: 3; column-gap: 28px;
}
.kj-az__item { break-inside: avoid; margin: 0 0 2px; }
.kj-az__item a {
    display: flex; align-items: baseline; gap: 10px;
    padding: 5px 8px; border-radius: 8px; color: var(--kj-text); text-decoration: none;
}
.kj-az__item a:hover { background: var(--kj-bg-soft); color: var(--kj-brand-ink); text-decoration: none; }
.kj-az__name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The count is secondary — tabular so the digits line up down the column. */
.kj-az__n {
    flex: 0 0 auto; font-size: .82rem; color: var(--kj-text-soft);
    font-variant-numeric: tabular-nums;
}

/* A root category or a state, as against a town or a child category. Weight only — a second
   colour here would compete with the link colour for the same meaning. */
.kj-az__item.is-top .kj-az__name { font-weight: 600; }

.kj-az__other { margin: 8px 0 0; }

@media (max-width: 900px) { .kj-az__list { columns: 2; } }
@media (max-width: 640px) {
    .kj-az__list { columns: 1; }
    .kj-az__jump { position: static; }
}

/* ⚠ THE FOOTER'S SIGN-IN IS A LIST ITEM NOW, NOT A STRAGGLER AFTER `.kj-wrap`. It used to
   render outside the columns entirely, which put a lone "Sign in" in the bottom-left corner
   under the copyright with no heading over it (operator, 2026-09-01). The control is the
   same renderer the header uses, so these rules only have to undo its button chrome. */
.kj-footer__acct .kj-acct--out,
.kj-footer__acct .kj-acct__me {
    display: inline; padding: 0; border: 0; background: none;
    font: inherit; color: var(--kj-brand-ink);
}
.kj-footer__acct .kj-acct--in { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px; }
.kj-footer__acct .kj-acct__btn {
    padding: 0; border: 0; background: none; cursor: pointer;
    font: inherit; font-size: .86rem; color: var(--kj-text-soft); text-decoration: underline;
}
.kj-footer__acct .kj-acct__btn:hover { color: var(--kj-brand-ink); }

/* ---------------------------------------------------------- the scope hero

   ⚠ A SCOPE PAGE OPENS THE SAME WAY THE HOMEPAGE DOES. /find/{category} is the homepage
   with a scope, so it gets the homepage's band: the heading centred over the search box
   rather than left-aligned beside it, and the band's own border-bottom as the rule between
   the search half of the page and the results half. Measured before this existed: the
   search box bottom and the sidebar top were both at y=290, touching exactly. */
.kj-hero--scope { padding: 22px 0 20px; }

/* A category name is a noun, not a promise - it does not need the homepage's display size,
   and shrinking it keeps the band shallow enough that the first result stays near the fold. */
.kj-hero--scope h1 { font-size: 1.6rem; margin: 0 0 14px; }

@media (max-width: 640px) {
    .kj-hero--scope { padding: 16px 0 14px; }
    .kj-hero--scope h1 { font-size: 1.3rem; margin-bottom: 10px; }
}

/* ⚠ THE CRUMBS IN THEIR NEW HOME NEED THEIR OLD MARGIN GONE. The base rule opens with
   `margin: 18px 0 8px` because it used to be the first thing under the header; at the top
   of the results column that 18px is a gap between the column and its own first row. */
.kj-crumbs--results { margin: 0 0 12px; }


/* ---------------------------------------------------------- the age stamp

   ⚠ MUTED, AND NEVER THE LOUDEST THING ON THE LINE. Freshness is a tiebreaker a reader
   consults after the title, the employer and the salary have already been read; at the
   same weight as the rest of the meta line it competes with all three. Tabular numerals so
   a column of "3d ago" does not jitter. */
.kj-age {
    color: var(--kj-text-faint);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.kj-card__foot .kj-age { margin-left: auto; font-size: .78rem; }

/* ---------------------------------------------------------- the pager

   A numbered pager plus infinite scroll - operator ruling 2026-09-01. The pager is the real
   navigation and stays visible throughout; the scroll handler only keeps it in step. */

.kj-pager {
    display: flex; align-items: center; justify-content: center;
    flex-wrap: wrap; gap: 6px; margin-top: 26px;
}

.kj-pager__n, .kj-pager__step {
    display: inline-flex; align-items: center; justify-content: center; gap: 4px;
    min-width: 38px; height: 38px; padding: 0 10px;
    border: 1px solid var(--kj-border); border-radius: var(--kj-radius);
    background: var(--kj-surface); color: var(--kj-text);
    font-size: .92rem; font-weight: 600; font-variant-numeric: tabular-nums;
    text-decoration: none;
}

.kj-pager__n:hover, .kj-pager__step:hover {
    border-color: var(--kj-brand); color: var(--kj-brand-ink);
}

/* ⚠ THE CURRENT PAGE IS A <span>, SO IT NEEDS ITS OWN GROUND RATHER THAN A LINK STATE.
   It is not a link - navigating to the page you are on is a control that does nothing -
   and `aria-current` is what says so to a screen reader. The fill is what says it to
   everybody else, and it takes the on-brand ink because a brand fill under body text is the
   contrast defect this estate has already shipped once. */
.kj-pager__n.is-on {
    background: var(--kj-brand); border-color: var(--kj-brand);
    color: var(--kj-text-on-brand); cursor: default;
}

.kj-pager__gap { padding: 0 2px; color: var(--kj-text-faint); }

/* ⚠ THE SENTINEL IS NOT `display: none`. An IntersectionObserver never fires on a box
   with no layout, so hiding it would silently disable infinite scroll and leave a pager
   that works - which is the failure mode nobody reports because the page still functions. */
.kj-more {
    min-height: 1px; margin-top: 18px;
    display: flex; align-items: center; justify-content: center;
}

.kj-more__status { margin: 0; color: var(--kj-text-soft); font-size: .9rem; }

.kj-more.is-busy .kj-more__status::before {
    content: ''; display: inline-block; width: 14px; height: 14px; margin-right: 8px;
    border: 2px solid var(--kj-border); border-top-color: var(--kj-brand);
    border-radius: 50%; vertical-align: -2px;
    animation: kj-spin .7s linear infinite;
}

@keyframes kj-spin { to { transform: rotate(360deg); } }

@media (prefers-reduced-motion: reduce) {
    .kj-more.is-busy .kj-more__status::before { animation: none; }
}

/* ---------------------------------------------------------- the sort menu

   A <select> with a menu drawn over it - operator ruling 2026-09-01. The wrapper carries
   the stacking context and the panel sits inside it, which is the same shape `.kj-pick`
   already uses two sections up; a popup that invents its own layering is how one of them
   ends up under the map. */

.kj-sort { position: relative; z-index: var(--kj-z-chrome); }

/* ⚠ `display: none`, NOT VISUALLY HIDDEN. A visually-hidden <select> is still in the tab
   order, so a keyboard user would meet the real control and its replacement one after the
   other and change the sort twice. A `display: none` control is still SUBMITTED - only
   `disabled` takes a field off the wire - so the form is unaffected. */
.kj-sort.is-enhanced .kj-sort__native { display: none; }

.kj-sort__btn {
    display: inline-flex; align-items: center; gap: 7px;
    height: 32px; padding: 0 10px;
    background: var(--kj-surface); color: var(--kj-text);
    border: 1px solid var(--kj-border-strong); border-radius: var(--kj-radius);
    font: inherit; font-size: .88rem; cursor: pointer;
}
.kj-sort__btn:hover { border-color: var(--kj-brand); color: var(--kj-brand-ink); }
.kj-sort__ico { flex: 0 0 auto; color: var(--kj-text-faint); }
.kj-sort__btn:hover .kj-sort__ico { color: var(--kj-brand-ink); }
.kj-sort__now { white-space: nowrap; }
.kj-sort__caret { flex: 0 0 auto; color: var(--kj-text-faint); transition: transform .14s ease; }
.kj-sort__btn[aria-expanded="true"] .kj-sort__caret { transform: rotate(180deg); }

.kj-sort__menu {
    position: absolute; top: calc(100% + 6px); right: 0; z-index: var(--kj-z-pop);
    min-width: 210px; margin: 0; padding: 5px; list-style: none;
    background: var(--kj-surface); border: 1px solid var(--kj-border);
    border-radius: var(--kj-radius); box-shadow: var(--kj-shadow-lift); text-align: left;
}

.kj-sort__opt {
    display: flex; align-items: center; gap: 9px;
    padding: 7px 9px; border-radius: 6px; cursor: pointer;
    font-size: .9rem; color: var(--kj-text); white-space: nowrap;
}
.kj-sort__opt:hover, .kj-sort__opt:focus { background: var(--kj-bg-sunk); outline: none; }
.kj-sort__opt:focus-visible { box-shadow: inset 0 0 0 2px var(--kj-brand); }

/* ⚠ THE CHOSEN OPTION IS MARKED BY INK AND WEIGHT, NOT BY A FILL. A filled row in an open
   menu reads as the row under the pointer - which is what `:hover` already means - so two
   rows would look equally "current" and neither would be. */
.kj-sort__opt.is-on { color: var(--kj-brand-ink); font-weight: 600; }
.kj-sort__opt.is-on .kj-sort__ico { color: var(--kj-brand-ink); }

/* ---------------------------------------------------------- the card, at rest and under
   the pointer

   Operator ruling 2026-09-01: *"can we have a hover+glow outline effect? so the normal
   state is more muted than the hover state? also the cursor should be a pointer on the
   cards"* - for every card, including the /near/ column.

   ⚠ THIS BLOCK OWNS ALL THREE STATES, AND THAT IS THE POINT OF IT. Resting, featured and
   hover were three rules six hundred lines apart, and two of them collided: `.kj-row:hover`
   and `.kj-row.is-featured` are BOTH (0,2,0), and the featured rule came later - so a
   featured row's border never changed under the pointer, on the one card most likely to be
   pointed at. Specificity ties are decided by document order, which is not a decision
   anybody made.

   ⚠ THE FEATURED OUTLINE IS A PER-SITE TOKEN, NOT A PER-SITE RULE. the BRAND BLUE - operator ruling 2026-09-01. It used to be --kj-accent, which is byte-identical to dekat.co's amber because the whole accent family was copied across and never re-derived, so every featured job on a blue site was outlined in an orange nobody had chosen. One
   rule in both stylesheets, one value each, declared where the other per-site colours live. */

.kj-card, .kj-row {
    /* ⚠ `position: relative` IS LOAD-BEARING, NOT TIDINESS - it is what the stretched
       link below is positioned against. */
    position: relative;
    cursor: pointer;
    transition: border-color .16s ease, box-shadow .18s ease, transform .18s ease;
}

.kj-card.is-featured, .kj-row.is-featured { border-color: var(--kj-featured-line); }

/* ⚠ `:focus-within` RIDES WITH `:hover`, or the card lights up for a mouse and stays dead
   for a keyboard - and the keyboard user is the one who cannot see where the pointer is.
   The `.is-featured` selectors are spelled out so this wins on specificity rather than on
   being further down the file, which is the tie that caused the bug above. */
.kj-card:hover, .kj-card:focus-within,
.kj-card.is-featured:hover, .kj-card.is-featured:focus-within,
.kj-row:hover, .kj-row:focus-within,
.kj-row.is-featured:hover, .kj-row.is-featured:focus-within {
    border-color: var(--kj-brand);
    box-shadow: 0 0 0 3px var(--kj-glow), var(--kj-shadow-lift);
    transform: translateY(-2px);
}

/* ⚠ THE WHOLE CARD IS THE LINK, WHICH IS WHAT MAKES THE POINTER HONEST. A `cursor:
   pointer` over a card whose only link is its title tells the visitor something that is
   not true, and they find out by clicking and having nothing happen. This stretches the
   title's own anchor over the card instead of adding a second one: one link per card, so
   a screen reader announces one destination, and middle-click and open-in-new-tab work.

   ⚠ THE COST IS TEXT SELECTION. The overlay sits above the text, so a card's title and
   salary can no longer be dragged over and copied. That is the standing trade of this
   pattern; it is worth it here because these cards exist to be opened. */
.kj-card__title a::after, .kj-row__title a::after {
    content: ''; position: absolute; inset: 0; z-index: var(--kj-z-pop);
}

/* Anything that is itself interactive has to sit ABOVE that overlay or it stops working. */
.kj-row__call, .kj-card__foot a, .kj-row__tags a {
    position: relative; z-index: var(--kj-z-above);
}

@media (prefers-reduced-motion: reduce) {
    .kj-card, .kj-row { transition: none; }
    .kj-card:hover, .kj-card:focus-within,
    .kj-row:hover, .kj-row:focus-within { transform: none; }
}

/* ============================================================ the category strip

   A dense band of top-level categories with their counts, mirroring dekat.co's - operator
   ruling 2026-09-01. A jobseeker who arrives with no term in mind browses by field, and
   until now kerja0's homepage offered no way to do that above the fold.

   ⚠ IT DOES NOT SCROLL SIDEWAYS AT ANY WIDTH (dekat's Q3-A, 2026-08-31, applies here for
   the same reason it applied there): a horizontal scroller hides how many categories there
   are and, on a trackpad, hides that there is anything to scroll at all. Two wrapped rows
   show enough to read the shape of the catalogue.

   ⚠ AND THE CLAMP IS ADDED BY SCRIPT, NOT DECLARED HERE. If the clamp were the default, a
   visitor with no JavaScript would get `overflow: hidden` and no way to reach the rest. The
   list ships fully expanded; site.js measures it and clamps only when it ACTUALLY overflows,
   so on a desktop where every chip fits on one row no toggle ever appears. */

.kj-catstrip {
    background: var(--kj-surface); border-bottom: 1px solid var(--kj-border);
    padding: 10px 0;
}
.kj-chips--compact { gap: 6px; flex-wrap: wrap; margin: 0; }
.kj-chips--compact .kj-chip {
    padding: 5px 11px; font-size: .84rem; border-radius: 999px; gap: 6px;
}
.kj-chips--compact .kj-chip__n { font-size: .74rem; opacity: .75; }

.kj-catstrip__more {
    display: none;
    margin: 8px 0 0; padding: 5px 12px;
    border: 1px solid var(--kj-border); border-radius: 999px;
    background: transparent; color: var(--kj-text-soft);
    font-size: .82rem; font-weight: 600; cursor: pointer;
}
.kj-catstrip__more:hover { color: var(--kj-text); border-color: var(--kj-border-strong); }
.kj-catstrip.is-clamped .kj-catstrip__more { display: inline-flex; }

.kj-catstrip.is-clamped .kj-chips--compact {
    /* Two rows. Expressed in the chip's own metrics rather than a pixel guess, so it
       tracks the root font size instead of breaking when someone enlarges type. */
    --kj-chip-row: 2rem;
    max-height: calc(var(--kj-chip-row) * 2 + 6px);
    overflow: hidden;
}
.kj-catstrip.is-clamped.is-open .kj-chips--compact { max-height: none; }

/* ============================================================ the category tree

   A checkable, collapsible, two-level tree - operator ruling Q1-A/Q3-B, 2026-09-01. Every
   category is present including the empty ones, which is only bearable because the branches
   fold; the tree is scrolled rather than paged so a long catalogue does not push Apply off
   the screen. */

.kj-tree, .kj-tree__kids { list-style: none; margin: 0; padding: 0; }

/* ⚠ NO CAP HERE EITHER, AND THIS IS THE HALF THE OPERATOR ACTUALLY POINTED AT: *"the
   filter's content is being hidden when overflow... no scrollbar even for the category
   section too"*. A 380px window over 123 categories hid the ones below it behind a
   scrollbar the sidebar's own hidden-gutter rule had already made invisible - so a branch
   opened at the bottom of the list simply vanished. The tree is now as tall as it is, and
   the sidebar it sits in is as tall as the tree. */
.kj-tree { overscroll-behavior: contain; }

.kj-tree__row { display: flex; align-items: center; gap: 0; }

/* ⚠ THE ROW HEIGHT IS THE HOUSE FACET DENSITY, NOT A TASTE CALL. Every other facet row
   in this sidebar is `.kj-check`: .88rem over line-height 1.35 with 2px of vertical
   padding. The tree shipped at .93rem over the inherited 1.6 with 4px, which is 31px a row
   against their 23 - so a hundred-odd categories read as loose beside the checkboxes
   directly above them. Matching the number they already use is what makes it look
   deliberate rather than merely smaller. */
.kj-tree__toggle {
    flex: 0 0 auto; width: 18px; height: 20px; padding: 0;
    display: grid; place-items: center;
    background: none; border: 0; cursor: pointer; color: var(--kj-text-faint);
    border-radius: 4px;
}
.kj-tree__toggle:hover { color: var(--kj-brand-ink); background: var(--kj-bg-soft); }
.kj-tree__toggle svg { transition: transform .14s ease; }
.kj-tree__toggle[aria-expanded="true"] svg { transform: rotate(90deg); }

/* A leaf root still needs the indent, or its label starts 18px left of its siblings. */
.kj-tree__toggle--none { cursor: default; }

.kj-tree__label {
    display: flex; align-items: center; gap: 7px; flex: 1 1 auto; min-width: 0;
    padding: 2px 5px; border-radius: 5px; cursor: pointer; line-height: 1.35;
}
.kj-tree__label:hover { background: var(--kj-bg-soft); }
/* ⚠ SIZE AND APPEARANCE COME FROM THE DRAWN-CONTROL BLOCK, NOT FROM HERE. This rule used
   to set `accent-color` and a 14px box, which is what left the tree with a browser-painted
   tick while every other control on the page was hand-drawn. One definition, further down. */

.kj-tree__name {
    flex: 1 1 auto; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    font-size: .88rem; line-height: 1.35;
}
.kj-tree__n {
    flex: 0 0 auto; font-size: .74rem; color: var(--kj-text-faint);
    font-variant-numeric: tabular-nums; font-style: normal;
}

/* A root reads as a heading without being one - weight only, so it does not compete with
   the facet's own <h3>. */
.kj-tree__branch > .kj-tree__row .kj-tree__name { font-weight: 600; }

/* ⚠ THE CHILDREN ARE HIDDEN BY DEFAULT AND REVEALED BY A CLASS, NOT BY <details>. A nested
   <details> inside the facet's own <details> collapses BOTH when the outer one is toggled,
   and the browser's built-in marker fights the chevron. */
.kj-tree__kids { display: none; margin-left: 18px; }
.kj-tree__branch.is-open > .kj-tree__kids { display: block; }

/* ⚠ A CHILD TICKED BY ITS PARENT IS SHOWN AS IMPLIED, NOT AS CHOSEN. The query already
   matches the whole subtree, so ticking a parent covers its children - but leaving the
   child boxes blank makes it look as though they were excluded. Dimmed and indeterminate
   says "covered by the row above" without claiming the visitor picked it. */
.kj-tree__kids input:indeterminate + .kj-tree__name { color: var(--kj-text-soft); }

/* The tree wants the width the other facets spend on their side padding, because a category
   name is long and the count sits on the same line. */
.kj-facet--tree .kj-facet__body { padding: 0 10px 10px; }

/* ⚠ AND IT GOES BACK TO A COMFORTABLE SIZE IN THE DRAWER. Below 900px the sidebar is a
   full-screen drawer driven by thumbs, where a 23px row is under the 24px minimum target
   size and the density buys nothing - there is no adjacent column competing for the space.
   Compactness is a desktop-sidebar concern only. */
@media (max-width: 900px) {
    .kj-tree__toggle { width: 26px; height: 30px; }
    .kj-tree__label { padding: 5px 6px; gap: 9px; }
    .kj-tree__name { font-size: .95rem; }
    .kj-tree__kids { margin-left: 26px; }
}
