/* ════════════════════════════════════════════════════════════
   MY JOBS — page-specific styling

   Loaded after /style.css, which supplies every token, the fixed nav and
   the card chrome. Nothing here redefines a colour, a font or a spacing
   scale: the page is meant to read as part of the portfolio, not as a tool
   bolted to the side of it (§24, §32).

   The job card deliberately reuses .pcard's language — 1px border, 3px
   cyan left edge, the rim light on hover — so the grid looks like the
   projects grid a visitor has already seen.
   ════════════════════════════════════════════════════════════ */

.page-jobs {
  /* Clear of the fixed header, matching the offset the home page's anchors
     use. --nav-h is published by script.js there; this page does not load
     it, so the measured height is the value rather than the fallback. */
  padding-top: calc(73px + 40px);
  padding-bottom: 100px;
}

/* The same shell the CV generator uses, so the two tools on this site are
   laid out the same way rather than each inventing a width. */
.jobs-wrap {
  max-width: 1600px;
  margin: 0 auto;
  padding: 0 clamp(16px, 2.5vw, 28px);
}

/* ══ TWO COLUMNS ══
   Controls on the left, results on the right. The controls are a fixed
   measure — they do not get better with more room — and every pixel
   beyond that belongs to the grid, which at this width fits three cards
   across instead of two. */
.jobs-layout {
  display: grid;
  grid-template-columns: minmax(340px, 420px) 1fr;
  gap: 28px;
  align-items: start;
}

/* The controls follow the reader down a results list that no longer ends.
   `max-height` with its own scroll is what makes that safe: a sticky
   element taller than the viewport strands its own bottom off-screen,
   and the filters live at the bottom of this one. */
.jobs-side {
  position: sticky;
  top: calc(73px + 16px);
  max-height: calc(100vh - 73px - 32px);
  overflow-y: auto;
  overscroll-behavior: contain;
  /* The stats strip and the filter rows size themselves against this
     column, not the viewport — at 1400px wide the page is wide and this
     column is still 400px, and viewport media queries cannot tell the
     difference. */
  container-type: inline-size;
}

.jobs-main {
  min-width: 0;
}

/* One column below the width where a sidebar stops paying for itself —
   the same breakpoint the CV generator uses. Sticky is released with it:
   pinned at the top of a phone it would cover the results. */
@media (max-width: 1000px) {
  .jobs-layout {
    grid-template-columns: 1fr;
    gap: 22px;
  }

  .jobs-side {
    position: static;
    max-height: none;
    overflow: visible;
  }
}

/* ══ HEADER ══
   The page opens with one claim, one heartbeat and five figures. The long
   explanation that used to sit here is still on the page, folded into a
   <details> below the counters — it is the page's honesty, and it should
   be one click away rather than a wall of text in front of the results. */
.jobs-head {
  margin-bottom: 26px;
}

/* ── The hero panel ──
   The claim, the heartbeat and the counters on one surface. The glow sits
   behind the top-left corner, where the eye starts, and fades out well
   before the counters so it never competes with a figure. */
.jobs-hero {
  /* The portfolio's global `section` rule is 110px/64px of page gutter.
     This panel became a <section> when it absorbed the controls, and
     inherited it: 130px vanished from a 420px column and every row inside
     shrank to fit. .jobs-section neutralises the same rule a few hundred
     lines below for exactly this reason. */
  padding: 0;
  position: relative;
  border: 1px solid var(--border);
  border-radius: 18px;
  overflow: hidden;
  background:
    radial-gradient(90% 120% at 0% 0%,
      color-mix(in srgb, var(--cyan) 10%, transparent) 0%, transparent 55%),
    linear-gradient(170deg,
      color-mix(in srgb, var(--surface2) 70%, var(--surface)) 0%, var(--surface) 62%);
}

/* The same hairline the job cards carry, so the page reads as one set of
   surfaces rather than a panel and some cards. */
.jobs-hero::before {
  content: '';
  position: absolute;
  inset: 0 0 auto;
  height: 2px;
  background: linear-gradient(to right,
    var(--cyan), color-mix(in srgb, var(--cyan) 25%, transparent) 55%, transparent);
}

/* One line, sized against the sidebar it sits in — not the viewport,
   which on a 1600px screen set 56px type inside a 360px column. This is
   the matched-roles headline only: it is a fixed phrase. A job title,
   which the apply page puts in the same element, has to wrap. */
.jobs-hero .jobs-title {
  font-size: clamp(1.5rem, 7.5cqi, 2.9rem);
  white-space: nowrap;
}

.jobs-hero-top {
  /* Tight under the status box: it and the counters are one readout, and
     the air between them was reading as a section break. */
  padding: clamp(24px, 4vw, 38px) clamp(20px, 4vw, 40px) clamp(10px, 1.4vw, 14px);
}

/* One line, at every width this page is read at.

   The size is driven by the viewport rather than fixed, so the headline
   grows into a wide screen and shrinks to fit a narrow one instead of
   breaking. There is no max-width and no `balance` here on purpose: both
   were added to tidy a wrap that should not happen at all, and together
   they turned one line into three. */
.jobs-title {
  font-family: 'Bebas Neue', sans-serif;
  /* Sized against the column it sits in, not the viewport. The hero is
     in a 340-420px sidebar on a screen that may be 1600px wide, so a vw
     size here set 56px type in a 360px box and the headline was clipped
     by the panel it lives in. cqi reads the container instead. */
  letter-spacing: 1px;
  line-height: 1.02;
  margin: 14px 0 18px;
  color: var(--text);
  /* The accent reads as light falling across the words rather than as a
     second colour. It stops well before the end so the line still ends in
     the body colour and stays legible against either theme. */
  background: linear-gradient(100deg,
    var(--text) 0%, var(--text) 42%, color-mix(in srgb, var(--cyan) 85%, var(--text)) 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* A gradient that resolves to transparent text is a blank heading if the
   clip is unsupported, so the colour is restored where it is. */
@supports not ((-webkit-background-clip: text) or (background-clip: text)) {
  .jobs-title { color: var(--text); background: none; }
}

/* ── The heartbeat ──
   One line, fixed height: state, then when the last pass finished. The
   contents change every few seconds, and a box that resizes itself each
   time reads as a fault rather than as a status, so the height is set and
   the text inside it changes without moving it. */
.jobs-freshness {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  min-height: 42px;
  margin: 0;
  padding: 10px 16px;
  border: 1px solid color-mix(in srgb, var(--cyan) 28%, transparent);
  border-radius: 10px;
  background: var(--cyan-dim);
  font-family: 'JetBrains Mono', monospace;
  font-size: .72rem;
  letter-spacing: .4px;
  color: var(--cyan);
  white-space: nowrap;
  overflow: hidden;
}

/* The lamp is always lit — it says the page is connected. Scanning is
   what makes it blink, so the difference between "watching" and "working"
   is the motion, not the presence. */
.jobs-pulse {
  flex-shrink: 0;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--cyan);
}

.jobs-freshness.is-scanning .jobs-pulse {
  animation: jobsPulse 1.1s steps(1, end) infinite;
}

/* steps(1) rather than a fade: an on/off blink reads as activity, where a
   slow pulse reads as decoration. */
@keyframes jobsPulse {
  0%, 49%   { opacity: 1; box-shadow: 0 0 6px var(--cyan-glow); }
  50%, 100% { opacity: .2; box-shadow: none; }
}

.jobs-status-state {
  flex-shrink: 0;
  color: var(--cyan);
}

/* Tabular figures so the clock does not shuffle the line sideways on
   every tick. */
.jobs-status-sync {
  min-width: 0;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
  overflow: hidden;
  text-overflow: ellipsis;
}


/* ── Live counters ──
   auto-fit so the row reflows to two or three across on a phone instead
   of shrinking five columns until the figures collide. */
/* Five separate tiles rather than one divided strip. The strip was drawn
   by letting a 1px grid gap show the container's background through, which
   is neat until the count and the column count disagree: at two columns
   the fifth tile leaves a sixth cell empty, and that empty cell rendered
   as a solid block of divider colour — a phantom tile. Tiles that own
   their own border cannot produce one, at any width. */
/* A strip across the foot of the hero. The 1px gap shows the panel's
   border colour through as the dividers, which costs no extra borders —
   but only while every row is full. Five tiles and an auto-fit column
   count disagree constantly, and a half-empty row renders its empty cell
   as a solid block of divider colour: a phantom tile.

   So the columns are stated per breakpoint and the odd tile is told to
   span the gap it would otherwise leave. Every row fills at every width,
   which is the only way this technique is safe. */
/* ── Live counters ──
   Separate tiles rather than one divided strip. The strip was drawn by
   letting a 1px grid gap show the panel's border through, which is neat
   until the tile count and the column count disagree — five tiles in two
   columns leaves an empty cell that renders as a solid block of divider
   colour. Tiles that own their border cannot produce one at any width,
   and the gap between them is what the eye reads as separation anyway.

   Centred, because every value here is a bare number: a figure set left
   under a label of a different length looks misaligned at a glance even
   when it is not. */
.jobs-stats {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px;
  margin: 0;
  padding: 14px clamp(20px, 4vw, 40px) 18px;
}

/* Five tiles into two columns leaves the last one alone on its row, so
   it takes the whole row instead of sitting half-width beside nothing.
   It is the one you can open, and the extra width gives the chevron
   somewhere to be. */
.jstat:last-child {
  grid-column: 1 / -1;
}

.jstat {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 7px;
  padding: 14px 12px 13px;
  text-align: center;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: linear-gradient(160deg,
    color-mix(in srgb, var(--surface2) 65%, var(--surface)) 0%, var(--surface) 72%);
  transition: border-color var(--dur-base) var(--ease-out);
}

.jstat dt {
  order: 2;
  margin: 0;
  font-family: 'JetBrains Mono', monospace;
  font-size: .52rem;
  letter-spacing: 1.3px;
  text-transform: uppercase;
  color: var(--text-dim);
}

/* The figure above its label. A number read before its name is one you
   can scan a column of; read after it, it is a sentence. */
.jstat dd {
  order: 1;
  margin: 0;
  font-family: 'Bebas Neue', sans-serif;
  font-size: 1.9rem;
  line-height: .9;
  letter-spacing: .5px;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

/* The two figures the page exists to produce get the accent; the rest are
   context. */
#stat-matched { color: var(--cyan); }
#stat-checked { color: var(--green); }

/* Three and then five across as the panel earns the room — which happens
   when the layout collapses and the sidebar becomes the full page width. */
/* Six columns to get three tiles across, because the second row has two
   and they have to split it evenly.

   This was three columns with the fourth tile spanning two of them, which
   left the bottom row as two-thirds and one-third — one tile visibly
   wider than the tile beside it. Three columns cannot be halved; six can
   be, so the top row takes two each and the bottom row three each. */
@container (min-width: 520px) {
  .jobs-stats { grid-template-columns: repeat(6, minmax(0, 1fr)); }
  .jstat { grid-column: span 2; }
  .jstat:nth-child(4),
  .jstat:last-child { grid-column: span 3; }
}

@container (min-width: 820px) {
  .jobs-stats { grid-template-columns: repeat(5, minmax(0, 1fr)); }
  .jstat,
  .jstat:nth-child(4),
  .jstat:last-child { grid-column: auto; }
}

/* ── The one tile you can open ── */
.jstat-open {
  cursor: pointer;
}

.jstat-open:hover,
.jstat-open:has(.jstat-trigger:focus-visible) {
  border-color: color-mix(in srgb, var(--cyan) 45%, transparent);
}

.jstat-trigger {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  line-height: inherit;
  cursor: pointer;
}

/* Stretches the button across the whole tile so the label is part of the
   target, without nesting the label inside the control. */
.jstat-trigger::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
}

.jstat-trigger:focus-visible {
  outline: none;
}

.jstat-chev {
  width: 7px;
  height: 7px;
  border-right: 2px solid var(--cyan);
  border-bottom: 2px solid var(--cyan);
  transform: translateY(-3px) rotate(45deg);
  transition: transform var(--dur-base) var(--ease-out);
}

.jstat-trigger[aria-expanded="true"] .jstat-chev {
  transform: translateY(1px) rotate(-135deg);
}

/* ── The breakdown ── */
/* An overlay anchored to the Sources tile, not a section of the panel.
   Positioned from JS so it escapes the panel's rounded clip and the
   sidebar's own scroll; styled here as the card it now is. */
.sources-panel {
  z-index: 40;
  padding: 4px 14px 10px;
  border: 1px solid color-mix(in srgb, var(--cyan) 30%, var(--border));
  border-radius: 12px;
  background: var(--surface2);
  box-shadow: 0 18px 44px rgba(0, 0, 0, .55);
  overflow-y: auto;
  overscroll-behavior: contain;
}

.sources-panel[hidden] {
  display: none;
}

.sources-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.source-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding: 6px 0;
  font-size: .8rem;
  color: var(--text);
}

.source-row + .source-row {
  border-top: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
}

.source-count {
  flex-shrink: 0;
  font-family: 'JetBrains Mono', monospace;
  font-size: .68rem;
  color: var(--cyan);
  font-variant-numeric: tabular-nums;
}

/* A feed that was asked and could not answer is listed rather than left
   out, so a short list reads as one being down and not as there only
   being seven of them. */
.source-row.is-down .source-name,
.source-row.is-down .source-count {
  color: var(--text-dim);
}

.source-row.is-down .source-count {
  font-style: italic;
}

/* ══ REFINE ══
   A section of the panel, not a card of its own. The controls and the
   figures they act on were two framed boxes stacked with a gap, which
   read as two unrelated tools; inside one panel, divided by a rule, they
   read as one. */
.jobs-deck {
  border-top: 1px solid var(--border);
  padding: 16px clamp(20px, 4vw, 40px) 4px;
}

.jobs-deck-title {
  margin: 0 0 2px;
  font-family: 'JetBrains Mono', monospace;
  font-size: .55rem;
  font-weight: 500;
  letter-spacing: 1.6px;
  text-transform: uppercase;
  color: var(--text-dim);
}

/* ══ FILTERS ══ */
/* Two stated rows rather than one bar left to wrap.

   Wrapping decided the grouping by arithmetic — whatever happened to fit —
   and flex made it worse, because items on the last row share the leftover
   space between them and came out half again as wide as the ones above.
   The split is meaningful, so it is declared: the three controls that
   decide WHICH jobs on top, the four that decide how they are ranked and
   shown underneath. Each row is its own grid and fills the card. */
.jobs-filters {
  display: grid;
  gap: 14px;
  /* The panel supplies the frame and the gutter; the form is just rows. */
  padding: 0;
  border: 0;
  background: transparent;
  margin: 0 0 18px;
}

/* The fields share the bar rather than hugging their own text.

   A native <select> is as wide as its longest option, whatever is
   currently chosen. The dropdown that replaced it is as wide as the label
   it is showing, so the controls collapsed to the width of "All types" and
   the bar came out a row of small boxes with a gap at the end.

   Growing from a common basis fixes both halves of that: the row fills,
   and the widths stop depending on the chosen value, so picking a longer
   option no longer shifts every control beside it. */
.jf-row {
  display: grid;
  align-items: end;
  gap: 14px 12px;
}

/* Narrow first: in a 400px sidebar every control takes its own line.
   Wider arrangements are added back by container query as the deck earns
   the room — viewport queries cannot see this, because the page can be
   1500px wide while this column is still 400px. */
.jf-row-find,
.jf-row-refine {
  grid-template-columns: minmax(0, 1fr);
}

@container (min-width: 440px) {
  .jf-row-find,
  .jf-row-refine {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  /* Still the widest thing in the row, so it keeps the whole line. */
  .jf-search { grid-column: span 2; }

  /* Reset follows the two selects onto a line of its own, and a small
     button alone against the left edge of an empty row reads as something
     left behind rather than as the end of the form. It takes the line.

     (A stray `.jf-reset,` sat here — a selector with no block, left from
     an edit. The parser dropped it, so the rule it was meant to carry had
     simply never applied.) */
  .jf-reset {
    grid-column: span 2;
    justify-self: stretch;
  }
}

@container (min-width: 820px) {
  /* Search carries the most, so it takes the most. */
  .jf-row-find { grid-template-columns: 2fr 1fr 1fr; }
  .jf-search { grid-column: auto; }
  /* Two selects and Reset, which fit on one line at this width — so
     Reset gives up the whole row it takes when stacked, and goes back to
     being sized by its own text beside them.

     This read `repeat(4, minmax(0, 1fr)) auto auto` and described four
     controls and two buttons. Location, Country, Type and Sort were
     removed from this row and one of the buttons with them; the template
     was left behind, declaring three empty columns. */
  .jf-row-refine { grid-template-columns: repeat(2, minmax(0, 1fr)) auto; }

  .jf-reset {
    grid-column: auto;
    justify-self: start;
  }
}

/* Takes its line. Stacked — which is every width until the filter column
   is wide enough to put all three controls in a row — Reset sits alone
   under two full-width selects, and at its text width it read as a stray
   rather than as the end of the form. It is sized by its text again at
   820px, where it shares the row with them. */
.jf-reset {
  justify-self: stretch;
}

.jf-label {
  display: block;
  font-family: 'JetBrains Mono', monospace;
  font-size: .58rem;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  color: var(--text-dim);
  margin-bottom: 7px;
}

/* Only the things the shared control design cannot know: this bar wants
   its controls a touch smaller than a full-width form field. The fill,
   the border, the radius, the caret and the focus outline all come from
   FORM CONTROLS and DROPDOWN in /style.css.

   The dropdown's button is listed alongside the inputs because it is the
   visible control here — the <select> it stands for is hidden. Without it
   the dropdowns came out 38px beside 36px inputs, all bottom-aligned and
   all very slightly the wrong height. */
.jobs-filters input,
.jobs-filters .combo-value {
  font-size: .85rem;
  padding-block: 9px;
  border-radius: 8px;
}

.jf-reset {
  font-family: 'JetBrains Mono', monospace;
  font-size: .62rem;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--text-dim);
  background: transparent;
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 10px 16px;
  min-height: 36px;
  cursor: pointer;
  transition: color var(--dur-base) var(--ease-out),
              border-color var(--dur-base) var(--ease-out);
}

.jf-reset:hover,
.jf-reset:focus-visible {
  color: var(--cyan);
  border-color: var(--cyan);
}


.jobs-warning {
  border: 1px solid color-mix(in srgb, var(--cyan) 35%, transparent);
  border-radius: 12px;
  background: var(--cyan-dim);
  color: var(--text);
  padding: 14px 18px;
  font-size: .85rem;
  line-height: 1.65;
  margin: 0 0 26px;
}

/* ══ SECTIONS ══ */
.jobs-section {
  /* The portfolio's global `section` rule carries the page gutter, which
     .jobs-wrap has already applied — inheriting it here indented the grid
     by 64px on each side a second time and cost it a whole column. The
     spacing between sections is this file's job, so the rest goes too. */
  padding: 0;
  margin-bottom: 60px;
}

/* ══ REGION TABS ══
   The two lists were stacked sections with headings. Stacked, the
   international list pushed the Bangladesh one off screen as it grew —
   and it grows every few seconds now. As tabs they cost one click and
   neither can bury the other.

   Styled as a tab strip rather than as two pills. Pills read as filter
   chips — "both could be on" — which is the wrong promise for a control
   where exactly one is always selected. A row sitting on a rule, with the
   chosen tab joined to the panel below by a bar in the accent colour, is
   the shape people already know means "these are views of one thing".

   The old .jobs-h2 rules are gone with the headings they styled. */
.jobs-tabs {
  display: flex;
  gap: 2px;
  margin: 0;
  /* The strip keeps its own rule. The active tab's indicator sits one
     pixel below the tab and covers this line, which is the whole reason
     the selected tab reads as joined to the panel beneath it — without
     the rule there is nothing for it to break through, and the tabs stop
     looking like tabs. */
  border-bottom: 1px solid var(--border);
}

.jobs-tab {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 11px 18px 13px;
  border: 0;
  border-radius: 10px 10px 0 0;
  background: transparent;
  color: var(--text-dim);
  font-family: 'Bebas Neue', sans-serif;
  font-size: 1.14rem;
  letter-spacing: 1.3px;
  line-height: 1;
  cursor: pointer;
  transition: color var(--dur-base) var(--ease-out),
              background-color var(--dur-base) var(--ease-out);
}

/* The indicator. It is always present so the tab does not change height
   when selected; unselected it is simply transparent. It sits one pixel
   below the tab so it covers the strip's own rule, which is what makes
   the selected tab read as joined to the panel underneath. */
.jobs-tab::after {
  content: '';
  position: absolute;
  left: 10px;
  right: 10px;
  bottom: -1px;
  height: 3px;
  border-radius: 3px 3px 0 0;
  background: transparent;
  transition: background-color var(--dur-base) var(--ease-out);
}

.jobs-tab:hover {
  color: var(--text);
  background: color-mix(in srgb, var(--text-dim) 8%, transparent);
}

.jobs-tab.is-active {
  color: var(--text);
  background: color-mix(in srgb, var(--cyan) 9%, transparent);
}

.jobs-tab.is-active::after {
  background: var(--cyan);
}

.jobs-tab:focus-visible {
  outline: 2px solid var(--cyan);
  outline-offset: -2px;
}

.jobs-tab-icon {
  font-size: .95rem;
  line-height: 1;
}

.jobs-tab .flag-bd {
  width: 20px;
  height: 13px;
  border-radius: 2px;
}

/* The count rides inside its tab. It is the number being chosen between,
   so it belongs to the choice rather than to the list below it. */
.jobs-n {
  font-family: 'JetBrains Mono', monospace;
  font-size: .64rem;
  letter-spacing: .5px;
  color: var(--text-dim);
  background: color-mix(in srgb, var(--text-dim) 14%, transparent);
  border-radius: 99px;
  padding: 3px 8px;
  margin: 0;
  min-width: 2.4ch;
  text-align: center;
}

.jobs-tab.is-active .jobs-n {
  color: var(--cyan);
  background: color-mix(in srgb, var(--cyan) 16%, transparent);
}

.jobs-n:empty { display: none; }

/* Visible to a screen reader, not to the eye. The search field carries
   its instruction in the placeholder, which a label must not rely on —
   a placeholder disappears the moment anyone types into it. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ══ SEARCH ══
   One field, full width, directly above the results it searches. */
.jobs-search {
  position: relative;
  display: flex;
  align-items: center;
  /* Clear of the tab strip above it. At zero the field's own border ran
     straight into the strip's rule and the two read as one thick line
     with the active tab's indicator buried in it. */
  margin: 14px 0 0;
}

.jobs-search-icon {
  position: absolute;
  left: 14px;
  width: 16px;
  height: 16px;
  color: var(--text-dim);
  pointer-events: none;
}

.jobs-search input {
  width: 100%;
  padding: 12px 14px 12px 40px;
  font-size: .92rem;
  border-radius: 10px;
}

/* A panel is focusable so the keyboard lands somewhere after a tab is
   chosen; it should not then draw a ring around the whole list. */
.jobs-section:focus {
  outline: none;
}

.jobs-section:focus-visible {
  outline: 2px solid var(--cyan);
  outline-offset: 4px;
  border-radius: 8px;
}

/* Full width, like the grid it stands in for. It was capped at 62ch, which
   is the right measure for a paragraph of prose and the wrong shape here:
   a short notice sitting in a box two thirds the width of the section read
   as a stray element rather than as the section's content. */
.jobs-empty {
  color: var(--text-dim);
  line-height: 1.7;
  margin: 0;
  padding: 26px 28px;
  border: 1px dashed var(--border);
  border-radius: 12px;
  background: color-mix(in srgb, var(--surface2) 35%, transparent);
}

/* ══ THE GRID ══
   One column at every width. The jobs are a list, not a gallery: at 1050px
   a row can carry the identity, the score, four facts and the actions on
   three lines, where three cards side by side carried the same content in
   twelve and still left most of each line empty. */
.job-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 14px;
  /* Every card the same height, across the whole grid and not just within
     a row. "1fr" on auto-rows makes each row as tall as the tallest card
     in the grid rather than the tallest in that row, and stretch lets each
     card fill the track it was given. The card is already a column flex
     with "margin-top: auto" on its footer, so the extra height goes into
     the gap above the actions and the buttons line up everywhere. */
  grid-auto-rows: 1fr;
  align-items: stretch;
}

.jobs-pagination {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  margin-top: 22px;
}

.jobs-pagination[hidden] {
  display: none;
}

.jobs-pagination button {
  min-height: 38px;
  padding: 8px 18px;
  border: 1px solid var(--border);
  border-radius: 99px;
  color: var(--text);
  background: var(--surface);
  font-family: 'JetBrains Mono', monospace;
  font-size: .66rem;
  letter-spacing: 1px;
  text-transform: uppercase;
  cursor: pointer;
  transition: color var(--dur-base) var(--ease-out),
              border-color var(--dur-base) var(--ease-out),
              background-color var(--dur-base) var(--ease-out);
}

.jobs-pagination button:hover:not(:disabled) {
  border-color: var(--cyan);
  color: var(--cyan);
  background: var(--cyan-dim);
}

.jobs-pagination button:disabled {
  opacity: .45;
  cursor: not-allowed;
}

.jobs-pagination span {
  min-width: 10ch;
  color: var(--text-dim);
  font-family: 'JetBrains Mono', monospace;
  font-size: .75rem;
  text-align: center;
}

@media (max-width: 400px) {
  .job-grid {
    grid-template-columns: 1fr;
  }
}

/* ══ THE CARD ══
   One job per row, full width of the results column.

   A row 1050px wide is a different object from a 340px card, and laying
   it out the same way wastes most of it: stacking title, then facts, then
   tags, then actions left two thirds of every line empty. So the identity
   and the score share the first line, the four facts run across the width
   below it, and the provenance sits opposite the actions at the foot.

   Every row is the same height because every part of it is fixed to one
   line: a title that would wrap is truncated, and the tags clip to a
   single row with the +N chip saying what was left out. Uniform rows are
   what make a list of 150 scannable — the eye learns where to look once
   and then stops re-learning it.

   The skin is still .pcard's, so the list reads as part of the portfolio:
   1px border, the rim light on hover, the cyan hairline along the top. */
.job {
  display: flex;
  flex-direction: column;
  background:
    radial-gradient(120% 100% at 0% 0%,
      color-mix(in srgb, var(--cyan) 7%, transparent) 0%, transparent 46%),
    linear-gradient(150deg,
      color-mix(in srgb, var(--surface2) 72%, var(--surface)) 0%,
      var(--surface) 58%);
  border: 1px solid color-mix(in srgb, var(--cyan) 20%, var(--border));
  border-radius: 12px;
  padding: 18px 22px 16px;
  position: relative;
  overflow: hidden;
  isolation: isolate;
  transition: border-color var(--dur-base) var(--ease-out),
              box-shadow var(--dur-base) var(--ease-out),
              transform var(--dur-base) var(--ease-out);
}

/* The accent that used to be a 3px left border, moved to a hairline along
   the top edge: it reads as part of the card instead of clipping the
   padding, and it can take the score's colour without shouting. */
.job::before {
  content: '';
  position: absolute;
  inset: 0 0 auto;
  height: 2px;
  background: linear-gradient(to right,
    var(--cyan), color-mix(in srgb, var(--cyan) 30%, transparent) 65%, transparent);
  opacity: .8;
}

.job:hover {
  border-color: color-mix(in srgb, var(--cyan) 55%, transparent);
  box-shadow: 0 18px 40px -26px var(--cyan-glow), 0 0 26px -14px var(--cyan-glow);
  transform: translateY(-3px);
}

[data-theme="light"] .job:hover {
  box-shadow: 0 0 34px -4px rgba(0, 120, 180, .3);
}

/* ── Already read ──
   A viewed row takes a colour of its own. Dimming alone was too quiet to
   find in a list of a hundred and three, and it read as "disabled" rather
   than as "done" — a job you have opened is still a job, and may still be
   the one worth applying to.

   Amber, because the palette's other two hues are already spoken for:
   cyan means live and green means a strong match, so a third hue is the
   only way to say a third thing. Declared as a local token so both themes
   set it once and everything below follows. */
.job.is-seen {
  --seen: #f0a830;
  border-color: color-mix(in srgb, var(--seen) 38%, transparent);
  background:
    radial-gradient(120% 100% at 0% 0%,
      color-mix(in srgb, var(--seen) 9%, transparent) 0%, transparent 48%),
    linear-gradient(150deg,
      color-mix(in srgb, var(--seen) 5%, var(--surface)) 0%, var(--surface) 62%);
}

[data-theme="light"] .job.is-seen {
  /* The neon amber is legible on near-black and washes out on white. */
  --seen: #a8681a;
}

.job.is-seen::before {
  background: linear-gradient(to right,
    var(--seen), color-mix(in srgb, var(--seen) 30%, transparent) 65%, transparent);
  opacity: 1;
}

.job.is-seen:hover {
  border-color: color-mix(in srgb, var(--seen) 70%, transparent);
  box-shadow: 0 18px 40px -26px color-mix(in srgb, var(--seen) 45%, transparent);
}

/* The title stays at full strength. It is the thing being recognised. */
.job.is-seen .job-mark {
  opacity: .8;
}

/* ── The badge ──
   Inside the card beside the company, in the card's own viewed colour, so
   the row and its label say the same thing. */
.job-seen {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  flex-shrink: 0;
  margin: 0;
  padding: 3px 9px 3px 7px;
  border: 1px solid color-mix(in srgb, var(--seen, var(--text-dim)) 45%, transparent);
  border-radius: 99px;
  background: color-mix(in srgb, var(--seen, var(--text-dim)) 14%, transparent);
  font-family: 'JetBrains Mono', monospace;
  font-size: .55rem;
  font-weight: 600;
  letter-spacing: 1.1px;
  text-transform: uppercase;
  color: var(--seen, var(--text-dim));
  white-space: nowrap;
}

.job-seen-tick {
  width: 9px;
  height: 9px;
  flex-shrink: 0;
}

/* ── Head: identity opposite the score ── */
.job-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 18px;
}

.job-identity {
  display: flex;
  align-items: center;
  gap: 12px;
  min-width: 0;
  flex: 1;
}

/* The logo when the feed published one, initials when it did not. Fixed
   size either way so titles start on the same line down the list. */
.job-mark {
  position: relative;
  flex-shrink: 0;
  width: 44px;
  height: 44px;
  border-radius: 9px;
  border: 1px solid var(--border);
  background: color-mix(in srgb, var(--cyan) 8%, var(--surface));
  overflow: hidden;
  transition: opacity var(--dur-base) var(--ease-out);
}

.job-mark::after {
  content: attr(data-initials);
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-family: 'JetBrains Mono', monospace;
  font-size: .72rem;
  font-weight: 700;
  letter-spacing: .5px;
  color: var(--cyan);
}

.job-titles {
  min-width: 0;
  flex: 1;
}

/* One line. At this width almost every title fits; the few that do not
   are truncated rather than allowed to set the height of the whole list.
   The full text is on the element's title attribute and in the match
   details, so it stays reachable. */
.job-title {
  display: block;
  min-width: 0;
  font-size: 1.08rem;
  font-weight: 600;
  line-height: 1.3;
  color: var(--text);
  margin: 0 0 2px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  transition: opacity var(--dur-base) var(--ease-out);
}

.job-company {
  display: flex;
  align-items: center;
  gap: 9px;
  margin: 0;
  font-size: .83rem;
  font-weight: 500;
  color: var(--text-dim);
}

.job-company-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ── The score ──
   The reason the page exists, so it gets the most contrast on the card.
   Colour steps with the band rather than being one flat accent: a 62% and
   a 95% should not look alike at a glance. */
.job-score {
  --score-accent: var(--cyan);
  display: flex;
  align-items: baseline;
  gap: 8px;
  flex-shrink: 0;
  min-width: 0;
  padding: 7px 12px 8px;
  text-align: left;
  line-height: 1.1;
  border: 1px solid color-mix(in srgb, var(--score-accent) 28%, transparent);
  border-radius: 9px;
  background: color-mix(in srgb, var(--score-accent) 8%, var(--surface));
}

.job-pct {
  display: block;
  font-family: 'Bebas Neue', sans-serif;
  font-size: 1.5rem;
  line-height: 1;
  letter-spacing: .5px;
  color: var(--cyan);
}

.job-score[data-band="90"] .job-pct,
.job-score[data-band="100"] .job-pct {
  color: var(--green);
}

.job-score[data-band="90"],
.job-score[data-band="100"] {
  --score-accent: var(--green);
}

.job-band {
  display: block;
  font-family: 'JetBrains Mono', monospace;
  font-size: .5rem;
  letter-spacing: .8px;
  text-transform: uppercase;
  color: var(--text-dim);
  white-space: nowrap;
}

.job-meter {
  display: block;
  height: 3px;
  background: color-mix(in srgb, var(--border) 75%, transparent);
  border-radius: 99px;
  margin: 14px 0 15px;
  overflow: hidden;
}

.job-meter-fill {
  display: block;
  height: 100%;
  background: linear-gradient(to right, var(--cyan), var(--green));
  border-radius: inherit;
}

/* ── Facts across the width ──
   Label above value, in four columns. Still labelled rather than reduced
   to icons: "Not specified" has to read as an absence, and against an
   icon alone it reads as a fault. */
.job-facts {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 0 20px;
  margin: 0 0 14px;
  padding: 0;
  font-size: .82rem;
}

.job-facts li {
  display: block;
  min-width: 0;
}

.job-fact-k {
  display: block;
  margin-bottom: 4px;
  font-family: 'JetBrains Mono', monospace;
  font-size: .55rem;
  letter-spacing: 1.2px;
  text-transform: uppercase;
  color: var(--text-dim);
}

.job-fact-v {
  display: block;
  color: var(--text);
  line-height: 1.4;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.job-facts .is-unset .job-fact-v {
  color: var(--text-dim);
  font-style: italic;
}

/* ── Skill tags, one row ──
   The +N chip already says how many were not shown, so a job with twenty
   matched skills costs the same height as one with three. */
.job-tags {
  list-style: none;
  display: flex;
  flex-wrap: nowrap;
  gap: 6px;
  margin: 0 0 14px;
  padding: 0;
  overflow: hidden;
}

.job-tag {
  flex-shrink: 0;
  font-family: 'JetBrains Mono', monospace;
  font-size: .6rem;
  letter-spacing: .3px;
  color: var(--cyan);
  border: 1px solid color-mix(in srgb, var(--cyan) 22%, transparent);
  border-radius: 99px;
  padding: 3px 9px;
  background: var(--cyan-dim);
}

.job-tag.is-more {
  color: var(--text-dim);
  border-color: var(--border);
  background: transparent;
}

/* ── Foot: provenance opposite the actions ──
   `margin-top: auto` is what makes the rows uniform: whatever height the
   grid track hands the card, the slack lands here and the buttons line up
   across every row. */
.job-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 18px;
  margin-top: auto;
  padding-top: 13px;
  border-top: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
}

.job-meta {
  margin: 0;
  padding: 0;
  border: 0;
  min-width: 0;
  font-family: 'JetBrains Mono', monospace;
  font-size: .6rem;
  color: var(--text-dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ── Match details ── */
.job-details {
  border-top: 1px solid var(--border);
  padding-top: 12px;
  margin-bottom: 14px;
}

.job-details-toggle {
  font-family: 'JetBrains Mono', monospace;
  font-size: .64rem;
  letter-spacing: 1px;
  text-transform: uppercase;
  color: var(--text-dim);
  cursor: pointer;
  list-style: none;
  transition: color var(--dur-base) var(--ease-out);
}

.job-details-toggle::-webkit-details-marker {
  display: none;
}

.job-details-toggle::before {
  content: '+ ';
  color: var(--cyan);
}

.job-details[open] .job-details-toggle::before {
  content: '− ';
}

.job-details-toggle:hover,
.job-details-toggle:focus-visible {
  color: var(--cyan);
}

.job-details-body {
  padding-top: 12px;
}

.job-why {
  font-size: .82rem;
  line-height: 1.65;
  color: var(--text-dim);
  margin: 0 0 14px;
}

.job-dh {
  font-family: 'JetBrains Mono', monospace;
  font-size: .58rem;
  letter-spacing: 1.2px;
  text-transform: uppercase;
  color: var(--text-dim);
  margin: 12px 0 6px;
}

.job-dl {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 5px 12px;
  font-size: .78rem;
}

.job-dl li::before {
  margin-right: 5px;
  font-family: 'JetBrains Mono', monospace;
}

.job-dl.is-matched li::before { content: '✓'; color: var(--green); }
.job-dl.is-related li::before { content: '≈'; color: var(--cyan); }
.job-dl.is-missing li::before { content: '✗'; color: var(--text-dim); }
.job-dl.is-missing li { color: var(--text-dim); }

/* The score's arithmetic, shown rather than asserted. */
.job-components {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 4px;
}

.job-components li {
  display: grid;
  grid-template-columns: 104px 1fr 30px;
  align-items: center;
  gap: 8px;
  font-family: 'JetBrains Mono', monospace;
  font-size: .58rem;
  color: var(--text-dim);
}

.job-comp-bar {
  height: 4px;
  background: var(--border);
  overflow: hidden;
}

.job-comp-bar span {
  display: block;
  height: 100%;
  background: var(--cyan);
}

.job-comp-v {
  text-align: right;
  color: var(--text);
}

/* ── Actions ── */
.job-actions {
  display: flex;
  gap: 10px;
  flex-shrink: 0;
}

.job-actions > * {
  min-height: 42px;
  display: grid;
  place-items: center;
}

.job-view {
  flex: 1;
  /* flex, not the grid that .job-actions > * sets, so the generated arrow
     sits beside the label instead of stacking on top of it. */
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  text-align: center;
  font-family: 'JetBrains Mono', monospace;
  font-size: .68rem;
  letter-spacing: 1.2px;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--bg);
  background: var(--cyan);
  border: 1px solid var(--cyan);
  border-radius: 8px;
  padding: 10px 14px;
  transition: background-color var(--dur-base) var(--ease-out),
              color var(--dur-base) var(--ease-out);
}

.job-view:hover,
.job-view:focus-visible {
  background: transparent;
  color: var(--cyan);
}

/* ══ SKELETONS (§22) ══ */
.job-skeleton {
  pointer-events: none;
  /* Shaped like what replaces it — same radius, roughly the same height —
     so the grid does not jump when the real cards land. */
  min-height: 300px;
  border-radius: 12px;
  gap: 12px;
}

.sk {
  display: block;
  height: 12px;
  border-radius: 6px;
  background: linear-gradient(90deg, var(--border) 25%, var(--surface2) 50%, var(--border) 75%);
  background-size: 200% 100%;
  animation: skShimmer 1.4s ease-in-out infinite;
}

.sk-l1 { width: 70%; height: 20px; }
.sk-l2 { width: 45%; }
.sk-l3 { width: 90%; }
.sk-l4 { width: 60%; }

@keyframes skShimmer {
  from { background-position: 200% 0; }
  to   { background-position: -200% 0; }
}

/* ══ NARROW ══ */
@media (max-width: 720px) {
  .page-jobs {
    padding-top: calc(69px + 28px);
  }

  .jobs-filters {
    padding: 14px;
    gap: 10px;
  }

  /* The filter rows used to be arranged here, by viewport width. They
     are laid out by container query now: the deck sits in a sidebar whose
     width no longer follows the viewport, so only the deck can say how
     many columns fit inside it. */
}

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

/* ════════════════════════════════════════════════════════════
   MATCH DETAILS MODAL (§9)

   A native <dialog>. Escape, the inert backdrop and the focus trap come
   from showModal() rather than from script, which is both less code and
   better behaved than a div pretending to be a dialog.

   On a phone it becomes a bottom sheet: a centred box on a 375px screen is
   a box with no room around it, and a sheet rising from the bottom puts
   the close button and the first line of content where a thumb already is.
   ════════════════════════════════════════════════════════════ */
.mm {
  /* The portfolio's reset zeroes margin on everything, which for a modal
     <dialog> is not cosmetic: the UA centres it with "margin: auto"
     against "inset: 0", so with the margin gone it was pinned to the top
     left corner of the viewport instead of the middle of it. */
  margin: auto;
  width: min(640px, calc(100vw - 32px));
  max-height: min(86vh, 900px);
  padding: 0;
  border: 1px solid color-mix(in srgb, var(--cyan) 35%, transparent);
  border-radius: 16px;
  background: var(--surface);
  color: var(--text);
  overflow: hidden;
}

.mm::backdrop {
  background: rgba(2, 5, 8, .72);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
}

[data-theme="light"] .mm::backdrop {
  background: rgba(10, 30, 50, .4);
}

/* The dialog itself is the flex column. .mm-inner used to carry
   max-height: inherit, which copies .mm's BORDER-box max-height into a box
   living inside .mm's border — so the panel could end up a pixel or two
   taller than its container and push the footer past the bottom edge.
   Constraining the element that owns the max-height avoids the arithmetic
   entirely. display only applies while open; a closed dialog must stay
   display: none. */
.mm[open] {
  display: flex;
  flex-direction: column;
  animation: mmIn .26s var(--ease-out);
}

.mm[open]::backdrop {
  animation: mmFade .26s var(--ease-out);
}

@keyframes mmIn {
  from { opacity: 0; transform: translateY(12px) scale(.98); }
  to   { opacity: 1; transform: none; }
}

@keyframes mmFade {
  from { opacity: 0; }
  to   { opacity: 1; }
}

.mm-inner {
  display: flex;
  flex-direction: column;
  /* A flex item will not shrink below its content unless told it may, so
     without this the body kept its full height, .mm-inner overflowed, and
     the footer with the job link sat below the bottom of the sheet where
     it could not be reached. */
  flex: 1 1 auto;
  min-height: 0;
}

.mm-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  padding: 22px 24px 16px;
  border-bottom: 1px solid var(--border);
}

.mm-kicker {
  font-family: 'JetBrains Mono', monospace;
  font-size: .58rem;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  color: var(--cyan);
  margin: 0 0 6px;
}

.mm-title {
  font-size: 1.15rem;
  font-weight: 600;
  line-height: 1.3;
  margin: 0;
  color: var(--text);
}

.mm-company {
  margin: 4px 0 0;
  font-size: .88rem;
  color: var(--text-dim);
}

.mm-close {
  flex-shrink: 0;
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  font-size: .9rem;
  color: var(--text-dim);
  background: transparent;
  border: 1px solid var(--border);
  border-radius: 4px;
  cursor: pointer;
  transition: color var(--dur-base) var(--ease-out),
              border-color var(--dur-base) var(--ease-out);
}

.mm-close:hover,
.mm-close:focus-visible {
  color: var(--cyan);
  border-color: var(--cyan);
}

.mm-head,
.mm-foot {
  flex: 0 0 auto;
}

.mm-body {
  flex: 1 1 auto;
  min-height: 0;
  padding: 20px 24px 24px;
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* ── The score ── */
.mm-score {
  text-align: center;
  padding: 6px 0 18px;
}

.mm-pct {
  display: block;
  font-family: 'Bebas Neue', sans-serif;
  font-size: 3.2rem;
  line-height: 1;
  letter-spacing: 1px;
  color: var(--cyan);
}

.mm-score[data-band="90"] .mm-pct,
.mm-score[data-band="100"] .mm-pct {
  color: var(--green);
}

.mm-band {
  display: block;
  font-family: 'JetBrains Mono', monospace;
  font-size: .62rem;
  letter-spacing: 1.6px;
  text-transform: uppercase;
  color: var(--text-dim);
  margin: 6px 0 14px;
}

/* ── Facts and verdicts ── */
.mm-facts,
.mm-verdicts {
  list-style: none;
  margin: 0 0 18px;
  padding: 16px 0 0;
  border-top: 1px solid var(--border);
  display: grid;
  gap: 8px;
  font-size: .86rem;
}

.mm-facts li {
  display: grid;
  grid-template-columns: 96px 1fr;
  gap: 12px;
  align-items: baseline;
}

.mm-verdicts li {
  display: grid;
  grid-template-columns: 18px 96px 1fr;
  gap: 8px;
  align-items: baseline;
}

.mm-k {
  font-family: 'JetBrains Mono', monospace;
  font-size: .58rem;
  letter-spacing: 1.2px;
  text-transform: uppercase;
  color: var(--text-dim);
}

.mm-v {
  color: var(--text);
}

.mm-facts .is-unset .mm-v {
  color: var(--text-dim);
  font-style: italic;
}

.mm-tick {
  font-family: 'JetBrains Mono', monospace;
}

.mm-verdicts .is-yes .mm-tick { color: var(--green); }
.mm-verdicts .is-no .mm-tick { color: var(--text-dim); }
.mm-verdicts .is-no .mm-v { color: var(--text-dim); }

.mm-h {
  font-family: 'JetBrains Mono', monospace;
  font-size: .58rem;
  letter-spacing: 1.4px;
  text-transform: uppercase;
  color: var(--text-dim);
  margin: 18px 0 8px;
}

.mm-why {
  font-size: .88rem;
  line-height: 1.7;
  color: var(--text-dim);
  margin: 0;
}

.mm-foot {
  padding: 16px 24px;
  border-top: 1px solid var(--border);
  background: var(--bg);
}

.mm-foot .job-view {
  display: block;
}

/* ── Bottom sheet on a phone ── */
@media (max-width: 560px) {
  .mm {
    /* Width comes from the insets rather than from 100vw, so the gap is
       stated once and the sheet fills whatever is left. */
    width: auto;
    max-width: none;
    max-height: 92vh;
    margin: 0;
    /* Pinned to the bottom edge rather than centred, with a margin either
       side. Edge to edge, the card had no outside: its border ran off
       both sides of the screen and the text started hard against the
       glass. A small gap is what makes it read as a card lying on the
       page rather than as the page itself. */
    position: fixed;
    inset: auto 10px 0 10px;
    /* The sides are visible now, so they get a border too. */
    border-width: 1px 1px 0;
    border-radius: 14px 14px 0 0;
  }

  .mm[open] {
    animation: mmSheet .28s var(--ease-out);
  }

  @keyframes mmSheet {
    from { transform: translateY(100%); }
    to   { transform: none; }
  }

  .mm-head { padding: 18px 18px 14px; }
  .mm-body { padding: 16px 18px 20px; }
  .mm-foot { padding: 14px 18px calc(14px + env(safe-area-inset-bottom)); }

  .mm-facts li { grid-template-columns: 84px 1fr; }
  .mm-verdicts li { grid-template-columns: 18px 84px 1fr; }
}

@media (prefers-reduced-motion: reduce) {
  .mm[open],
  .mm[open]::backdrop {
    animation: none;
  }
}

/* The secondary action on a card, beside View job. */
.job-more {
  flex: 1;
  /* nowrap: "Match details" broke onto two lines in the narrow column the
     actions get at the foot of a row. */
  white-space: nowrap;
  font-family: 'JetBrains Mono', monospace;
  font-size: .68rem;
  letter-spacing: 1.2px;
  text-transform: uppercase;
  color: var(--text-dim);
  background: transparent;
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 10px 12px;
  cursor: pointer;
  transition: color var(--dur-base) var(--ease-out),
              border-color var(--dur-base) var(--ease-out);
}

.job-more:hover,
.job-more:focus-visible {
  color: var(--cyan);
  border-color: var(--cyan);
}

/* Two actions in the modal footer, the destructive-free one first.

   The direction and alignment are stated rather than left to default,
   because this is a <footer> and the site stylesheet styles <footer> by
   element: `footer { display: flex; justify-content: space-between;
   align-items: center }`, and under 960px `footer { flex-direction:
   column }`. Those are meant for the page footer, but a type selector
   does not know that, and the modal's footer is a footer too. The class
   here out-specifies them for display, so the leak was invisible until
   the viewport went narrow — then the two actions stacked and shrank to
   their text instead of sitting side by side.

   align-items: stretch is the flex default and is set for the same
   reason: `center` from that rule was collapsing each action to its own
   height. */
.mm-foot-split {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  justify-content: flex-start;
  align-items: stretch;
  gap: 10px;
}

/* Side by side while both labels fit on one line, stacked when they do
   not. The basis decides it, so there is no breakpoint to guess at.

   160px is measured, not chosen: the longer of the two labels, "Original
   job post", sets on one line at 158px wide. ("Apply", at 76px, has room
   to spare — it is the longer label that decides this.) Two of those plus
   the 10px gap need 330px of row, which is about a 390px viewport once
   the dialog's own padding is taken off. Below that they stack, each
   takes the full width, and both labels still fit on one line.

   It was 190px while the second button read "Prepare application", which
   needed 197px and pushed the stacking point out to roughly a 450px
   viewport. Shortening that label to "Apply" is what lets this come down.
   Measured at both ends: 430px keeps them in a row at 181px each, 375px
   stacks them at full width, and neither breaks a label in two.

   With `flex: 1` and no basis they stayed in a row at every size, and on
   a 318px phone the label broke across two lines inside a button
   narrower than the one beside it. */
.mm-foot-split .job-more,
.mm-foot-split .job-view {
  flex: 1 1 160px;
  text-align: center;
  text-decoration: none;
  display: block;
}

/* ════════════════════════════════════════════════════════════
   PREPARE APPLICATION PAGE
   ════════════════════════════════════════════════════════════ */
/* The same shell the matched-roles page and the CV generator use, so the
   three tools on this site are laid out the same way. */
.ap-wrap {
  max-width: 1600px;
}

#app {
  display: grid;
  /* A fixed measure for the role, the rest for the draft. The draft is
     the work; the posting beside it is reference, and reference does not
     get better with more width — but a 20-row letter does. */
  grid-template-columns: minmax(340px, 420px) minmax(0, 1fr);
  align-items: start;
  gap: 28px;
}

@media (max-width: 1000px) {
  #app { grid-template-columns: 1fr; gap: 22px; }
}

#app[hidden] {
  display: none;
}

#app > .ap-sidebar {
  grid-column: 1;
  grid-row: 2 / span 2;
  position: sticky;
  top: 92px;
  align-self: start;
  display: grid;
  gap: 18px;
  min-width: 0;
}

.ap-sidebar .jobs-head {
  margin: 0;
}

/* A job title can be three words or fifteen, so it wraps and it is
   balanced. Sized for the 420px column it sits in: at the old
   clamp(2rem, 4vw, 3.2rem) a long title set 51px type in a 400px box and
   ran straight across the draft panel beside it. */
#app .jobs-title {
  font-size: clamp(1.5rem, 1.9vw, 2.1rem);
  line-height: 1.1;
  white-space: normal;
  text-wrap: balance;
  overflow-wrap: anywhere;
}

#ap-company {
  color: var(--muted);
  font-size: 1rem;
}

.ap-status {
  color: var(--text-dim);
  line-height: 1.7;
  padding: 40px 0;
}

.ap-status a {
  color: var(--cyan);
}

.ap-panel {
  min-width: 0;
  border: 1px solid color-mix(in srgb, var(--cyan) 24%, var(--border));
  border-radius: 6px;
  background: linear-gradient(150deg,
    color-mix(in srgb, var(--surface2) 72%, var(--surface)) 0%,
    var(--surface) 58%);
  padding: 24px;
  margin: 0;
  box-shadow: 0 16px 44px -38px var(--cyan-glow);
}

.ap-role {
  grid-column: auto;
  grid-row: auto;
  position: static;
  border-top: 3px solid var(--cyan);
}

.ap-draft {
  grid-column: 2;
  grid-row: 2;
  border-top: 3px solid var(--green);
}

.ap-send {
  grid-column: 2;
  grid-row: 3;
}

.ap-h {
  font-family: 'Bebas Neue', sans-serif;
  font-size: 1.4rem;
  letter-spacing: 1.4px;
  color: var(--text);
  margin: 0 0 20px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--border);
}

.ap-note {
  font-size: .76rem;
  line-height: 1.65;
  color: var(--text-dim);
  margin: 14px 0 0;
}

.ap-controls {
  display: grid;
  grid-template-columns: minmax(135px, 170px) minmax(150px, 1fr) auto;
  align-items: flex-end;
  gap: 14px;
  margin-bottom: 24px;
}

.ap-salary {
  min-width: 0;
}

.ap-controls .jf-reset {
  min-height: 40px;
  white-space: nowrap;
}

/* Shape and colour come from FORM CONTROLS in /style.css; what is left
   is that the message field is a writing surface rather than a one-line
   box. */
.ap-textarea {
  min-height: 500px;
  line-height: 1.7;
  white-space: pre-wrap;
  resize: vertical;
}


.ap-spaced {
  margin-top: 22px;
}

.ap-input,
.ap-textarea {
  border-radius: 5px;
}

#ap-sources {
  border-top: 1px solid var(--border);
  padding-top: 12px;
}

.ap-recipient {
  font-size: .86rem;
  line-height: 1.7;
  color: var(--text-dim);
  margin: 0 0 16px;
  padding: 14px 16px;
  border: 1px solid var(--border);
  border-left: 3px solid var(--cyan);
  border-radius: 4px;
  background: color-mix(in srgb, var(--surface2) 72%, var(--surface));
}

.ap-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
}

/* ── Marked as applied ──
   Green, like the double tick on the card it is reporting, rather than
   the cyan this page uses for everything it offers to do. This is not an
   action: it is the record of one, and the two should not look alike. */
.ap-applied {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  margin: 14px 0 0;
  padding: 12px 14px;
  font-size: .82rem;
  line-height: 1.6;
  color: var(--text-dim);
  border: 1px solid color-mix(in srgb, var(--green) 40%, transparent);
  border-left: 3px solid var(--green);
  border-radius: 4px;
  background: color-mix(in srgb, var(--green) 9%, transparent);
}

.ap-applied strong {
  color: var(--green);
  font-weight: 600;
}

/* The same double tick the card carries, so the two read as one fact
   stated twice rather than as two different things. */
.ap-applied-tick {
  flex: none;
  font-family: 'JetBrains Mono', monospace;
  letter-spacing: -2px;
  color: var(--green);
}

.ap-actions .job-view,
.ap-actions .ap-link {
  flex: 1 1 200px;
  /* The shared .job-view is a flex row carrying a generated arrow. Left
     to wrap, the arrow dropped onto a second line under the label and the
     button came out 57px tall. */
  display: flex;
  flex-wrap: nowrap;
  align-items: center;
  justify-content: center;
  white-space: nowrap;
  text-align: center;
  text-decoration: none;
  cursor: pointer;
  min-height: 42px;
  /* Not grid. .job-view carries a generated arrow, and in a grid that
     arrow becomes a second row — the label on one line and the arrow
     underneath, 57px tall. The flex row set above keeps them side by
     side; this is left as a note so it is not reintroduced. */
  gap: 7px;
}

@media (max-width: 980px) {
  #app {
    grid-template-columns: minmax(0, 1fr);
    gap: 18px;
  }

  #app > .jobs-head,
  #app > .ap-panel,
  #app > .ap-sidebar {
    grid-column: 1;
    grid-row: auto;
  }

  #app > .ap-sidebar {
    position: static;
  }
}

@media (max-width: 560px) {
  .ap-panel {
    padding: 18px 16px;
  }

  .ap-controls {
    grid-template-columns: minmax(0, 1fr);
    gap: 12px;
  }

  .ap-textarea {
    min-height: 420px;
  }


  .ap-actions .job-view,
  .ap-actions .ap-link {
    flex: 1 1 100%;
  }
}

/* The flag is a flex item inside the tab now, centred by the flexbox
   rather than by vertical-align, so the baseline nudge that the old
   heading needed is gone with it. */

/* ══ MATCH RANGE SLIDER (standalone full-width card) ═════════
   A dual-colour track — red on the left (excluded), green on
   the right (included) — with a glowing thumb and tick marks.
   Sits between the page header and the filter form. */
/* Inside the control deck now, so it draws no frame of its own: the 3px
   cyan edge and the rounded corners it used to carry were the marks of a
   standalone card, and two framed boxes inside one framed box is what the
   deck exists to stop. */
.match-range-card {
  border: 0;
  background: transparent;
  padding: 10px 0 16px;
  margin-bottom: 0;
}

.match-range-label {
  display: flex;
  align-items: baseline;
  gap: 4px;
  font-family: 'JetBrains Mono', monospace;
  font-size: .76rem;
  letter-spacing: 1.2px;
  text-transform: uppercase;
  color: var(--text-dim);
  margin-bottom: 0;
}

.match-range-label output {
  font-weight: 700;
  font-size: 1.35em;
  color: var(--cyan);
  font-family: 'JetBrains Mono', monospace;
  min-width: 2.4ch;
  text-align: right;
  letter-spacing: 0;
  text-transform: none;
}

.jf-match-count {
  font-size: .85em;
  font-weight: 400;
  opacity: .55;
  margin-left: 6px;
  letter-spacing: .5px;
  text-transform: none;
}

/* ── The <input type="range"> ──────────────────────────────── */
input[type="range"]#minMatch {
  --range-pct: 44%;
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 7px;
  border-radius: 4px;
  border: none;
  outline: none;
  cursor: pointer;
  margin-top: 10px;
  background: linear-gradient(
    to right,
    #f43f5e 0%,
    #f43f5e var(--range-pct),
    #10b981 var(--range-pct),
    #10b981 100%
  );
  transition: box-shadow .15s ease;
}

input[type="range"]#minMatch:focus-visible {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--cyan) 30%, transparent);
}

/* Webkit (Chrome, Edge, Safari) thumb */
input[type="range"]#minMatch::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: var(--cyan);
  border: 3px solid var(--surface);
  box-shadow: 0 0 10px rgba(0, 212, 255, .45),
              0 2px 6px rgba(0, 0, 0, .3);
  cursor: pointer;
  transition: box-shadow .2s ease, transform .15s ease;
}

input[type="range"]#minMatch::-webkit-slider-thumb:hover {
  box-shadow: 0 0 16px rgba(0, 212, 255, .65),
              0 2px 8px rgba(0, 0, 0, .4);
  transform: scale(1.15);
}

input[type="range"]#minMatch:active::-webkit-slider-thumb {
  transform: scale(1.05);
}

/* Firefox thumb */
input[type="range"]#minMatch::-moz-range-thumb {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--cyan);
  border: 3px solid var(--surface);
  box-shadow: 0 0 10px rgba(0, 212, 255, .45),
              0 2px 6px rgba(0, 0, 0, .3);
  cursor: pointer;
}

input[type="range"]#minMatch::-moz-range-track {
  height: 7px;
  border-radius: 4px;
  background: transparent;
  border: none;
}

/* Light theme tweaks */
[data-theme="light"] input[type="range"]#minMatch::-webkit-slider-thumb {
  box-shadow: 0 0 8px rgba(0, 180, 220, .3),
              0 2px 6px rgba(0, 0, 0, .15);
}
[data-theme="light"] input[type="range"]#minMatch::-webkit-slider-thumb:hover {
  box-shadow: 0 0 14px rgba(0, 180, 220, .45),
              0 2px 8px rgba(0, 0, 0, .2);
}

/* ── Tick labels beneath the track ─────────────────────────── */
.jf-range-ticks {
  display: flex;
  justify-content: space-between;
  padding: 4px 2px 0;
  font-family: 'JetBrains Mono', monospace;
  font-size: .58rem;
  color: var(--text-dim);
  opacity: .45;
  pointer-events: none;
  user-select: none;
}

/* ══ PROGRESSIVE CARD ENTRANCE ═══════════════════════════════ */
@keyframes cardEnter {
  from {
    opacity: 0;
    transform: translateY(22px) scale(.97);
  }
  to {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

.job-enter {
  animation: cardEnter .4s cubic-bezier(.22, 1, .36, 1) both;
}

/* ══ MODERN PASS — layout rhythm and interaction ══
   Appended rather than edited in place so each override sits next to the
   reason for it. Everything here is spacing, radius and motion; none of
   it changes what the page says. */

/* An arrow that leans into the hover. Decorative, so it is generated
   rather than written into the markup where a screen reader would read
   "Apply →" as two things. */
.job-view::after {
  content: '→';
  transition: transform var(--dur-base) var(--ease-out);
}

.job-view:hover::after,
.job-view:focus-visible::after {
  transform: translateX(3px);
}

@media (max-width: 460px) {
  .job-grid { grid-template-columns: 1fr; }
  .job { padding: 18px 16px 16px; }
  .job-facts li { grid-template-columns: 62px 1fr; }
}

/* A sticky filter bar was tried here and removed. This one is two rows of
   controls tall; pinned to the top it covered the first row of results
   and clipped their titles, which costs more than the scroll it saves.
   The search field keeping up with typing — see SEARCH_DEBOUNCE_MS in
   app.js — is what actually made filtering feel immediate. */

/* Anyone who has asked the system for less motion gets the content
   without the choreography, including the staggered entry the scan
   would otherwise run every few seconds. */
@media (prefers-reduced-motion: reduce) {
  .job-enter { animation: none; }
  .job:hover { transform: none; }
  .job-view::after { transition: none; }
}

/* ══ FOCUS ══
   Keyboard focus has to be visible on every new control this page added,
   and the ring has to clear the rounded corners rather than cut them. */
.jobs-pagination button:focus-visible,
.job-more:focus-visible,
.job-view:focus-visible {
  outline: 2px solid var(--cyan);
  outline-offset: 2px;
}

/* A card is not focusable itself, but it contains the controls that are,
   and lifting it on focus-within tells the keyboard user which card they
   are standing in — the same signal the mouse gets from hover. */
.job:focus-within {
  border-color: color-mix(in srgb, var(--cyan) 55%, transparent);
  box-shadow: 0 18px 40px -26px var(--cyan-glow);
}

@media (prefers-reduced-motion: reduce) {
  .jobs-freshness.is-scanning .jobs-pulse { animation: none; }
  .sk { animation: none; }
}

/* The status box is already a fixed two-line panel at every width, so
   the narrow-screen rule that used to reshape a pill into one is gone. */

/* The headline is short enough to hold one line in the sidebar at every
   width, so the phone override that used to let it wrap is gone. It is
   kept as a floor only: below this the container is the viewport, and
   7.5cqi of a 320px screen is smaller than a headline should ever be. */
@media (max-width: 480px) {
  .jobs-title { font-size: clamp(1.7rem, 9cqi, 2.4rem); }
}

/* Two tabs at their natural width need about 410px, which a phone does
   not have — they overflowed the row rather than wrapping, because a
   flex row does not wrap unless told to. Sharing the row evenly keeps
   both visible and both tappable; the label truncates before the count
   does, since the count is the part being compared. */
@media (max-width: 560px) {
  .jobs-tabs {
    gap: 4px;
  }

  .jobs-tab {
    flex: 1 1 0;
    min-width: 0;
    justify-content: center;
    gap: 6px;
    padding: 10px 8px;
    font-size: .98rem;
  }

  .jobs-tab-text {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .jobs-n {
    flex-shrink: 0;
  }
}

/* ── Narrow ──
   Below this the row has to become a card again: four fact columns and a
   single line of tags stop fitting, and a nowrap title truncates to
   nothing useful. */
@media (max-width: 760px) {
  .job-facts {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 12px 16px;
  }

  .job-tags {
    flex-wrap: wrap;
    max-height: 48px;
    overflow: hidden;
  }

  .job-title {
    white-space: normal;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
  }

  .job-foot {
    flex-direction: column;
    align-items: stretch;
    gap: 12px;
  }

  .job-meta {
    white-space: normal;
  }

}

@media (max-width: 460px) {
  .job-head {
    flex-wrap: wrap;
  }

  .job-score {
    width: 100%;
  }
}

/* ══ RESULTS HEADER ══
   Tabs and search ride together at the top of the results column while
   the list scrolls under them. On a list that keeps growing while it is
   being read, scrolling back up to switch region or type a word was the
   last friction left.

   It needs an opaque background, not a translucent one: cards passing
   underneath are the same surface colour as this bar, and through a blur
   they read as a smear rather than as content going behind a header. */
.jobs-sticky {
  position: sticky;
  /* Flush against the fixed site header, not 16px below it. Pinned at
     73px + 16px the bar left a 16px channel between the two, and cards
     scrolling past showed through it — a sliver of a job title sliding
     above the tabs. The air is kept as padding inside the bar instead, so
     the background covers the gap and the tabs still start at 89px, level
     with the panel in the other column. */
  top: 73px;
  z-index: 20;
  margin: 0 0 18px;
  padding-top: 16px;
  /* The bar's own closing edge, below the search field rather than on it,
     so the field keeps its border and the header keeps a clean bottom
     line for cards to pass under. */
  padding-bottom: 14px;
  border-bottom: 1px solid var(--border);
  background: var(--bg);
}

/* Below the width where the layout is one column, the sidebar is already
   static and a second pinned bar would take a third of a phone screen. */
@media (max-width: 1000px) {
  .jobs-sticky {
    position: static;
    background: transparent;
  }
}

/* ── The closing date ──
   Part of the provenance line, but the only part of it that is a
   constraint rather than a fact about the past — so it is set apart, and
   lit when it is close enough to matter. */
.job-deadline {
  color: var(--text);
}

.job-deadline.is-urgent {
  color: #f0a830;
  font-weight: 600;
}

[data-theme="light"] .job-deadline.is-urgent {
  color: #a8681a;
}

/* ── Applied ──
   A stronger state than viewed, so a stronger colour: green, which on
   this page already means "this one is good" — a job you acted on earns
   it more than one you glanced at. Amber stays for viewed, cyan for
   untouched, and the three are distinguishable at a glance down a list. */
.job.is-applied {
  --seen: var(--green);
  border-color: color-mix(in srgb, var(--green) 42%, transparent);
  background:
    radial-gradient(120% 100% at 0% 0%,
      color-mix(in srgb, var(--green) 10%, transparent) 0%, transparent 48%),
    linear-gradient(150deg,
      color-mix(in srgb, var(--green) 6%, var(--surface)) 0%, var(--surface) 62%);
}

.job.is-applied::before {
  background: linear-gradient(to right,
    var(--green), color-mix(in srgb, var(--green) 30%, transparent) 65%, transparent);
  opacity: 1;
}

.job.is-applied:hover {
  border-color: color-mix(in srgb, var(--green) 72%, transparent);
  box-shadow: 0 18px 40px -26px color-mix(in srgb, var(--green) 45%, transparent);
}

/* Unlike viewed, nothing is dimmed: an application in flight is the most
   relevant row on the page, not the least. */
.job.is-applied .job-mark {
  opacity: 1;
}

.job-seen.is-applied-badge {
  border-color: color-mix(in srgb, var(--green) 50%, transparent);
  background: color-mix(in srgb, var(--green) 16%, transparent);
  color: var(--green);
}

/* ── Writing in progress ──
   Beside the panel heading rather than over the textarea: the assembled
   draft is already there and readable, so this reports that a better one
   is coming, not that nothing has arrived. */
.ap-writing {
  margin-left: 10px;
  font-family: 'JetBrains Mono', monospace;
  font-size: .58rem;
  letter-spacing: 1.2px;
  text-transform: uppercase;
  color: var(--cyan);
  animation: jobsPulse 1.4s ease-in-out infinite;
}

.ap-writing[hidden] { display: none; }

.ap-written {
  color: var(--cyan);
}
