/* ============================================================================
   cand-tabs.css — stylesheet for app/cand-tabs.jsx (the candidate record's tab
   strip). Namespace: `.e8-cand-tabs-`. Grepped before it was claimed; app/app.css
   is one namespace and a duplicate class name silently overrides across features,
   later definition wins.

   Loaded AFTER app/app.css, so an equal-specificity rule here wins on order. The
   two rules that reach OUT of the namespace do so from inside it
   (`.e8-cand-tabs-row .e8-rail-trigger`, (0,2,0)) and cannot match anywhere else
   on the app - the job record's copy of that button is untouched.

   Why the strip is no longer `.e8-tabs`: app.css:4708 masks the right 18px of
   every `.e8-page .e8-tabs` under a VIEWPORT media query, unconditionally and
   permanently, which is what hid "Skills" at 390. The fades here are STATE - they
   exist only when there is more strip in that direction - and the reflow rules
   are `@container e8rec`, because the resizable rail squeezes this column without
   the window changing size at all.

   Sizing note: no rule here sets a raw px font-size. 11.5 / 13.5 come from
   --ui-text-xs / --ui-text-base; there is no 14.
   ============================================================================ */

/* ---------------------------------------------------------------- the row --- */
/* The hairline lives on the ROW, not on the scroller, so it runs unbroken under
   the tabs AND under the Details trigger - one bar, not two fragments. */
.e8-cand-tabs-row {
  --e8-cand-tabs-inset: 10px;  /* underline inset == button padding, so it tracks the label */
  --e8-cand-tabs-fade: 28px;

  display: flex;
  align-items: stretch;
  gap: 12px;
  border-bottom: 1px solid var(--ui-border);
}

/* The scroller's positioned wrapper: it carries the two edge fades, which must NOT
   scroll with the content and must not sit over the row's hairline. */
.e8-cand-tabs-shell {
  position: relative;
  display: flex;
  flex: 1;
  min-width: 0;
}

.e8-cand-tabs-shell::before,
.e8-cand-tabs-shell::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  width: var(--e8-cand-tabs-fade);
  z-index: 1;
  opacity: 0;
  pointer-events: none;
}
.e8-cand-tabs-shell::before {
  left: 0;
  background: linear-gradient(to right, var(--ui-bg-app), transparent);
}
.e8-cand-tabs-shell::after {
  right: 0;
  background: linear-gradient(to left, var(--ui-bg-app), transparent);
}
/* State, not decoration: a fade means "there is more strip this way". Both are 0 by
   default, so a strip that fits shows neither and nothing is ever dimmed for free. */
.e8-cand-tabs-shell.is-fade-l::before { opacity: 1; }
.e8-cand-tabs-shell.is-fade-r::after { opacity: 1; }

/* -------------------------------------------------------------- the strip --- */
.e8-cand-tabs-sc {
  position: relative;          /* containing block for the measured underline */
  display: flex;
  align-items: stretch;
  flex: 1;
  min-width: 0;
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
  -ms-overflow-style: none;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;  /* a flick at the end of the strip must not page-swipe the record */
  /* NO scroll-snap here, and the reason is measured rather than assumed. The review asked for
     "an edge fade plus a snap so a partially-scrolled label never lands mid-glyph", so it was
     built - `scroll-snap-type: x proximity` + `scroll-snap-align: start` + a 20px
     `scroll-padding-inline` to match the sliver sync() leaves - and then tested at 390:

       key 4 (Messages) landed at scrollLeft 63 instead of 104, and the tab it had just
       selected measured NOT fully visible. Every other tab was unaffected.

     Snap and sync() disagree about what "shown" means. sync() has two moves: pull a tab in from
     the left (target = offsetLeft - 20, which IS a start-snap position, so those three agreed),
     or pull one in from the RIGHT, aligning its trailing edge inside the window - a position no
     `snap-align: start` tab occupies, so proximity snapped it back to the previous tab's start
     and re-clipped the tab it had just revealed. `snap-align: end` only moves the failure to the
     other three. A snap set cannot express "whichever edge was off-screen", which is the whole
     job, so the guarantee stays in JS where it can see which direction it is travelling. The
     resting positions the tween produces are glyph-aligned by construction, and a hand-scroll
     that stops mid-label stops under the edge fade below. */
}
.e8-cand-tabs-sc::-webkit-scrollbar { width: 0; height: 0; }

/* ---------------------------------------------------------------- a tab ----- */
.e8-cand-tabs-btn {
  position: relative;
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 36px;
  padding: 0 var(--e8-cand-tabs-inset);
  border: 0;
  background: transparent;
  border-radius: var(--ui-radius) var(--ui-radius) 0 0;
  cursor: pointer;
  font-family: inherit;
  font-size: var(--ui-text-base);
  /* Weight is CONSTANT across states on purpose. A 500 -> 600 bump on select changes the
     button's width, which moves every tab after it and shifts the row on every switch;
     the reference bar for this record is "no layout shift when the body swaps". Selection
     is carried by ink and by the underline. */
  font-weight: 500;
  /* SECONDARY, not tertiary. MEASURED against the record's own background: tertiary ink is
     2.59:1 in light mode - the idle tabs on this strip were failing AA as shipped, inherited
     from DSc.Tabs. Secondary is 4.78:1 light / 7.86:1 dark and still leaves a wide gap to the
     active tab's 16.1:1, which is what actually carries selection. */
  color: var(--ui-text-secondary);
  white-space: nowrap;
  transition: none;
}
.e8-cand-tabs-btn:hover { color: var(--ui-text); background: var(--ui-fill); }
.e8-cand-tabs-btn.is-on { color: var(--ui-text); }
.e8-cand-tabs-btn.is-on:hover { background: transparent; }
/* An INSET ring, not the global outward --ui-focus-ring: this button lives in an
   overflow-x:auto box, which clips a 4px outward box-shadow - and here it is clipped on the
   BLOCK axis too, because the button is exactly as tall as the scroller, so the ring rendered
   as two vertical bars with no top or bottom.

   SPECIFICITY, and why this selector is anchored twice. The one-class form
   `.e8-cand-tabs-btn:focus-visible` is (0,2,0) and LOSES to product.css:260
   `.elev8 :is(button, a, input, select, textarea, [tabindex]):focus-visible` at (0,2,1) - `:is()`
   contributes its most specific argument, and the bare type selectors there add the extra 1.
   So the inset outline above never painted at all: the global rule's `outline: none` won, and
   what was on screen was its clipped `box-shadow` ring. Adding the scroller ancestor takes this
   to (0,3,0), which beats it on class count. `box-shadow: none` is then required as well -
   losing the outline war was hiding the fact that both rings were being asked for at once. */
.e8-cand-tabs-sc .e8-cand-tabs-btn:focus-visible {
  outline: 2px solid var(--ui-accent);
  outline-offset: -3px;
  box-shadow: none;
  border-radius: var(--ui-radius);
  color: var(--ui-text);
}

/* -------------------------------------------------------------- the count --- */
/* Fixed height + centred content + tabular figures: every badge on the strip sits on
   one baseline whatever its digit count, and a 1 -> 2 digit change cannot nudge it.

   WAVE-3 SEAM FIX — THE BORDER IS GONE AND THE BOX IS THE HOUSE PILL. cand-record.css:127
   writes the record's small-pill contract down: 11.5 / 600 / radius-pill / --ui-fill /
   18.7px tall, which `.e8-rfr-pill`, `.e8-ovdis-count` (both app.css),
   `.e8-cand-overview-actkind` and `.e8-cand-rec-na-urg` all already measure. Measured at
   1440 on #/candidate/c-okafor before this change, these three badges came out 18px tall
   with `padding: 0 5px` AND a 1px `--ui-border` ring - the only ringed pill anywhere on the
   record, sitting one row above `.e8-ovdis-count` ("Also in play  2"), which is the same
   number in the same ink on the same fill with no ring. Two shapes for one idea.
   The stability the fixed box buys is kept - explicit height, min-width, centred content,
   tabular figures - it is only the ring that goes and the height that moves 18 -> 19 to land
   on the 18.7px the contract names. Nothing else on the strip is touched. */
.e8-cand-tabs-count {
  flex: none;
  box-sizing: border-box;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 19px;
  height: 19px;
  padding: 0 6px;
  border: 0;
  border-radius: var(--ui-radius-pill);
  background: var(--ui-fill);
  font-size: var(--ui-text-xs);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  line-height: 1;
  /* Same measurement as the label: tertiary on --ui-fill is 2.43:1 in light mode, secondary
     is 4.50:1. A count is a number someone has to read. */
  color: var(--ui-text-secondary);
}
/* The accent lives in the FILL, not in the digit. Accent ink on the accent wash measured
   6.35:1 light but only 4.40:1 dark - a marginal AA failure on 11.5px text, and the kind of
   thing that is never noticed because the number is short. --ui-text on the same wash clears
   14:1 in both themes, and the tint still reads as "this is the tab you are on". */
.e8-cand-tabs-btn.is-on .e8-cand-tabs-count {
  background: var(--ui-wash);
  border-color: transparent;
  color: var(--ui-text);
}

/* ---------------------------------------------------------- the underline --- */
/* Width and offset arrive as measured DATA (custom properties set in the JSX). The
   element is inside the scroller, so it travels with the content when the strip is
   scrolled by hand and needs no second update. */
/* Y is measured too, and not pinned to the scroller's bottom edge. Those are the same line only
   while the strip is ONE row; under the wrap block below the first row's tabs sit a full row
   above it, and a `bottom: 0` underline would have marked the wrong row. --e8-cand-tabs-y is the
   active button's own bottom minus this element's 2px height, so on an unwrapped strip it
   resolves to precisely the old position. */
.e8-cand-tabs-ind {
  position: absolute;
  left: 0;
  top: 0;
  height: 2px;
  width: max(0px, calc(var(--e8-cand-tabs-w, 0px) - var(--e8-cand-tabs-inset) * 2));
  transform: translate(calc(var(--e8-cand-tabs-x, 0px) + var(--e8-cand-tabs-inset)), var(--e8-cand-tabs-y, 0px));
  background: var(--ui-accent);
  border-radius: var(--ui-radius-pill) var(--ui-radius-pill) 0 0;
  pointer-events: none;
  transition: none;
}

/* ------------------------------------------------------------ the trailer --- */
.e8-cand-tabs-hint {
  flex: none;
  align-self: center;
  font-size: var(--ui-text-xs);
  /* The inherited rule faded tertiary to 70% alpha, which measured about 2.0:1 on the record
     background. Full tertiary is 2.59:1 light / 5.84:1 dark - still under AA in light mode, and
     that is a TOKEN limitation this lane is not authorised to change, so it is reported rather
     than papered over. Louder than tertiary would make a keyboard hint compete with the tabs. */
  color: var(--ui-text-tertiary);
  white-space: nowrap;
}
/* The Details trigger keeps `.e8-rail-trigger`'s look (one button, one style, shared
   with the job record) and gets its geometry from the strip it sits in. */
.e8-cand-tabs-row .e8-rail-trigger {
  align-self: center;
  min-height: 32px;
  margin-bottom: 4px;
}
/* Its label, split off in the JSX so the narrow block below can drop it. Unstyled at width -
   this exists to be a display target, not a look. */
.e8-cand-tabs-triglbl { white-space: nowrap; }

/* ------------------------------------------------- the column's own gutter --- */
/* The one rule here that is not about the strip, and it is here because the strip is what MOVES.
   MEASURED at 834x1112: the tablist's width read 686 on Overview / Skills / Messages and 696 on
   Experience / Files - exactly the 10px classic scrollbar. Those two panels are short enough not
   to overflow `.e8-content.e8-rbody-content`, so the scrollbar disappears, the column widens, and
   every box in it - the tab strip, the underline, the Details trigger - jumps 10px sideways on
   the switch. The reference's bar is "no layout shift when the body swaps" (§2.8); this is the
   whole column shifting, on every switch, in both directions.

   `scrollbar-gutter: stable` reserves the space whether or not the bar is drawn, so the width is
   the same on all five tabs. It costs nothing where a scrollbar is already permanent, and nothing
   at all on overlay-scrollbar platforms.

   SCOPE. The property belongs to the record shell's scroller, which is `.e8-rbody-content` -
   shared by every record type and owned by app.css, not by this lane. `:has(.e8-cand-tabs-row)`
   is true only on the candidate record (that class has exactly one call site, CandTabStrip), so
   this reaches the column this batch measured and nothing else. It is a SCOPED fix, not a copy:
   the same one line on `.e8-recordbody .e8-rbody-content` in app.css would fix the identical
   shift on the job, client, contact, consultant and engagement records, and this rule should be
   deleted when it lands there. Reported rather than done, because app.css is not this lane's. */
.e8-rbody-content:has(.e8-cand-tabs-row) { scrollbar-gutter: stable; }

/* ================================================================= reflow ====
   @container e8rec, never @media: the rail squeezes this column without the
   viewport changing, and a viewport query here has shipped as a bug twice. 840 is
   the same threshold at which the shell collapses the rail - so the strip goes to
   touch geometry exactly when the Details trigger appears, which is the only time
   this row is driven by a finger.

   BOTH blocks are placed after every rule they override. A conditional at-rule
   adds no specificity, so one written above its own base rule never applies.
   ============================================================================ */
@container e8rec (max-width: 840px) {
  .e8-cand-tabs-row { --e8-cand-tabs-inset: 12px; }
  /* 44px: the reference bar for a touch target, and 10px more than the 34px this
     strip measured before the rewrite. */
  .e8-cand-tabs-btn { min-height: 44px; }
  .e8-cand-tabs-row .e8-rail-trigger { min-height: 44px; margin-bottom: 0; }

  /* STICKY, and only where the record is one column. MEASURED at 390: the strip sat at y=607
     inside `.e8-rbody-content`, whose scrollHeight is 2483 against a 659px window - so Overview
     is 3.8 screens and every tab switch began with a scroll back to the top to find the control.
     `top: 0` is the scroller's own top edge.

     THIS ROW IS NOT ALONE ANY MORE, and the note that used to sit here said it was. It read
     "Nothing else in this scroller is sticky (checked: zero position: sticky|fixed descendants),
     so there is no second bar to stack under" - true when written, false as soon as
     cand-overview.css made the activity day header sticky in the same column. Measured at 390:
     both stuck to y=163, this row at z-index 2 and 89px tall, the day header at z-index 1 and
     19px, i.e. entirely behind it. The row therefore PUBLISHES its height as
     `--e8-cand-rec-sticky-top` (see the effect in cand-tabs.jsx), which is the variable
     cand-overview.css was already reading, and the day header stacks under it instead of behind
     it. Anything else that goes sticky in this column reads the same variable.

     The background is required, not decoration: a transparent sticky row would let the record
     scroll through the tab labels. --ui-bg-app is what `.e8-recordbody .e8-rbody-content` already
     paints (app.css:2326), so the row keeps reading as part of the page rather than as a bar
     bolted over it, and the row's existing hairline becomes the edge that separates the two.

     SCOPE, stated rather than implied: this is deliberately NOT applied above 840. The desktop
     column scrolls too, so the same argument could be made there - but that is a visible change
     to the desktop record that nobody has measured or asked for, and it competes with the rail
     for the top of the screen. The finding was about the one-column layout; the fix is too. */
  .e8-cand-tabs-row {
    position: sticky;
    top: 0;
    z-index: 2;
    background: var(--ui-bg-app);
  }
}

/* Below 640 the row is a phone's. Five tabs cannot fit 390 in ONE row at any honest type size, so
   the strip stops pretending: it spends every pixel of the row on the tabs, drops the trigger's
   word, and then WRAPS to a second row rather than scrolling.

   MEASURED at 390 before the trigger label was dropped (recordbody 390, rail collapsed):
     scroller clientWidth 242 / scrollWidth 451 - 209px, 46% of the strip unreachable at rest.
   MEASURED at 390 with the label dropped but still scrolling (this build, before the wrap):
     scroller clientWidth 288 / scrollWidth 451. Visible per tab: Overview 83, Experience 119,
     Skills 58, Messages 29 of 113, Files 0 of 79. One tab entirely absent and one sliced to a
     glyph under a 36px fade is not a scroll cue - it reads as a bug, and 46% -> 36% of the strip
     hidden is not the difference between broken and working.

   So: WRAP. The reference's bar for this piece is "never clips; scrolls or wraps deliberately",
   and the products it names (Ashby, Lever) take the second row rather than hiding a tab. Inside
   the 288px scroller the five tabs lay out 83+119+58 = 260 on row one and 113+79 = 192 on row
   two, both inside the window, so nothing is clipped, nothing is faded, and no gesture is needed
   to reach Files. The cost is 44px of height, paid once, against a control that was previously
   unreachable at rest; the sticky rule above is what keeps that 88px from being spent twice.

   `overflow-x` stays `auto` rather than becoming `hidden`: if a container ever gets narrower than
   a single tab, wrapping cannot save it and the scroller must still be able to reach the label.
   The JS half needs no branch - with no overflow, sync()'s clamp resolves to scrollLeft 0 and
   readEdges() reports both fades off, which is the state-not-decoration rule paying for itself. */
@container e8rec (max-width: 639px) {
  .e8-cand-tabs-row { gap: 8px; --e8-cand-tabs-fade: 36px; }
  .e8-cand-tabs-triglbl { display: none; }
  /* A 44px square - the same touch target as the tabs beside it, now spent on the target rather
     than on the label. `padding-inline: 0` so the shared .e8-rail-trigger padding cannot push it
     back past 44. */
  .e8-cand-tabs-row .e8-rail-trigger { min-width: 44px; justify-content: center; padding-inline: 0; }
  /* Wrapping is CONDITIONAL on it being needed - `flex-wrap` only breaks a line that does not
     fit. MEASURED: container 560 keeps all five on one 44px row (451 into 458), container 390
     takes 3+2 and container 360 takes 2+3; every tab fully visible in all three. So the second
     row is not a fixed cost of the phone layout, it appears at the width where the alternative
     is a hidden tab.
     `align-content: flex-start` so the rows keep their own height instead of being stretched
     apart if anything ever gives the scroller a taller box. */
  .e8-cand-tabs-sc { flex-wrap: wrap; align-content: flex-start; }
}

/* Motion is opt-in. Base state above is `transition: none`, so a reduced-motion user
   gets an underline that simply appears in its new place, and a scroll that jumps -
   the JS half reads the same query before it calls scrollTo.
   `.is-armed` lands one frame after the first measurement, so the underline never
   animates in from x=0 on load. */
@media (prefers-reduced-motion: no-preference) {
  .e8-cand-tabs-ind.is-armed {
    transition: transform 190ms cubic-bezier(.2, .8, .3, 1), width 190ms cubic-bezier(.2, .8, .3, 1);
  }
  .e8-cand-tabs-btn { transition: color 120ms ease-out, background 120ms ease-out; }
  .e8-cand-tabs-shell::before,
  .e8-cand-tabs-shell::after { transition: opacity 140ms ease-out; }
}
