:root {
  --border: #e2e2e2;
  --text-muted: #666;
  --accent: #2451ff;

  /* Round 146, per explicit request ("make fonts bigger or use a font size
     that can adjust depending on the platform"). Every font-size in this
     file is now in rem rather than px, so all of them scale together from
     this one root size -- and, just as importantly, they now RESPECT THE
     READER'S OWN TEXT-SIZE SETTING. 100% means "whatever this browser's
     default is," which is 16px until someone changes it in their OS or
     browser accessibility settings; a px-based stylesheet ignores that
     preference entirely, which is the accessibility bug this also fixes.
     The conversion bumped the small end of the scale up a step as well
     (11px -> 12, 12 -> 13, 13 -> 14, 14 -> 15), since the 11-13px range
     this file leaned on is genuinely too small on a phone. Headings kept
     their sizes. The media queries below then raise the root itself on
     smaller screens, so a phone gets larger type across the board without
     any per-element overrides. */
  font-size: 100%;
}

/* Tablet and small-laptop widths: a step up from the desktop base. */
@media (max-width: 860px) {
  :root { font-size: 106.25%; }  /* ~17px */
}

/* Phones: another step. Combined with the rem conversion above, body copy
   that was 13px on desktop lands at ~15.75px here. */
@media (max-width: 640px) {
  :root { font-size: 112.5%; }   /* ~18px */
}

* { box-sizing: border-box; }

/* Several elements below (.auth-form, .header-account-logout, .filters)
   set their own `display` property for layout (flex, contents, etc.).
   Since an author stylesheet rule always beats the browser's built-in
   `[hidden] { display: none }` default at equal specificity, those rules
   were silently overriding the `hidden` attribute app.js toggles via JS
   -- meaning the sign-in form, the "Log out" group, and the saved-
   searches panel never actually visually hid, even though the
   underlying login/logout/session logic was working correctly. This is
   the fix: !important guarantees `hidden` always wins over any other
   display rule, present or future. */
[hidden] {
  display: none !important;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  max-width: 1200px;
  margin: 0 auto;
  padding: 32px 20px 80px;
  color: #111;
}

header h1 { margin-bottom: 4px; }

.logo {
  display: block;
  height: 80px;
  width: auto;
}
.subtitle { color: var(--text-muted); margin-top: 0; }

/* Round 157, per request: "page titles should be in a heading style and
   centered on the page."

   These were <p class="subtitle"> next to the logo -- styled as muted body
   text and left-aligned, which made the name of the page you were on the
   least prominent thing in the header. They are now <h2 class="page-title">,
   which is also the correct semantics: every page has exactly one <h1> (the
   logo, linking home) and this is the next level down, so screen readers and
   the document outline now agree with what the page looks like.

   index.html is the deliberate exception. Its line is a tagline describing
   the board ("Filtering N tech and tech-adjacent postings...") rather than a
   page title, so it stays a muted <p>. Setting a full-width sentence at
   heading size would have made the busiest page in the app shout its own
   subtitle. It is centred with everything else. */
.header-brand {
  text-align: center;
}

.header-brand .logo {
  margin-left: auto;
  margin-right: auto;
}

.page-title {
  margin: 6px 0 0;
  font-size: 1.5rem;
  font-weight: 600;
  color: #111;
  letter-spacing: -0.01em;
}

/* Round 145, per explicit request ("put my profile, etc right at the
   top corner in the same row as the hamburger menu ... this looks ok
   now everywhere but the index page"): replaces the old .header-top/
   .header-left pair, which stacked the hamburger ABOVE the logo+
   subtitle inside one flex-direction:column .header-left block, with
   .header-right sitting alongside that whole stacked block in a
   flex-wrap row. That worked by coincidence on the 8 plain pages (short
   or no subtitle, header-right holding only "My profile") but broke on
   index.html specifically: its long subtitle ("Filtering N tech and
   tech-adjacent employment postings...") plus the logged-out sign-in
   FORM in header-right gave the row a large enough combined preferred
   width to force header-right onto its own line below the whole
   header-left block -- landing well below the hamburger, not beside it.
   Splitting into a dedicated .header-topbar (hamburger + .header-right,
   nothing else) below is a structural guarantee instead of a width-
   dependent coincidence: the account/profile info is now ALWAYS in the
   literal same flex row as the hamburger button, on every page,
   regardless of subtitle length or which auth-state markup
   .header-right happens to be showing. .header-brand (logo + subtitle)
   moved to its own row underneath, full width, unaffected by whatever
   .header-right needs. */
.header-topbar {
  display: flex;
  align-items: center;
  gap: 20px;
  flex-wrap: wrap;
}

.header-brand {
  margin-top: 12px;
}

.nav-menu {
  position: relative;
}

.hamburger-btn {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: 4px;
  width: 40px;
  height: 40px;
  padding: 0;
  background: white;
  border: 1px solid var(--border);
  border-radius: 6px;
  cursor: pointer;
}

.hamburger-btn:hover {
  background: #f5f5f5;
}

.hamburger-btn span {
  display: block;
  width: 18px;
  height: 2px;
  background: #111;
}

.nav-menu-dropdown {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  display: flex;
  flex-direction: column;
  min-width: 150px;
  padding: 6px;
  background: white;
  border: 1px solid var(--border);
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  z-index: 20;
}

.nav-menu-dropdown a {
  padding: 8px 10px;
  border-radius: 6px;
  color: #111;
  font-size: 0.9375rem;
  text-decoration: none;
}

.nav-menu-dropdown a:hover {
  background: #f3f4f6;
}

.header-right {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 10px;
  /* Round 144, per bug report ("that stuff is now on the left when
     logged out"): .header-top used to rely on `justify-content:
     space-between` to keep .header-left and .header-right pinned to
     opposite ends. That works fine when both fit on one line -- but on
     index.html specifically, the logged-out state shows a whole sign-in
     FORM (email input + button) in .header-right, which is often wide
     enough to force .header-top's flex-wrap to drop .header-right onto
     its own line below .header-left. Once .header-right is alone on a
     line, `justify-content: space-between` has nothing to distribute
     space "between" -- a lone flex item on a space-between line renders
     flush at the START of that line, i.e. flush LEFT, not right. Fixing
     it with `margin-left: auto` instead: an auto margin on a flex item
     consumes all its line's free space before justify-content is even
     applied, so .header-right ends up pinned to the right edge of
     whichever line it's on, whether that's shared with .header-left or
     its own line after wrapping. (.header-top's own justify-content:
     space-between was removed above since it's now redundant --
     margin-left: auto already produces the same result for the normal
     two-item, single-line case, and was the literal cause of this bug
     in the wrapped case.) */
  margin-left: auto;
}

/* Round 146: .header-right-inline was removed here (it was only ever
   applied on index.html). It set flex-direction: row so #header-account
   and the logged-out sign-in FORM (#auth-widget) could sit side by side --
   but those are mutually exclusive states, one of them is always hidden,
   so there was never a second visible item to lay out beside the first.
   Worse, #auth-widget's wrapper was never hidden on sign-in (only its
   children were), so it stayed in this flex row as an empty 260px-wide
   box and pushed the account block ~300px in from the right edge on
   index.html -- the reason that page's "My profile | Log out" didn't sit
   in the corner the way every other page's does. app.js now hides the
   wrapper as a unit and index.html uses a plain .header-right, so all
   nine pages share one layout. */

/* Round 144: the whole "My profile [| Log out]" + "Signed in as X"
   block, stacked -- see the module-level comment above for why this
   replaced Round 141/142's single-row layout. flex-direction: column so
   the links row sits above the signed-in-as line; align-items: flex-end
   keeps both rows right-aligned against each other and against the rest
   of .header-right. */
.header-account {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 2px;
}

/* The "My profile [| Log out]" row itself. */
.header-account-links {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* Wraps the "|" divider and the Log out button so both can be shown/
   hidden together (see nav.js, app.js, and resume-match.html's own
   script, all of which toggle this alongside #auth-logged-in below).
   display: contents makes this wrapper invisible as a box -- its
   children lay out as if they were direct children of
   .header-account-links, participating in that flex row's own gap --
   while still letting a single `hidden` attribute here hide both at
   once. (The project's existing `[hidden] { display: none !important }`
   rule, see the top of this file, still applies correctly to a
   display:contents element and hides its children along with it, same
   as any other display value.) */
.header-account-logout {
  display: contents;
}

.header-account-divider {
  color: var(--text-muted);
  font-size: 0.9375rem;
}

/* Round 144, per explicit request ("make sure my profile and log out
   have the same font size"): reuses .profile-link's own font-size/
   weight/color rules verbatim by sharing that class on the <button>
   (see the header markup) rather than duplicating the values here where
   they could quietly drift out of sync again later. This rule only
   strips the <button>-specific chrome (background, border, default
   padding) that .profile-link alone doesn't need to reset since it's
   already an <a>. */
.header-logout-btn {
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  font: inherit;
}

/* The "Signed in as X" line, now on its own row below
   .header-account-links -- see the module-level comment above. Was
   previously .auth-logged-in, which held the logout button too (see
   Round 140); the button moved up into .header-account-links (Round
   144), so this is plain text now, no flex layout needed. */
.header-signed-in-as {
  margin: 2px 0 0;
  font-size: 0.875rem;
  color: var(--text-muted);
}

.profile-link {
  font-size: 0.9375rem;
  font-weight: 600;
  color: var(--accent);
  text-decoration: none;
}

.profile-link:hover {
  text-decoration: underline;
}

.auth-widget {
  min-width: 260px;
  text-align: right;
}

.auth-form {
  display: flex;
  gap: 6px;
  justify-content: flex-end;
  flex-wrap: wrap;
}

.auth-form input {
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 7px 10px;
  font-size: 0.875rem;
  width: 190px;
}

.auth-form button {
  background: var(--accent);
  color: white;
  border: none;
  border-radius: 6px;
  padding: 7px 12px;
  font-size: 0.875rem;
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
}

.auth-form button:disabled {
  opacity: 0.6;
  cursor: default;
}

.auth-status {
  font-size: 0.8125rem;
  margin: 6px 0 0;
  color: var(--text-muted);
}

.auth-status.auth-status-error { color: #b3261e; }
.auth-status.auth-status-success { color: #1a7f4b; }

/* Round 119: small inline spinner shown next to .auth-status text while a
   long-running request is in flight (originally added for resume upload,
   which can now take a while when local LLM skill extraction is
   configured -- see app/resume_llm.py). Generic enough to reuse for any
   other status message that needs the same treatment. Hidden by default;
   toggle the "hidden" attribute rather than a class, same pattern the
   rest of this project uses for show/hide. */
.status-spinner {
  display: inline-block;
  width: 12px;
  height: 12px;
  margin-right: 6px;
  border: 2px solid var(--border);
  border-top-color: var(--accent);
  border-radius: 50%;
  vertical-align: -2px;
  animation: status-spinner-spin 0.7s linear infinite;
}

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

/* Round 126: site-wide "your resume matches are ready" banner -- see
   nav.js for how/why this gets inserted at the very top of the page
   (as document.body's first child), on every page, not just
   resume-match.html. Reuses .status-spinner above for the pending
   state's spinner. */
.site-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 20px;
  padding: 10px 14px;
  border-radius: 6px;
  border: 1px solid var(--border);
  background: #f5f7ff;
  font-size: 0.9375rem;
}

.site-banner a { color: var(--accent); }

.site-banner-success {
  border-color: #1a7f4b;
  background: #eafaf1;
  color: #1a7f4b;
}

.site-banner-error {
  border-color: #b3261e;
  background: #fdecea;
  color: #b3261e;
}

.site-banner-dismiss {
  flex-shrink: 0;
  background: none;
  border: none;
  cursor: pointer;
  font-size: 1.125rem;
  line-height: 1;
  color: inherit;
  padding: 0 4px;
}

.layout {
  display: flex;
  align-items: flex-start;
  gap: 28px;
  margin-top: 24px;
}

.sidebar {
  flex: 0 0 280px;
  width: 280px;
  display: flex;
  flex-direction: column;
  gap: 16px;
  position: sticky;
  top: 20px;
  /* Without a height cap, a sticky element just pins at `top` until its
     containing block scrolls past -- if the sidebar's own content (all the
     filter sections) is taller than the viewport, everything past the fold
     is stuck off-screen until you scroll almost to the bottom of the whole
     page. Capping the height and scrolling internally means the sidebar
     scrolls along with mouse/trackpad input the whole time, immediately. */
  max-height: calc(100vh - 40px);
  overflow-y: auto;
  padding-right: 4px;
}

#results {
  flex: 1;
  min-width: 0;
}

.results-toolbar {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 14px;
  flex-wrap: wrap;
  margin-bottom: 10px;
}

/* Round 111: shown next to the sort control when a signed-in user has a
   saved resume, so it's clear the Recommended view is personalized right
   now, not just showing badges with no explanation of why. */
.personalized-note {
  font-size: 0.8125rem;
  color: var(--text-muted);
  margin: 0;
}

.personalized-note a {
  color: var(--accent);
}

.sort-label {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 0.875rem;
  color: var(--text-muted);
}

.sort-label select {
  padding: 6px 8px;
  border: 1px solid var(--border);
  border-radius: 6px;
  font-size: 0.875rem;
}

/* Round 146, per explicit request ("search and filters should be expanded
   to fit the whole page. results should fit the whole page below that").
   The stacking rule below already existed -- but on a real phone it was
   never reached, because none of the pages carried a viewport meta tag, so
   mobile browsers laid out against a ~980px virtual viewport and this
   max-width:860px query simply never matched. That tag is now on all ten
   pages (see any page's <head>), which is what actually makes this block
   fire. What's added here is the rest of "fit the whole page": the sidebar
   drops its sticky-scroll behavior (pointless once it's a full-width block
   at the top of the document rather than a column beside the results),
   both regions are explicitly full width, and the horizontal breathing
   room shrinks so the content uses the screen instead of the gutters. */
@media (max-width: 860px) {
  .layout {
    flex-direction: column;
    gap: 20px;
  }
  .sidebar {
    position: static;
    width: 100%;
    flex-basis: auto;
    max-height: none;
    overflow-y: visible;
    /* Only existed to keep the internal scrollbar off the filter
       controls; there's no internal scrollbar in this layout. */
    padding-right: 0;
  }
  #results {
    width: 100%;
  }
  /* The search row is the one thing that should stay wide rather than
     stacking into a narrow column -- it's the primary control. */
  .sidebar .filters:first-child {
    flex-direction: column;
  }
}

@media (max-width: 640px) {
  body {
    /* 20px each side was ~10% of a 390px phone screen spent on gutters. */
    padding: 20px 12px 60px;
  }
  .layout {
    gap: 16px;
    margin-top: 16px;
  }
  /* iOS Safari zooms the whole page in when a focused form control's text
     is smaller than 16px -- which every input and select here was, so
     tapping the search box shoved the layout sideways and left the reader
     to pinch back out. Anything below 1rem gets floored here specifically
     to stop that; it is a behavior fix, not just a size preference. */
  .filters input[type="text"],
  .filters select,
  .auth-form input,
  input[type="email"],
  input[type="text"],
  select,
  textarea {
    font-size: 1rem;
  }
  /* Bigger tap targets to go with the bigger type. */
  .filters input[type="text"],
  .filters select {
    padding: 10px 12px;
  }
  button,
  .secondary-button {
    min-height: 44px;
  }
  /* The logo is 80px tall on desktop, which eats a quarter of a phone
     screen before any content appears. */
  .logo {
    height: 56px;
  }
  .job-card {
    padding: 14px;
  }
}

.filters {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin: 0;
}

.sidebar .filters:first-child {
  flex-direction: column;
  align-items: stretch;
}

.filters input[type="text"],
.filters select {
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: 6px;
  font-size: 0.9375rem;
}

/* Round 161: #q is wrapped in a .tag-entry-field now, same as #location, so
   both inputs sit at auto height inside their own column wrapper rather than
   stretching, and the wrapper carries the flex sizing. */
#q,
#location { flex: 0 0 auto; }

.sidebar #q,
.sidebar #location,
.sidebar #company {
  width: 100%;
  min-width: 0;
}

.sidebar .tag-entry-field { min-width: 0; }

.checkbox {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 0.9375rem;
  color: var(--text-muted);
}

button {
  background: var(--accent);
  color: white;
  border: none;
  border-radius: 6px;
  padding: 8px 18px;
  font-size: 0.9375rem;
  cursor: pointer;
}

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

.secondary-button {
  background: white;
  color: var(--accent);
  border: 1px solid var(--accent);
}

.secondary-button:disabled {
  opacity: 0.6;
  cursor: default;
}

/* Round 142: the resume-match sidebar's "Apply filters" button -- full
   width to match the rest of the sidebar controls (inputs, selects,
   details panels all span the sidebar's 280px), rather than shrinking to
   its text like a normal inline .secondary-button would. */
.apply-filters-button {
  width: 100%;
  text-align: center;
}

.tooltip-wrap {
  display: inline-block;
}

/* Round 160, per report: "expand the space for the saved search description
   to go all the way across that section, and put a hard return between it
   and Email alerts / Edit / Remove."

   This used to be a single horizontal row: label on the left, the three
   controls on the right, sharing one line. The controls carry
   white-space: nowrap and so never yield any width, which left the label
   with whatever was left over of a 280px sidebar -- around 90px in
   practice. A saved search's default label is generated from its filters
   (see describe_params in app/main.py), so it is routinely a long phrase,
   and it was wrapping into a narrow ragged column four or five lines tall.

   Stacking the two is the fix rather than shrinking the controls: the
   label now spans the full width of the sidebar section on its own line,
   and the controls sit on their own line beneath it. That also gives the
   "hard return" the report asked for, as an actual layout break rather
   than a <br> inside the markup. */
.saved-search-item {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 6px;
  padding: 8px 0;
  border-bottom: 1px solid var(--border);
  font-size: 0.875rem;
}

.saved-search-item:last-child { border-bottom: none; }

/* flex: 0 0 auto, not the flex: 1 this had while the item was a row. In a
   column container `flex: 1` grows the LABEL's height to fill the item,
   which pushes the controls to the bottom of a stretched box and reopens
   the gap this change was meant to close. Width now comes from
   align-items: stretch on the parent, which is what "all the way across
   that section" asks for. */
.saved-search-label {
  flex: 0 0 auto;
  min-width: 0;
  overflow-wrap: break-word;
}

/* Dropped white-space: nowrap along with the row layout. It existed to stop
   the controls giving up width to the label when they shared a line; on
   their own line in a 280px sidebar it would instead force a horizontal
   overflow. flex-wrap lets the three controls fall onto a second line on a
   narrow screen instead. */
.saved-search-controls {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
}

.saved-search-controls label {
  display: flex;
  align-items: center;
  gap: 4px;
  font-size: 0.8125rem;
  color: var(--text-muted);
}

/* Round 160, generalised in Round 161: the multi-value entry fields. One set
   of rules now serves both the location filter and the keyword search, since
   they are the same control with different words in them.

   The chips sit OUTSIDE the entry box rather than inside it, deliberately --
   a chips-inside-input control has to fake focus/caret behaviour with JS to
   feel right, and gets fragile the moment a value is long enough to wrap.
   Stacked elements need none of that and wrap naturally in a 280px sidebar.

   Round 161 moved them BELOW the input rather than above it. Above, adding a
   chip pushed that field's input down while its neighbours stayed put, so
   two of these fields side by side drifted out of line with each other and
   with the company dropdown as soon as either had a value. Input first means
   every field's box keeps the same top edge no matter what is in it, which
   is what makes align-items: start hold the row together (see
   .resume-filters-panel's grid). It also reads better: type in the box, see
   what you have added directly beneath it. */
.tag-entry-field {
  display: flex;
  flex-direction: column;
  /* Round 209: these wrappers are <form> elements now (so a phone keyboard's
     return key commits a chip -- see nav.js). Browsers give <form> a default
     bottom margin; without this the two fields would drift apart from every
     other control in the sidebar. */
  margin: 0;
  /* Inherits the flex sizing the bare input used to carry as a direct child
     of .filters, since this wrapper is now the flex item in its place. No
     margin: .filters is a flex container with gap: 8px, which already
     spaces this from the controls around it. */
  flex: 1;
  min-width: 140px;
}

/* Round 199. The index sidebar's inputs get their look from
   `.filters input[type="text"]`, but the profile page's skills field is the
   same control outside .filters, so it needs the same box here. The values
   are copied from that rule deliberately rather than shared with it: this
   is the only other place the control appears, and folding both into one
   selector would change how the sidebar renders for the sake of tidiness. */
.tag-entry-field input[type="text"] {
  /* box-sizing is already border-box globally (the `*` rule at the top). */
  width: 100%;
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: 6px;
  font-size: 0.9375rem;
}

.tag-entry-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 6px;
}

.tag-entry-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: #f0f2ff;
  color: var(--accent);
  border-radius: 4px;
  padding: 3px 6px 3px 8px;
  font-size: 0.8125rem;
  /* A value can be long ("Washington, District of Columbia"); let it wrap
     inside the chip rather than push the sidebar wider. */
  max-width: 100%;
  overflow-wrap: anywhere;
}

.tag-entry-chip-remove {
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  color: inherit;
  font-size: 1rem;
  line-height: 1;
  /* Bigger than it looks: the visible glyph is small, so the button gets
     real height to be tappable on a phone without enlarging the chip. */
  min-width: 18px;
  min-height: 18px;
}

.tag-entry-chip-remove:hover { color: #111; }

/* Round 199. A skill the user typed that is not in the skills catalogue. It
   saves fine -- the server accepts any string -- but it will never match a
   posting, so without a mark the field looks like it worked and silently
   does nothing. Amber rather than red: it is a "did you mean?", not an
   error, and a skill can be legitimately absent from the catalogue simply
   because no currently-listed job mentions it yet. */
.tag-entry-chip-unknown {
  background: #fff4e0;
  color: #8a5300;
}


.tag-entry-hint {
  color: var(--text-muted);
  font-size: 0.75rem;
  line-height: 1.4;
  margin: 4px 0 0;
}

/* Round 203: the subscribe banner on the job board. Quiet on purpose -- it
   sits above every visitor's first view of the board, so it reads as a note
   rather than as an ad, and it is dismissible. Accent-tinted rather than
   loud, matching the chips and the alignment badges. */
.subscribe-banner {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
  /* ROUND 279, per request: light yellow. The border moves with it -- the old
     #d5dbff was a tint of the old blue fill, and leaving it would have put a
     blue outline around a yellow banner. #ede3a1 is the same one-step-darker
     relationship to #fcf6c2 that #d5dbff had to #f0f2ff. */
  background: #fcf6c2;
  border: 1px solid #ede3a1;
  border-radius: 8px;
  padding: 12px 14px;
  margin-top: 20px;
}

.subscribe-banner-text {
  flex: 1 1 320px;
  margin: 0;
  font-size: 0.9375rem;
  line-height: 1.5;
  color: var(--text);
  /* The strong lead is the point; the rest is supporting. */
  min-width: 0;
}

.subscribe-banner-cta {
  flex: 0 0 auto;
  background: var(--accent);
  color: #fff;
  text-decoration: none;
  font-size: 0.9375rem;
  padding: 8px 16px;
  border-radius: 6px;
  white-space: nowrap;
}

.subscribe-banner-cta:hover { filter: brightness(0.94); }

.subscribe-banner-dismiss {
  flex: 0 0 auto;
  background: none;
  border: none;
  cursor: pointer;
  color: var(--text-muted);
  font-size: 1.25rem;
  line-height: 1;
  /* Bigger than the glyph so it is tappable on a phone without enlarging
     the banner -- same reasoning as .tag-entry-chip-remove. */
  min-width: 32px;
  min-height: 32px;
}

.subscribe-banner-dismiss:hover { color: #111; }

/* On a phone this banner sits above the reader's first sight of the board,
   so its height is the whole cost of having it. Measured: the first draft
   ran to 203px -- taller than the entire regrouped filter sidebar beneath
   it, which would have spent the mobile fix on the banner announcing it.
   Smaller type, tighter padding, and the dismiss control pulled out of the
   flow into the corner rather than taking a row of its own. */
/* Round 205: the compact sign-in link that stands in for the header's email
   form on phones.
   Round 207: the link now also carries the `hidden` ATTRIBUTE in the markup,
   and that is the part that matters. Before, "only one of these two controls
   is ever shown" was a fact that lived entirely in this file. A browser or an
   edge cache holding a stale style.css next to a fresh index.html -- which is
   exactly what happened -- showed BOTH the link and the email form, because
   the rule that hides one of them had not arrived. `hidden` moves the default
   into the same file as the markup, so the two cannot be out of step: with no
   CSS at all the page degrades to the desktop layout it had before any of
   this, which is wrong-looking on a phone but never self-contradictory.
   The display:none below is now belt and braces, kept because the rule reads
   as a lie without it. */
.auth-mobile-signin {
  /* No display here. Round 207 moved the show/hide decision into a <style>
     block in index.html, beside the markup -- see the comment there. Two
     copies of it would be exactly the drift this project keeps getting
     caught by. */
  font-size: 0.9375rem;
  font-weight: 600;
  color: var(--accent);
  text-decoration: none;
  /* A tap target, not just a word: 44px is the usual minimum and the link
     is only two syllables wide. */
  padding: 10px 4px;
  min-height: 44px;
  align-items: center;
}

.auth-mobile-signin:hover { text-decoration: underline; }

/* Round 205, per request: the subtitle and the banner text are smaller on a
   phone. Both sit between the top of the page and the first job card, and on
   a 375x667 screen that run was 829px -- the reader scrolled a whole screen
   before seeing a job. Measured savings: subtitle 63px -> 36px, banner
   132px -> 104px. */
@media (max-width: 640px) {
  .subtitle {
    font-size: 0.8125rem;
    line-height: 1.35;
    /* The count line wraps to three lines at 1rem on a 360px screen and two
       at this size. Capping the width stops it stretching to a single long
       line on a slightly wider phone and then re-wrapping unpredictably. */
    max-width: 22em;
    margin-left: auto;
    margin-right: auto;
  }
}

@media (max-width: 640px) {
  .subscribe-banner {
    position: relative;
    gap: 8px;
    padding: 9px 34px 9px 11px;
    margin-top: 12px;
  }
  .subscribe-banner-text {
    flex-basis: 100%;
    /* Round 205: was 0.875rem. */
    font-size: 0.8125rem;
    line-height: 1.4;
  }
  .subscribe-banner-dismiss {
    position: absolute;
    top: 2px;
    right: 2px;
    min-width: 30px;
    min-height: 30px;
  }
  /* Deliberately NOT full width. A block button forces its own row; inline
     it sits under the text and costs about half as much height. */
  .subscribe-banner-cta {
    padding: 6px 13px;
    font-size: 0.8125rem;
  }
}

.saved-searches-empty {
  color: var(--text-muted);
  font-size: 0.875rem;
}

/* Round 160: the saved-search counter under the list. Deliberately quiet
   while there is room left, and only picks up emphasis at the cap, when it
   stops being trivia and becomes the explanation for why the save button
   just went grey. */
.saved-searches-count {
  color: var(--text-muted);
  font-size: 0.8125rem;
  margin: 10px 0 0;
}

.saved-searches-count-full {
  /* #111, not var(--text) -- this stylesheet defines only --border,
     --text-muted and --accent at :root, so var(--text) would resolve to
     nothing and silently inherit. #111 is the literal the rest of the file
     already uses for primary text. */
  color: #111;
  font-weight: 600;
}

.job-card {
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 16px;
  margin-bottom: 12px;
}

.job-card h3 { margin: 0 0 4px; font-size: 1.0625rem; }
.job-card h3 a { color: #111; text-decoration: none; }
.job-card h3 a:hover { text-decoration: underline; }

.job-meta {
  color: var(--text-muted);
  font-size: 0.875rem;
  margin-bottom: 8px;
}

.tag {
  display: inline-block;
  border-radius: 4px;
  padding: 2px 8px;
  font-size: 0.8125rem;
  margin: 2px 4px 0 0;
}

/* Role/category tags (backend, data, product, etc.) vs. seniority tags
   (intern/junior/senior/staff) on a job card share the same base .tag
   layout above but get their own color so they're visually distinguishable
   at a glance -- role green, seniority purple. */
.tag-role {
  background: #e6f7ec;
  color: #1a7f4b;
}

.tag-seniority {
  background: #f2e9fb;
  color: #6a3fa0;
}

.skill {
  display: inline-block;
  background: #fff4e5;
  color: #b3691c;
  border-radius: 4px;
  padding: 2px 8px;
  font-size: 0.8125rem;
  margin: 2px 4px 0 0;
}

.exclusion-chip-list {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 8px 0;
}

.exclusion-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  background: #fdecea;
  color: #9c2c2c;
  border: 1px solid #f3c6c2;
  border-radius: 4px;
  padding: 3px 6px 3px 10px;
  font-size: 0.875rem;
}

.exclusion-chip-remove {
  border: none;
  background: none;
  color: #9c2c2c;
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
  padding: 2px 4px;
}

.exclusion-chip-remove:hover {
  color: #6e1f1f;
}

.exclusion-add-form {
  display: flex;
  gap: 8px;
  margin-top: 8px;
}

.exclusion-add-form input[type="text"] {
  flex: 1;
  padding: 6px 10px;
  border: 1px solid #d0d0d0;
  border-radius: 4px;
  font-size: 0.9375rem;
}

.badge {
  display: inline-block;
  background: #e6f7ec;
  color: #1a7f4b;
  border-radius: 4px;
  padding: 1px 8px;
  font-size: 0.75rem;
  font-weight: 600;
  margin-left: 8px;
}

/* Round 111: same shape as .badge (mission-driven), distinct accent color
   so the two reasons a job is boosted in Recommended read as visually
   different at a glance -- reuses the same blue as .skill-matched /
   .badge-values rather than inventing a third color in this file. */
.badge-resume-match {
  display: inline-block;
  background: #e7f0ff;
  color: var(--accent);
  border-radius: 4px;
  padding: 1px 8px;
  font-size: 0.75rem;
  font-weight: 600;
  margin-left: 8px;
}

.values-filters,
.tag-filters {
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 16px;
  margin-top: 0;
}

.filters summary {
  font-size: 1rem;
  font-weight: 600;
  margin: 0 0 6px;
  cursor: pointer;
  list-style: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  user-select: none;
}

/* Hide the browser's default disclosure triangle so we can draw our own
   consistent chevron that rotates on open, instead of the mismatched
   native marker each browser renders differently. */
.filters summary::-webkit-details-marker { display: none; }
.filters summary::marker { content: ""; }

.filters summary::after {
  content: "▾";
  color: var(--text-muted);
  font-size: 0.8125rem;
  transition: transform 0.15s ease;
}

.filters[open] summary::after {
  transform: rotate(180deg);
}

.filters[open] summary {
  margin-bottom: 10px;
}

/* <details> elements aren't flex containers by default the way the old
   <section class="values-filters"> was, so restate the same box styling
   explicitly on the shared .filters class now that values-filters and
   tag-filters are both <details>. */
details.filters {
  display: block;
}

.filters-note {
  color: var(--text-muted);
  font-size: 0.8125rem;
  margin: 0 0 12px;
  max-width: 640px;
}

.filters-note a {
  color: var(--accent);
}

.values-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 8px 16px;
  width: 100%;
}

.link-button {
  background: none;
  border: none;
  color: var(--accent);
  font-size: 0.875rem;
  font-weight: 600;
  cursor: pointer;
  padding: 8px 0 0;
  text-align: left;
}

.link-button:hover { text-decoration: underline; }

/* Round 150: the profile page's per-search "Edit filters" control is an <a>
   (it navigates to the job search page, which owns the filter sidebar) but
   has to sit level with the real <button>s beside it. Anchors underline by
   default and buttons don't -- the :hover rule above already supplies the
   underline for both, so this just removes the resting one. */
a.link-button {
  text-decoration: none;
  display: inline-block;
}

/* Round 152, per report: "links appear to be aligned slightly below the
   other text."

   .link-button carries `padding: 8px 0 0` because most of its uses are
   standalone controls sitting under a block of content ("Show 12 more"
   under a tag grid, "Log out" at the foot of the profile page), where that
   top padding is the gap that separates them from what's above.

   Inside .saved-search-controls it is not that. That's a flex row with
   align-items: center, so each item's BOX is centered -- and 8px of top
   padding pushes the text down inside a box that is itself still centered,
   landing the "Edit filters" and "Remove" text about 4px below the "Email
   alerts" beside it. Measured, not eyeballed: the label's text center sat
   at 650.6px and the two controls' at ~654.6px.

   Zeroing the padding fixes the offset; matching the font size to the
   label's 0.8125rem fixes the rest, since align-items: center aligns box
   centers rather than baselines and two different font sizes centered
   against each other still read as misaligned. */
.saved-search-controls .link-button {
  padding: 0;
  font-size: 0.8125rem;
  line-height: 1.45;
}

.job-badges {
  margin-bottom: 8px;
}

.badge-values {
  display: inline-block;
  background: #f0f2ff;
  color: var(--accent);
  border-radius: 4px;
  padding: 1px 8px;
  font-size: 0.75rem;
  font-weight: 600;
  margin: 0 6px 4px 0;
}

/* Round 117: political alignment score badge on job cards -- same shape
   as .badge/.badge-resume-match again, but a third distinct accent
   (purple) since this is a different kind of signal (a computed score,
   not a single boolean flag) and needs to read as visually separate from
   both the mission-driven and resume-match badges. */
.badge-alignment-score {
  display: inline-block;
  background: #f5edff;
  color: #6b2fd6;
  border-radius: 4px;
  padding: 1px 8px;
  font-size: 0.75rem;
  font-weight: 600;
  margin-left: 8px;
}

/* Round 117: the "minimum alignment score" filter is a label wrapping a
   select, styled like a compact form field rather than a full-width
   .filters select -- keeps it visually grouped with its own text label
   inside the Values filters <details> instead of floating unlabeled. */
.alignment-score-filter {
  display: flex;
  flex-direction: column;
  gap: 6px;
  font-size: 0.875rem;
  color: var(--text-muted);
  margin: 0 0 12px;
}

.alignment-score-filter select {
  padding: 6px 8px;
  border: 1px solid var(--border);
  border-radius: 6px;
  font-size: 0.9375rem;
}

.empty, .loading {
  color: var(--text-muted);
  text-align: center;
  padding: 40px 0;
}

.pagination {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 16px;
  padding: 24px 0 8px;
}

.pagination button {
  background: #fff;
  color: var(--accent);
  border: 1px solid var(--accent);
  padding: 6px 14px;
  font-size: 0.875rem;
  border-radius: 4px;
  cursor: pointer;
}

.pagination button:hover:not(:disabled) {
  background: var(--accent);
  color: #fff;
}

.pagination button:disabled {
  color: var(--text-muted);
  border-color: var(--border);
  cursor: default;
}

.pagination-status {
  color: var(--text-muted);
  font-size: 0.875rem;
}

.plain-link {
  color: inherit;
  text-decoration: none;
}

.plain-link:hover {
  text-decoration: underline;
}

.faq-page {
  max-width: 760px;
  margin: 0 auto;
}

/* Signup page's Free vs Paid comparison (Round 101). */
.plan-comparison {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 20px;
  margin-top: 8px;
}

@media (max-width: 640px) {
  .plan-comparison {
    grid-template-columns: 1fr;
  }
}

.plan-card {
  position: relative;
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 24px;
}

.plan-card h2 {
  font-size: 1.25rem;
  margin: 0 0 4px;
}

.plan-price {
  color: var(--text-muted);
  font-size: 1rem;
  margin: 0 0 16px;
}

.plan-tagline {
  font-weight: 600;
  margin: 0 0 8px;
}

.plan-features {
  margin: 0 0 20px;
  padding-left: 20px;
  line-height: 1.6;
}

.plan-features li {
  margin-bottom: 6px;
}

.plan-signup-form {
  margin-top: auto;
  /* Round 153: .auth-form is right-aligned because its original home is the
     page header, where it sits at the end of a row. Inside a plan card it
     reads as a stray right-aligned block against left-aligned copy, which
     became visible once the sign-in form moved into the subscriber card. */
  justify-content: flex-start;
  align-items: center;
}

.plan-card-paid {
  background: #fafafa;
}

.plan-badge {
  position: absolute;
  top: 20px;
  right: 20px;
  background: var(--text-muted);
  color: #fff;
  font-size: 0.75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 3px 8px;
  border-radius: 12px;
}

.plan-note {
  color: var(--text-muted);
  font-size: 0.875rem;
  line-height: 1.6;
  margin: 0;
}

/* Round 160, per report: the signup page's plan card wanted "a hard return
   before the On the roadmap header" and "a return between" the two closing
   paragraphs. Both are already separate <p> elements, so what was missing
   was space, not markup -- .plan-note's margin:0 (right when a note sits
   tight under the list it annotates) meant two consecutive notes rendered
   as one unbroken block of grey text, and a following .plan-tagline heading
   butted straight up against the note above it.

   Written as sibling combinators rather than one-off classes so the rule
   describes the situation ("a note directly after another note", "a heading
   directly after a note") and holds for any future copy added to these
   cards, instead of needing a new class each time. A note that follows a
   list still gets margin:0, which is the case the original rule was for. */
.plan-note + .plan-note {
  margin-top: 14px;
}

.plan-note + .plan-tagline {
  margin-top: 20px;
}

/* Profile page (Round 101). */
.profile-section {
  margin-bottom: 28px;
}

.profile-section h2 {
  font-size: 1.0625rem;
  margin-bottom: 8px;
}

.profile-field {
  margin: 0 0 6px;
}

.profile-field .label {
  color: var(--text-muted);
  font-size: 0.875rem;
  display: inline-block;
  min-width: 90px;
}

.profile-plan-badge {
  display: inline-block;
  background: #eef1ff;
  color: var(--accent);
  font-size: 0.8125rem;
  font-weight: 600;
  padding: 2px 8px;
  border-radius: 10px;
}

/* Round 187: applied when the account actually has the subscriber features,
   whether that came from paying, the access list, or a grandfathered
   account -- so it tracks has_full_access, not plan == "paid". Green rather
   than the default blue-grey so "you have this" reads at a glance without
   needing the words next to it. */
.profile-plan-badge-paid {
  background: #e6f6ec;
  color: #1a7f4b;
}


.faq-back {
  margin-bottom: 24px;
}

.faq-back a {
  color: var(--accent);
  text-decoration: none;
  font-size: 0.9375rem;
}

.faq-back a:hover {
  text-decoration: underline;
}

.trends-page {
  max-width: 900px;
  margin: 0 auto;
}

.trends-intro {
  color: var(--text-muted);
  line-height: 1.6;
  margin-bottom: 28px;
}

.trends-section {
  margin-bottom: 40px;
}

.trends-section h2 {
  font-size: 1.125rem;
  margin-bottom: 4px;
}

.trends-note {
  color: var(--text-muted);
  font-size: 0.875rem;
  margin-top: 0;
  margin-bottom: 16px;
}

/* Round 147, per report ("graphs look either stretched or squished on
   mobile"). Chart.js sizes a responsive chart from its container once
   maintainAspectRatio is off (see trends.js), so height is decided here --
   deliberately, because the right height for these charts does not scale
   with width. A phone column is ~366px wide; at Chart.js's default 2:1 that
   produced a 183px-tall chart with a legend, axis labels and several series
   crammed into it, which is the "squished" report. Fixed heights that get
   TALLER on a narrow screen (not shorter) is the correct relationship: less
   width means labels wrap and series need more vertical room to stay
   readable, not less.

   position: relative is required, not cosmetic -- Chart.js measures the
   parent's content box to size the canvas, and without a positioned
   ancestor a resize can feed the canvas's own growing height back into that
   measurement and ratchet the chart taller on every reflow. */
.trends-chart-wrap {
  position: relative;
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 16px;
  height: 380px;
}

/* The by-country chart is a horizontal bar per country, so its height has
   to grow with the number of bars rather than with the viewport. */
.trends-chart-wrap-tall {
  height: 480px;
}

.trends-chart-wrap-tall canvas {
  height: 100% !important;
}

@media (max-width: 860px) {
  .trends-chart-wrap { height: 400px; }
  .trends-chart-wrap-tall { height: 520px; }
}

@media (max-width: 640px) {
  .trends-chart-wrap {
    /* Taller than it is wide on a phone, and less padding so the plot area
       keeps as much of the screen as possible. */
    height: 420px;
    padding: 10px;
  }
  .trends-chart-wrap-tall { height: 560px; }
}

.trends-empty {
  color: var(--text-muted);
  font-style: italic;
  padding: 24px 0;
  text-align: center;
}

.faq-item {
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0 20px;
  margin-bottom: 12px;
}

.faq-item:hover {
  border-color: var(--accent);
}

/* Collapsible FAQ questions, built on native <details>/<summary> --
   no JS needed for the expand/collapse itself (only for opening the
   right item when a page is loaded with a #hash, see faq.html's inline
   script). Chrome/Safari/Firefox all support styling the default
   disclosure triangle away and drawing our own +/- indicator instead. */
.faq-question {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 18px 0;
  cursor: pointer;
  list-style: none;
}

.faq-question::-webkit-details-marker {
  display: none;
}

.faq-question::after {
  content: "+";
  flex-shrink: 0;
  font-size: 1.375rem;
  font-weight: 400;
  line-height: 1;
  color: var(--text-muted);
}

.faq-item[open] .faq-question::after {
  content: "\2212";
}

.faq-question h2 {
  font-size: 1.125rem;
  line-height: 1.4;
  margin: 0;
}

.faq-answer p {
  margin: 0 0 12px;
  line-height: 1.6;
}

.faq-answer ul {
  margin: 0 0 12px;
  padding-left: 22px;
  line-height: 1.6;
}

.faq-answer li {
  margin-bottom: 6px;
}

.faq-answer {
  padding-bottom: 20px;
}

.correction-notice {
  font-weight: 600;
  background: #fff8e1;
  border: 1px solid #f0d98c;
  border-radius: 6px;
  padding: 10px 12px;
  display: inline-block;
}

.correction-form {
  display: flex;
  flex-direction: column;
  gap: 6px;
  max-width: 480px;
}

.correction-form label {
  font-size: 0.875rem;
  font-weight: 600;
  margin-top: 10px;
}

/* Round 140 bugfix: this used to be plain ".correction-form input" with
   no type qualifier, which meant it ALSO matched the two "save my resume"
   checkboxes on resume-match.html (a <label class="save-resume-checkbox-
   label"><input type="checkbox">...) -- a checkbox is still an <input>,
   and CSS doesn't care that the border/padding/full-width styling below
   was only ever meant for the page's text inputs and textarea. The
   visible bug this caused: those checkboxes rendered as full-width,
   bordered, padded boxes instead of an actual checkbox -- reported as
   "checkbox options ... out of alignment." :not([type="checkbox"])
   excludes them here instead; .save-resume-checkbox-label's own rule
   further down still governs how the checkbox actually looks. */
.correction-form input:not([type="checkbox"]),
.correction-form textarea {
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 8px 10px;
  font-size: 0.9375rem;
  font-family: inherit;
  width: 100%;
}

.correction-form textarea {
  resize: vertical;
}

.correction-form button {
  background: var(--accent);
  color: white;
  border: none;
  border-radius: 6px;
  padding: 9px 14px;
  font-size: 0.9375rem;
  font-weight: 600;
  cursor: pointer;
  margin-top: 14px;
  align-self: flex-start;
}

.correction-form button:disabled {
  opacity: 0.6;
  cursor: default;
}

/* Round 149: bulleted lists inside an FAQ answer (the ICE-contracts answer
   uses one to separate the three ways a company can fail that filter).
   The FAQ's other answers are all prose, so there was no list styling. */
.faq-list {
  margin: 8px 0 12px;
  padding-left: 22px;
  line-height: 1.55;
}

.faq-list li {
  margin-bottom: 8px;
}

/* Round 148: the explanation that replaces a disabled "Apply filters to my
   matches" button. Sits immediately under it so the reason is where the
   click would have been. */
.apply-filters-note {
  margin: 8px 0 0;
}

.apply-filters-button:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

/* Round 147: the saved search currently loaded into the sidebar for
   editing. Marked visually because the filters no longer describe "what I
   am browsing" but "what this saved search will become when I press
   Update" -- without the connection being visible, the changed button label
   is the only clue, and it is at the other end of the sidebar. */
.saved-search-item-editing {
  border-left: 3px solid var(--accent);
  padding-left: 8px;
  background: #f5f7ff;
}

/* Round 147: the alignment-score breakdown table in the FAQ. Replaces
   nine paragraphs of prose that each described one weight -- a table is
   simply the right shape for "here is a list of things and what each is
   worth," and it makes the true-only half of the list visible at a glance
   (a dash in the "confirmed false" column) rather than something the
   reader has to hold in their head across several paragraphs. */
.score-table {
  width: 100%;
  border-collapse: collapse;
  margin: 12px 0 8px;
  font-size: 0.9375rem;
}

.score-table th,
.score-table td {
  text-align: left;
  padding: 7px 10px;
  border-bottom: 1px solid var(--border);
}

.score-table thead th {
  font-size: 0.8125rem;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--text-muted);
  border-bottom-width: 2px;
}

.score-table td:nth-child(2),
.score-table td:nth-child(3),
.score-table th:nth-child(2),
.score-table th:nth-child(3) {
  text-align: right;
  white-space: nowrap;
  width: 8.5em;
}

.score-pos { color: #15803d; font-weight: 600; }
.score-neg { color: #b91c1c; font-weight: 600; }
.score-na { color: var(--text-muted); }

.score-table-note {
  font-size: 0.875rem;
  color: var(--text-muted);
  margin: 0 0 20px;
}

.faq-answer h3 {
  font-size: 1.0625rem;
  margin: 22px 0 8px;
}

/* A narrow screen can't fit three columns of this comfortably, and the
   numbers are the part that must stay readable -- so the table scrolls
   sideways inside its own box rather than squeezing the label column into
   one word per line. */
@media (max-width: 640px) {
  .score-table {
    display: block;
    overflow-x: auto;
    white-space: nowrap;
  }
}

/* Round 147, per explicit request ("add a site map at the bottom of all
   pages").

   Round 150, per explicit request ("please center the site map"): was a
   grid of auto-fit 1fr columns, which stretched the three groups edge to
   edge across the full page width and left each one hard against the left
   margin of its own column -- on a wide screen the last group ended up
   marooned in the far right corner, nowhere near the footer text below it.

   Flex with justify-content: center instead of grid, so the groups are
   sized by their content and sit as one centered cluster under the page,
   with the columns' own text centered too. Still no separate breakpoint
   for phones: flex-wrap drops them to a single centered column when there
   isn't room, the same way the auto-fit grid used to. */
.site-map {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: flex-start;
  gap: 24px 56px;
  margin-top: 56px;
  padding-top: 24px;
  border-top: 1px solid var(--border);
  text-align: center;
}

.site-map-group {
  /* Wide enough that a two-word link doesn't wrap mid-phrase, narrow
     enough that three groups still fit one row on a laptop. */
  min-width: 150px;
}

.site-map-group h2 {
  font-size: 0.875rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
  margin: 0 0 8px;
}

.site-map-group ul {
  list-style: none;
  margin: 0;
  padding: 0;
}

.site-map-group li {
  margin-bottom: 6px;
}

.site-map-group a {
  color: #111;
  text-decoration: none;
  font-size: 0.9375rem;
}

.site-map-group a:hover {
  color: var(--accent);
  text-decoration: underline;
}

.site-footer {
  text-align: center;
  padding: 24px 16px;
  color: var(--muted, #6b7280);
  font-size: 0.8125rem;
}

/* Round 109: resume-match page (frontend/resume-match.html) */
.resume-detected-skills {
  margin: 10px 0 0;
}

.resume-detected-skills .skill {
  background: #e7f0ff;
  color: var(--accent);
}

.match-score-pill {
  display: inline-block;
  background: var(--accent);
  color: white;
  border-radius: 999px;
  padding: 1px 9px;
  font-size: 0.75rem;
  font-weight: 600;
  margin-left: 8px;
}

/* A resume-match job card's skill chips get a distinct accent color when
   they're one of the skills we actually detected in the uploaded resume
   (vs. the job's other listed skills, shown the normal muted-orange
   .skill color) -- makes it visually obvious at a glance *why* a job
   matched, not just that it did. */
.skill-matched {
  display: inline-block;
  background: #e7f0ff;
  color: var(--accent);
  border-radius: 4px;
  padding: 2px 8px;
  font-size: 0.8125rem;
  font-weight: 600;
  margin: 2px 4px 0 0;
}

/* Round 152: a matched skill the person is CURRENTLY working with, as
   opposed to one from a role years back. Filled rather than tinted, so at a
   glance a fifteen-year skill list separates into "what I do now" and "what
   I've done" without needing a legend. These count double in the ranking --
   see CURRENT_SKILL_BONUS in app/resume_match.py. */
.skill-current {
  background: var(--accent);
  color: #fff;
}

.resume-results-empty {
  color: var(--text-muted);
  font-size: 0.875rem;
}

/* Round 147, per explicit request ("on the resume matches page, please put
   the filters above the section that will fill with the matches"): the
   resume-match page no longer uses the two-column .layout/.sidebar pair at
   all -- its filters are a full-width panel sitting directly above the
   results (see resume-match.html). Laid out as an auto-fitting grid rather
   than the sidebar's single stacked column, so the extra width is actually
   used on a desktop instead of leaving one tall thin column of controls,
   and so it collapses to one column on a phone without a second breakpoint. */
.resume-filters-panel {
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 16px 18px;
  margin: 0 0 20px;
}

.resume-filters-heading {
  font-size: 1.125rem;
  margin: 0 0 12px;
}

/* The plain search/location/company/checkbox row. */
.resume-filters-panel > .filters:first-of-type {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  /* Round 161, per screenshot: was align-items: center, which was correct
     while every control in this row was a single input of the same height.
     Round 160 turned the location filter into a stack (input plus a hint
     line, and now chips too), and centring a 40px select against a 120px
     stack pushed the search box and the company dropdown halfway down the
     row -- visibly out of line with the location box beside them, which is
     exactly what was reported.

     `start` lines every control up on its top edge. It works because the
     multi-value fields put their INPUT first in the stack (see
     .tag-entry-field below), so the three boxes a reader sees as "the row"
     share a top edge whether or not any chips are showing. */
  align-items: start;
  gap: 10px 14px;
}

.resume-filters-panel > .filters:first-of-type input[type="text"],
.resume-filters-panel > .filters:first-of-type select {
  width: 100%;
  min-width: 0;
}

/* The collapsible groups (values / tags / seniority / skills) sit two or
   three across on a wide screen, one across on a narrow one. */
.resume-filters-panel > details.filters {
  display: block;
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 10px 12px;
  margin-top: 12px;
}

.resume-filters-panel .filters-note {
  grid-column: 1 / -1;
}

.resume-filters-panel .apply-filters-button {
  margin-top: 14px;
}

/* Round 147, per report ("the match section gains an opacity overlay and
   doesn't filter the matches"): the dim now always comes with this line
   sitting directly above the results explaining what it's waiting for --
   see setResultsBusy() in resume-match.html for why a bare dim was the
   wrong affordance for an operation that can take minutes. */
.resume-results-busy {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 12px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: #f8f9ff;
  color: var(--text-muted);
  font-size: 0.875rem;
}

/* Round 146, per explicit request ("the matched jobs should be filtered
   and stay displayed on the page"): "Apply filters to my matches"
   re-runs matching as a background task that can take a while, and the
   page used to hide the whole results block for its full duration --
   which looked exactly like the matches had been thrown away. The list
   now stays put and just dims while the refreshed, filtered set is on
   its way. pointer-events:none so a click can't open a job card that's
   about to be replaced by a different result set. */
#resume-results.results-updating {
  opacity: 0.45;
  pointer-events: none;
  transition: opacity 120ms ease-in;
}

/* Round 110: opt-in saved-resume UI (frontend/resume-match.html) */
.save-resume-checkbox-label {
  display: flex;
  align-items: flex-start;
  gap: 6px;
  font-size: 0.875rem;
  color: var(--text-muted);
  margin: 8px 0;
  font-weight: normal;
}

.save-resume-checkbox-label input[type="checkbox"] {
  margin-top: 2px;
}

.saved-resume-uploaded {
  color: var(--text-muted);
  font-size: 0.8125rem;
  font-weight: normal;
}

/* Round 140, per explicit request ("padding at the top and on the bottom
   of the buttons in the saved skills section"): these buttons previously
   only had breathing room on their right (margin-right below, to keep
   them from touching each other), none above or below, so they sat flush
   against the text/rows immediately before and after them. */
#saved-resume-status .secondary-button {
  margin: 10px 8px 10px 0;
}

/* --------------------------------------------------------------------------
   Tag-group search (Round 154)
   --------------------------------------------------------------------------
   Per explicit request: "add a search panel for the skills." The skills
   filter is 185 canonical names and grows every round, so "Show 52 more"
   followed by scanning an alphabetical wall stopped being a way to find
   anything. See TagFilterGroup in frontend/app.js. */
.tag-search {
  display: block;
  margin-bottom: 10px;
}

.tag-search-input {
  width: 100%;
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 7px 10px;
  font-size: 0.875rem;
  font-family: inherit;
}

.tag-search-input:focus {
  outline: 2px solid var(--accent);
  outline-offset: -1px;
}

.tag-search-empty,
.tag-search-note {
  margin: 6px 0 0;
  color: var(--text-muted);
  font-size: 0.8125rem;
}

/* The label text for the search box. Present for screen readers -- a bare
   placeholder is not an accessible name, and it disappears as soon as
   anything is typed -- but visually redundant next to the placeholder and
   the panel heading, so it's removed from the visual flow rather than
   display:none'd, which would take it out of the accessibility tree too. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Round 156: the sliding-scale explanation sits directly under the tagline
   rather than down with the small print, so it needs a little more air above
   it than a .plan-note normally gets. */
.plan-sliding-note {
  margin-top: 4px;
  margin-bottom: 14px;
}

/* Round 157, per report on the About page: "the formatting is a bit messed
   up there too, more space inside the rectangle below the text than above
   it."

   .faq-item sets `padding: 0 20px` -- no vertical padding at all. On the FAQ
   page that is fine, because .faq-item is a <details> whose <summary> brings
   its own padding. The About page reuses the same card WITHOUT a summary, so
   its first paragraph sits flush against the top border while the last
   paragraph's own 12px bottom margin still pushes the border away below.
   Measured, not eyeballed: 0px above, 12px below.

   Scoped with :first-child so it only touches the summary-less cards -- the
   FAQ's own items are unaffected, and zeroing the final margin means the
   18px is the whole story top and bottom rather than padding plus whatever
   margin the last element happens to carry. */
.faq-item > .faq-answer:first-child {
  padding: 18px 0;
}

.faq-item > .faq-answer:first-child > :last-child {
  margin-bottom: 0;
}


/* Round 166: the one button on this site that destroys something. Styled
   apart from every other action deliberately -- it sits at the end of a page
   whose other controls are all blue and safe, and a person about to do
   something irreversible should be able to tell from a glance that this is
   not one of those. Red fill rather than a red outline because the outlined
   style is already what .secondary-button means here (a safe secondary
   action), and reusing it would say the opposite of what is true. */
.delete-confirm-button {
  background: #b3261e;
  border: 1px solid #b3261e;
  color: #fff;
}

.delete-confirm-button:hover:not(:disabled) {
  background: #8f1d17;
  border-color: #8f1d17;
}

.delete-confirm-button:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

/* Round 194: the "Viewed" marker on a job card's title. Same shape as the
   other .badge-* chips on this file so it sits with them, but deliberately the
   only GREY one -- every other badge is a reason to look at a job
   (mission-driven, matches your resume, alignment score) and earns a colour.
   "Viewed" is the opposite: it is there to help you skip past something, so it
   should be the quietest thing on the card and must not compete with the
   coloured badges for attention.

   "Quiet" still has to be readable, though -- grey text is where accessibility
   quietly goes wrong. #5c5c5c on #f0f0f0 measures 5.87:1, comfortably past AA's
   4.5:1 for normal text, and in fact better contrast than the existing
   mission-driven badge manages (#1a7f4b on #e6f7ec is 4.52:1). If you ever
   lighten this, measure it rather than eyeballing it. */
.badge-viewed {
  display: inline-block;
  background: #f0f0f0;
  color: #5c5c5c;
  border: 1px solid #e0e0e0;
  border-radius: 4px;
  padding: 1px 8px;
  font-size: 0.75rem;
  font-weight: 600;
  margin-left: 8px;
  /* The chip sits inside the <h3> beside the title link, which is a larger font
     size -- without this it rides on the text baseline and looks dropped. */
  vertical-align: middle;
}

/* ---------------------------------------------------------------------------
   Round 267: the board tabs.

   Round 266 shipped these as plain links and they rendered as unstyled purple
   underlined anchors in production. The markup was fine; the browser was
   holding a cached style.css beside fresh HTML -- which is EXACTLY the Round
   207 failure, recorded at the top of index.html, happening a second time.
   index.html now loads style.css?v=r267. Bump that token whenever a rule here
   has to arrive with the markup that depends on it.

   Shape follows the reference the maintainer sent: an active tab as a filled
   pill with a heavy underline bar, inactive tabs as plain text, and a hairline
   rule running the width of the row that the active tab sits on top of.

   Uses only --accent, --text-muted and --border, the three custom properties
   this stylesheet actually defines at :root.
   --------------------------------------------------------------------------- */
.board-tabs {
  display: flex;
  align-items: flex-end;
  gap: 0.25rem;
  /* The hairline the tabs sit on. */
  border-bottom: 1px solid var(--border);
  margin: 1.25rem 0 0;
  padding: 0;
  flex-wrap: wrap;
}

.board-tab {
  position: relative;
  padding: 0.7rem 1.4rem;
  border-radius: 8px 8px 0 0;
  text-decoration: none;
  font-weight: 500;
  font-size: 1rem;
  line-height: 1.2;
  color: var(--text-muted);
  background: transparent;
  /* ROUND 279, per request: an outline on the UNSELECTED tabs, so the row reads
     as a set of things you can pick rather than as a heading with some words
     after it. Same 1px and the same var(--border) as the hairline the row sits
     on, which is the divider immediately underneath.

     Declared TRANSPARENT here and coloured below on :not(.is-active), rather
     than added only to the inactive tabs. A border that appears and disappears
     changes the box by 2px and the whole row would jog sideways on every tab
     switch -- the same reason .board-tab::after is always present and merely
     transparent when inactive. */
  border: 1px solid transparent;
  /* 44px minimum touch target. */
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  /* Reserve the active tab's heavier weight so switching tabs does not shift
     the row's width -- a tab bar that reflows on click reads as a glitch. */
  transition: background-color 0.12s ease, color 0.12s ease;
}

.board-tab::after {
  /* The underline bar. Always present, transparent when inactive, so the tab
     does not grow by 3px when it becomes active. */
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;            /* sits on the row's own hairline */
  height: 3px;
  background: transparent;
  border-radius: 2px 2px 0 0;
}

.board-tab:not(.is-active) {
  border-color: var(--border);
}

.board-tab:hover,
.board-tab:focus-visible {
  background: rgba(36, 81, 255, 0.06);
  color: var(--accent);
}

.board-tab.is-active {
  background: rgba(36, 81, 255, 0.1);
  color: var(--accent);
  font-weight: 700;
}

.board-tab.is-active::after {
  background: var(--accent);
}

/* ROUND 276: the resume page's copy of the tab row sits below the "Find my
   matches" button instead of up in the header, so it needs breathing room from
   the button rather than from a page title. Scoped to that page's own modifier
   class -- the job list's tabs are still header tabs and must not move. */
.resume-board-tabs {
  margin-top: 1.75rem;
}

@media (max-width: 520px) {
  .board-tabs {
    gap: 0;
  }
  .board-tab {
    padding: 0.65rem 0.9rem;
    font-size: 0.95rem;
    flex: 1 1 auto;
    justify-content: center;
  }
}
